diff --git a/scripts/feed-production-assessment-checkpoint.mjs b/scripts/feed-production-assessment-checkpoint.mjs index ee71315d..17555935 100644 --- a/scripts/feed-production-assessment-checkpoint.mjs +++ b/scripts/feed-production-assessment-checkpoint.mjs @@ -6,7 +6,7 @@ import { createHash } from 'node:crypto'; import { readFileSync, writeFileSync } from 'node:fs'; import { isAbsolute, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { ACCOUNT, CORE, FEED, SQL, REPO, sourceIdentity, projectionIdentity } from './feed-production-assessment.mjs'; +import { ACCOUNT, CORE, FEED, SQL, sourceIdentity, projectionIdentity } from './feed-production-assessment.mjs'; import { validateCarryoverReceipt, validateContext } from './feed-production-assessment-recovery.mjs'; const check = (ok, message) => { if (!ok) throw new Error(message); }; @@ -41,15 +41,15 @@ export async function verifyCheckpoint(releaseSha, manifest, carryover, { query, check(Array.isArray(rows) && rows.length === 1, `checkpoint_${kind}_not_unique`); return rows[0]; }; - const run = await one('runs', [REPO, receipt.sourceSha]); + const run = await one('runs', [receipt.repository, receipt.sourceSha]); check(run.id === receipt.analysisId && run.status === 'completed' && run.idempotency_key === receipt.idempotencyKey, 'checkpoint_live_run_identity_mismatch'); - const row = await one('source', [receipt.analysisId, REPO]); + const row = await one('source', [receipt.analysisId, receipt.repository]); check(Object.entries(run).every(([key, value]) => row[key] === value), 'checkpoint_live_run_changed'); - const source = sourceIdentity(row, receipt.sourceSha, receipt.analysisId, receipt.web.agentId); + const source = sourceIdentity(row, receipt.sourceSha, receipt.analysisId, receipt.web.agentId, receipt.repository); check(Object.entries(manifest.source).every(([key, value]) => Object.hasOwn(source, key) && source[key] === value), 'checkpoint_live_source_changed'); - const projection = stage === 'projection' ? projectionIdentity(await one('projection', [REPO]), source, receipt.sourceSha) : null; + const projection = stage === 'projection' ? projectionIdentity(await one('projection', [receipt.repository]), source, receipt.sourceSha, receipt.repository) : null; check(stage === 'source' || projection, 'checkpoint_projection_not_ready'); return { format: 'ghfind-production-assessment-checkpoint-result-v1', status: stage === 'source' ? 'source_verified' : 'passed', releaseSha, sourceSha: receipt.sourceSha, intentId: receipt.intentId, receiptSHA256: manifest.receiptSHA256, diff --git a/scripts/feed-production-assessment-checkpoint.test.mjs b/scripts/feed-production-assessment-checkpoint.test.mjs index f6ab4ab9..93bb4ad5 100644 --- a/scripts/feed-production-assessment-checkpoint.test.mjs +++ b/scripts/feed-production-assessment-checkpoint.test.mjs @@ -88,6 +88,18 @@ test('completed checkpoint is freshly verified without mutating receipt or provi assert.equal(result.status,'passed'); assert.equal(result.providerRequests,0); assert.equal(result.observedAt,123); assert.deepEqual(calls.map(([k])=>k),['runs','source','projection']); assert.equal(JSON.stringify(m),before); }); +test('historical checkpoint retains its original repository identity', async()=>{ + const legacy='hikariming/ghfind', m=checkpoint(), calls=[]; + m.receipt.repository=legacy; m.receiptSHA256=checkpointHash(m.receipt); + const historical={ + runs:[run({repo_key:legacy,status:'completed'})], + source:[source({repo_key:legacy,requested_repo_key:legacy,aggregate_key:legacy})], + projection:[projection({job_repo:legacy})], + }; + const result=await verifyCheckpoint('b'.repeat(40),m,carryover,{query:async(k,p)=>{calls.push([k,p]);return historical[k];}}); + assert.equal(result.status,'passed'); + assert.deepEqual(calls,[['runs',[legacy,SHA]],['source',[ID,legacy]],['projection',[legacy]]]); +}); test('tampering, mismatched carryover, and nonterminal checkpoints fail before reads',async()=>{ for(const mutate of [m=>m.receipt.polls=1,m=>m.receipt.analysisId='other',m=>{m.receipt.phase='waiting';m.receiptSHA256=checkpointHash(m.receipt);},m=>delete m.source.providerThreadId]){ const m=checkpoint(); mutate(m); let reads=0; diff --git a/scripts/feed-production-assessment.mjs b/scripts/feed-production-assessment.mjs index e2aea079..3c0453f6 100644 --- a/scripts/feed-production-assessment.mjs +++ b/scripts/feed-production-assessment.mjs @@ -381,12 +381,12 @@ function candidate(row) { sourceHash: row.source_hash, }; } -function runIdentity(row, sha, wantedId) { +function runIdentity(row, sha, wantedId, repository = REPO) { check( row && id(row.id) && (!wantedId || row.id === wantedId) && - row.repo_key === REPO && + row.repo_key === repository && row.requested_ref === sha && Object.entries(versions).every(([k, v]) => k === "skill_version" ? acceptedSkillVersions.has(row[k]) : row[k] === v, @@ -608,8 +608,8 @@ export async function startAssessment(sha, path, options = {}) { c.close(); } } -export function sourceIdentity(row, sha, analysisId, agentId) { - const run = runIdentity(row, sha, analysisId); +export function sourceIdentity(row, sha, analysisId, agentId, repository = REPO) { + const run = runIdentity(row, sha, analysisId, repository); check( row.status === "completed" && row.create_attempts >= 1 && @@ -630,9 +630,9 @@ export function sourceIdentity(row, sha, analysisId, agentId) { ].every(hash) && row.source_hash === row.analysis_sha256 && id(row.receipt_id) && - row.requested_repo_key === REPO && + row.requested_repo_key === repository && row.source_kind === "app_submission" && - row.aggregate_key === REPO && + row.aggregate_key === repository && row.contract_version === 1 && Number.isSafeInteger(row.sequence) && row.sequence > 0 && @@ -680,7 +680,7 @@ export function executionIdentity(row, source) { "projection_terminal_failure", ); } -export function projectionIdentity(row, source, sha) { +export function projectionIdentity(row, source, sha, repository = REPO) { if (!row) return null; if ( Number.isSafeInteger(row.source_version) && @@ -701,7 +701,7 @@ export function projectionIdentity(row, source, sha) { row.event_source_version === row.source_version && row.event_receipt_id === row.receipt_id && row.event_contract_version === 1 && - row.job_repo === REPO && + row.job_repo === repository && typeof row.resolved_commit_sha === "string" && /^[a-f0-9]{40}$/.test(row.resolved_commit_sha) && ["app_submission", "agent_submission", "verified_backfill"].includes( @@ -736,7 +736,7 @@ export function projectionIdentity(row, source, sha) { row.event_receipt_id === source.receiptId && row.event_source_hash === source.sourceHash && row.event_contract_version === 1 && - row.job_repo === REPO && + row.job_repo === repository && row.resolved_commit_sha === sha && row.source_kind === "app_submission" && hash(row.source_hash) &&