diff --git a/docs/automation/handoffs/pre-phase10/CONTRACT.md b/docs/automation/handoffs/pre-phase10/CONTRACT.md index 6297395..66c9b65 100644 --- a/docs/automation/handoffs/pre-phase10/CONTRACT.md +++ b/docs/automation/handoffs/pre-phase10/CONTRACT.md @@ -21,7 +21,7 @@ The controller snapshot also has no explicit protected candidate-scope checkpoin ## Portable evidence -Native test reports, process observations and source manifests are stored as individually compressed `.gz` files under `evidence/` with exact hashes in TEST_EVIDENCE.json. Native XML/JSON contents are preserved, not rewritten to improve results. The local raw `.artifacts/automation-handoff-01/` evidence remains separately available; large videos, traces, model weights and document corpora are not imported into this package. Visual review uses its separately indexed synthetic comparison package. +Native test reports, process observations and source manifests are stored as individually compressed `.gz` files under `evidence/` with exact hashes in TEST_EVIDENCE.json. XML, Jest and ordinary metadata bytes are preserved. Playwright reports with inline attachment bodies use an explicitly labelled transport projection: only `results[].attachments[].body` is externalized. Native cases, counters, errors, retries, statuses, labels and identities remain intact; an indexed audit retains original report/body hashes, sizes, native statistics and original producer paths. This avoids committing roughly77MiB of compressed inline screenshots. The original raw reports remain untouched under `.artifacts/automation-handoff-01/`; no failed result is removed or improved. Importing consumers must recognise this projection and obtain the original raw artifact if their policy requires complete inline bodies. Large videos, traces and model weights are not imported. Actual synthetic PDFs/previews are supplied in the separate visual comparison package. The package verifier only reads bounded local files/Git and optionally verifies the configured remote. It does not execute report commands, install dependencies, run a controller, inspect protected approval keys or grant readiness. Installed pinned Ajv/jsdom from the existing frontend dev dependency tree provide actual JSON Schema/XML validation; no dependency was added. External consumers may use their own qualified parsers and must not trust this model-writable verifier as their security boundary. diff --git a/docs/automation/handoffs/pre-phase10/HANDOFF_SCHEMA.json b/docs/automation/handoffs/pre-phase10/HANDOFF_SCHEMA.json index d7647f7..15bc03c 100644 --- a/docs/automation/handoffs/pre-phase10/HANDOFF_SCHEMA.json +++ b/docs/automation/handoffs/pre-phase10/HANDOFF_SCHEMA.json @@ -90,7 +90,8 @@ }, "artifact": { "type": "object", "additionalProperties": false, "required": ["id", "path", "sha256", "description"], - "properties": { "id": { "type": "string" }, "path": { "type": "string", "minLength": 1 }, "sha256": { "$ref": "#/definitions/sha" }, "description": { "type": "string" } } + "properties": { "id": { "type": "string" }, "path": { "type": "string", "minLength": 1 }, "sha256": { "$ref": "#/definitions/sha" }, "description": { "type": "string" }, "projection": { "const": "PLAYWRIGHT_INLINE_ATTACHMENT_BODIES_EXTERNALIZED" }, "sourceSha256": { "$ref": "#/definitions/sha" }, "sourceRelativePath": { "type": "string" }, "externalizedAttachmentCount": { "type": "integer", "minimum": 1 }, "projectionAuditArtifactId": { "type": "string" } }, + "dependencies": { "projection": ["sourceSha256", "sourceRelativePath", "externalizedAttachmentCount", "projectionAuditArtifactId"] } }, "report": { "type": "object", "additionalProperties": false, diff --git a/docs/automation/handoffs/pre-phase10/package.test.mjs b/docs/automation/handoffs/pre-phase10/package.test.mjs index 498113a..13e9523 100644 --- a/docs/automation/handoffs/pre-phase10/package.test.mjs +++ b/docs/automation/handoffs/pre-phase10/package.test.mjs @@ -8,7 +8,10 @@ import { fileURLToPath } from 'node:url'; import { validatePackage } from './verify.mjs'; const directory = path.dirname(fileURLToPath(import.meta.url)); -const artifactRoot = path.resolve(directory, '../../../../.artifacts'); +const repository = fs.realpathSync(path.resolve(directory, '../../../..')); +const artifactRoot = path.join(repository, '.artifacts'); +fs.mkdirSync(artifactRoot, { recursive: true }); +assert.ok(fs.realpathSync(artifactRoot).startsWith(repository + path.sep)); const hash = bytes => crypto.createHash('sha256').update(bytes).digest('hex'); function shadow(change) { const temporary = fs.mkdtempSync(path.join(artifactRoot, 'handoff-verify-')); @@ -17,7 +20,7 @@ function shadow(change) { change(temporary); assert.throws(() => validatePackage(temporary)); } finally { - const resolved = path.resolve(temporary); + const resolved = fs.realpathSync(temporary); assert.ok(resolved.startsWith(artifactRoot + path.sep) && path.basename(resolved).startsWith('handoff-verify-')); fs.rmSync(resolved, { recursive: true }); } diff --git a/docs/automation/handoffs/pre-phase10/verify.mjs b/docs/automation/handoffs/pre-phase10/verify.mjs index 5baa9d4..0fcc952 100644 --- a/docs/automation/handoffs/pre-phase10/verify.mjs +++ b/docs/automation/handoffs/pre-phase10/verify.mjs @@ -133,6 +133,12 @@ export function validatePackage(base = directory) { const bytes = readPackage(item.path); return item.path.endsWith('.gz') ? zlib.gunzipSync(bytes, { maxOutputLength: 8 * 1024 * 1024 }) : bytes; }; + for (const item of evidence.artifacts.filter(entry => entry.projection)) { + const audit = JSON.parse(artifact(item.projectionAuditArtifactId).toString('utf8')); + const bytes = artifact(item.id); + demand(audit.sourceSha256 === item.sourceSha256 && audit.projectedSha256 === digest(bytes) && audit.removedBodies.length === item.externalizedAttachmentCount, 'Attachment-body export audit differs'); + demand(JSON.stringify(audit.sourceStats) === JSON.stringify(JSON.parse(bytes.toString('utf8')).stats), 'Native browser statistics changed during export'); + } for (const report of evidence.reports) { const expectedCommit = report.bindingScope === 'HANDOFF_VERIFIER' ? evidence.verifierCommit : evidence.testedCommit; demand(report.sourceCommit === expectedCommit, 'Report source correspondence missing');