@principles/core 1.270.5 → 1.272.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/runtime-v2/__tests__/admission-gate.test.js +23 -6
- package/dist/runtime-v2/__tests__/admission-gate.test.js.map +1 -1
- package/dist/runtime-v2/__tests__/adversarial-loop.test.js +22 -0
- package/dist/runtime-v2/__tests__/adversarial-loop.test.js.map +1 -1
- package/dist/runtime-v2/__tests__/context-payload-validation.test.js +1 -1
- package/dist/runtime-v2/__tests__/context-payload-validation.test.js.map +1 -1
- package/dist/runtime-v2/__tests__/evaluator-repair-loop.test.js +20 -0
- package/dist/runtime-v2/__tests__/evaluator-repair-loop.test.js.map +1 -1
- package/dist/runtime-v2/__tests__/evidence-guards.test.js +2 -2
- package/dist/runtime-v2/__tests__/evidence-guards.test.js.map +1 -1
- package/dist/runtime-v2/__tests__/golden-path-diagnostician-e2e.test.js +1 -1
- package/dist/runtime-v2/__tests__/golden-path-diagnostician-e2e.test.js.map +1 -1
- package/dist/runtime-v2/__tests__/governance-experience.test.js +19 -0
- package/dist/runtime-v2/__tests__/governance-experience.test.js.map +1 -1
- package/dist/runtime-v2/__tests__/pain-evidence-contract.test.js +3 -3
- package/dist/runtime-v2/__tests__/pain-evidence-contract.test.js.map +1 -1
- package/dist/runtime-v2/__tests__/pain-signal-bridge-result-shaping.test.js +1 -1
- package/dist/runtime-v2/__tests__/pain-signal-bridge-result-shaping.test.js.map +1 -1
- package/dist/runtime-v2/__tests__/rollout-reviewer-verdict-paths.test.js +26 -5
- package/dist/runtime-v2/__tests__/rollout-reviewer-verdict-paths.test.js.map +1 -1
- package/dist/runtime-v2/__tests__/rollout-rule-candidate-contract.test.d.ts +2 -0
- package/dist/runtime-v2/__tests__/rollout-rule-candidate-contract.test.d.ts.map +1 -0
- package/dist/runtime-v2/__tests__/rollout-rule-candidate-contract.test.js +243 -0
- package/dist/runtime-v2/__tests__/rollout-rule-candidate-contract.test.js.map +1 -0
- package/dist/runtime-v2/admission-gate.d.ts +8 -1
- package/dist/runtime-v2/admission-gate.d.ts.map +1 -1
- package/dist/runtime-v2/admission-gate.js +14 -0
- package/dist/runtime-v2/admission-gate.js.map +1 -1
- package/dist/runtime-v2/context-payload.d.ts +9 -3
- package/dist/runtime-v2/context-payload.d.ts.map +1 -1
- package/dist/runtime-v2/context-payload.js +3 -1
- package/dist/runtime-v2/context-payload.js.map +1 -1
- package/dist/runtime-v2/evidence-triage/__tests__/observation-resolver.test.js +2 -2
- package/dist/runtime-v2/evidence-triage/__tests__/observation-resolver.test.js.map +1 -1
- package/dist/runtime-v2/evidence-triage/observation-resolver.d.ts +1 -1
- package/dist/runtime-v2/evidence-triage/observation-resolver.d.ts.map +1 -1
- package/dist/runtime-v2/evidence-triage/observation-resolver.js +2 -2
- package/dist/runtime-v2/evidence-triage/observation-resolver.js.map +1 -1
- package/dist/runtime-v2/evidence-triage/types.d.ts +1 -1
- package/dist/runtime-v2/evidence-triage/types.d.ts.map +1 -1
- package/dist/runtime-v2/evolution/evolution-types.d.ts +8 -4
- package/dist/runtime-v2/evolution/evolution-types.d.ts.map +1 -1
- package/dist/runtime-v2/evolution/evolution-types.js +3 -1
- package/dist/runtime-v2/evolution/evolution-types.js.map +1 -1
- package/dist/runtime-v2/governance-experience-contract.d.ts +16 -16
- package/dist/runtime-v2/governance-experience-contract.d.ts.map +1 -1
- package/dist/runtime-v2/governance-experience-contract.js +4 -1
- package/dist/runtime-v2/governance-experience-contract.js.map +1 -1
- package/dist/runtime-v2/governance-experience.d.ts.map +1 -1
- package/dist/runtime-v2/governance-experience.js +30 -8
- package/dist/runtime-v2/governance-experience.js.map +1 -1
- package/dist/runtime-v2/index.d.ts +11 -1
- package/dist/runtime-v2/index.d.ts.map +1 -1
- package/dist/runtime-v2/index.js +7 -0
- package/dist/runtime-v2/index.js.map +1 -1
- package/dist/runtime-v2/internalization/__tests__/artificer-code-bearing.test.d.ts +2 -0
- package/dist/runtime-v2/internalization/__tests__/artificer-code-bearing.test.d.ts.map +1 -0
- package/dist/runtime-v2/internalization/__tests__/artificer-code-bearing.test.js +114 -0
- package/dist/runtime-v2/internalization/__tests__/artificer-code-bearing.test.js.map +1 -0
- package/dist/runtime-v2/internalization/__tests__/evaluator-gate-authority.test.d.ts +2 -0
- package/dist/runtime-v2/internalization/__tests__/evaluator-gate-authority.test.d.ts.map +1 -0
- package/dist/runtime-v2/internalization/__tests__/evaluator-gate-authority.test.js +256 -0
- package/dist/runtime-v2/internalization/__tests__/evaluator-gate-authority.test.js.map +1 -0
- package/dist/runtime-v2/internalization/__tests__/evaluator-prompt-builder.test.js +1 -1
- package/dist/runtime-v2/internalization/__tests__/internalization-transition-decision.test.js +15 -14
- package/dist/runtime-v2/internalization/__tests__/internalization-transition-decision.test.js.map +1 -1
- package/dist/runtime-v2/internalization/__tests__/owner-decision-architecture.test.d.ts +2 -0
- package/dist/runtime-v2/internalization/__tests__/owner-decision-architecture.test.d.ts.map +1 -0
- package/dist/runtime-v2/internalization/__tests__/owner-decision-architecture.test.js +56 -0
- package/dist/runtime-v2/internalization/__tests__/owner-decision-architecture.test.js.map +1 -0
- package/dist/runtime-v2/internalization/__tests__/owner-decision-review.test.d.ts +2 -0
- package/dist/runtime-v2/internalization/__tests__/owner-decision-review.test.d.ts.map +1 -0
- package/dist/runtime-v2/internalization/__tests__/owner-decision-review.test.js +229 -0
- package/dist/runtime-v2/internalization/__tests__/owner-decision-review.test.js.map +1 -0
- package/dist/runtime-v2/internalization/__tests__/owner-decision.test.d.ts +2 -0
- package/dist/runtime-v2/internalization/__tests__/owner-decision.test.d.ts.map +1 -0
- package/dist/runtime-v2/internalization/__tests__/owner-decision.test.js +1054 -0
- package/dist/runtime-v2/internalization/__tests__/owner-decision.test.js.map +1 -0
- package/dist/runtime-v2/internalization/__tests__/owner-override-resume.test.d.ts +2 -0
- package/dist/runtime-v2/internalization/__tests__/owner-override-resume.test.d.ts.map +1 -0
- package/dist/runtime-v2/internalization/__tests__/owner-override-resume.test.js +328 -0
- package/dist/runtime-v2/internalization/__tests__/owner-override-resume.test.js.map +1 -0
- package/dist/runtime-v2/internalization/__tests__/rule-activation-contract.test.d.ts +2 -0
- package/dist/runtime-v2/internalization/__tests__/rule-activation-contract.test.d.ts.map +1 -0
- package/dist/runtime-v2/internalization/__tests__/rule-activation-contract.test.js +187 -0
- package/dist/runtime-v2/internalization/__tests__/rule-activation-contract.test.js.map +1 -0
- package/dist/runtime-v2/internalization/artificer-code-bearing.d.ts +39 -0
- package/dist/runtime-v2/internalization/artificer-code-bearing.d.ts.map +1 -0
- package/dist/runtime-v2/internalization/artificer-code-bearing.js +33 -0
- package/dist/runtime-v2/internalization/artificer-code-bearing.js.map +1 -0
- package/dist/runtime-v2/internalization/evaluator-output.d.ts +63 -2
- package/dist/runtime-v2/internalization/evaluator-output.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/evaluator-output.js +139 -2
- package/dist/runtime-v2/internalization/evaluator-output.js.map +1 -1
- package/dist/runtime-v2/internalization/evaluator-prompt-builder.d.ts +54 -2
- package/dist/runtime-v2/internalization/evaluator-prompt-builder.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/evaluator-prompt-builder.js +49 -2
- package/dist/runtime-v2/internalization/evaluator-prompt-builder.js.map +1 -1
- package/dist/runtime-v2/internalization/evaluator-runner.d.ts +45 -8
- package/dist/runtime-v2/internalization/evaluator-runner.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/evaluator-runner.js +465 -43
- package/dist/runtime-v2/internalization/evaluator-runner.js.map +1 -1
- package/dist/runtime-v2/internalization/index.d.ts +2 -0
- package/dist/runtime-v2/internalization/index.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/index.js +1 -0
- package/dist/runtime-v2/internalization/index.js.map +1 -1
- package/dist/runtime-v2/internalization/internalization-orchestrator.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/internalization-orchestrator.js +10 -54
- package/dist/runtime-v2/internalization/internalization-orchestrator.js.map +1 -1
- package/dist/runtime-v2/internalization/internalization-transition-decision.d.ts +7 -1
- package/dist/runtime-v2/internalization/internalization-transition-decision.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/internalization-transition-decision.js +9 -2
- package/dist/runtime-v2/internalization/internalization-transition-decision.js.map +1 -1
- package/dist/runtime-v2/internalization/owner-decision-review.d.ts +98 -0
- package/dist/runtime-v2/internalization/owner-decision-review.d.ts.map +1 -0
- package/dist/runtime-v2/internalization/owner-decision-review.js +287 -0
- package/dist/runtime-v2/internalization/owner-decision-review.js.map +1 -0
- package/dist/runtime-v2/internalization/owner-resolution-service.d.ts +100 -0
- package/dist/runtime-v2/internalization/owner-resolution-service.d.ts.map +1 -0
- package/dist/runtime-v2/internalization/owner-resolution-service.js +385 -0
- package/dist/runtime-v2/internalization/owner-resolution-service.js.map +1 -0
- package/dist/runtime-v2/internalization/owner-retry.d.ts +12 -0
- package/dist/runtime-v2/internalization/owner-retry.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/owner-retry.js +23 -0
- package/dist/runtime-v2/internalization/owner-retry.js.map +1 -1
- package/dist/runtime-v2/internalization/owner-review.d.ts +199 -0
- package/dist/runtime-v2/internalization/owner-review.d.ts.map +1 -0
- package/dist/runtime-v2/internalization/owner-review.js +470 -0
- package/dist/runtime-v2/internalization/owner-review.js.map +1 -0
- package/dist/runtime-v2/internalization/peer-runner-contracts.d.ts +5 -1
- package/dist/runtime-v2/internalization/peer-runner-contracts.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/peer-runner-contracts.js.map +1 -1
- package/dist/runtime-v2/internalization/pitask-metadata.d.ts +84 -0
- package/dist/runtime-v2/internalization/pitask-metadata.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/pitask-metadata.js +166 -0
- package/dist/runtime-v2/internalization/pitask-metadata.js.map +1 -1
- package/dist/runtime-v2/internalization/revision-reopen.d.ts +56 -0
- package/dist/runtime-v2/internalization/revision-reopen.d.ts.map +1 -0
- package/dist/runtime-v2/internalization/revision-reopen.js +144 -0
- package/dist/runtime-v2/internalization/revision-reopen.js.map +1 -0
- package/dist/runtime-v2/internalization/rollout-reviewer-runner.d.ts +25 -0
- package/dist/runtime-v2/internalization/rollout-reviewer-runner.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/rollout-reviewer-runner.js +202 -23
- package/dist/runtime-v2/internalization/rollout-reviewer-runner.js.map +1 -1
- package/dist/runtime-v2/internalization/rule-activation-contract.d.ts +35 -0
- package/dist/runtime-v2/internalization/rule-activation-contract.d.ts.map +1 -0
- package/dist/runtime-v2/internalization/rule-activation-contract.js +36 -0
- package/dist/runtime-v2/internalization/rule-activation-contract.js.map +1 -0
- package/dist/runtime-v2/pain-signal-bridge.d.ts +2 -0
- package/dist/runtime-v2/pain-signal-bridge.d.ts.map +1 -1
- package/dist/runtime-v2/pain-signal-bridge.js +4 -3
- package/dist/runtime-v2/pain-signal-bridge.js.map +1 -1
- package/dist/runtime-v2/pain-to-principle-service.d.ts +1 -0
- package/dist/runtime-v2/pain-to-principle-service.d.ts.map +1 -1
- package/dist/runtime-v2/pain-to-principle-service.js +1 -0
- package/dist/runtime-v2/pain-to-principle-service.js.map +1 -1
- package/dist/runtime-v2/runner/__tests__/pain-signal-bridge-admission.test.js +6 -6
- package/dist/runtime-v2/runner/__tests__/pain-signal-bridge-admission.test.js.map +1 -1
- package/dist/runtime-v2/store/context/sqlite-context-assembler.d.ts.map +1 -1
- package/dist/runtime-v2/store/context/sqlite-context-assembler.js +25 -5
- package/dist/runtime-v2/store/context/sqlite-context-assembler.js.map +1 -1
- package/dist/runtime-v2/store/context/sqlite-context-assembler.test.js +39 -9
- package/dist/runtime-v2/store/context/sqlite-context-assembler.test.js.map +1 -1
- package/dist/runtime-v2/store/runtime-state-manager.d.ts +12 -0
- package/dist/runtime-v2/store/runtime-state-manager.d.ts.map +1 -1
- package/dist/runtime-v2/store/runtime-state-manager.js +17 -0
- package/dist/runtime-v2/store/runtime-state-manager.js.map +1 -1
- package/dist/runtime-v2/store/task/__tests__/sqlite-task-store-cas.test.d.ts +2 -0
- package/dist/runtime-v2/store/task/__tests__/sqlite-task-store-cas.test.d.ts.map +1 -0
- package/dist/runtime-v2/store/task/__tests__/sqlite-task-store-cas.test.js +114 -0
- package/dist/runtime-v2/store/task/__tests__/sqlite-task-store-cas.test.js.map +1 -0
- package/dist/runtime-v2/store/task/memory-task-store.d.ts +1 -0
- package/dist/runtime-v2/store/task/memory-task-store.d.ts.map +1 -1
- package/dist/runtime-v2/store/task/memory-task-store.js +8 -0
- package/dist/runtime-v2/store/task/memory-task-store.js.map +1 -1
- package/dist/runtime-v2/store/task/sqlite-task-store.d.ts +20 -0
- package/dist/runtime-v2/store/task/sqlite-task-store.d.ts.map +1 -1
- package/dist/runtime-v2/store/task/sqlite-task-store.js +81 -0
- package/dist/runtime-v2/store/task/sqlite-task-store.js.map +1 -1
- package/dist/runtime-v2/store/task/task-store.d.ts +9 -0
- package/dist/runtime-v2/store/task/task-store.d.ts.map +1 -1
- package/dist/runtime-v2/store/workspace-isolation.test.js +1 -0
- package/dist/runtime-v2/store/workspace-isolation.test.js.map +1 -1
- package/dist/runtime-v2/task-status.d.ts +2 -1
- package/dist/runtime-v2/task-status.d.ts.map +1 -1
- package/dist/runtime-v2/task-status.js +5 -1
- package/dist/runtime-v2/task-status.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1054 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRI-629 / PRI-630 — Unified Owner Decision 核心契约测试。
|
|
3
|
+
*
|
|
4
|
+
* 覆盖 SPEC §33 验收矩阵的纯逻辑/服务层部分:
|
|
5
|
+
* A. metadata 序列化/hydration (humanReviewContext + ownerResolutions)
|
|
6
|
+
* B. human review 分类 (decision-capable vs recovery;legacy 推断;fail closed)
|
|
7
|
+
* C. capability 派生 (INV-01: allowedActions.length>0 ⇔ eligible)
|
|
8
|
+
* D. reviewKey stale 防护
|
|
9
|
+
* E. effective decision resolver (INV-03 机器判定保留)
|
|
10
|
+
* F. resolution service: 幂等 / 冲突 / stale / revise_once epoch 语义
|
|
11
|
+
* G. Recover guard (decision-capable 拒绝)
|
|
12
|
+
* H. epoch-aware repair causeId (P0)
|
|
13
|
+
* I. transition 仲裁使用 effective decision
|
|
14
|
+
* K. evaluator 收敛 schema 不变量 (SPEC §18.4)
|
|
15
|
+
*/
|
|
16
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion -- legacy fixture assertions predate staged-file linting. */
|
|
17
|
+
import { describe, it, expect } from 'vitest';
|
|
18
|
+
import { serializePITaskMetadata, parsePITaskMetadata, hydratePITaskRecord, createPITaskDiagnosticJson, } from '../pitask-metadata.js';
|
|
19
|
+
import { HUMAN_REVIEW_REASON, LEGACY_EVALUATOR_BUDGET_EXHAUSTED, buildOwnerReviewKey, collectOwnerDecisionFacts, deriveOwnerDecisionCapability, resolveEffectiveRunnerDecision, effectiveDecisionFor, planOwnerVerdictOverrideResume, detectHardGateFailureFromArtifact, } from '../owner-review.js';
|
|
20
|
+
import { applyOwnerResolution, reviewStoreFromStateManager, sanitizeOwnerInstruction, } from '../owner-resolution-service.js';
|
|
21
|
+
import { buildOwnerDecisionReview } from '../owner-decision-review.js';
|
|
22
|
+
import { buildRepairRevisionCauseId, reopenTaskForRevision, resolveRolloutRevisionTarget } from '../revision-reopen.js';
|
|
23
|
+
import { decideInternalizationTransition, transitionInputFromTask } from '../internalization-transition-decision.js';
|
|
24
|
+
import { ownerRetryNeedsHumanReviewTask } from '../owner-retry.js';
|
|
25
|
+
import { DefaultEvaluatorValidator } from '../evaluator-output.js';
|
|
26
|
+
import { deriveRequirementLedger } from '../evaluator-prompt-builder.js';
|
|
27
|
+
import { MemoryPIArtifactStore } from '../pi-artifact-store.js';
|
|
28
|
+
import { MemoryTaskStore } from '../../store/task/memory-task-store.js';
|
|
29
|
+
// ── fixtures ─────────────────────────────────────────────────────────────────
|
|
30
|
+
const EVAL_ID = 'evaluator-001';
|
|
31
|
+
const ARTIFER_ID = 'artificer-repair-evaluator-001-r2';
|
|
32
|
+
const SCRIBE_ID = 'scribe-001';
|
|
33
|
+
const RUN_ID = 'run-evaluator-001';
|
|
34
|
+
const ARTIFACT_ID = `pi-art-${EVAL_ID}-${RUN_ID}`;
|
|
35
|
+
function baseMeta(overrides = {}) {
|
|
36
|
+
return {
|
|
37
|
+
dependencyTaskIds: [ARTIFER_ID],
|
|
38
|
+
channel: 'prompt',
|
|
39
|
+
timeoutMs: 300_000,
|
|
40
|
+
inputArtifactRefs: [],
|
|
41
|
+
outputArtifactRefs: [],
|
|
42
|
+
...overrides,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function budgetExhaustedRepairPayload() {
|
|
46
|
+
return {
|
|
47
|
+
requiredChanges: ['fix timeout handling'],
|
|
48
|
+
concerns: ['c1'],
|
|
49
|
+
previousScore: 0.7,
|
|
50
|
+
repairIteration: 2,
|
|
51
|
+
sourceArtificerArtifactId: 'pi-art-artificer-old',
|
|
52
|
+
sourceEvaluatorTaskId: 'evaluator-previous',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function nhrContext(reasonCode = HUMAN_REVIEW_REASON.evaluatorRepairBudgetExhausted) {
|
|
56
|
+
return {
|
|
57
|
+
reasonCode,
|
|
58
|
+
sourceRunId: RUN_ID,
|
|
59
|
+
sourceArtifactId: ARTIFACT_ID,
|
|
60
|
+
sourceArtifactHash: 'a'.repeat(64),
|
|
61
|
+
revisionEpoch: 0,
|
|
62
|
+
createdAt: '2026-08-30T00:00:00.000Z',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function evaluatorNhrTask(overrides = {}) {
|
|
66
|
+
return {
|
|
67
|
+
taskId: EVAL_ID,
|
|
68
|
+
taskKind: 'evaluator',
|
|
69
|
+
status: overrides.status ?? 'needs_human_review',
|
|
70
|
+
attemptCount: 2,
|
|
71
|
+
maxAttempts: 3,
|
|
72
|
+
createdAt: '2026-08-29T00:00:00.000Z',
|
|
73
|
+
updatedAt: '2026-08-30T00:00:00.000Z',
|
|
74
|
+
diagnosticJson: createPITaskDiagnosticJson(baseMeta({
|
|
75
|
+
runnerDecision: 'needs_revision',
|
|
76
|
+
completionIntent: { decision: 'needs_revision', sourceRunId: RUN_ID, revisionEpoch: 0, status: 'pending' },
|
|
77
|
+
humanReviewContext: nhrContext(),
|
|
78
|
+
...overrides.meta,
|
|
79
|
+
})),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function artificerTaskWithPayload(repairIteration) {
|
|
83
|
+
return {
|
|
84
|
+
taskId: ARTIFER_ID,
|
|
85
|
+
taskKind: 'artificer',
|
|
86
|
+
status: 'succeeded',
|
|
87
|
+
attemptCount: 1,
|
|
88
|
+
maxAttempts: 3,
|
|
89
|
+
createdAt: '2026-08-29T00:00:00.000Z',
|
|
90
|
+
updatedAt: '2026-08-29T00:00:00.000Z',
|
|
91
|
+
diagnosticJson: createPITaskDiagnosticJson(baseMeta({
|
|
92
|
+
dependencyTaskIds: [SCRIBE_ID],
|
|
93
|
+
repairPayload: {
|
|
94
|
+
requiredChanges: ['fix timeout handling'],
|
|
95
|
+
concerns: ['c1'],
|
|
96
|
+
previousScore: 0.7,
|
|
97
|
+
repairIteration,
|
|
98
|
+
sourceArtificerArtifactId: 'pi-art-artificer-old',
|
|
99
|
+
sourceEvaluatorTaskId: 'evaluator-previous',
|
|
100
|
+
},
|
|
101
|
+
})),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function makeArtifactStoreWithDecisionArtifact(content = {
|
|
105
|
+
taskId: EVAL_ID,
|
|
106
|
+
sourceArtificerArtifactId: 'pi-art-artificer-old',
|
|
107
|
+
evaluation: {
|
|
108
|
+
decision: 'needs_revision', summary: 's', score: 0.7,
|
|
109
|
+
strengths: [], concerns: ['c'], requiredChanges: ['fix timeout handling'],
|
|
110
|
+
},
|
|
111
|
+
sourceTrace: { artificerArtifactId: 'pi-art-artificer-old' },
|
|
112
|
+
risks: [], generatedAt: '2026-08-30T00:00:00.000Z',
|
|
113
|
+
}) {
|
|
114
|
+
const store = new MemoryPIArtifactStore();
|
|
115
|
+
void store.upsertArtifact({
|
|
116
|
+
artifactId: ARTIFACT_ID,
|
|
117
|
+
artifactKind: 'principle',
|
|
118
|
+
sourceTaskId: EVAL_ID,
|
|
119
|
+
lineageArtifactIds: [],
|
|
120
|
+
validationStatus: 'pending',
|
|
121
|
+
contentJson: JSON.stringify(content),
|
|
122
|
+
createdAt: '2026-08-30T00:00:00.000Z',
|
|
123
|
+
updatedAt: '2026-08-30T00:00:00.000Z',
|
|
124
|
+
});
|
|
125
|
+
return store;
|
|
126
|
+
}
|
|
127
|
+
function makeFactStore(tasks, artifacts) {
|
|
128
|
+
return {
|
|
129
|
+
getTask: async (id) => tasks.find((t) => t.taskId === id) ?? null,
|
|
130
|
+
listArtifactsBySourceTask: async (id) => artifacts.listBySourceTaskId(id).then((list) => list.map((a) => ({
|
|
131
|
+
artifactId: a.artifactId, artifactKind: a.artifactKind, validationStatus: a.validationStatus, contentJson: a.contentJson,
|
|
132
|
+
}))),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/** in-memory stateManager 满足 owner-resolution-service / owner-retry 需要的面 */
|
|
136
|
+
function makeInMemoryStateManager(tasks, artifacts) {
|
|
137
|
+
const store = new MemoryTaskStore();
|
|
138
|
+
for (const t of tasks)
|
|
139
|
+
void store.createTask({ ...t });
|
|
140
|
+
const runs = new Map([
|
|
141
|
+
[RUN_ID, { runId: RUN_ID, taskId: EVAL_ID, outputPayload: JSON.stringify({
|
|
142
|
+
taskId: EVAL_ID,
|
|
143
|
+
sourceArtificerArtifactId: 'pi-art-artificer-old',
|
|
144
|
+
evaluation: {
|
|
145
|
+
decision: 'needs_revision', summary: 's', score: 0.7,
|
|
146
|
+
strengths: [], concerns: ['c'], requiredChanges: ['fix timeout handling'],
|
|
147
|
+
},
|
|
148
|
+
sourceTrace: { artificerArtifactId: 'pi-art-artificer-old', scribeArtifactId: 'pi-art-scribe' },
|
|
149
|
+
risks: [], generatedAt: '2026-08-30T00:00:00.000Z',
|
|
150
|
+
}) }],
|
|
151
|
+
]);
|
|
152
|
+
const sm = {
|
|
153
|
+
_store: store,
|
|
154
|
+
piArtifactStore: artifacts,
|
|
155
|
+
async getTask(id) { return store.getTask(id); },
|
|
156
|
+
async getRunsByTask(id) { return [...runs.values()].filter((r) => r.taskId === id); },
|
|
157
|
+
async updateTask(id, patch) { return store.updateTask(id, patch); },
|
|
158
|
+
async updateTaskDiagnosticJson(id, json) { await store.updateTask(id, { diagnosticJson: json }); },
|
|
159
|
+
async updateTaskIfDiagnosticJsonUnchanged(id, expected, patch) {
|
|
160
|
+
return store.updateTaskIfDiagnosticJsonUnchanged(id, expected, patch);
|
|
161
|
+
},
|
|
162
|
+
async updateTaskIfDiagnosticJsonAndArtifactsUnchanged(input) {
|
|
163
|
+
for (const bound of input.artifacts) {
|
|
164
|
+
const current = await artifacts.getArtifactById(bound.artifactId);
|
|
165
|
+
if (!current
|
|
166
|
+
|| current.sourceTaskId !== bound.sourceTaskId
|
|
167
|
+
|| JSON.stringify(current.lineageArtifactIds) !== bound.lineageArtifactIdsJson
|
|
168
|
+
|| current.contentJson !== bound.contentJson)
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
return store.updateTaskIfDiagnosticJsonUnchanged(input.taskId, input.expectedDiagnosticJson, input.patch);
|
|
172
|
+
},
|
|
173
|
+
async markTaskSucceeded(id, resultRef) { await store.updateTask(id, { status: 'succeeded', resultRef }); },
|
|
174
|
+
};
|
|
175
|
+
return sm;
|
|
176
|
+
}
|
|
177
|
+
// ── A. metadata contract ──────────────────────────────────────────────────────
|
|
178
|
+
describe('PRI-629 metadata: humanReviewContext + ownerResolutions round-trip', () => {
|
|
179
|
+
it('serializes and re-parses humanReviewContext + ownerResolutions (legacy rows without them still hydrate)', () => {
|
|
180
|
+
const resolution = {
|
|
181
|
+
resolutionId: 'ores_abc', reviewKey: 'odk_k', action: 'accept_current', status: 'applied',
|
|
182
|
+
ownerId: 'owner-1', decidedAt: '2026-08-30T01:00:00.000Z', appliedAt: '2026-08-30T01:00:05.000Z',
|
|
183
|
+
sourceRunId: RUN_ID, sourceArtifactId: ARTIFACT_ID, sourceArtifactHash: 'h'.repeat(64),
|
|
184
|
+
revisionEpoch: 0, machineDecision: 'needs_revision', effectiveDecision: 'approved',
|
|
185
|
+
};
|
|
186
|
+
const json = serializePITaskMetadata(baseMeta({
|
|
187
|
+
humanReviewContext: nhrContext(),
|
|
188
|
+
ownerResolutions: [resolution],
|
|
189
|
+
}));
|
|
190
|
+
const parsed = parsePITaskMetadata(json);
|
|
191
|
+
expect(parsed?.humanReviewContext?.reasonCode).toBe('evaluator_repair_budget_exhausted');
|
|
192
|
+
expect(parsed?.ownerResolutions?.[0]?.effectiveDecision).toBe('approved');
|
|
193
|
+
const hydrated = hydratePITaskRecord({ ...evaluatorNhrTask(), diagnosticJson: json });
|
|
194
|
+
expect(hydrated?.ownerResolutions?.length).toBe(1);
|
|
195
|
+
// legacy: 无新字段的旧行照常 hydrate
|
|
196
|
+
const legacy = parsePITaskMetadata(serializePITaskMetadata(baseMeta()));
|
|
197
|
+
expect(legacy?.humanReviewContext).toBeUndefined();
|
|
198
|
+
expect(legacy?.ownerResolutions).toBeUndefined();
|
|
199
|
+
});
|
|
200
|
+
it('fails closed on malformed resolution records (rc-1/rc-3)', () => {
|
|
201
|
+
const bad = {
|
|
202
|
+
resolutionId: '', reviewKey: 'odk_k', action: 'accept_current', status: 'pending',
|
|
203
|
+
ownerId: 'o', decidedAt: 't', sourceRunId: RUN_ID, sourceArtifactId: ARTIFACT_ID,
|
|
204
|
+
sourceArtifactHash: 'h', revisionEpoch: 0, machineDecision: 'needs_revision',
|
|
205
|
+
};
|
|
206
|
+
const json = serializePITaskMetadata(baseMeta({ ownerResolutions: [bad] }));
|
|
207
|
+
expect(parsePITaskMetadata(json)).toBeNull();
|
|
208
|
+
});
|
|
209
|
+
it('PRI-634: humanReviewContext.detail round-trips through serialize → parse (legacy 无 detail 照常 hydrate)', () => {
|
|
210
|
+
const json = serializePITaskMetadata(baseMeta({
|
|
211
|
+
humanReviewContext: { ...nhrContext(), detail: '1 rule/validated artifact(s) rejected: pi-art-x (missing: goldenTrace)' },
|
|
212
|
+
}));
|
|
213
|
+
const parsed = parsePITaskMetadata(json);
|
|
214
|
+
expect(parsed?.humanReviewContext?.detail)
|
|
215
|
+
.toBe('1 rule/validated artifact(s) rejected: pi-art-x (missing: goldenTrace)');
|
|
216
|
+
// legacy: 无 detail 的 context 照常 hydrate,字段为 undefined 而非空串
|
|
217
|
+
const legacy = parsePITaskMetadata(serializePITaskMetadata(baseMeta({ humanReviewContext: nhrContext() })));
|
|
218
|
+
expect(legacy?.humanReviewContext?.detail).toBeUndefined();
|
|
219
|
+
});
|
|
220
|
+
it('PRI-634: malformed humanReviewContext.detail fails closed (rc-1/rc-2 trust boundary)', () => {
|
|
221
|
+
// detail 一旦存在必须是 non-empty string;object/array/number/空串一律
|
|
222
|
+
// 整条 metadata fail closed,禁止静默 hydrate 成类型谎言 (detail?: string)。
|
|
223
|
+
for (const badDetail of [{}, [], 123, '']) {
|
|
224
|
+
const json = serializePITaskMetadata(baseMeta({
|
|
225
|
+
humanReviewContext: { ...nhrContext(), detail: badDetail },
|
|
226
|
+
}));
|
|
227
|
+
expect(parsePITaskMetadata(json)).toBeNull();
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
it('rejects duplicate reviewKey within ownerResolutions (authority corruption fails closed)', () => {
|
|
231
|
+
const rec = (id) => ({
|
|
232
|
+
resolutionId: id, reviewKey: 'odk_same', action: 'reject_current', status: 'applied',
|
|
233
|
+
ownerId: 'o', decidedAt: 't', sourceRunId: RUN_ID, sourceArtifactId: ARTIFACT_ID,
|
|
234
|
+
sourceArtifactHash: 'h', revisionEpoch: 0, machineDecision: 'needs_revision',
|
|
235
|
+
});
|
|
236
|
+
const json = serializePITaskMetadata(baseMeta({ ownerResolutions: [rec('ores_1'), rec('ores_2')] }));
|
|
237
|
+
expect(parsePITaskMetadata(json)).toBeNull();
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
// ── B/C. classification & capability ─────────────────────────────────────────
|
|
241
|
+
describe('PRI-629 classification + capability (INV-01/INV-02/§22)', () => {
|
|
242
|
+
it('decision-capable context + durable artifact → eligible with accept/revise/reject', async () => {
|
|
243
|
+
const facts = await collectOwnerDecisionFacts(makeFactStore([evaluatorNhrTask(), artificerTaskWithPayload(2)], makeArtifactStoreWithDecisionArtifact()), EVAL_ID);
|
|
244
|
+
const cap = deriveOwnerDecisionCapability(facts);
|
|
245
|
+
expect(cap.eligible).toBe(true);
|
|
246
|
+
expect(cap.allowedActions).toEqual(['accept_current', 'revise_once', 'reject_current']);
|
|
247
|
+
expect(cap.reviewKey).toMatch(/^odk_[0-9a-f]{64}$/);
|
|
248
|
+
});
|
|
249
|
+
it('unknown reasonCode → recovery (fail closed, §22)', async () => {
|
|
250
|
+
const task = evaluatorNhrTask({ meta: { humanReviewContext: nhrContext('something_new_unknown') } });
|
|
251
|
+
const facts = await collectOwnerDecisionFacts(makeFactStore([task, artificerTaskWithPayload(2)], makeArtifactStoreWithDecisionArtifact()), EVAL_ID);
|
|
252
|
+
const cap = deriveOwnerDecisionCapability(facts);
|
|
253
|
+
expect(cap.eligible).toBe(false);
|
|
254
|
+
expect(cap.attention).toBe('recovery');
|
|
255
|
+
});
|
|
256
|
+
it('recovery reasons (dispatch not wired / seed failed) never eligible', async () => {
|
|
257
|
+
for (const reason of [HUMAN_REVIEW_REASON.rolloutDispatchNotWired, HUMAN_REVIEW_REASON.evaluatorRepairSeedFailed]) {
|
|
258
|
+
const task = evaluatorNhrTask({ meta: { humanReviewContext: nhrContext(reason) } });
|
|
259
|
+
const facts = await collectOwnerDecisionFacts(makeFactStore([task, artificerTaskWithPayload(2)], makeArtifactStoreWithDecisionArtifact()), EVAL_ID);
|
|
260
|
+
expect(deriveOwnerDecisionCapability(facts).eligible).toBe(false);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
it('legacy evaluator: needs_revision + dep repairIteration>=2 + epoch-matched intent → decision-capable (§23)', async () => {
|
|
264
|
+
const meta = baseMeta({
|
|
265
|
+
runnerDecision: 'needs_revision',
|
|
266
|
+
humanReviewContext: undefined,
|
|
267
|
+
completionIntent: { decision: 'needs_revision', sourceRunId: RUN_ID, revisionEpoch: 0, status: 'pending' },
|
|
268
|
+
});
|
|
269
|
+
const task = evaluatorNhrTask({ meta });
|
|
270
|
+
const facts = await collectOwnerDecisionFacts(makeFactStore([task, artificerTaskWithPayload(2)], makeArtifactStoreWithDecisionArtifact()), EVAL_ID);
|
|
271
|
+
const cap = deriveOwnerDecisionCapability(facts);
|
|
272
|
+
expect(cap.eligible).toBe(true);
|
|
273
|
+
expect(cap.reasonCode).toBe(LEGACY_EVALUATOR_BUDGET_EXHAUSTED);
|
|
274
|
+
expect(cap.legacy).toBe(true);
|
|
275
|
+
});
|
|
276
|
+
it('legacy ambiguous (no repairPayload on dep / intent epoch stale) → recovery, not guessed', async () => {
|
|
277
|
+
const artificerFirstRound = {
|
|
278
|
+
...artificerTaskWithPayload(2),
|
|
279
|
+
diagnosticJson: createPITaskDiagnosticJson(baseMeta({ dependencyTaskIds: [SCRIBE_ID] })),
|
|
280
|
+
};
|
|
281
|
+
const task = evaluatorNhrTask({ meta: baseMeta({
|
|
282
|
+
runnerDecision: 'needs_revision',
|
|
283
|
+
humanReviewContext: undefined,
|
|
284
|
+
completionIntent: { decision: 'needs_revision', sourceRunId: RUN_ID, revisionEpoch: 0, status: 'pending' },
|
|
285
|
+
}) });
|
|
286
|
+
const facts = await collectOwnerDecisionFacts(makeFactStore([task, artificerFirstRound], makeArtifactStoreWithDecisionArtifact()), EVAL_ID);
|
|
287
|
+
expect(deriveOwnerDecisionCapability(facts).eligible).toBe(false);
|
|
288
|
+
});
|
|
289
|
+
it('missing decision artifact → not eligible (no reviewKey without durable facts)', async () => {
|
|
290
|
+
const emptyStore = new MemoryPIArtifactStore();
|
|
291
|
+
const facts = await collectOwnerDecisionFacts(makeFactStore([evaluatorNhrTask(), artificerTaskWithPayload(2)], emptyStore), EVAL_ID);
|
|
292
|
+
const cap = deriveOwnerDecisionCapability(facts);
|
|
293
|
+
expect(cap.eligible).toBe(false);
|
|
294
|
+
expect(cap.blockers).toContain('decision_artifact_missing');
|
|
295
|
+
});
|
|
296
|
+
it('existing resolution for current epoch removes the item from the inbox', async () => {
|
|
297
|
+
const task = evaluatorNhrTask({ meta: baseMeta({
|
|
298
|
+
runnerDecision: 'needs_revision',
|
|
299
|
+
completionIntent: { decision: 'needs_revision', sourceRunId: RUN_ID, revisionEpoch: 0, status: 'pending' },
|
|
300
|
+
humanReviewContext: nhrContext(),
|
|
301
|
+
ownerResolutions: [{
|
|
302
|
+
resolutionId: 'ores_x', reviewKey: 'odk_prev', action: 'revise_once', status: 'applied',
|
|
303
|
+
ownerId: 'o', decidedAt: 't', appliedAt: 't', sourceRunId: RUN_ID, sourceArtifactId: ARTIFACT_ID,
|
|
304
|
+
sourceArtifactHash: 'h', revisionEpoch: 0, machineDecision: 'needs_revision',
|
|
305
|
+
}],
|
|
306
|
+
}) });
|
|
307
|
+
const facts = await collectOwnerDecisionFacts(makeFactStore([task, artificerTaskWithPayload(2)], makeArtifactStoreWithDecisionArtifact()), EVAL_ID);
|
|
308
|
+
expect(deriveOwnerDecisionCapability(facts).blockers).toContain('resolution_already_recorded_for_current_epoch');
|
|
309
|
+
});
|
|
310
|
+
it('hard gate failure (adversarialResult.passed=false) blocks accept_current but keeps revise/reject (§9.1/§20)', async () => {
|
|
311
|
+
const store = makeArtifactStoreWithDecisionArtifact({
|
|
312
|
+
taskId: EVAL_ID,
|
|
313
|
+
sourceArtificerArtifactId: 'pi-art-artificer-old',
|
|
314
|
+
evaluation: {
|
|
315
|
+
decision: 'needs_revision', summary: 's', score: 0.7,
|
|
316
|
+
strengths: [], concerns: ['c'], requiredChanges: ['fix'],
|
|
317
|
+
},
|
|
318
|
+
sourceTrace: { artificerArtifactId: 'pi-art-artificer-old' },
|
|
319
|
+
risks: [], generatedAt: 't',
|
|
320
|
+
adversarialResult: { passed: false, failedCases: [] },
|
|
321
|
+
});
|
|
322
|
+
const facts = await collectOwnerDecisionFacts(makeFactStore([evaluatorNhrTask(), artificerTaskWithPayload(2)], store), EVAL_ID);
|
|
323
|
+
const cap = deriveOwnerDecisionCapability(facts);
|
|
324
|
+
expect(cap.allowedActions).toEqual(['revise_once', 'reject_current']);
|
|
325
|
+
expect(cap.blockers).toContain('deterministic_hard_gate_failed');
|
|
326
|
+
expect(detectHardGateFailureFromArtifact('{"adversarialResult":{"passed":false}}')).toBe(true);
|
|
327
|
+
expect(detectHardGateFailureFromArtifact('{"adversarialResult":{"passed":true}}')).toBe(false);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
// ── D. reviewKey ──────────────────────────────────────────────────────────────
|
|
331
|
+
describe('PRI-629 reviewKey (SPEC §6 stale protection)', () => {
|
|
332
|
+
const base = {
|
|
333
|
+
taskId: EVAL_ID, revisionEpoch: 0, sourceRunId: RUN_ID,
|
|
334
|
+
sourceArtifactId: ARTIFACT_ID, sourceArtifactHash: 'a'.repeat(64),
|
|
335
|
+
machineDecision: 'needs_revision', humanReviewReason: 'evaluator_repair_budget_exhausted',
|
|
336
|
+
};
|
|
337
|
+
it('changes when any bound fact changes', () => {
|
|
338
|
+
const k1 = buildOwnerReviewKey(base);
|
|
339
|
+
expect(buildOwnerReviewKey({ ...base, sourceRunId: 'run-other' })).not.toBe(k1);
|
|
340
|
+
expect(buildOwnerReviewKey({ ...base, sourceArtifactHash: 'b'.repeat(64) })).not.toBe(k1);
|
|
341
|
+
expect(buildOwnerReviewKey({ ...base, revisionEpoch: 1 })).not.toBe(k1);
|
|
342
|
+
expect(buildOwnerReviewKey({ ...base, machineDecision: 'rejected' })).not.toBe(k1);
|
|
343
|
+
expect(buildOwnerReviewKey(base)).toBe(k1);
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
// ── E. effective decision resolver (INV-03) ──────────────────────────────────
|
|
347
|
+
describe('PRI-629 resolveEffectiveRunnerDecision (single resolution point, §8)', () => {
|
|
348
|
+
const resolution = (status, epoch = 0) => ({
|
|
349
|
+
resolutionId: 'ores_x', reviewKey: 'odk_k', action: 'accept_current', status,
|
|
350
|
+
ownerId: 'o', decidedAt: 't', sourceRunId: RUN_ID, sourceArtifactId: ARTIFACT_ID,
|
|
351
|
+
sourceArtifactHash: 'h', revisionEpoch: epoch, machineDecision: 'needs_revision',
|
|
352
|
+
effectiveDecision: 'approved',
|
|
353
|
+
});
|
|
354
|
+
it('applied override in current epoch wins; machine runnerDecision never mutated', () => {
|
|
355
|
+
const pi = hydratePITaskRecord(evaluatorNhrTask({ meta: baseMeta({
|
|
356
|
+
runnerDecision: 'needs_revision', humanReviewContext: nhrContext(), ownerResolutions: [resolution('applied')],
|
|
357
|
+
}) }));
|
|
358
|
+
expect(resolveEffectiveRunnerDecision(pi)).toBe('approved');
|
|
359
|
+
expect(pi.runnerDecision).toBe('needs_revision');
|
|
360
|
+
});
|
|
361
|
+
it('pending override does NOT change effective decision until applied', () => {
|
|
362
|
+
const pi = hydratePITaskRecord(evaluatorNhrTask({ meta: baseMeta({
|
|
363
|
+
runnerDecision: 'needs_revision', ownerResolutions: [resolution('pending')],
|
|
364
|
+
}) }));
|
|
365
|
+
expect(resolveEffectiveRunnerDecision(pi)).toBe('needs_revision');
|
|
366
|
+
});
|
|
367
|
+
it('stale-epoch override is ignored (new epoch → machine verdict)', () => {
|
|
368
|
+
const pi = hydratePITaskRecord(evaluatorNhrTask({ meta: baseMeta({
|
|
369
|
+
runnerDecision: undefined, revisionCount: 1, ownerResolutions: [resolution('applied', 0)],
|
|
370
|
+
}) }));
|
|
371
|
+
expect(resolveEffectiveRunnerDecision(pi)).toBeUndefined();
|
|
372
|
+
});
|
|
373
|
+
it('effectiveDecisionFor maps rollout accept to approve_rollout, never approved (INV-08)', () => {
|
|
374
|
+
expect(effectiveDecisionFor('rollout_reviewer', 'accept_current')).toBe('approve_rollout');
|
|
375
|
+
expect(effectiveDecisionFor('evaluator', 'accept_current')).toBe('approved');
|
|
376
|
+
expect(effectiveDecisionFor('rollout_reviewer', 'reject_current')).toBe('reject');
|
|
377
|
+
expect(effectiveDecisionFor('evaluator', 'reject_current')).toBe('rejected');
|
|
378
|
+
expect(effectiveDecisionFor('evaluator', 'revise_once')).toBeUndefined();
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
// ── F. resolution service ─────────────────────────────────────────────────────
|
|
382
|
+
describe('PRI-629 applyOwnerResolution (SPEC §7/§10/§11)', () => {
|
|
383
|
+
async function setup() {
|
|
384
|
+
const artifacts = makeArtifactStoreWithDecisionArtifact();
|
|
385
|
+
const task = evaluatorNhrTask();
|
|
386
|
+
const sm = makeInMemoryStateManager([task, artificerTaskWithPayload(2)], artifacts);
|
|
387
|
+
const factStore = {
|
|
388
|
+
getTask: (id) => sm.getTask(id),
|
|
389
|
+
listArtifactsBySourceTask: async (id) => (await artifacts.listBySourceTaskId(id)).map((a) => ({
|
|
390
|
+
artifactId: a.artifactId, artifactKind: a.artifactKind, validationStatus: a.validationStatus, contentJson: a.contentJson,
|
|
391
|
+
})),
|
|
392
|
+
};
|
|
393
|
+
const facts = await collectOwnerDecisionFacts(factStore, EVAL_ID);
|
|
394
|
+
const cap = deriveOwnerDecisionCapability(facts);
|
|
395
|
+
const hash = facts.decisionArtifact.contentHash;
|
|
396
|
+
const req = (action, extra = {}) => ({
|
|
397
|
+
action,
|
|
398
|
+
reviewKey: cap.reviewKey,
|
|
399
|
+
expectedRevisionEpoch: 0,
|
|
400
|
+
expectedSourceRunId: RUN_ID,
|
|
401
|
+
expectedSourceArtifactId: ARTIFACT_ID,
|
|
402
|
+
expectedSourceArtifactHash: hash,
|
|
403
|
+
...extra,
|
|
404
|
+
});
|
|
405
|
+
return { sm, req, deps: { stateManager: sm, now: () => '2026-08-30T02:00:00.000Z' } };
|
|
406
|
+
}
|
|
407
|
+
const identity = { ownerId: 'owner-1' };
|
|
408
|
+
it('accept_current: pending resolution + atomic flip to pending (runnerWillApply)', async () => {
|
|
409
|
+
const { sm, deps, req } = await setup();
|
|
410
|
+
const outcome = await applyOwnerResolution(deps, { taskId: EVAL_ID, request: req('accept_current'), identity });
|
|
411
|
+
expect(outcome.status).toBe('resolved');
|
|
412
|
+
if (outcome.status === 'resolved') {
|
|
413
|
+
expect(outcome.action).toBe('accept_current');
|
|
414
|
+
expect(outcome.effectiveDecision).toBe('approved');
|
|
415
|
+
expect(outcome.runnerWillApply).toBe(true);
|
|
416
|
+
}
|
|
417
|
+
const updated = await sm.getTask(EVAL_ID);
|
|
418
|
+
expect(updated?.status).toBe('pending');
|
|
419
|
+
const pi = hydratePITaskRecord(updated);
|
|
420
|
+
expect(pi?.runnerDecision).toBe('needs_revision'); // INV-03: machine verdict preserved
|
|
421
|
+
expect(pi?.ownerResolutions?.[0]?.status).toBe('pending');
|
|
422
|
+
expect(pi?.completionIntent?.decision).toBe('needs_revision'); // intent 保留 — runner 恢复门用
|
|
423
|
+
});
|
|
424
|
+
it('same reviewKey + same action → idempotent success (double click safe)', async () => {
|
|
425
|
+
const { sm, deps, req } = await setup();
|
|
426
|
+
const first = await applyOwnerResolution(deps, { taskId: EVAL_ID, request: req('reject_current'), identity });
|
|
427
|
+
const second = await applyOwnerResolution(deps, { taskId: EVAL_ID, request: req('reject_current'), identity });
|
|
428
|
+
expect(first.status).toBe('resolved');
|
|
429
|
+
expect(second.status).toBe('resolved');
|
|
430
|
+
const pi = hydratePITaskRecord((await sm.getTask(EVAL_ID)));
|
|
431
|
+
expect(pi?.ownerResolutions?.length).toBe(1); // 无第二条记录
|
|
432
|
+
});
|
|
433
|
+
it('same reviewKey/action with changed lineage evidence digest is not an idempotent replay', async () => {
|
|
434
|
+
const { sm, deps, req } = await setup();
|
|
435
|
+
const decision = await sm.piArtifactStore.getArtifactById(ARTIFACT_ID);
|
|
436
|
+
expect(decision).not.toBeNull();
|
|
437
|
+
await sm.piArtifactStore.upsertArtifact({
|
|
438
|
+
...decision,
|
|
439
|
+
lineageArtifactIds: ['pi-art-artificer-old'],
|
|
440
|
+
});
|
|
441
|
+
await sm.piArtifactStore.upsertArtifact({
|
|
442
|
+
artifactId: 'pi-art-artificer-old',
|
|
443
|
+
artifactKind: 'principle',
|
|
444
|
+
sourceTaskId: ARTIFER_ID,
|
|
445
|
+
lineageArtifactIds: [],
|
|
446
|
+
validationStatus: 'pending',
|
|
447
|
+
contentJson: JSON.stringify({
|
|
448
|
+
implementationSummary: 'Implementation A',
|
|
449
|
+
affectedTools: ['write_file'],
|
|
450
|
+
risks: [],
|
|
451
|
+
}),
|
|
452
|
+
createdAt: '2026-08-30T00:00:00.000Z',
|
|
453
|
+
updatedAt: '2026-08-30T00:00:00.000Z',
|
|
454
|
+
});
|
|
455
|
+
const firstReview = await buildOwnerDecisionReview(reviewStoreFromStateManager(sm), EVAL_ID);
|
|
456
|
+
expect(firstReview).not.toBeNull();
|
|
457
|
+
const artificer = await sm.piArtifactStore.getArtifactById('pi-art-artificer-old');
|
|
458
|
+
await sm.piArtifactStore.upsertArtifact({
|
|
459
|
+
...artificer,
|
|
460
|
+
contentJson: JSON.stringify({
|
|
461
|
+
implementationSummary: 'Implementation B',
|
|
462
|
+
affectedTools: ['write_file'],
|
|
463
|
+
risks: [],
|
|
464
|
+
}),
|
|
465
|
+
updatedAt: '2026-08-30T00:01:00.000Z',
|
|
466
|
+
});
|
|
467
|
+
const secondReview = await buildOwnerDecisionReview(reviewStoreFromStateManager(sm), EVAL_ID);
|
|
468
|
+
expect(secondReview?.reviewKey).toBe(firstReview?.reviewKey);
|
|
469
|
+
expect(secondReview?.evidence.digest).not.toBe(firstReview?.evidence.digest);
|
|
470
|
+
await sm.piArtifactStore.upsertArtifact(artificer);
|
|
471
|
+
const firstRequest = req('reject_current', {
|
|
472
|
+
expectedEvidenceDigest: firstReview.evidence.digest,
|
|
473
|
+
});
|
|
474
|
+
const first = await applyOwnerResolution(deps, {
|
|
475
|
+
taskId: EVAL_ID,
|
|
476
|
+
request: firstRequest,
|
|
477
|
+
identity,
|
|
478
|
+
});
|
|
479
|
+
expect(first.status).toBe('resolved');
|
|
480
|
+
await sm.piArtifactStore.upsertArtifact({
|
|
481
|
+
...artificer,
|
|
482
|
+
contentJson: JSON.stringify({
|
|
483
|
+
implementationSummary: 'Implementation B',
|
|
484
|
+
affectedTools: ['write_file'],
|
|
485
|
+
risks: [],
|
|
486
|
+
}),
|
|
487
|
+
updatedAt: '2026-08-30T00:01:00.000Z',
|
|
488
|
+
});
|
|
489
|
+
const replay = await applyOwnerResolution(deps, {
|
|
490
|
+
taskId: EVAL_ID,
|
|
491
|
+
request: req('reject_current', {
|
|
492
|
+
expectedEvidenceDigest: secondReview.evidence.digest,
|
|
493
|
+
}),
|
|
494
|
+
identity,
|
|
495
|
+
});
|
|
496
|
+
expect(replay.status).toBe('stale_owner_decision');
|
|
497
|
+
});
|
|
498
|
+
it('revalidates independently persisted evidence after the CAS read and refuses a raced digest', async () => {
|
|
499
|
+
const { sm, deps, req } = await setup();
|
|
500
|
+
const decision = await sm.piArtifactStore.getArtifactById(ARTIFACT_ID);
|
|
501
|
+
await sm.piArtifactStore.upsertArtifact({
|
|
502
|
+
...decision,
|
|
503
|
+
lineageArtifactIds: ['pi-art-artificer-old'],
|
|
504
|
+
});
|
|
505
|
+
await sm.piArtifactStore.upsertArtifact({
|
|
506
|
+
artifactId: 'pi-art-artificer-old',
|
|
507
|
+
artifactKind: 'principle',
|
|
508
|
+
sourceTaskId: ARTIFER_ID,
|
|
509
|
+
lineageArtifactIds: [],
|
|
510
|
+
validationStatus: 'pending',
|
|
511
|
+
contentJson: JSON.stringify({ implementationSummary: 'Evidence A', affectedTools: ['write_file'] }),
|
|
512
|
+
createdAt: '2026-08-30T00:00:00.000Z',
|
|
513
|
+
updatedAt: '2026-08-30T00:00:00.000Z',
|
|
514
|
+
});
|
|
515
|
+
const rendered = await buildOwnerDecisionReview(reviewStoreFromStateManager(sm), EVAL_ID);
|
|
516
|
+
expect(rendered).not.toBeNull();
|
|
517
|
+
const originalGetTask = sm.getTask.bind(sm);
|
|
518
|
+
let evaluatorReads = 0;
|
|
519
|
+
sm.getTask = async (id) => {
|
|
520
|
+
const task = await originalGetTask(id);
|
|
521
|
+
if (id === EVAL_ID && ++evaluatorReads === 3) {
|
|
522
|
+
const artifact = await sm.piArtifactStore.getArtifactById('pi-art-artificer-old');
|
|
523
|
+
await sm.piArtifactStore.upsertArtifact({
|
|
524
|
+
...artifact,
|
|
525
|
+
contentJson: JSON.stringify({ implementationSummary: 'Evidence B', affectedTools: ['write_file'] }),
|
|
526
|
+
updatedAt: '2026-08-30T00:01:00.000Z',
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
return task;
|
|
530
|
+
};
|
|
531
|
+
const outcome = await applyOwnerResolution(deps, {
|
|
532
|
+
taskId: EVAL_ID,
|
|
533
|
+
request: req('reject_current', { expectedEvidenceDigest: rendered.evidence.digest }),
|
|
534
|
+
identity,
|
|
535
|
+
});
|
|
536
|
+
expect(outcome.status).toBe('stale_owner_decision');
|
|
537
|
+
const unchanged = hydratePITaskRecord((await originalGetTask(EVAL_ID)));
|
|
538
|
+
expect(unchanged?.status).toBe('needs_human_review');
|
|
539
|
+
expect(unchanged?.ownerResolutions).toBeUndefined();
|
|
540
|
+
});
|
|
541
|
+
it('does not persist when evidence changes after the final review rebuild but before the task mutation', async () => {
|
|
542
|
+
const { sm, deps, req } = await setup();
|
|
543
|
+
const rendered = await buildOwnerDecisionReview(reviewStoreFromStateManager(sm), EVAL_ID);
|
|
544
|
+
expect(rendered).not.toBeNull();
|
|
545
|
+
const originalAtomicUpdate = sm.updateTaskIfDiagnosticJsonAndArtifactsUnchanged.bind(sm);
|
|
546
|
+
let intercepted = false;
|
|
547
|
+
sm.updateTaskIfDiagnosticJsonAndArtifactsUnchanged = async (...args) => {
|
|
548
|
+
if (!intercepted) {
|
|
549
|
+
intercepted = true;
|
|
550
|
+
const decision = await sm.piArtifactStore.getArtifactById(ARTIFACT_ID);
|
|
551
|
+
await sm.piArtifactStore.upsertArtifact({
|
|
552
|
+
...decision,
|
|
553
|
+
contentJson: JSON.stringify({ changedAfterFinalReview: true }),
|
|
554
|
+
updatedAt: '2026-08-30T00:02:00.000Z',
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
return originalAtomicUpdate(...args);
|
|
558
|
+
};
|
|
559
|
+
const outcome = await applyOwnerResolution(deps, {
|
|
560
|
+
taskId: EVAL_ID,
|
|
561
|
+
request: req('reject_current', { expectedEvidenceDigest: rendered.evidence.digest }),
|
|
562
|
+
identity,
|
|
563
|
+
});
|
|
564
|
+
expect(intercepted).toBe(true);
|
|
565
|
+
expect(outcome.status).toBe('stale_owner_decision');
|
|
566
|
+
const unchanged = hydratePITaskRecord((await sm.getTask(EVAL_ID)));
|
|
567
|
+
expect(unchanged?.status).toBe('needs_human_review');
|
|
568
|
+
expect(unchanged?.ownerResolutions).toBeUndefined();
|
|
569
|
+
});
|
|
570
|
+
it('same reviewKey + different action → already_resolved (conflict)', async () => {
|
|
571
|
+
const { deps, req } = await setup();
|
|
572
|
+
await applyOwnerResolution(deps, { taskId: EVAL_ID, request: req('accept_current'), identity });
|
|
573
|
+
const conflict = await applyOwnerResolution(deps, { taskId: EVAL_ID, request: req('reject_current'), identity });
|
|
574
|
+
expect(conflict).toEqual({ status: 'already_resolved', existingAction: 'accept_current' });
|
|
575
|
+
});
|
|
576
|
+
it('stale reviewKey (artifact changed) → stale_owner_decision', async () => {
|
|
577
|
+
const { deps, req } = await setup();
|
|
578
|
+
const outcome = await applyOwnerResolution(deps, {
|
|
579
|
+
taskId: EVAL_ID,
|
|
580
|
+
// 浏览器持有旧 hash
|
|
581
|
+
request: req('accept_current', { expectedSourceArtifactHash: 'deadbeef'.repeat(8) }),
|
|
582
|
+
identity,
|
|
583
|
+
});
|
|
584
|
+
expect(outcome.status).toBe('stale_owner_decision');
|
|
585
|
+
});
|
|
586
|
+
it('revise_once: reopens the latest repair artificer with owner-res causeId; repairIteration stays 2 (§11)', async () => {
|
|
587
|
+
const { sm, deps, req } = await setup();
|
|
588
|
+
const outcome = await applyOwnerResolution(deps, { taskId: EVAL_ID, request: req('revise_once', { ownerInstruction: '不要增加新要求,只处理上一轮未解决问题。' }), identity });
|
|
589
|
+
expect(outcome.status).toBe('resolved');
|
|
590
|
+
if (outcome.status === 'resolved') {
|
|
591
|
+
expect(outcome.applied).toBe(true);
|
|
592
|
+
expect(outcome.targetTaskId).toBe(ARTIFER_ID);
|
|
593
|
+
}
|
|
594
|
+
// 目标 artificer 被 reopen: revisionCount+1、causeId、反馈注入
|
|
595
|
+
const artPi = hydratePITaskRecord((await sm.getTask(ARTIFER_ID)));
|
|
596
|
+
expect(artPi?.revisionCount).toBe(1);
|
|
597
|
+
expect(artPi?.revisionCauseId).toMatch(/^owner-res-ores_/);
|
|
598
|
+
expect(artPi?.revisionFeedback).toContain('不要增加新要求');
|
|
599
|
+
// repairPayload.repairIteration 保持 2 — 自动预算永不增加 (INV-07)
|
|
600
|
+
expect(artPi?.repairPayload?.repairIteration).toBe(2);
|
|
601
|
+
// evaluator 任务留在 NHR,resolution applied → 不再出现在 inbox
|
|
602
|
+
const evalPi = hydratePITaskRecord((await sm.getTask(EVAL_ID)));
|
|
603
|
+
expect(evalPi?.status).toBe('needs_human_review');
|
|
604
|
+
expect(evalPi?.ownerResolutions?.[0]?.status).toBe('applied');
|
|
605
|
+
});
|
|
606
|
+
it('P1 review: action outside allowedActions → not_decision_capable (server-side enforcement)', async () => {
|
|
607
|
+
// hard gate 失败 → capability 只允许 revise_once/reject_current
|
|
608
|
+
const gateStore = makeArtifactStoreWithDecisionArtifact({
|
|
609
|
+
taskId: EVAL_ID,
|
|
610
|
+
sourceArtificerArtifactId: 'pi-art-artificer-old',
|
|
611
|
+
evaluation: {
|
|
612
|
+
decision: 'needs_revision', summary: 's', score: 0.7,
|
|
613
|
+
strengths: [], concerns: ['c'], requiredChanges: ['fix'],
|
|
614
|
+
},
|
|
615
|
+
sourceTrace: { artificerArtifactId: 'pi-art-artificer-old' },
|
|
616
|
+
risks: [], generatedAt: 't',
|
|
617
|
+
adversarialResult: { passed: false, failedCases: [] },
|
|
618
|
+
});
|
|
619
|
+
const task = evaluatorNhrTask();
|
|
620
|
+
const sm = makeInMemoryStateManager([task, artificerTaskWithPayload(2)], gateStore);
|
|
621
|
+
const factStore = {
|
|
622
|
+
getTask: (id) => sm.getTask(id),
|
|
623
|
+
listArtifactsBySourceTask: async (id) => (await gateStore.listBySourceTaskId(id)).map((a) => ({
|
|
624
|
+
artifactId: a.artifactId, artifactKind: a.artifactKind, validationStatus: a.validationStatus, contentJson: a.contentJson,
|
|
625
|
+
})),
|
|
626
|
+
};
|
|
627
|
+
const facts = await collectOwnerDecisionFacts(factStore, EVAL_ID);
|
|
628
|
+
const cap = deriveOwnerDecisionCapability(facts);
|
|
629
|
+
expect(cap.allowedActions).not.toContain('accept_current');
|
|
630
|
+
const hash = facts.decisionArtifact.contentHash;
|
|
631
|
+
const outcome = await applyOwnerResolution({ stateManager: sm, now: () => '2026-08-30T02:00:00.000Z' }, {
|
|
632
|
+
taskId: EVAL_ID,
|
|
633
|
+
request: {
|
|
634
|
+
action: 'accept_current', reviewKey: cap.reviewKey,
|
|
635
|
+
expectedRevisionEpoch: 0, expectedSourceRunId: RUN_ID,
|
|
636
|
+
expectedSourceArtifactId: ARTIFACT_ID, expectedSourceArtifactHash: hash,
|
|
637
|
+
},
|
|
638
|
+
identity,
|
|
639
|
+
});
|
|
640
|
+
expect(outcome.status).toBe('not_decision_capable');
|
|
641
|
+
if (outcome.status === 'not_decision_capable') {
|
|
642
|
+
expect(outcome.blockers.some((b) => b.startsWith('action_not_permitted:'))).toBe(true);
|
|
643
|
+
}
|
|
644
|
+
// 任务未被重启
|
|
645
|
+
expect((await sm.getTask(EVAL_ID))?.status).toBe('needs_human_review');
|
|
646
|
+
});
|
|
647
|
+
it('sanitizeOwnerInstruction bounds and strips control characters (§13)', () => {
|
|
648
|
+
expect(sanitizeOwnerInstruction(undefined)).toBeUndefined();
|
|
649
|
+
expect(sanitizeOwnerInstruction(' ')).toBeUndefined();
|
|
650
|
+
expect(sanitizeOwnerInstruction(42)).toBeUndefined();
|
|
651
|
+
expect(sanitizeOwnerInstruction('a\u0000b')).toBe('a b');
|
|
652
|
+
expect(sanitizeOwnerInstruction('x'.repeat(900)).length).toBe(500);
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
// ── G. Recover guard ──────────────────────────────────────────────────────────
|
|
656
|
+
describe('PRI-629 Recover guard (SPEC §17/INV-06)', () => {
|
|
657
|
+
it('decision-capable NHR → rejected with owner_decision_required + nextAction', async () => {
|
|
658
|
+
const artifacts = makeArtifactStoreWithDecisionArtifact();
|
|
659
|
+
const sm = makeInMemoryStateManager([evaluatorNhrTask(), artificerTaskWithPayload(2)], artifacts);
|
|
660
|
+
const outcome = await ownerRetryNeedsHumanReviewTask(sm, EVAL_ID);
|
|
661
|
+
expect(outcome).toEqual({
|
|
662
|
+
status: 'rejected', taskKind: 'evaluator', previousStatus: 'needs_human_review',
|
|
663
|
+
reason: 'owner_decision_required', nextAction: 'open_governance_focus',
|
|
664
|
+
});
|
|
665
|
+
// 任务未被改动
|
|
666
|
+
expect((await sm.getTask(EVAL_ID))?.status).toBe('needs_human_review');
|
|
667
|
+
});
|
|
668
|
+
it('P0 review: applied-only resolution + recovery NHR → Recover allowed (technical retry)', async () => {
|
|
669
|
+
const artifacts = makeArtifactStoreWithDecisionArtifact();
|
|
670
|
+
const appliedRes = {
|
|
671
|
+
resolutionId: 'ores_applied', reviewKey: 'odk_prev', action: 'accept_current', status: 'applied',
|
|
672
|
+
ownerId: 'o', decidedAt: 't', appliedAt: 't', sourceRunId: RUN_ID, sourceArtifactId: ARTIFACT_ID,
|
|
673
|
+
sourceArtifactHash: 'h', revisionEpoch: 0, machineDecision: 'needs_revision',
|
|
674
|
+
};
|
|
675
|
+
const task = evaluatorNhrTask({ meta: baseMeta({
|
|
676
|
+
runnerDecision: 'needs_revision',
|
|
677
|
+
humanReviewContext: nhrContext(HUMAN_REVIEW_REASON.rolloutDispatchNotWired),
|
|
678
|
+
ownerResolutions: [appliedRes],
|
|
679
|
+
}) });
|
|
680
|
+
const sm = makeInMemoryStateManager([task, artificerTaskWithPayload(2)], artifacts);
|
|
681
|
+
const outcome = await ownerRetryNeedsHumanReviewTask(sm, EVAL_ID);
|
|
682
|
+
expect(outcome.status).toBe('requeued');
|
|
683
|
+
});
|
|
684
|
+
it('P0 review: pending resolution + recovery NHR → Recover still refused (runner will apply)', async () => {
|
|
685
|
+
const artifacts = makeArtifactStoreWithDecisionArtifact();
|
|
686
|
+
const pendingRes = {
|
|
687
|
+
resolutionId: 'ores_pending', reviewKey: 'odk_prev', action: 'accept_current', status: 'pending',
|
|
688
|
+
ownerId: 'o', decidedAt: 't', sourceRunId: RUN_ID, sourceArtifactId: ARTIFACT_ID,
|
|
689
|
+
sourceArtifactHash: 'h', revisionEpoch: 0, machineDecision: 'needs_revision',
|
|
690
|
+
};
|
|
691
|
+
const task = evaluatorNhrTask({ meta: baseMeta({
|
|
692
|
+
runnerDecision: 'needs_revision',
|
|
693
|
+
humanReviewContext: nhrContext(HUMAN_REVIEW_REASON.rolloutDispatchNotWired),
|
|
694
|
+
ownerResolutions: [pendingRes],
|
|
695
|
+
}) });
|
|
696
|
+
const sm = makeInMemoryStateManager([task, artificerTaskWithPayload(2)], artifacts);
|
|
697
|
+
const outcome = await ownerRetryNeedsHumanReviewTask(sm, EVAL_ID);
|
|
698
|
+
expect(outcome.status).toBe('rejected');
|
|
699
|
+
if (outcome.status === 'rejected') {
|
|
700
|
+
expect(outcome.reason).toBe('owner_decision_required');
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
it('ambiguous legacy (no decision facts) → recover remains allowed', async () => {
|
|
704
|
+
const artifacts = makeArtifactStoreWithDecisionArtifact();
|
|
705
|
+
// 无 repairPayload 的首轮 artificer + 无 context → 不可判定 → 不阻断
|
|
706
|
+
const artificerFirstRound = {
|
|
707
|
+
...artificerTaskWithPayload(2),
|
|
708
|
+
diagnosticJson: createPITaskDiagnosticJson(baseMeta({ dependencyTaskIds: [SCRIBE_ID] })),
|
|
709
|
+
};
|
|
710
|
+
const task = evaluatorNhrTask({ meta: baseMeta({ runnerDecision: 'needs_revision', humanReviewContext: undefined }) });
|
|
711
|
+
const sm = makeInMemoryStateManager([task, artificerFirstRound], artifacts);
|
|
712
|
+
const outcome = await ownerRetryNeedsHumanReviewTask(sm, EVAL_ID);
|
|
713
|
+
expect(outcome.status).toBe('requeued');
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
// ── H. epoch-aware repair causeId (P0, SPEC §12) ─────────────────────────────
|
|
717
|
+
describe('PRI-629 buildRepairRevisionCauseId (P0)', () => {
|
|
718
|
+
it('rc0 keeps legacy format; owner revision epochs get -rcN suffix', () => {
|
|
719
|
+
const t0 = hydratePITaskRecord(artificerTaskWithPayload(2));
|
|
720
|
+
expect(buildRepairRevisionCauseId(t0)).toBe(`repair-${ARTIFER_ID}`);
|
|
721
|
+
const t1 = hydratePITaskRecord({ ...artificerTaskWithPayload(2), diagnosticJson: createPITaskDiagnosticJson(baseMeta({
|
|
722
|
+
dependencyTaskIds: [SCRIBE_ID], revisionCount: 1,
|
|
723
|
+
repairPayload: budgetExhaustedRepairPayload(),
|
|
724
|
+
})) });
|
|
725
|
+
expect(buildRepairRevisionCauseId(t1)).toBe(`repair-${ARTIFER_ID}-rc1`);
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
// ── I. transition arbitration uses effective decision ─────────────────────────
|
|
729
|
+
describe('PRI-629 transition arbitration consumes effective decision (§8)', () => {
|
|
730
|
+
it('accept_current applied → evaluator ADVANCE; machine needs_revision would have been REVISION_REQUIRED', () => {
|
|
731
|
+
const withOverride = hydratePITaskRecord(evaluatorNhrTask({ status: 'succeeded', meta: baseMeta({
|
|
732
|
+
runnerDecision: 'needs_revision',
|
|
733
|
+
ownerResolutions: [{
|
|
734
|
+
resolutionId: 'ores_x', reviewKey: 'odk_k', action: 'accept_current', status: 'applied',
|
|
735
|
+
ownerId: 'o', decidedAt: 't', appliedAt: 't', sourceRunId: RUN_ID, sourceArtifactId: ARTIFACT_ID,
|
|
736
|
+
sourceArtifactHash: 'h', revisionEpoch: 0, machineDecision: 'needs_revision', effectiveDecision: 'approved',
|
|
737
|
+
}],
|
|
738
|
+
}) }));
|
|
739
|
+
const transition = decideInternalizationTransition(transitionInputFromTask(withOverride));
|
|
740
|
+
expect(transition.kind).toBe('ADVANCE');
|
|
741
|
+
const withoutOverride = hydratePITaskRecord(evaluatorNhrTask({ status: 'succeeded', meta: baseMeta({
|
|
742
|
+
runnerDecision: 'needs_revision',
|
|
743
|
+
}) }));
|
|
744
|
+
expect(decideInternalizationTransition(transitionInputFromTask(withoutOverride)).kind).toBe('REVISION_REQUIRED');
|
|
745
|
+
});
|
|
746
|
+
it('rollout reject_current applied → TERMINAL_REJECT; accept_current → ADVANCE (dispatch side)', () => {
|
|
747
|
+
const res = (action, eff) => ({
|
|
748
|
+
resolutionId: 'ores_x', reviewKey: 'odk_k', action, status: 'applied',
|
|
749
|
+
ownerId: 'o', decidedAt: 't', appliedAt: 't', sourceRunId: RUN_ID, sourceArtifactId: ARTIFACT_ID,
|
|
750
|
+
sourceArtifactHash: 'h', revisionEpoch: 0, machineDecision: 'needs_revision',
|
|
751
|
+
effectiveDecision: eff,
|
|
752
|
+
});
|
|
753
|
+
const mk = (r) => hydratePITaskRecord({
|
|
754
|
+
taskId: 'rollout-1', taskKind: 'rollout_reviewer', status: 'succeeded',
|
|
755
|
+
attemptCount: 1, maxAttempts: 3, createdAt: 't', updatedAt: 't',
|
|
756
|
+
diagnosticJson: createPITaskDiagnosticJson(baseMeta({
|
|
757
|
+
dependencyTaskIds: [EVAL_ID], runnerDecision: 'needs_revision', ownerResolutions: [r],
|
|
758
|
+
})),
|
|
759
|
+
});
|
|
760
|
+
expect(decideInternalizationTransition(transitionInputFromTask(mk(res('accept_current', 'approve_rollout')))).kind).toBe('ADVANCE');
|
|
761
|
+
expect(decideInternalizationTransition(transitionInputFromTask(mk(res('reject_current', 'reject')))).kind).toBe('TERMINAL_REJECT');
|
|
762
|
+
});
|
|
763
|
+
it('planOwnerVerdictOverrideResume picks latest epoch-matched override; revise_once ignored', () => {
|
|
764
|
+
const pi = hydratePITaskRecord(evaluatorNhrTask({ meta: baseMeta({
|
|
765
|
+
ownerResolutions: [
|
|
766
|
+
{ resolutionId: 'a', reviewKey: 'k1', action: 'revise_once', status: 'applied', ownerId: 'o', decidedAt: '2026-01-01T00:00:00Z', sourceRunId: RUN_ID, sourceArtifactId: ARTIFACT_ID, sourceArtifactHash: 'h', revisionEpoch: 0, machineDecision: 'needs_revision' },
|
|
767
|
+
{ resolutionId: 'b', reviewKey: 'k2', action: 'accept_current', status: 'pending', ownerId: 'o', decidedAt: '2026-01-02T00:00:00Z', sourceRunId: RUN_ID, sourceArtifactId: ARTIFACT_ID, sourceArtifactHash: 'h', revisionEpoch: 0, machineDecision: 'needs_revision', effectiveDecision: 'approved' },
|
|
768
|
+
],
|
|
769
|
+
}) }));
|
|
770
|
+
const plan = planOwnerVerdictOverrideResume(pi);
|
|
771
|
+
expect(plan?.resolution.resolutionId).toBe('b');
|
|
772
|
+
expect(plan?.overrideDecision).toBe('approved');
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
describe('PRI-629 P1 review: reopen crash-window idempotency (same cause, terminal residue)', () => {
|
|
776
|
+
it('same revisionCauseId on a terminal task → true no-op (revisionCount unchanged)', async () => {
|
|
777
|
+
// 模拟双写中断残留: metadata 已写 causeId/revisionCount,但 status 仍是 terminal
|
|
778
|
+
const crashed = {
|
|
779
|
+
...artificerTaskWithPayload(2),
|
|
780
|
+
status: 'succeeded',
|
|
781
|
+
diagnosticJson: createPITaskDiagnosticJson(baseMeta({
|
|
782
|
+
dependencyTaskIds: [SCRIBE_ID],
|
|
783
|
+
revisionCount: 1,
|
|
784
|
+
revisionCauseId: 'owner-res-ores_x',
|
|
785
|
+
repairPayload: budgetExhaustedRepairPayload(),
|
|
786
|
+
})),
|
|
787
|
+
};
|
|
788
|
+
const store = new MemoryTaskStore();
|
|
789
|
+
void store.createTask({ ...crashed });
|
|
790
|
+
const sm = {
|
|
791
|
+
async getTask(id) { return store.getTask(id); },
|
|
792
|
+
async updateTaskDiagnosticJson(id, json) { await store.updateTask(id, { diagnosticJson: json }); },
|
|
793
|
+
async updateTask(id, patch) { return store.updateTask(id, patch); },
|
|
794
|
+
};
|
|
795
|
+
const outcome = await reopenTaskForRevision(sm, ARTIFER_ID, {
|
|
796
|
+
revisionCauseId: 'owner-res-ores_x',
|
|
797
|
+
reason: 'owner_revise_once',
|
|
798
|
+
});
|
|
799
|
+
expect(outcome.ok).toBe(true);
|
|
800
|
+
const after = hydratePITaskRecord((await store.getTask(ARTIFER_ID)));
|
|
801
|
+
expect(after?.revisionCount).toBe(1); // 不再 +1
|
|
802
|
+
expect(after?.revisionCauseId).toBe('owner-res-ores_x');
|
|
803
|
+
});
|
|
804
|
+
it('reopen is a SINGLE task-row mutation (status+attemptCount+metadata together)', async () => {
|
|
805
|
+
const store = new MemoryTaskStore();
|
|
806
|
+
void store.createTask({ ...artificerTaskWithPayload(2) });
|
|
807
|
+
const writes = [];
|
|
808
|
+
const sm = {
|
|
809
|
+
async getTask(id) { return store.getTask(id); },
|
|
810
|
+
async updateTaskDiagnosticJson(id, json) {
|
|
811
|
+
writes.push({ op: 'diagnostic_only' });
|
|
812
|
+
await store.updateTask(id, { diagnosticJson: json });
|
|
813
|
+
},
|
|
814
|
+
async updateTask(id, patch) {
|
|
815
|
+
writes.push(patch);
|
|
816
|
+
return store.updateTask(id, patch);
|
|
817
|
+
},
|
|
818
|
+
};
|
|
819
|
+
await reopenTaskForRevision(sm, ARTIFER_ID, {
|
|
820
|
+
revisionCauseId: 'owner-res-ores_y',
|
|
821
|
+
reason: 'owner_revise_once',
|
|
822
|
+
});
|
|
823
|
+
// 不允许"只写 metadata"的中间态 mutation
|
|
824
|
+
expect(writes.some((w) => 'op' in w)).toBe(false);
|
|
825
|
+
const mutating = writes.filter((w) => !('op' in w));
|
|
826
|
+
expect(mutating).toHaveLength(1);
|
|
827
|
+
const single = mutating[0];
|
|
828
|
+
expect(single.status).toBe('pending');
|
|
829
|
+
expect(single.attemptCount).toBe(0);
|
|
830
|
+
expect(typeof single.diagnosticJson).toBe('string');
|
|
831
|
+
const after = hydratePITaskRecord((await store.getTask(ARTIFER_ID)));
|
|
832
|
+
expect(after?.status).toBe('pending');
|
|
833
|
+
expect(after?.revisionCount).toBe(1);
|
|
834
|
+
});
|
|
835
|
+
});
|
|
836
|
+
// ── N. rollout revision target routing (shared implementation) ────────────────
|
|
837
|
+
describe('PRI-629 resolveRolloutRevisionTarget (§18 routing)', () => {
|
|
838
|
+
it('code_tool_hook routes to artificer; other channels route to scribe', async () => {
|
|
839
|
+
const scribe = { taskId: SCRIBE_ID, taskKind: 'scribe', status: 'succeeded', attemptCount: 0, maxAttempts: 3, createdAt: 't', updatedAt: 't', diagnosticJson: createPITaskDiagnosticJson(baseMeta({ dependencyTaskIds: [] })) };
|
|
840
|
+
const artificer = artificerTaskWithPayload(2);
|
|
841
|
+
const evaluator = evaluatorNhrTask({ status: 'succeeded', meta: baseMeta({ dependencyTaskIds: [ARTIFER_ID] }) });
|
|
842
|
+
const rollout = {
|
|
843
|
+
taskId: 'rollout-1', taskKind: 'rollout_reviewer', status: 'needs_human_review',
|
|
844
|
+
attemptCount: 1, maxAttempts: 3, createdAt: 't', updatedAt: 't',
|
|
845
|
+
diagnosticJson: createPITaskDiagnosticJson(baseMeta({ dependencyTaskIds: [EVAL_ID], channel: 'code_tool_hook' })),
|
|
846
|
+
};
|
|
847
|
+
const allTasks = [scribe, artificer, evaluator, rollout];
|
|
848
|
+
const getTask = async (id) => allTasks.find((t) => t.taskId === id) ?? null;
|
|
849
|
+
expect(await resolveRolloutRevisionTarget(getTask, 'rollout-1', 'code_tool_hook')).toEqual({ taskId: ARTIFER_ID, kind: 'artificer' });
|
|
850
|
+
expect(await resolveRolloutRevisionTarget(getTask, 'rollout-1', 'prompt')).toEqual({ taskId: SCRIBE_ID, kind: 'scribe' });
|
|
851
|
+
});
|
|
852
|
+
});
|
|
853
|
+
describe('PRI-630 P1 review: deriveRequirementLedger (stable requirement identity across rounds)', () => {
|
|
854
|
+
it('round with echoed ledger: open items keep ids; non-exact restatement becomes new entry', () => {
|
|
855
|
+
const ledger = deriveRequirementLedger([
|
|
856
|
+
{ id: 'req-1', statement: 'A', status: 'resolved' },
|
|
857
|
+
{ id: 'req-2', statement: 'B', status: 'still_open' },
|
|
858
|
+
], ['B (reworded)', 'new blocker C']);
|
|
859
|
+
// 核心: req-2 保留原 id 与原 statement (身份不漂移);非精确改述作为
|
|
860
|
+
// 新条目 (可预测 — 文本模糊匹配在 LLM 改述上不可靠)
|
|
861
|
+
expect(ledger).toEqual([
|
|
862
|
+
{ id: 'req-2', statement: 'B', status: 'still_open' },
|
|
863
|
+
{ id: 'req-3', statement: 'B (reworded)', status: 'new' },
|
|
864
|
+
{ id: 'req-4', statement: 'new blocker C', status: 'new' },
|
|
865
|
+
]);
|
|
866
|
+
});
|
|
867
|
+
it('regressed items keep ids too; exact-duplicate restatements are not re-added', () => {
|
|
868
|
+
const ledger = deriveRequirementLedger([
|
|
869
|
+
{ id: 'req-2', statement: 'fix timeout', status: 'regressed' },
|
|
870
|
+
{ id: 'req-5', statement: 'add retry', status: 'still_open' },
|
|
871
|
+
], ['fix timeout', 'add retry backoff']);
|
|
872
|
+
expect(ledger.map((e) => e.id)).toEqual(['req-2', 'req-5', 'req-6']);
|
|
873
|
+
expect(ledger[2]).toEqual({ id: 'req-6', statement: 'add retry backoff', status: 'new' });
|
|
874
|
+
// 'fix timeout' 精确重复 carried → 不再作为新条目
|
|
875
|
+
expect(ledger.some((e) => e.statement === 'fix timeout' && e.status === 'new')).toBe(false);
|
|
876
|
+
});
|
|
877
|
+
it('no ledger (first repair round): sequential numbering from requiredChanges', () => {
|
|
878
|
+
expect(deriveRequirementLedger(undefined, ['X', 'Y'])).toEqual([
|
|
879
|
+
{ id: 'req-1', statement: 'X', status: 'new' },
|
|
880
|
+
{ id: 'req-2', statement: 'Y', status: 'new' },
|
|
881
|
+
]);
|
|
882
|
+
});
|
|
883
|
+
});
|
|
884
|
+
// ── K. evaluator convergence schema invariant (SPEC §18.4) ────────────────────
|
|
885
|
+
describe('PRI-630 evaluator convergence invariants', () => {
|
|
886
|
+
const validator = new DefaultEvaluatorValidator();
|
|
887
|
+
const baseOutput = (decision, requiredChanges) => ({
|
|
888
|
+
taskId: EVAL_ID,
|
|
889
|
+
sourceArtificerArtifactId: 'pi-art-artificer-old',
|
|
890
|
+
evaluation: {
|
|
891
|
+
decision, summary: 's', score: 0.7, strengths: [], concerns: ['c'], requiredChanges,
|
|
892
|
+
},
|
|
893
|
+
sourceTrace: { artificerArtifactId: 'pi-art-artificer-old' },
|
|
894
|
+
risks: [], generatedAt: 't',
|
|
895
|
+
});
|
|
896
|
+
it('needs_revision with empty requiredChanges → validator rejects (裁决与依据脱节)', async () => {
|
|
897
|
+
const result = await validator.validate(baseOutput('needs_revision', []), EVAL_ID);
|
|
898
|
+
expect(result.valid).toBe(false);
|
|
899
|
+
expect(result.errors.join('; ')).toContain('PRI-630');
|
|
900
|
+
});
|
|
901
|
+
it('approved/rejected with empty requiredChanges remain valid; valid statuses array accepted', async () => {
|
|
902
|
+
expect((await validator.validate(baseOutput('approved', []), EVAL_ID)).valid).toBe(true);
|
|
903
|
+
const withStatuses = {
|
|
904
|
+
...baseOutput('needs_revision', ['fix']),
|
|
905
|
+
evaluation: {
|
|
906
|
+
...baseOutput('needs_revision', ['fix']).evaluation,
|
|
907
|
+
priorRequirementStatuses: [{ id: 'req-1', status: 'still_open' }],
|
|
908
|
+
},
|
|
909
|
+
};
|
|
910
|
+
const result = await validator.validate(withStatuses, EVAL_ID);
|
|
911
|
+
expect(result.valid).toBe(true);
|
|
912
|
+
});
|
|
913
|
+
// ── 评审轮 2 P1: requirementLedger 收紧为 validator-enforced contract ──
|
|
914
|
+
// convergence context 携带 authoritative expected {id, statement};
|
|
915
|
+
// repair round 必须同时输出 statuses + ledger,且二者互洽。
|
|
916
|
+
const EXPECTED = [
|
|
917
|
+
{ id: 'req-1', statement: 'fix timeout handling' },
|
|
918
|
+
{ id: 'req-2', statement: 'add retry backoff' },
|
|
919
|
+
];
|
|
920
|
+
const okStatuses = [
|
|
921
|
+
{ id: 'req-1', status: 'resolved' },
|
|
922
|
+
{ id: 'req-2', status: 'still_open' },
|
|
923
|
+
];
|
|
924
|
+
const okLedger = [
|
|
925
|
+
{ id: 'req-1', statement: 'fix timeout handling', status: 'resolved' },
|
|
926
|
+
{ id: 'req-2', statement: 'add retry backoff', status: 'still_open' },
|
|
927
|
+
];
|
|
928
|
+
it('R2: repair round missing priorRequirementStatuses → rejected', async () => {
|
|
929
|
+
const result = await validator.validate(baseOutput('needs_revision', ['fix']), EVAL_ID, undefined, {
|
|
930
|
+
expectedRequirements: EXPECTED,
|
|
931
|
+
});
|
|
932
|
+
expect(result.valid).toBe(false);
|
|
933
|
+
expect(result.errors.join('; ')).toContain('priorRequirementStatuses');
|
|
934
|
+
});
|
|
935
|
+
it('R2: repair round missing requirementLedger entirely → rejected (no longer prompt-only)', async () => {
|
|
936
|
+
const out = {
|
|
937
|
+
...baseOutput('needs_revision', ['fix']),
|
|
938
|
+
evaluation: {
|
|
939
|
+
...baseOutput('needs_revision', ['fix']).evaluation,
|
|
940
|
+
priorRequirementStatuses: okStatuses,
|
|
941
|
+
// requirementLedger 缺失 — LLM 忘了输出也必须被机器拒绝
|
|
942
|
+
},
|
|
943
|
+
};
|
|
944
|
+
const result = await validator.validate(out, EVAL_ID, undefined, { expectedRequirements: EXPECTED });
|
|
945
|
+
expect(result.valid).toBe(false);
|
|
946
|
+
expect(result.errors.join('; ')).toContain('requirementLedger');
|
|
947
|
+
});
|
|
948
|
+
it('R2: statuses partial coverage → rejected with the missing id named', async () => {
|
|
949
|
+
const out = {
|
|
950
|
+
...baseOutput('needs_revision', ['fix']),
|
|
951
|
+
evaluation: {
|
|
952
|
+
...baseOutput('needs_revision', ['fix']).evaluation,
|
|
953
|
+
priorRequirementStatuses: [okStatuses[0]],
|
|
954
|
+
requirementLedger: okLedger,
|
|
955
|
+
},
|
|
956
|
+
};
|
|
957
|
+
const result = await validator.validate(out, EVAL_ID, undefined, { expectedRequirements: EXPECTED });
|
|
958
|
+
expect(result.valid).toBe(false);
|
|
959
|
+
expect(result.errors.join('; ')).toContain('req-2');
|
|
960
|
+
});
|
|
961
|
+
it('R2: statuses duplicate id → rejected (each expected id exactly once)', async () => {
|
|
962
|
+
const out = {
|
|
963
|
+
...baseOutput('needs_revision', ['fix']),
|
|
964
|
+
evaluation: {
|
|
965
|
+
...baseOutput('needs_revision', ['fix']).evaluation,
|
|
966
|
+
priorRequirementStatuses: [okStatuses[0], okStatuses[0], okStatuses[1]],
|
|
967
|
+
requirementLedger: okLedger,
|
|
968
|
+
},
|
|
969
|
+
};
|
|
970
|
+
const result = await validator.validate(out, EVAL_ID, undefined, { expectedRequirements: EXPECTED });
|
|
971
|
+
expect(result.valid).toBe(false);
|
|
972
|
+
expect(result.errors.join('; ')).toContain('req-1');
|
|
973
|
+
});
|
|
974
|
+
it('R2: ledger renumbered (req-2 renamed to req-9) → rejected as missing + unexpected id', async () => {
|
|
975
|
+
const out = {
|
|
976
|
+
...baseOutput('needs_revision', ['fix']),
|
|
977
|
+
evaluation: {
|
|
978
|
+
...baseOutput('needs_revision', ['fix']).evaluation,
|
|
979
|
+
priorRequirementStatuses: okStatuses,
|
|
980
|
+
requirementLedger: [
|
|
981
|
+
okLedger[0],
|
|
982
|
+
{ id: 'req-9', statement: 'add retry backoff', status: 'still_open' },
|
|
983
|
+
],
|
|
984
|
+
},
|
|
985
|
+
};
|
|
986
|
+
const result = await validator.validate(out, EVAL_ID, undefined, { expectedRequirements: EXPECTED });
|
|
987
|
+
expect(result.valid).toBe(false);
|
|
988
|
+
const errs = result.errors.join('; ');
|
|
989
|
+
expect(errs).toContain('req-2'); // 缺失
|
|
990
|
+
expect(errs).toContain('req-9'); // 幻觉/重编号 id 不允许
|
|
991
|
+
});
|
|
992
|
+
it('R2: ledger statement drift → rejected (must match authoritative context verbatim)', async () => {
|
|
993
|
+
const out = {
|
|
994
|
+
...baseOutput('needs_revision', ['fix']),
|
|
995
|
+
evaluation: {
|
|
996
|
+
...baseOutput('needs_revision', ['fix']).evaluation,
|
|
997
|
+
priorRequirementStatuses: okStatuses,
|
|
998
|
+
requirementLedger: [
|
|
999
|
+
okLedger[0],
|
|
1000
|
+
{ id: 'req-2', statement: 'add retry backoff (paraphrased)', status: 'still_open' },
|
|
1001
|
+
],
|
|
1002
|
+
},
|
|
1003
|
+
};
|
|
1004
|
+
const result = await validator.validate(out, EVAL_ID, undefined, { expectedRequirements: EXPECTED });
|
|
1005
|
+
expect(result.valid).toBe(false);
|
|
1006
|
+
expect(result.errors.join('; ')).toContain('req-2');
|
|
1007
|
+
expect(result.errors.join('; ')).toContain('statement');
|
|
1008
|
+
});
|
|
1009
|
+
it('R2: ledger status inconsistent with priorRequirementStatuses → rejected', async () => {
|
|
1010
|
+
const out = {
|
|
1011
|
+
...baseOutput('needs_revision', ['fix']),
|
|
1012
|
+
evaluation: {
|
|
1013
|
+
...baseOutput('needs_revision', ['fix']).evaluation,
|
|
1014
|
+
priorRequirementStatuses: okStatuses,
|
|
1015
|
+
requirementLedger: [
|
|
1016
|
+
okLedger[0],
|
|
1017
|
+
{ id: 'req-2', statement: 'add retry backoff', status: 'resolved' }, // statuses 说 still_open
|
|
1018
|
+
],
|
|
1019
|
+
},
|
|
1020
|
+
};
|
|
1021
|
+
const result = await validator.validate(out, EVAL_ID, undefined, { expectedRequirements: EXPECTED });
|
|
1022
|
+
expect(result.valid).toBe(false);
|
|
1023
|
+
expect(result.errors.join('; ')).toContain('req-2');
|
|
1024
|
+
expect(result.errors.join('; ')).toContain('status');
|
|
1025
|
+
});
|
|
1026
|
+
it('R2: fully compliant statuses + ledger → valid; first round (no convergence) unaffected', async () => {
|
|
1027
|
+
const out = {
|
|
1028
|
+
...baseOutput('needs_revision', ['fix']),
|
|
1029
|
+
evaluation: {
|
|
1030
|
+
...baseOutput('needs_revision', ['fix']).evaluation,
|
|
1031
|
+
priorRequirementStatuses: okStatuses,
|
|
1032
|
+
requirementLedger: okLedger,
|
|
1033
|
+
},
|
|
1034
|
+
};
|
|
1035
|
+
expect((await validator.validate(out, EVAL_ID, undefined, {
|
|
1036
|
+
expectedRequirements: EXPECTED,
|
|
1037
|
+
})).valid).toBe(true);
|
|
1038
|
+
// 第一轮: 未提供 convergence 上下文 → 两者皆省略仍合法
|
|
1039
|
+
expect((await validator.validate(baseOutput('needs_revision', ['fix']), EVAL_ID)).valid).toBe(true);
|
|
1040
|
+
});
|
|
1041
|
+
it('malformed priorRequirementStatuses rejected (rc-1/rc-4)', async () => {
|
|
1042
|
+
const bad = {
|
|
1043
|
+
...baseOutput('needs_revision', ['fix']),
|
|
1044
|
+
evaluation: {
|
|
1045
|
+
...baseOutput('needs_revision', ['fix']).evaluation,
|
|
1046
|
+
priorRequirementStatuses: [{ id: 'req-1', status: 'done' }],
|
|
1047
|
+
},
|
|
1048
|
+
};
|
|
1049
|
+
const result = await validator.validate(bad, EVAL_ID);
|
|
1050
|
+
expect(result.valid).toBe(false);
|
|
1051
|
+
expect(result.errors.join('; ')).toContain('priorRequirementStatuses');
|
|
1052
|
+
});
|
|
1053
|
+
});
|
|
1054
|
+
//# sourceMappingURL=owner-decision.test.js.map
|