From 3556fa25f1dc6ff5385e9e0d54fd66edd6680c53 Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 11 Sep 2026 15:54:22 -0500 Subject: [PATCH] Keep reassessment lineage scoped to its owning run --- .../native-status-arbiter-corpus.test.ts | 37 ++++++++++++++++--- .../status-decision-committer.ts | 22 ++++++++++- 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/server/src/__tests__/native-status-arbiter-corpus.test.ts b/server/src/__tests__/native-status-arbiter-corpus.test.ts index 9f623870bd..3bb8090436 100644 --- a/server/src/__tests__/native-status-arbiter-corpus.test.ts +++ b/server/src/__tests__/native-status-arbiter-corpus.test.ts @@ -2135,14 +2135,39 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => { ]); }); - it("records superseding assessment lineage when a board transition has no native decision predecessor", async () => { + it.each(["none", "same_run", "other_run"])("records run-scoped superseding assessment lineage with %s predecessor", async (predecessor) => { const fixture = corpus.fixtures.find((candidate) => candidate.mode === "native"); if (!fixture) throw new Error("native corpus fixture missing"); const seeded = await seedFixture(fixture); + let priorDecisionId: string | null = null; + if (predecessor !== "none") { + let priorRunId = seeded.runId; + let priorAssessmentId = seeded.assessmentId; + if (predecessor === "other_run") { + priorRunId = randomUUID(); + const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, seeded.runId)); + await db.insert(heartbeatRuns).values({ ...run, id: priorRunId }); + const [result] = await db.select().from(nativeRunResults).where(eq(nativeRunResults.id, seeded.resultId!)); + const resultId = randomUUID(); + await db.insert(nativeRunResults).values({ ...result, id: resultId, runId: priorRunId }); + const [assessment] = await db.select().from(workAssessments).where(eq(workAssessments.id, seeded.assessmentId)); + priorAssessmentId = randomUUID(); + await db.insert(workAssessments).values({ ...assessment, id: priorAssessmentId, runId: priorRunId, + resultId, inputDigest: `later-run:${priorRunId}` }); + } + const [prior] = await db.insert(statusDecisions).values({ + companyId, issueId: seeded.issueId, runId: priorRunId, assessmentId: priorAssessmentId, + decisionVersion: 1, policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, + fromStatus: "in_progress", toStatus: "blocked", reasonCode: "prior_authoritative_decision", + decisionJson: { statusAction: "blocked" }, decisionDigest: `prior:${seeded.issueId}`, + applicationState: "applied", appliedAt: new Date(), + }).returning(); + priorDecisionId = prior.id; + } await db.update(issues).set({ status: "blocked", statusVersion: 1, - lastStatusDecisionId: null, + lastStatusDecisionId: priorDecisionId, }).where(eq(issues.id, seeded.issueId)); const supersedingAssessmentId = randomUUID(); await db.insert(workAssessments).values({ @@ -2156,7 +2181,7 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => { triggerActorCompanyId: companyId, priorIssueStatus: "blocked", priorStatusVersion: 1, - priorDecisionId: null, + priorDecisionId, policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, assessmentJson: { reason: "board_transition_without_native_decision" }, inputDigest: `board-transition-assessment:${seeded.issueId}`, @@ -2171,7 +2196,7 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => { assessmentId: supersedingAssessmentId, priorStatus: "blocked", priorStatusVersion: 1, - priorDecisionId: null, + priorDecisionId, decision: { policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION, statusAction: "preserve", @@ -2182,7 +2207,9 @@ describe("P6-31 Section 18.13 executable status-authority corpus", () => { }, }); - expect(committed.decision.supersedesDecisionId).toBeNull(); + const [persistedDecision] = await db.select().from(statusDecisions) + .where(eq(statusDecisions.id, committed.decision.id)); + expect(persistedDecision.supersedesDecisionId).toBe(priorDecisionId); await expect(db.select({ supersedesAssessmentId: workAssessments.supersedesAssessmentId, }).from(workAssessments).where(eq(workAssessments.id, supersedingAssessmentId))).resolves.toEqual([ diff --git a/server/src/services/native-runtime/status-decision-committer.ts b/server/src/services/native-runtime/status-decision-committer.ts index 12dadca4f3..54445fcb48 100644 --- a/server/src/services/native-runtime/status-decision-committer.ts +++ b/server/src/services/native-runtime/status-decision-committer.ts @@ -747,13 +747,31 @@ async function materializeDecisionEffect(input: { } const prior = await input.tx.select({ assessmentId: statusDecisions.assessmentId, + runId: statusDecisions.runId, }).from(statusDecisions).where(and( eq(statusDecisions.id, input.issue.lastStatusDecisionId), eq(statusDecisions.companyId, input.companyId), + eq(statusDecisions.issueId, input.issue.id), )).limit(1).then((rows) => rows[0] ?? null); if (!prior) throw new Error("native_superseded_assessment_missing"); + // Decisions can supersede earlier issue decisions across runs, while + // evidence assessments are deliberately owned by one run. A historical + // run's reassessment already points at its own preceding assessment. + const existing = prior.runId !== input.runId + ? await input.tx.select({ supersedesAssessmentId: workAssessments.supersedesAssessmentId }) + .from(workAssessments).where(and( + eq(workAssessments.id, lineage.currentAssessmentId), + eq(workAssessments.companyId, input.companyId), + eq(workAssessments.issueId, input.issue.id), + eq(workAssessments.runId, input.runId), + )).limit(1).then((rows) => rows[0] ?? null) + : null; + const supersedesAssessmentId = prior.runId === input.runId + ? prior.assessmentId + : existing?.supersedesAssessmentId; + if (!supersedesAssessmentId) throw new Error("native_superseding_assessment_not_linked"); const [assessment] = await input.tx.update(workAssessments).set({ - supersedesAssessmentId: prior.assessmentId, + supersedesAssessmentId, }).where(and( eq(workAssessments.id, lineage.currentAssessmentId), eq(workAssessments.companyId, input.companyId), @@ -774,7 +792,7 @@ async function materializeDecisionEffect(input: { payload: { supersedesDecisionId: input.issue.lastStatusDecisionId, assessmentId: assessment.id, - supersedesAssessmentId: prior.assessmentId, + supersedesAssessmentId, }, }; }