diff --git a/docs/automation/handoffs/pre-phase10/package.test.mjs b/docs/automation/handoffs/pre-phase10/package.test.mjs index 7be90be..498113a 100644 --- a/docs/automation/handoffs/pre-phase10/package.test.mjs +++ b/docs/automation/handoffs/pre-phase10/package.test.mjs @@ -83,3 +83,9 @@ test('artifact paths cannot escape the imported package', () => shadow(base => { test('an imported schema cannot weaken the trusted schema', () => shadow(base => { fs.writeFileSync(path.join(base, 'HANDOFF_SCHEMA.json'), '{}'); })); +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('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 c1cbe0f..5baa9d4 100644 --- a/docs/automation/handoffs/pre-phase10/verify.mjs +++ b/docs/automation/handoffs/pre-phase10/verify.mjs @@ -162,6 +162,7 @@ export function validatePackage(base = directory) { 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(); 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 }); for (const [name, expected] of Object.entries(source)) { demand(!path.isAbsolute(name) && !name.split(/[\\/]/).includes('..'), 'Unsafe source path'); const file = path.resolve(root, name); @@ -171,7 +172,10 @@ export function validatePackage(base = directory) { 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(); 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); + for (const [name, expected] of Object.entries(verifierFiles)) { + verifyBytes(fs.readFileSync(path.join(root, name)), expected); + verifyBytes(execFileSync('rtk', ['proxy', 'git', 'show', `${evidence.verifierCommit}:${name}`], { cwd: root }), expected); + } execFileSync('rtk', ['proxy', 'git', 'merge-base', '--is-ancestor', evidence.verifierCommit, 'HEAD'], { cwd: root }); execFileSync('rtk', ['proxy', 'git', 'merge-base', '--is-ancestor', evidence.testedCommit, 'HEAD'], { cwd: root }); const delta = execFileSync('rtk', ['proxy', 'git', 'diff', '--name-only', evidence.testedCommit, 'HEAD'], { cwd: root, encoding: 'utf8' }).trim().split(/\r?\n/).filter(Boolean); @@ -181,6 +185,20 @@ export function validatePackage(base = directory) { const ids = new Set(documents.BLOCKERS.items.map(item => item.id)); demand(ids.size === documents.BLOCKERS.items.length, 'Duplicate blocker IDs'); demand(declared.unresolvedBlockerIds.every(id => ids.has(id)), 'Unresolved blocker is not registered'); + const open = documents.BLOCKERS.items.filter(item => item.status !== 'CLOSED').map(item => item.id).sort(); + demand(JSON.stringify([...declared.unresolvedBlockerIds].sort()) === JSON.stringify(open), 'Unresolved findings were omitted'); + demand(declared.permissibleDeferrals.every(id => documents.BLOCKERS.items.some(item => item.id === id && item.classification === 'CONDITIONAL_DEVELOPMENT' && item.deferralAuthority)), 'A mandatory or unsupported condition was relabelled as deferrable'); + const decisions = new Map(declared.verdicts.map(item => [item.criterion, item.status])); + demand(decisions.size === declared.verdicts.length, 'Duplicate acceptance criteria'); + for (const [criterion, status] of [['CV AI Accuracy', 'BLOCKED'], ['Human/Device Acceptance', 'NOT TESTED'], ['Hosted CI', 'BLOCKED'], ['Production', 'NO-GO']]) + demand(decisions.get(criterion) === status, 'Unqualified model/human/hosted/release acceptance was promoted'); + const probe = JSON.parse(artifact('model-probe-results').toString('utf8')); + 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 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();