diff --git a/docs/automation/handoffs/pre-phase10/ENTRY_PROPOSAL_SCHEMA.json b/docs/automation/handoffs/pre-phase10/ENTRY_PROPOSAL_SCHEMA.json index 65e478f..9a381f6 100644 --- a/docs/automation/handoffs/pre-phase10/ENTRY_PROPOSAL_SCHEMA.json +++ b/docs/automation/handoffs/pre-phase10/ENTRY_PROPOSAL_SCHEMA.json @@ -34,8 +34,8 @@ "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" }, + "proposedBaselineDiagnostic": { "const": "PASS_OWNER_APPROVED_S01_Y_METRIC" }, + "metricDisposition": { "const": "OWNER_APPROVED_S01_METRIC_ONLY" }, "humanVisual": { "const": "NOT_TESTED" }, "actualPrinting": { "const": "NOT_TESTED" } } diff --git a/docs/automation/handoffs/pre-phase10/S01_METRIC_APPROVAL.json b/docs/automation/handoffs/pre-phase10/S01_METRIC_APPROVAL.json new file mode 100644 index 0000000..54d6f85 --- /dev/null +++ b/docs/automation/handoffs/pre-phase10/S01_METRIC_APPROVAL.json @@ -0,0 +1,13 @@ +{ + "kind": "OWNER_METRIC_DISPOSITION_ONLY", + "receivedLocalDate": "2026-10-30", + "timezone": "Europe/Paris", + "source": "Direct human reply to the explicit scoped approval question in this task; transcribed evidence, not a protected controller signature", + "candidateCommit": "ba04d817fed26d659bfa2a56b089d6cac851dc34", + "designVersion": 2, + "metric": "S01_Y_BASELINE_TO_BASELINE", + "positionToleranceMm": 2, + "ownerReply": "Approve the revised S01 metric only", + "preserve": ["Original oracle and28Windows/20Linux failures", "Horizontal2mm limit", "All other font/color/page/content/bounds/marker checks", "Version1 metric and historical references"], + "notGranted": ["Human visual acceptance", "Physical printing acceptance", "Phase10 entry", "Cleanup permission", "Controller/policy activation", "Production deployment"] +} diff --git a/docs/automation/handoffs/pre-phase10/package.test.mjs b/docs/automation/handoffs/pre-phase10/package.test.mjs index 9b45d9c..aea8295 100644 --- a/docs/automation/handoffs/pre-phase10/package.test.mjs +++ b/docs/automation/handoffs/pre-phase10/package.test.mjs @@ -104,6 +104,20 @@ test('an imported schema cannot weaken the trusted schema', () => shadow(base => test('model quality cannot become PASS from the rejected development probes', () => shadow(base => { json(base, 'ACCEPTANCE', value => { value.verdicts.find(item => item.criterion === 'CV AI Accuracy').status = 'PASS'; }); })); + +test('the scoped S01 disposition cannot authorise a larger tolerance', () => shadow(base => { + json(base, 'S01_METRIC_APPROVAL', value => { value.positionToleranceMm = 3; }); +})); + +test('approved S01 cannot conceal a failing revised baseline', () => shadow(base => { + json(base, 'TEST_EVIDENCE', index => { + const artifact = index.artifacts.find(item => item.id === 'p10-windows-classification'); + const file = path.join(base, artifact.path); + const value = JSON.parse(zlib.gunzipSync(fs.readFileSync(file))); + value.rows[0].pdfBaselinePx += 100; + const bytes = zlib.gzipSync(JSON.stringify(value)); fs.writeFileSync(file, bytes); artifact.sha256 = hash(bytes); + }); +})); test('a mandatory entry blocker cannot be self-deferred', () => shadow(base => { json(base, 'ACCEPTANCE', value => { value.permissibleDeferrals.push('P10-ENTRY-01'); }); })); diff --git a/docs/automation/handoffs/pre-phase10/verify.mjs b/docs/automation/handoffs/pre-phase10/verify.mjs index b75cfc9..1822f30 100644 --- a/docs/automation/handoffs/pre-phase10/verify.mjs +++ b/docs/automation/handoffs/pre-phase10/verify.mjs @@ -19,6 +19,36 @@ export function verifyBytes(bytes, expected) { demand(Buffer.isBuffer(bytes) && /^[a-f0-9]{64}$/.test(expected) && digest(bytes) === expected, 'Missing or changed evidence'); } +export function qualifyRevisedS01(oracle, baseline, measurements) { + const unique = rows => { + demand(Array.isArray(rows) && rows.length > 0 && new Set(rows.map(row => row.fixture)).size === rows.length, 'S01 missing or duplicate fixtures'); + return new Map(rows.map(row => [row.fixture, row])); + }; + const originals = unique(oracle.fixtures), coordinates = unique(baseline.rows), documents = unique(measurements.fixtures); + demand(oracle.total === originals.size && originals.size === coordinates.size && originals.size === documents.size, 'S01 corpus membership differs'); + const tolerance = oracle.tolerances; + demand(tolerance.positionMm === 2 && tolerance.fontPt === .5 && tolerance.pageDimensionMm === .5 && tolerance.rgbComponent === 1 / 255, 'S01 original tolerances changed'); + const finite = value => demand(typeof value === 'number' && Number.isFinite(value), 'S01 nonfinite coordinate'); + const fixtures = [...originals].map(([fixture, original]) => { + const row = coordinates.get(fixture), document = documents.get(fixture); + demand(row && document && document.settings.designVersion === 2 && /^[a-f0-9]{64}$/.test(row.pdfSha256) && row.pdfSha256 === document.pdfSha256, 'S01 source/version binding differs'); + finite(row.domBaselinePx); finite(row.pdfBaselinePx); + const baselineDeltaMm = Math.abs(row.domBaselinePx - row.pdfBaselinePx) * 25.4 / 96; + // Only the S01 vertical cell-top comparison is superseded by the owner's baseline metric. + const failures = original.failures.filter(value => !value.startsWith('S01: position delta ')); + demand(original.comparisons.filter(value => value.token === 'S01').length === 1, 'S01 marker missing or duplicated'); + for (const comparison of original.comparisons) { + for (const key of ['deltaXmm', 'deltaYmm', 'fontDeltaPt']) { finite(comparison[key]); demand(comparison[key] >= 0, 'S01 invalid difference'); } + if (comparison.colorDelta !== null) { finite(comparison.colorDelta); demand(comparison.colorDelta >= 0, 'S01 invalid color difference'); } + const vertical = comparison.token === 'S01' ? baselineDeltaMm : comparison.deltaYmm; + if (comparison.previewPage !== comparison.pdfPage || Math.max(comparison.deltaXmm, vertical) > 2 || comparison.fontDeltaPt > .5 || (comparison.colorDelta !== null && comparison.colorDelta > 1 / 255)) + failures.push(`${comparison.token}: retained position/page/font/color check failed`); + } + return { fixture, pdfSha256: row.pdfSha256, baselineDeltaMm, pass: failures.length === 0, failures }; + }); + return { metric: 'S01_Y_BASELINE_TO_BASELINE', positionToleranceMm: 2, total: fixtures.length, passed: fixtures.filter(row => row.pass).length, fixtures }; +} + export function measureNative(format, text) { demand(Buffer.byteLength(text, 'utf8') <= 8 * 1024 * 1024, 'Native report exceeds its input bound'); const result = { passed: 0, failed: 0, skipped: 0, retries: 0, unexecuted: 0, names: [] }; @@ -177,7 +207,7 @@ export function validatePackage(base = directory) { verifyBytes(fs.readFileSync(file), expected); } const verifierFiles = JSON.parse(artifact(evidence.verifierSourceArtifactId).toString('utf8')); - const expectedVerifierFiles = ['verify.mjs', 'verify.test.mjs', 'package.test.mjs', 'verify_git.py', 'HANDOFF_SCHEMA.json'].map(name => 'docs/automation/handoffs/pre-phase10/' + name).sort(); + const expectedVerifierFiles = ['verify.mjs', 'verify.test.mjs', 'package.test.mjs', 'verify_git.py', 'HANDOFF_SCHEMA.json', 'S01_METRIC_APPROVAL.json'].map(name => 'docs/automation/handoffs/pre-phase10/' + name).sort(); demand(JSON.stringify(Object.keys(verifierFiles).sort()) === JSON.stringify(expectedVerifierFiles), 'Missing verifier source membership'); for (const [name, expected] of Object.entries(verifierFiles)) { verifyBytes(fs.readFileSync(path.join(root, name)), expected); @@ -203,14 +233,24 @@ export function validatePackage(base = directory) { demand(probe.cases?.length === 3 && probe.cases.every(item => item.status === 'REJECTED_OR_FAILED'), 'This snapshot lacks its recorded negative CV-model evidence'); const oracles = evidence.reports.filter(item => ['windows-template-oracle', 'linux-template-oracle'].includes(item.id)); demand(oracles.length === 2 && oracles.every(item => item.status === 'PARTIAL' && item.exitCode !== 0), 'Original PDF failures were concealed'); - demand(decisions.get('Automated PDF Fidelity') === 'FAIL' && decisions.get('PDF Spacing') === 'PARTIAL', 'Revised PDF failures were promoted'); + const approval = JSON.parse(artifact('s01-metric-approval').toString('utf8')); + verifyBytes(artifact('s01-metric-approval'), digest(readSafe('S01_METRIC_APPROVAL.json'))); + demand(approval.candidateCommit === 'ba04d817fed26d659bfa2a56b089d6cac851dc34' && evidence.testedCommit === approval.candidateCommit && approval.metric === 'S01_Y_BASELINE_TO_BASELINE' && approval.positionToleranceMm === 2 && approval.designVersion === 2 && approval.ownerReply === 'Approve the revised S01 metric only', 'S01 owner approval scope differs'); + const revisedS01 = {}; + for (const platform of ['windows', 'linux']) { + const parse = id => JSON.parse(artifact(id).toString('utf8')); + const result = qualifyRevisedS01(parse(platform + '-template-oracle-native'), parse('p10-' + platform + '-classification'), parse(platform + '-template-measurements')); + demand(result.total === 36 && result.passed === 36, 'Approved revised S01 fidelity failed'); + revisedS01[platform] = { passed: result.passed, total: result.total }; + } + demand(decisions.get('Automated PDF Fidelity') === 'PASS' && decisions.get('PDF Spacing') === 'PARTIAL', 'Scoped PDF or human spacing decision differs'); const stages = documents.GATES.stages; demand(stages.map(item => item.sequence).join(',') === '5,6,7,8,9,10,11' && stages[0].manualApprovals.includes('phase10-development-entry') && stages[0].manualApprovals.includes('candidate-specific-cleanup-scope') && stages.every(item => item.permissibleConditionalDeferrals.length === 0), 'Source-derived stage/approval conditions changed'); const observedLocalCommit = execFileSync('rtk', ['proxy', 'git', 'rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' }).trim(); const observedWorkingTreeClean = !execFileSync('rtk', ['proxy', 'git', 'status', '--porcelain'], { cwd: root, encoding: 'utf8' }).trim(); const branch = execFileSync('rtk', ['proxy', 'git', 'branch', '--show-current'], { cwd: root, encoding: 'utf8' }).trim(); demand(branch === declared.sourceBranch, 'Wrong checkout branch'); - return { integrity: 'PASS', testedCommit: evidence.testedCommit, observedLocalCommit, observedWorkingTreeClean, reports: evidence.reports.length, artifacts: evidence.artifacts.length, phase10: 'BLOCKED', ...requireProtectedEntry() }; + return { integrity: 'PASS', testedCommit: evidence.testedCommit, observedLocalCommit, observedWorkingTreeClean, reports: evidence.reports.length, artifacts: evidence.artifacts.length, revisedS01, phase10: 'BLOCKED', ...requireProtectedEntry() }; } if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { diff --git a/docs/automation/handoffs/pre-phase10/verify.test.mjs b/docs/automation/handoffs/pre-phase10/verify.test.mjs index 2f84962..fc59d16 100644 --- a/docs/automation/handoffs/pre-phase10/verify.test.mjs +++ b/docs/automation/handoffs/pre-phase10/verify.test.mjs @@ -1,6 +1,50 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { measureNative, verifyBytes, assertPass, requireProtectedEntry } from './verify.mjs'; +import * as verifier from './verify.mjs'; + +function s01Fixture() { + const hash = 'a'.repeat(64); + return [{ total: 1, tolerances: { positionMm: 2, fontPt: .5, pageDimensionMm: .5, rgbComponent: 1 / 255 }, fixtures: [{ fixture: 'short-en-modern', pass: false, failures: ['S01: position delta x=0.00mm y=2.92mm'], comparisons: [{ token: 'S01', previewPage: 1, pdfPage: 1, deltaXmm: 0, deltaYmm: 2.92, fontDeltaPt: 0, colorDelta: 0 }] }] }, + { rows: [{ fixture: 'short-en-modern', pdfSha256: hash, domBaselinePx: 92, pdfBaselinePx: 92 }] }, + { fixtures: [{ fixture: 'short-en-modern', pdfSha256: hash, settings: { designVersion: 2 } }] }]; +} + +test('approved S01 changes only the vertical coordinate metric and retains historical results', () => { + const inputs = s01Fixture(); + assert.equal(verifier.qualifyRevisedS01(...inputs).passed, 1); + assert.equal(inputs[0].fixtures[0].pass, false); + assert.equal(inputs[0].fixtures[0].failures.length, 1); +}); +test('approved S01 still rejects horizontal and actual vertical shifts above2mm', () => { + for (const horizontal of [true, false]) { + const inputs = s01Fixture(); + if (horizontal) inputs[0].fixtures[0].comparisons[0].deltaXmm = 2.01; + else inputs[1].rows[0].pdfBaselinePx += 2.01 * 96 / 25.4; + assert.equal(verifier.qualifyRevisedS01(...inputs).passed, 0); + } +}); +test('approved S01 retains font color and page checks', () => { + for (const patch of [{ fontDeltaPt: .51 }, { colorDelta: .01 }, { pdfPage: 2 }]) { + const inputs = s01Fixture(); Object.assign(inputs[0].fixtures[0].comparisons[0], patch); + assert.equal(verifier.qualifyRevisedS01(...inputs).passed, 0); + } +}); +test('approved S01 retains every unrelated original failure', () => { + const inputs = s01Fixture(); inputs[0].fixtures[0].failures.push('page 1: hidden section leaked'); + assert.equal(verifier.qualifyRevisedS01(...inputs).passed, 0); +}); +test('approved S01 rejects missing duplicate foreign PDFs and version1 input', () => { + for (const mutate of [x => x[1].rows.pop(), x => x[1].rows.push(x[1].rows[0]), x => x[1].rows[0].pdfSha256 = 'b'.repeat(64), x => x[2].fixtures[0].settings.designVersion = 1]) { + const inputs = s01Fixture(); mutate(inputs); assert.throws(() => verifier.qualifyRevisedS01(...inputs)); + } +}); +test('approved S01 rejects nonfinite coordinates and recomputes claimed deltas', () => { + const inputs = s01Fixture(); inputs[1].rows[0].pdfBaselinePx = NaN; + assert.throws(() => verifier.qualifyRevisedS01(...inputs)); + inputs[1].rows[0].pdfBaselinePx = 112; inputs[1].rows[0].baselineDeltaMm = 0; + assert.equal(verifier.qualifyRevisedS01(...inputs).passed, 0); +}); test('native Jest membership and counters agree before PASS', () => { const result = measureNative('jest', JSON.stringify({ numTotalTests: 1, numPassedTests: 1, numFailedTests: 0, numPendingTests: 0, testResults: [{ name: 'fictional.test.ts', assertionResults: [{ fullName: 'preserves facts', status: 'passed' }] }] }));