diff --git a/packages/paperclip-runner/spec/fixtures/status-authority-phase5.json b/packages/paperclip-runner/spec/fixtures/status-authority-phase5.json index 9ff3193308d..aa1eaeb7c6a 100644 --- a/packages/paperclip-runner/spec/fixtures/status-authority-phase5.json +++ b/packages/paperclip-runner/spec/fixtures/status-authority-phase5.json @@ -1,6 +1,6 @@ { "schema": "paperclip.status-authority-conformance.v1", - "corpusRevision": 1, + "corpusRevision": 2, "policyVersion": "status-authority-v1", "fixtures": [ { @@ -143,7 +143,7 @@ "mode": "native", "covers": { "decisionRows": ["SD-11"], "terminalRows": [], "attentionRows": [], "livenessRows": [], "reconciliationRows": [], "compatibilityRows": [], "migrationRows": [] }, "tags": ["required_review"], - "given": { "priorIssueStatus": "in_progress", "turnTerminalState": "completed", "runTerminalState": "succeeded", "reportedWorkDisposition": "needs_review", "nativeFinalization": "present", "completionState": "intentional_human_judgment", "trigger": "attention_candidate" }, + "given": { "priorIssueStatus": "in_progress", "turnTerminalState": "completed", "runTerminalState": "succeeded", "reportedWorkDisposition": "needs_review", "nativeFinalization": "present", "completionState": "intentional_human_judgment", "reviewGate": "completion", "trigger": "attention_candidate" }, "expected": { "runStatus": "succeeded", "statusAction": "in_review", "reasonCode": "attention_requires_human_authority", "requiredEffects": ["create_interaction", "notify_owner"], "forbiddenEffects": ["bind_blocker"], "livePathKind": "interaction", "preserveClaim": true, "nativeRecords": true, "decisionCount": 1, "maxWakeCount": 1, "maxNotificationCount": 1 }, "replay": { "attempts": 2, "sameDecisionDigest": true, "maxSemanticDecisions": 1, "maxDomainEffectsPerKey": 1 } }, @@ -153,8 +153,8 @@ "covers": { "decisionRows": ["SD-12"], "terminalRows": [], "attentionRows": ["ATT-02", "ATT-03"], "livenessRows": [], "reconciliationRows": ["REC-02"], "compatibilityRows": [], "migrationRows": [] }, "tags": ["repeated_question", "excessive_human_request", "deterministic_replay"], "given": { "priorIssueStatus": "in_progress", "turnTerminalState": "completed", "runTerminalState": "succeeded", "reportedWorkDisposition": "yielded", "nativeFinalization": "present", "completionState": "equivalent_attention_family", "trigger": "attention_candidate" }, - "expected": { "runStatus": "succeeded", "statusAction": "preserve", "reasonCode": "attention_duplicate_suppressed", "requiredEffects": ["link_canonical_request"], "forbiddenEffects": ["create_interaction", "notify_owner", "enqueue_continuation"], "livePathKind": null, "preserveClaim": true, "nativeRecords": true, "decisionCount": 1, "maxWakeCount": 0, "maxNotificationCount": 0 }, - "replay": { "attempts": 6, "sameDecisionDigest": true, "maxSemanticDecisions": 1, "maxDomainEffectsPerKey": 1 } + "expected": { "runStatus": "succeeded", "statusAction": "preserve", "reasonCode": "attention_duplicate_suppressed", "requiredEffects": ["link_canonical_request"], "forbiddenEffects": ["create_interaction", "notify_owner", "enqueue_continuation"], "livePathKind": null, "preserveClaim": true, "nativeRecords": true, "decisionCount": 0, "maxWakeCount": 0, "maxNotificationCount": 0 }, + "replay": { "attempts": 6, "sameDecisionDigest": true, "maxSemanticDecisions": 0, "maxDomainEffectsPerKey": 1 } }, { "id": "attention-budget-exhausted", diff --git a/packages/paperclip-runner/spec/paperclip-native-runner-spike-spec.md b/packages/paperclip-runner/spec/paperclip-native-runner-spike-spec.md index 6dd21d4c023..0c310f2ab52 100644 --- a/packages/paperclip-runner/spec/paperclip-native-runner-spike-spec.md +++ b/packages/paperclip-runner/spec/paperclip-native-runner-spike-spec.md @@ -4915,6 +4915,7 @@ interface StatusAuthorityConformanceFixtureV1 { reportedWorkDisposition: ReportedWorkDisposition | null; nativeFinalization: "present" | "missing" | "invalid" | "not_applicable"; completionState: string; + reviewGate?: "completion" | "mid_work"; trigger: string; fault?: string; }; @@ -4943,6 +4944,14 @@ interface StatusAuthorityConformanceFixtureV1 { } ``` +`reviewGate` disambiguates an intentional human-judgment interaction that gates +completion from one raised while productive work remains. A completion gate may +resolve to `in_review` under SD-11; a mid-work interaction still requires the +response-wake or preserve branches from that row. `decisionCount` counts status +decisions newly persisted for the fixture stimulus. Linking an equivalent +request to a pre-existing canonical family does not count that family's earlier +decision. + Fixture assertions are database assertions, not only response assertions. Every case records the terminal turn/run facts, original result/evidence retention, issue `status` and `status_version`, contract/policy version, assessment and diff --git a/scripts/check-runner-phase5-spec.mjs b/scripts/check-runner-phase5-spec.mjs index 80f24e27abc..4c6bb08bf03 100644 --- a/scripts/check-runner-phase5-spec.mjs +++ b/scripts/check-runner-phase5-spec.mjs @@ -121,6 +121,9 @@ check("fixture IDs and required fields are valid", () => { if (!fixture.given || requiredGiven.some((key) => !(key in fixture.given))) { throw new Error(`${fixture.id}: incomplete given facts`); } + if (fixture.given.reviewGate !== undefined && !["completion", "mid_work"].includes(fixture.given.reviewGate)) { + throw new Error(`${fixture.id}: invalid reviewGate`); + } if (!fixture.expected || requiredExpected.some((key) => !(key in fixture.expected))) { throw new Error(`${fixture.id}: incomplete expected facts`); } @@ -208,16 +211,29 @@ check("non-terminal status fixtures carry an atomic liveness path", () => { return "in_review, blocked, and in_progress paths validated"; }); -check("duplicate attention cannot create an unbounded loop", () => { +check("duplicate attention creates no status decision or unbounded loop", () => { const duplicates = corpus.fixtures.filter((fixture) => fixture.expected.reasonCode === "attention_duplicate_suppressed"); if (duplicates.length === 0) throw new Error("no duplicate-attention fixture"); const unsafe = duplicates.filter((fixture) => + fixture.expected.decisionCount !== 0 || fixture.expected.maxWakeCount !== 0 || fixture.expected.maxNotificationCount !== 0 || - fixture.replay.maxSemanticDecisions > 1 || + fixture.replay.maxSemanticDecisions !== 0 || fixture.replay.maxDomainEffectsPerKey > 1); - if (unsafe.length) throw new Error(`unbounded duplicate fixture(s): ${unsafe.map((fixture) => fixture.id).join(", ")}`); - return `${duplicates.length} duplicate fixture(s) bounded at zero wakes/notifications`; + if (unsafe.length) throw new Error(`unsafe duplicate fixture(s): ${unsafe.map((fixture) => fixture.id).join(", ")}`); + return `${duplicates.length} duplicate fixture(s) bounded at zero decisions/wakes/notifications`; +}); + +check("completion-gating human judgment is explicit", () => { + const fixture = corpus.fixtures.find((candidate) => candidate.id === "human-authority-required"); + if (!fixture) throw new Error("missing human-authority-required fixture"); + if (!fixture.covers.decisionRows.includes("SD-11") || + fixture.given.completionState !== "intentional_human_judgment" || + fixture.given.reviewGate !== "completion" || + fixture.expected.statusAction !== "in_review") { + throw new Error("human-authority-required does not pin the SD-11 completion-review branch"); + } + return "human-authority-required pins reviewGate=completion"; }); check("replay effects are at-most-once per semantic key", () => {