diff --git a/docs/automation/handoffs/pre-phase10/CONTRACT.md b/docs/automation/handoffs/pre-phase10/CONTRACT.md index 66c9b65..3dc4bbf 100644 --- a/docs/automation/handoffs/pre-phase10/CONTRACT.md +++ b/docs/automation/handoffs/pre-phase10/CONTRACT.md @@ -1,5 +1,7 @@ # Proposed evidence adapter v1 +P10-ENTRY-FINAL addendum: the frozen source manifest also covers tracked Python/JavaScript PDF diagnostics in `docs/p10-entry-final/`. Omitting a diagnostic from both source manifests must fail verification. `ENTRY_PROPOSAL_SCHEMA.json` describes a separate evidence-only owner decision proposal; its constants prohibit execution permission, approved removals or a GO decision. It grants no authority and does not replace the controller's protected approvals. + Contract ID: `urn:jobjakt:pre-phase10-evidence:1`. This is a **proposed evidence import contract**, not a replacement for AUTONOMOUS-ROADMAP-01's committed `automation/schemas/backlog.schema.json` or `result.schema.json`. Both happen to use version1; their document kinds and authority are different. The separate controller snapshot is recorded in GATES.json. Its worktree/policy/state/runtime was not modified or executed. | Package field | Existing controller integration | diff --git a/docs/automation/handoffs/pre-phase10/ENTRY_PROPOSAL_SCHEMA.json b/docs/automation/handoffs/pre-phase10/ENTRY_PROPOSAL_SCHEMA.json new file mode 100644 index 0000000..65e478f --- /dev/null +++ b/docs/automation/handoffs/pre-phase10/ENTRY_PROPOSAL_SCHEMA.json @@ -0,0 +1,67 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "urn:jobjakt:p10-entry-final:proposal:1", + "title": "Evidence-only Phase10 entry proposal; never an approval", + "type": "object", + "additionalProperties": false, + "required": ["schemaVersion", "workstreamId", "authority", "candidateCommit", "acceptedBranch", "applicationFixCommit", "packageHeadRule", "entryDecision", "eligibleForAutomaticExecution", "cleanupScope", "pdfDecisions", "outstandingBlockerIds", "deferralRequests", "requiredDecisions", "testEvidence", "publication", "productionDecision", "generatedUtc"], + "properties": { + "schemaVersion": { "const": 1 }, + "workstreamId": { "const": "P10-ENTRY-FINAL" }, + "authority": { "const": "EVIDENCE_ONLY_NOT_OWNER_APPROVAL" }, + "candidateCommit": { "$ref": "#/definitions/commit" }, + "acceptedBranch": { "const": "test/pre-phase10-ux-acceptance" }, + "applicationFixCommit": { "$ref": "#/definitions/commit" }, + "packageHeadRule": { "const": "Resolve final clean published envelope HEAD independently; bind it, the candidate snapshot, exact scope, policy and artifact hashes in protected approval. No self-referential commit claim." }, + "entryDecision": { "const": "NO-GO" }, + "eligibleForAutomaticExecution": { "const": false }, + "productionDecision": { "const": "NO-GO" }, + "cleanupScope": { + "type": "object", "additionalProperties": false, + "required": ["disposition", "inventory", "proposedFirstAction", "approvedRemovalCandidates", "implementationPermission"], + "properties": { + "disposition": { "const": "ASSESSMENT_ONLY_NO_REMOVALS_APPROVED" }, + "inventory": { "type": "string", "minLength": 1 }, + "proposedFirstAction": { "type": "string", "minLength": 1 }, + "approvedRemovalCandidates": { "type": "array", "maxItems": 0 }, + "implementationPermission": { "const": false } + } + }, + "pdfDecisions": { + "type": "object", "additionalProperties": false, + "required": ["originalCorpus", "actualAccentClippingFix", "unchangedRevisedOracle", "proposedBaselineDiagnostic", "metricDisposition", "humanVisual", "actualPrinting"], + "properties": { + "originalCorpus": { "const": "PASS" }, + "actualAccentClippingFix": { "const": "PASS_AUTOMATED_FINITE_CORPUS" }, + "unchangedRevisedOracle": { "const": "FAIL" }, + "proposedBaselineDiagnostic": { "const": "PASS_DIAGNOSTIC_ONLY_NOT_GATE_REPLACEMENT" }, + "metricDisposition": { "const": "OWNER_DECISION_REQUIRED" }, + "humanVisual": { "const": "NOT_TESTED" }, + "actualPrinting": { "const": "NOT_TESTED" } + } + }, + "outstandingBlockerIds": { "$ref": "#/definitions/strings" }, + "deferralRequests": { + "type": "array", "items": { + "type": "object", "additionalProperties": false, "required": ["id", "scope", "sourceAuthority", "state"], + "properties": { "id": { "type": "string" }, "scope": { "type": "string" }, "sourceAuthority": { "type": "string" }, "state": { "const": "REQUESTED_NOT_GRANTED_OR_EXECUTABLE" } } + } + }, + "requiredDecisions": { + "type": "array", "minItems": 1, "items": { + "type": "object", "additionalProperties": false, "required": ["id", "action", "state", "authority"], + "properties": { "id": { "type": "string" }, "action": { "type": "string" }, "state": { "const": "PENDING" }, "authority": { "enum": ["EXPLICIT_OWNER_REFERENCE_DISPOSITION", "EXTERNAL_PROTECTED_OWNER_OR_OPERATOR_DECISION"] } } + } + }, + "testEvidence": { "$ref": "#/definitions/strings" }, + "publication": { + "type": "object", "additionalProperties": false, "required": ["status", "candidateRemoteCommit", "observedUtc", "finalHeadMustBeRechecked"], + "properties": { "status": { "const": "VERIFIED_CANDIDATE_SNAPSHOT" }, "candidateRemoteCommit": { "$ref": "#/definitions/commit" }, "observedUtc": { "type": "string", "format": "date-time" }, "finalHeadMustBeRechecked": { "const": true } } + }, + "generatedUtc": { "type": "string", "format": "date-time" } + }, + "definitions": { + "commit": { "type": "string", "pattern": "^[a-f0-9]{40}$" }, + "strings": { "type": "array", "items": { "type": "string", "minLength": 1 }, "uniqueItems": true } + } +} diff --git a/docs/automation/handoffs/pre-phase10/package.test.mjs b/docs/automation/handoffs/pre-phase10/package.test.mjs index 13e9523..9b45d9c 100644 --- a/docs/automation/handoffs/pre-phase10/package.test.mjs +++ b/docs/automation/handoffs/pre-phase10/package.test.mjs @@ -83,6 +83,21 @@ test('omitting a source file from both manifests cannot claim a complete freeze' test('artifact paths cannot escape the imported package', () => shadow(base => { json(base, 'TEST_EVIDENCE', index => { index.artifacts[0].path = '../../outside'; }); })); + +test('PDF diagnostic code cannot be omitted from both frozen source manifests', () => shadow(base => { + json(base, 'TEST_EVIDENCE', index => { + for (const id of [index.sourceManifestArtifactId, index.sourceAfterArtifactId]) { + const artifact = index.artifacts.find(item => item.id === id); + const file = path.join(base, artifact.path); + const data = JSON.parse(zlib.gunzipSync(fs.readFileSync(file)).toString('utf8')); + const name = Object.keys(data).find(item => item.startsWith('docs/p10-entry-final/') && item.endsWith('.py')); + assert.ok(name, 'PDF diagnostic source must be bound before qualification'); + delete data[name]; + const bytes = zlib.gzipSync(JSON.stringify(data)); + fs.writeFileSync(file, bytes); artifact.sha256 = hash(bytes); + } + }); +})); test('an imported schema cannot weaken the trusted schema', () => shadow(base => { fs.writeFileSync(path.join(base, 'HANDOFF_SCHEMA.json'), '{}'); })); diff --git a/docs/automation/handoffs/pre-phase10/verify.mjs b/docs/automation/handoffs/pre-phase10/verify.mjs index 0fcc952..b75cfc9 100644 --- a/docs/automation/handoffs/pre-phase10/verify.mjs +++ b/docs/automation/handoffs/pre-phase10/verify.mjs @@ -166,9 +166,10 @@ export function validatePackage(base = directory) { const after = JSON.parse(artifact(evidence.sourceAfterArtifactId).toString('utf8')); demand(JSON.stringify(source) === JSON.stringify(after), 'Source drifted during the regression run'); const prefixes = ['JobTrackerApi/', 'JobTrackerApi.Tests/', 'JobTrackerApi.MariaDb.Tests/', 'job-tracker-ui/', 'scripts/', 'tools/summarizer/', 'docs/pre-phase10-ux/', 'docs/redesign/phase-9/']; - const tracked = execFileSync('rtk', ['proxy', 'git', 'ls-files', '-z'], { cwd: root, encoding: 'utf8' }).split('\0').filter(name => prefixes.some(prefix => name.startsWith(prefix))).sort(); + const entryDiagnostic = name => name.startsWith('docs/p10-entry-final/') && /\.(?:py|cjs)$/.test(name); + const tracked = execFileSync('rtk', ['proxy', 'git', 'ls-files', '-z'], { cwd: root, encoding: 'utf8' }).split('\0').filter(name => prefixes.some(prefix => name.startsWith(prefix)) || entryDiagnostic(name)).sort(); demand(Object.keys(source).length <= 5000 && JSON.stringify(Object.keys(source).sort()) === JSON.stringify(tracked), 'Frozen source membership does not match the tracked qualification scope'); - execFileSync('rtk', ['proxy', 'git', 'diff', '--quiet', evidence.testedCommit, '--', ...prefixes], { cwd: root }); + execFileSync('rtk', ['proxy', 'git', 'diff', '--quiet', evidence.testedCommit, '--', ...prefixes, ...tracked.filter(entryDiagnostic)], { cwd: root }); for (const [name, expected] of Object.entries(source)) { demand(!path.isAbsolute(name) && !name.split(/[\\/]/).includes('..'), 'Unsafe source path'); const file = path.resolve(root, name);