@principles/core 1.259.0 → 1.259.1

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.
Files changed (141) hide show
  1. package/dist/runtime-v2/__tests__/architecture-regression.test.js +11 -2
  2. package/dist/runtime-v2/__tests__/architecture-regression.test.js.map +1 -1
  3. package/dist/runtime-v2/__tests__/evaluator-repair-loop.test.js +14 -6
  4. package/dist/runtime-v2/__tests__/evaluator-repair-loop.test.js.map +1 -1
  5. package/dist/runtime-v2/__tests__/evaluator-runner-vslice-v2.test.js +1 -0
  6. package/dist/runtime-v2/__tests__/evaluator-runner-vslice-v2.test.js.map +1 -1
  7. package/dist/runtime-v2/__tests__/evaluator-runner-vslice.test.js +5 -0
  8. package/dist/runtime-v2/__tests__/evaluator-runner-vslice.test.js.map +1 -1
  9. package/dist/runtime-v2/__tests__/internalization-orchestrator.test.js +4 -1
  10. package/dist/runtime-v2/__tests__/internalization-orchestrator.test.js.map +1 -1
  11. package/dist/runtime-v2/__tests__/internalization-state-machine.test.js +3 -2
  12. package/dist/runtime-v2/__tests__/internalization-state-machine.test.js.map +1 -1
  13. package/dist/runtime-v2/__tests__/pain-to-principle-service.test.js +47 -0
  14. package/dist/runtime-v2/__tests__/pain-to-principle-service.test.js.map +1 -1
  15. package/dist/runtime-v2/__tests__/rollout-reviewer-runner-vslice.test.js +66 -1
  16. package/dist/runtime-v2/__tests__/rollout-reviewer-runner-vslice.test.js.map +1 -1
  17. package/dist/runtime-v2/__tests__/rollout-reviewer-verdict-paths.test.d.ts +2 -0
  18. package/dist/runtime-v2/__tests__/rollout-reviewer-verdict-paths.test.d.ts.map +1 -0
  19. package/dist/runtime-v2/__tests__/rollout-reviewer-verdict-paths.test.js +234 -0
  20. package/dist/runtime-v2/__tests__/rollout-reviewer-verdict-paths.test.js.map +1 -0
  21. package/dist/runtime-v2/activation/__tests__/prompt-activation-reader-contract.test.js +23 -1
  22. package/dist/runtime-v2/activation/__tests__/prompt-activation-reader-contract.test.js.map +1 -1
  23. package/dist/runtime-v2/activation/prompt-activation-reader-contract.d.ts +11 -0
  24. package/dist/runtime-v2/activation/prompt-activation-reader-contract.d.ts.map +1 -1
  25. package/dist/runtime-v2/activation/prompt-activation-reader-contract.js +11 -4
  26. package/dist/runtime-v2/activation/prompt-activation-reader-contract.js.map +1 -1
  27. package/dist/runtime-v2/adapter/output-schema-registry.d.ts.map +1 -1
  28. package/dist/runtime-v2/adapter/output-schema-registry.js +2 -0
  29. package/dist/runtime-v2/adapter/output-schema-registry.js.map +1 -1
  30. package/dist/runtime-v2/feature-flags/__tests__/feature-flag-contract.test.js +7 -2
  31. package/dist/runtime-v2/feature-flags/__tests__/feature-flag-contract.test.js.map +1 -1
  32. package/dist/runtime-v2/feature-flags/feature-flag-contract.d.ts.map +1 -1
  33. package/dist/runtime-v2/feature-flags/feature-flag-contract.js +9 -5
  34. package/dist/runtime-v2/feature-flags/feature-flag-contract.js.map +1 -1
  35. package/dist/runtime-v2/index.d.ts +9 -5
  36. package/dist/runtime-v2/index.d.ts.map +1 -1
  37. package/dist/runtime-v2/index.js +4 -2
  38. package/dist/runtime-v2/index.js.map +1 -1
  39. package/dist/runtime-v2/internalization/__tests__/a-liveness-reconciliation.test.d.ts +2 -0
  40. package/dist/runtime-v2/internalization/__tests__/a-liveness-reconciliation.test.d.ts.map +1 -0
  41. package/dist/runtime-v2/internalization/__tests__/a-liveness-reconciliation.test.js +157 -0
  42. package/dist/runtime-v2/internalization/__tests__/a-liveness-reconciliation.test.js.map +1 -0
  43. package/dist/runtime-v2/internalization/__tests__/c2-live-runner-chain.test.js +29 -0
  44. package/dist/runtime-v2/internalization/__tests__/c2-live-runner-chain.test.js.map +1 -1
  45. package/dist/runtime-v2/internalization/__tests__/crash-liveness-regressions.test.d.ts +2 -0
  46. package/dist/runtime-v2/internalization/__tests__/crash-liveness-regressions.test.d.ts.map +1 -0
  47. package/dist/runtime-v2/internalization/__tests__/crash-liveness-regressions.test.js +311 -0
  48. package/dist/runtime-v2/internalization/__tests__/crash-liveness-regressions.test.js.map +1 -0
  49. package/dist/runtime-v2/internalization/__tests__/external-review-p0-regressions.test.d.ts +2 -0
  50. package/dist/runtime-v2/internalization/__tests__/external-review-p0-regressions.test.d.ts.map +1 -0
  51. package/dist/runtime-v2/internalization/__tests__/external-review-p0-regressions.test.js +171 -0
  52. package/dist/runtime-v2/internalization/__tests__/external-review-p0-regressions.test.js.map +1 -0
  53. package/dist/runtime-v2/internalization/__tests__/internalization-task-guards.test.js +11 -2
  54. package/dist/runtime-v2/internalization/__tests__/internalization-task-guards.test.js.map +1 -1
  55. package/dist/runtime-v2/internalization/__tests__/internalization-transition-decision.test.d.ts +2 -0
  56. package/dist/runtime-v2/internalization/__tests__/internalization-transition-decision.test.d.ts.map +1 -0
  57. package/dist/runtime-v2/internalization/__tests__/internalization-transition-decision.test.js +335 -0
  58. package/dist/runtime-v2/internalization/__tests__/internalization-transition-decision.test.js.map +1 -0
  59. package/dist/runtime-v2/internalization/__tests__/mvp-core-loop-journeys.test.d.ts +2 -0
  60. package/dist/runtime-v2/internalization/__tests__/mvp-core-loop-journeys.test.d.ts.map +1 -0
  61. package/dist/runtime-v2/internalization/__tests__/mvp-core-loop-journeys.test.js +515 -0
  62. package/dist/runtime-v2/internalization/__tests__/mvp-core-loop-journeys.test.js.map +1 -0
  63. package/dist/runtime-v2/internalization/__tests__/restart-safety.e2e.test.d.ts +2 -0
  64. package/dist/runtime-v2/internalization/__tests__/restart-safety.e2e.test.d.ts.map +1 -0
  65. package/dist/runtime-v2/internalization/__tests__/restart-safety.e2e.test.js +167 -0
  66. package/dist/runtime-v2/internalization/__tests__/restart-safety.e2e.test.js.map +1 -0
  67. package/dist/runtime-v2/internalization/__tests__/verdict-drift-regressions.test.d.ts +2 -0
  68. package/dist/runtime-v2/internalization/__tests__/verdict-drift-regressions.test.d.ts.map +1 -0
  69. package/dist/runtime-v2/internalization/__tests__/verdict-drift-regressions.test.js +601 -0
  70. package/dist/runtime-v2/internalization/__tests__/verdict-drift-regressions.test.js.map +1 -0
  71. package/dist/runtime-v2/internalization/artificer-runner.d.ts +7 -0
  72. package/dist/runtime-v2/internalization/artificer-runner.d.ts.map +1 -1
  73. package/dist/runtime-v2/internalization/artificer-runner.js +18 -2
  74. package/dist/runtime-v2/internalization/artificer-runner.js.map +1 -1
  75. package/dist/runtime-v2/internalization/evaluator-runner.d.ts +49 -0
  76. package/dist/runtime-v2/internalization/evaluator-runner.d.ts.map +1 -1
  77. package/dist/runtime-v2/internalization/evaluator-runner.js +390 -98
  78. package/dist/runtime-v2/internalization/evaluator-runner.js.map +1 -1
  79. package/dist/runtime-v2/internalization/internalization-orchestrator.d.ts +127 -0
  80. package/dist/runtime-v2/internalization/internalization-orchestrator.d.ts.map +1 -1
  81. package/dist/runtime-v2/internalization/internalization-orchestrator.js +345 -1
  82. package/dist/runtime-v2/internalization/internalization-orchestrator.js.map +1 -1
  83. package/dist/runtime-v2/internalization/internalization-task-guards.d.ts +9 -2
  84. package/dist/runtime-v2/internalization/internalization-task-guards.d.ts.map +1 -1
  85. package/dist/runtime-v2/internalization/internalization-task-guards.js +14 -3
  86. package/dist/runtime-v2/internalization/internalization-task-guards.js.map +1 -1
  87. package/dist/runtime-v2/internalization/internalization-transition-decision.d.ts +61 -0
  88. package/dist/runtime-v2/internalization/internalization-transition-decision.d.ts.map +1 -0
  89. package/dist/runtime-v2/internalization/internalization-transition-decision.js +84 -0
  90. package/dist/runtime-v2/internalization/internalization-transition-decision.js.map +1 -0
  91. package/dist/runtime-v2/internalization/peer-runner-contracts.d.ts +17 -1
  92. package/dist/runtime-v2/internalization/peer-runner-contracts.d.ts.map +1 -1
  93. package/dist/runtime-v2/internalization/peer-runner-contracts.js.map +1 -1
  94. package/dist/runtime-v2/internalization/pitask-metadata.d.ts +99 -2
  95. package/dist/runtime-v2/internalization/pitask-metadata.d.ts.map +1 -1
  96. package/dist/runtime-v2/internalization/pitask-metadata.js +148 -0
  97. package/dist/runtime-v2/internalization/pitask-metadata.js.map +1 -1
  98. package/dist/runtime-v2/internalization/rollout-reviewer-runner.d.ts +147 -0
  99. package/dist/runtime-v2/internalization/rollout-reviewer-runner.d.ts.map +1 -1
  100. package/dist/runtime-v2/internalization/rollout-reviewer-runner.js +672 -1
  101. package/dist/runtime-v2/internalization/rollout-reviewer-runner.js.map +1 -1
  102. package/dist/runtime-v2/internalization/scribe-runner.d.ts +2 -0
  103. package/dist/runtime-v2/internalization/scribe-runner.d.ts.map +1 -1
  104. package/dist/runtime-v2/internalization/scribe-runner.js +21 -1
  105. package/dist/runtime-v2/internalization/scribe-runner.js.map +1 -1
  106. package/dist/runtime-v2/runner/base-peer-runner.d.ts +9 -0
  107. package/dist/runtime-v2/runner/base-peer-runner.d.ts.map +1 -1
  108. package/dist/runtime-v2/runner/base-peer-runner.js +22 -0
  109. package/dist/runtime-v2/runner/base-peer-runner.js.map +1 -1
  110. package/dist/runtime-v2/signal-collector/__tests__/llm-payload-contract.test.d.ts +2 -0
  111. package/dist/runtime-v2/signal-collector/__tests__/llm-payload-contract.test.d.ts.map +1 -0
  112. package/dist/runtime-v2/signal-collector/__tests__/llm-payload-contract.test.js +130 -0
  113. package/dist/runtime-v2/signal-collector/__tests__/llm-payload-contract.test.js.map +1 -0
  114. package/dist/runtime-v2/signal-collector/index.d.ts +1 -1
  115. package/dist/runtime-v2/signal-collector/index.d.ts.map +1 -1
  116. package/dist/runtime-v2/signal-collector/index.js +1 -1
  117. package/dist/runtime-v2/signal-collector/index.js.map +1 -1
  118. package/dist/runtime-v2/signal-collector/llm-stage.d.ts +15 -0
  119. package/dist/runtime-v2/signal-collector/llm-stage.d.ts.map +1 -1
  120. package/dist/runtime-v2/signal-collector/llm-stage.js +28 -0
  121. package/dist/runtime-v2/signal-collector/llm-stage.js.map +1 -1
  122. package/dist/runtime-v2/signal-collector/types.d.ts +9 -1
  123. package/dist/runtime-v2/signal-collector/types.d.ts.map +1 -1
  124. package/dist/runtime-v2/signal-collector/types.js +13 -1
  125. package/dist/runtime-v2/signal-collector/types.js.map +1 -1
  126. package/dist/runtime-v2/store/reconciliation-cursor-store.d.ts +23 -0
  127. package/dist/runtime-v2/store/reconciliation-cursor-store.d.ts.map +1 -0
  128. package/dist/runtime-v2/store/reconciliation-cursor-store.js +34 -0
  129. package/dist/runtime-v2/store/reconciliation-cursor-store.js.map +1 -0
  130. package/dist/runtime-v2/store/sqlite-connection.d.ts.map +1 -1
  131. package/dist/runtime-v2/store/sqlite-connection.js +7 -0
  132. package/dist/runtime-v2/store/sqlite-connection.js.map +1 -1
  133. package/dist/runtime-v2/store/task/memory-task-store.d.ts.map +1 -1
  134. package/dist/runtime-v2/store/task/memory-task-store.js +22 -0
  135. package/dist/runtime-v2/store/task/memory-task-store.js.map +1 -1
  136. package/dist/runtime-v2/store/task/sqlite-task-store.d.ts.map +1 -1
  137. package/dist/runtime-v2/store/task/sqlite-task-store.js +20 -0
  138. package/dist/runtime-v2/store/task/sqlite-task-store.js.map +1 -1
  139. package/dist/runtime-v2/store/task/task-store.d.ts +14 -0
  140. package/dist/runtime-v2/store/task/task-store.d.ts.map +1 -1
  141. package/package.json +1 -1
@@ -0,0 +1,601 @@
1
+ /**
2
+ * P0 (verdict drift) — durable completion intent 是 recovery authority。
3
+ *
4
+ * 外部复核反例 (第 5 轮): runner 在 side effect 已部分发生、task 未 terminal
5
+ * 时 crash,retry/restart 重跑会重新调用 LLM;非确定性下新 verdict 覆盖旧
6
+ * runnerDecision,与已发生副作用形成治理矛盾:
7
+ * T1 rollout revision drift — pending r1 不得被新 approve 漂移;
8
+ * T2 activation drift — 已 activated 后不得漂移为 reject;
9
+ * T3 evaluator repair drift — 已 seed repair 后不得漂移为 approved;
10
+ * T4 evaluator validation drift — 已 validate bearer 后不得漂移为 needs_revision;
11
+ * T5 new epoch — 真正 revision reopen 后允许不同 verdict (禁止过度封锁)。
12
+ *
13
+ * 每个 drift 测试的 adapter 均带调用计数: 断言 resume 路径根本未调用 LLM。
14
+ */
15
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
16
+ import * as fs from 'node:fs';
17
+ import * as path from 'node:path';
18
+ import * as os from 'node:os';
19
+ import { RuntimeStateManager } from '../../store/runtime-state-manager.js';
20
+ import { InternalizationOrchestrator } from '../internalization-orchestrator.js';
21
+ import { RolloutReviewerRunner } from '../rollout-reviewer-runner.js';
22
+ import { EvaluatorRunner } from '../evaluator-runner.js';
23
+ import { DefaultRolloutReviewerValidator } from '../rollout-reviewer-output.js';
24
+ import { DefaultEvaluatorValidator } from '../evaluator-output.js';
25
+ import { SqliteConnection } from '../../store/sqlite-connection.js';
26
+ import { SqlitePIArtifactStore } from '../../store/artifact/sqlite-pi-artifact-store.js';
27
+ import { storeEmitter } from '../../store/event-emitter.js';
28
+ import { hydratePITaskRecord, createPITaskDiagnosticJson, mergePITaskMetadata } from '../pitask-metadata.js';
29
+ let workspaceDir;
30
+ let stateManager;
31
+ let orchestrator;
32
+ const SCRIBE_ID = 'scribe-d1-prompt';
33
+ const ARTIFICER_ID = 'artificer-d1-prompt';
34
+ const EVAL_ID = 'evaluator-d1-prompt';
35
+ const ROLLOUT_ID = 'rollout_reviewer-d1-prompt';
36
+ const SCRIBE_ART = 'pi-art-scribe-d1';
37
+ const ARTIFICER_ART = 'pi-art-artificer-d1';
38
+ beforeEach(async () => {
39
+ workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-drift-'));
40
+ stateManager = new RuntimeStateManager({ workspaceDir });
41
+ await stateManager.initialize();
42
+ orchestrator = new InternalizationOrchestrator({ stateManager }, { owner: 'drift', runtimeKind: 'test-double', dryRun: true });
43
+ });
44
+ afterEach(async () => {
45
+ await stateManager.close();
46
+ try {
47
+ fs.rmSync(workspaceDir, { recursive: true, force: true });
48
+ }
49
+ catch { /* temp */ }
50
+ });
51
+ function meta(o = {}) {
52
+ return createPITaskDiagnosticJson({
53
+ dependencyTaskIds: [], channel: 'prompt', timeoutMs: 300_000,
54
+ inputArtifactRefs: [], outputArtifactRefs: [], ...o,
55
+ });
56
+ }
57
+ async function mkTask(id, kind, deps) {
58
+ await stateManager.createTask({ taskId: id, taskKind: kind, status: 'pending', attemptCount: 0, maxAttempts: 3, diagnosticJson: meta({ correlationId: 'd1', dependencyTaskIds: deps }) });
59
+ }
60
+ async function succeed(id) {
61
+ await stateManager.acquireLease({ taskId: id, owner: 'drift', runtimeKind: 'test-double' });
62
+ await stateManager.markTaskSucceeded(id);
63
+ }
64
+ /** scribe(validated bearer) → artificer → evaluator(succeeded+approved) → rollout(pending) + 依赖 artifacts */
65
+ async function seedLineage() {
66
+ await mkTask(SCRIBE_ID, 'scribe', []);
67
+ await succeed(SCRIBE_ID);
68
+ await mkTask(ARTIFICER_ID, 'artificer', [SCRIBE_ID]);
69
+ await succeed(ARTIFICER_ID);
70
+ await mkTask(EVAL_ID, 'evaluator', [ARTIFICER_ID]);
71
+ await succeed(EVAL_ID);
72
+ const evalRaw = await stateManager.getTask(EVAL_ID);
73
+ if (!evalRaw)
74
+ throw new Error('seed: evaluator missing');
75
+ const evalPi = hydratePITaskRecord(evalRaw);
76
+ if (!evalPi)
77
+ throw new Error('seed: evaluator not hydratable');
78
+ await stateManager.updateTaskDiagnosticJson(EVAL_ID, createPITaskDiagnosticJson(mergePITaskMetadata(evalPi, { runnerDecision: 'approved' })));
79
+ await mkTask(ROLLOUT_ID, 'rollout_reviewer', [EVAL_ID]);
80
+ const conn = new SqliteConnection(workspaceDir);
81
+ const store = new SqlitePIArtifactStore(conn);
82
+ await store.upsertArtifact({
83
+ artifactId: 'pi-art-eval-d1', artifactKind: 'principle', sourceTaskId: EVAL_ID,
84
+ lineageArtifactIds: [], validationStatus: 'pending',
85
+ contentJson: JSON.stringify({ evaluation: { decision: 'approved', score: 0.9, strengths: [], concerns: [], requiredChanges: [] } }),
86
+ createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
87
+ });
88
+ await store.upsertArtifact({
89
+ artifactId: SCRIBE_ART, artifactKind: 'principle', sourceTaskId: SCRIBE_ID,
90
+ lineageArtifactIds: [], validationStatus: 'validated',
91
+ contentJson: JSON.stringify({ principleId: 'd1-p', text: '原则', principleDraft: { title: 'd1-p', statement: '原则' } }),
92
+ createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
93
+ });
94
+ conn.close();
95
+ return store;
96
+ }
97
+ function rolloutOutput(decision) {
98
+ return {
99
+ taskId: ROLLOUT_ID, sourceEvaluatorArtifactId: 'pi-art-eval-d1',
100
+ review: { decision, summary: 'd1', confidence: 0.9, requiredChanges: decision === 'needs_revision' ? ['必须改 X'] : [], rolloutRisks: [], safetyChecks: [] },
101
+ sourceTrace: { evaluatorArtifactId: 'pi-art-eval-d1' }, risks: [], generatedAt: new Date().toISOString(),
102
+ };
103
+ }
104
+ function evaluatorOutput(decision) {
105
+ return {
106
+ taskId: EVAL_ID, sourceArtificerArtifactId: ARTIFICER_ART,
107
+ evaluation: { decision, summary: 'd1', score: 0.9, strengths: [], concerns: [], requiredChanges: decision === 'needs_revision' ? ['必须改 X'] : [] },
108
+ sourceTrace: { artificerArtifactId: ARTIFICER_ART, scribeArtifactId: SCRIBE_ART },
109
+ risks: [], generatedAt: new Date().toISOString(),
110
+ };
111
+ }
112
+ /** 带 LLM 调用计数的 scripted adapter — drift 场景的"第二次"回答 */
113
+ function driftAdapter(payload, spy) {
114
+ return {
115
+ startRun: async () => { spy.llmCalls += 1; return { runId: 'run-drift-2', runtimeKind: 'test-double', startedAt: new Date().toISOString() }; },
116
+ pollRun: async () => ({ status: 'succeeded', runId: 'run-drift-2' }),
117
+ fetchOutput: async () => ({ runId: 'run-drift-2', payload }),
118
+ cancelRun: async () => undefined,
119
+ };
120
+ }
121
+ /**
122
+ * 手工构造真实 crash 态 (与生产原子写等价):
123
+ * run 行携带原 output → metadata 落 runnerDecision + pending completionIntent
124
+ * → 任务回到 pending (模拟 lease 过期回收)。
125
+ */
126
+ async function craftRolloutCrashState(decision, options) {
127
+ await stateManager.acquireLease({ taskId: ROLLOUT_ID, owner: 'drift', runtimeKind: 'test-double' });
128
+ const runs = await stateManager.getRunsByTask(ROLLOUT_ID);
129
+ const runId = runs[runs.length - 1]?.runId;
130
+ if (!runId)
131
+ throw new Error('craft: no run row after lease');
132
+ await stateManager.updateRunOutput(runId, JSON.stringify(rolloutOutput(decision)));
133
+ const raw = await stateManager.getTask(ROLLOUT_ID);
134
+ if (!raw)
135
+ throw new Error('craft: rollout missing');
136
+ const pi = hydratePITaskRecord(raw);
137
+ if (!pi)
138
+ throw new Error('craft: rollout not hydratable');
139
+ await stateManager.updateTaskDiagnosticJson(ROLLOUT_ID, createPITaskDiagnosticJson(mergePITaskMetadata(pi, {
140
+ runnerDecision: decision,
141
+ completionIntent: {
142
+ decision, sourceRunId: runId, revisionEpoch: pi.revisionCount ?? 0, status: 'pending',
143
+ ...(options?.revisionIteration !== undefined ? { revisionIteration: options.revisionIteration } : {}),
144
+ ...(options?.effect ? { effect: options.effect } : {}),
145
+ },
146
+ ...(options?.rolloutRevisionPayload ? { rolloutRevisionPayload: options.rolloutRevisionPayload } : {}),
147
+ })));
148
+ await stateManager.updateTask(ROLLOUT_ID, { status: 'pending', attemptCount: 0 });
149
+ return runId;
150
+ }
151
+ async function craftEvaluatorCrashState(decision, outputOverride) {
152
+ // seedLineage 后 evaluator 是 succeeded;crash 叙事里它当时 leased — 先回 pending
153
+ await stateManager.updateTask(EVAL_ID, { status: 'pending', attemptCount: 0 });
154
+ await stateManager.acquireLease({ taskId: EVAL_ID, owner: 'drift', runtimeKind: 'test-double' });
155
+ const runs = await stateManager.getRunsByTask(EVAL_ID);
156
+ const runId = runs[runs.length - 1]?.runId;
157
+ if (!runId)
158
+ throw new Error('craft: no run row after lease');
159
+ await stateManager.updateRunOutput(runId, JSON.stringify(outputOverride ?? evaluatorOutput(decision)));
160
+ const raw = await stateManager.getTask(EVAL_ID);
161
+ if (!raw)
162
+ throw new Error('craft: evaluator missing');
163
+ const pi = hydratePITaskRecord(raw);
164
+ if (!pi)
165
+ throw new Error('craft: evaluator not hydratable');
166
+ await stateManager.updateTaskDiagnosticJson(EVAL_ID, createPITaskDiagnosticJson(mergePITaskMetadata(pi, {
167
+ runnerDecision: decision,
168
+ completionIntent: { decision, sourceRunId: runId, revisionEpoch: pi.revisionCount ?? 0, status: 'pending' },
169
+ })));
170
+ await stateManager.updateTask(EVAL_ID, { status: 'pending', attemptCount: 0 });
171
+ return runId;
172
+ }
173
+ function makeRolloutRunner(adapter, deps) {
174
+ return new RolloutReviewerRunner({
175
+ stateManager, runtimeAdapter: adapter, eventEmitter: storeEmitter,
176
+ artifactStore: new SqlitePIArtifactStore(new SqliteConnection(workspaceDir)),
177
+ validator: new DefaultRolloutReviewerValidator(),
178
+ ...(deps.dispatch ? { dispatchActivation: deps.dispatch } : {}),
179
+ ...(deps.reopen ? { reopenRevisionTarget: deps.reopen } : {}),
180
+ }, { owner: 'drift', runtimeKind: 'test-double', pollIntervalMs: 5, timeoutMs: 5_000 });
181
+ }
182
+ async function readMeta(id) {
183
+ const raw = await stateManager.getTask(id);
184
+ return raw ? hydratePITaskRecord(raw) : null;
185
+ }
186
+ describe('P0 verdict drift — completion intent 是 recovery authority', () => {
187
+ it('T1 rollout revision drift: pending r1 之后 LLM 返回 approve 也不得成为 authority', async () => {
188
+ await seedLineage();
189
+ await craftRolloutCrashState('needs_revision', { revisionIteration: 1 });
190
+ const spy = { llmCalls: 0 };
191
+ const dispatchCalls = [];
192
+ const runner = makeRolloutRunner(driftAdapter(rolloutOutput('approve_rollout'), spy), {
193
+ dispatch: async (input) => { dispatchCalls.push(input.artifactId); return { decision: 'activated' }; },
194
+ reopen: async (input) => {
195
+ const r = await orchestrator.reopenTaskForRevision(input.targetTaskId, {
196
+ revisionCauseId: `rollout-${ROLLOUT_ID}-r${input.revisionIteration}`,
197
+ revisionFeedback: 'feedback',
198
+ });
199
+ return r.ok ? { ok: true, reason: r.reason } : { ok: false, reason: r.reason };
200
+ },
201
+ });
202
+ const result = await runner.run(ROLLOUT_ID);
203
+ expect(result.status).toBe('succeeded');
204
+ // LLM 根本未被调用 — 第二个 adapter 不是 authority
205
+ expect(spy.llmCalls).toBe(0);
206
+ // r1 被继续 materialize: scribe 被 reopen 一次
207
+ expect((await stateManager.getTask(SCRIBE_ID))?.status).toBe('pending');
208
+ const scribePi = await readMeta(SCRIBE_ID);
209
+ expect(scribePi?.revisionCauseId).toBe(`rollout-${ROLLOUT_ID}-r1`);
210
+ // activation = 0
211
+ expect(dispatchCalls).toEqual([]);
212
+ // 终态与 intent 一致
213
+ expect((await stateManager.getTask(ROLLOUT_ID))?.status).toBe('succeeded');
214
+ const rolloutPi = await readMeta(ROLLOUT_ID);
215
+ expect(rolloutPi?.runnerDecision).toBe('needs_revision');
216
+ expect(rolloutPi?.rolloutRevisionPayload?.status).toBe('applied');
217
+ expect(rolloutPi?.rolloutRevisionPayload?.revisionIteration).toBe(1);
218
+ expect(rolloutPi?.completionIntent?.status).toBe('applied');
219
+ });
220
+ it('T2 activation drift: activated 后 crash,LLM 返回 reject 也不得漂移 (无 reject+active)', async () => {
221
+ await seedLineage();
222
+ await craftRolloutCrashState('approve_rollout');
223
+ const spy = { llmCalls: 0 };
224
+ const dispatchCalls = [];
225
+ const runner = makeRolloutRunner(driftAdapter(rolloutOutput('reject'), spy), {
226
+ // run-1 已 activated 的 durable 效果 → 重放 dispatch = already_activated
227
+ dispatch: async () => { dispatchCalls.push(1); return { decision: 'already_activated', reason: 'idempotent_redispatch' }; },
228
+ reopen: async () => { throw new Error('T2: reopen must not be called'); },
229
+ });
230
+ const result = await runner.run(ROLLOUT_ID);
231
+ expect(result.status).toBe('succeeded');
232
+ expect(spy.llmCalls).toBe(0); // LLM 未被调用
233
+ expect(dispatchCalls.length).toBe(1); // activation 恰好一条 (幂等重放)
234
+ expect((await stateManager.getTask(ROLLOUT_ID))?.status).toBe('succeeded');
235
+ const pi = await readMeta(ROLLOUT_ID);
236
+ expect(pi?.runnerDecision).toBe('approve_rollout'); // final verdict 与 activation 一致
237
+ expect(pi?.completionIntent?.status).toBe('applied');
238
+ });
239
+ it('T3 evaluator repair drift: repair 已 seed 后 LLM 返回 approved 也不得绕过 repair', async () => {
240
+ await seedLineage();
241
+ const conn = new SqliteConnection(workspaceDir);
242
+ const artifactStore = new SqlitePIArtifactStore(conn);
243
+ // artificer 依赖产物 (repair seed lineage 用)
244
+ await artifactStore.upsertArtifact({
245
+ artifactId: ARTIFICER_ART, artifactKind: 'principle', sourceTaskId: ARTIFICER_ID,
246
+ lineageArtifactIds: [], validationStatus: 'validated',
247
+ contentJson: JSON.stringify({ plan: 'p' }),
248
+ createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
249
+ });
250
+ conn.close();
251
+ await craftEvaluatorCrashState('needs_revision');
252
+ // run-1 的 durable 副作用: 确定性 ID 的 repair 任务已 seed (crash 前发生)
253
+ const repairTaskId = `artificer-repair-${EVAL_ID}-r1`;
254
+ await stateManager.createTask({
255
+ taskId: repairTaskId, taskKind: 'artificer', status: 'pending', attemptCount: 0, maxAttempts: 3,
256
+ diagnosticJson: meta({
257
+ correlationId: 'd1', dependencyTaskIds: [SCRIBE_ID],
258
+ repairPayload: {
259
+ requiredChanges: ['必须改 X'], concerns: [], previousScore: 0.9,
260
+ repairIteration: 1, sourceArtificerArtifactId: ARTIFICER_ART, sourceEvaluatorTaskId: EVAL_ID,
261
+ },
262
+ }),
263
+ });
264
+ const spy = { llmCalls: 0 };
265
+ const seedCalls = [];
266
+ const evaluator = new EvaluatorRunner({
267
+ stateManager, runtimeAdapter: driftAdapter(evaluatorOutput('approved'), spy),
268
+ eventEmitter: storeEmitter, artifactStore: new SqlitePIArtifactStore(new SqliteConnection(workspaceDir)),
269
+ validator: new DefaultEvaluatorValidator(),
270
+ isRepairLoopEnabled: () => true,
271
+ seedArtificerRepairTask: async (params) => {
272
+ seedCalls.push(1);
273
+ // 生产接线语义: 确定性 ID 已存在 → 复用
274
+ const existing = await stateManager.getTask(repairTaskId);
275
+ if (existing)
276
+ return repairTaskId;
277
+ await stateManager.createTask({
278
+ taskId: repairTaskId, taskKind: 'artificer', status: 'pending', attemptCount: 0, maxAttempts: 3,
279
+ diagnosticJson: meta({ correlationId: 'd1', dependencyTaskIds: [SCRIBE_ID], repairPayload: params.repairPayload }),
280
+ });
281
+ return repairTaskId;
282
+ },
283
+ }, { owner: 'drift', runtimeKind: 'test-double', pollIntervalMs: 5, timeoutMs: 5_000 });
284
+ const result = await evaluator.run(EVAL_ID);
285
+ expect(result.status).toBe('succeeded');
286
+ expect(spy.llmCalls).toBe(0); // LLM 未被调用
287
+ expect((await stateManager.getTask(EVAL_ID))?.status).toBe('succeeded');
288
+ const pi = await readMeta(EVAL_ID);
289
+ expect(pi?.runnerDecision).toBe('needs_revision'); // 原 intent 被恢复
290
+ expect(pi?.completionIntent?.status).toBe('applied');
291
+ // repair 恰好一个 (确定性 ID 复用)
292
+ expect(await stateManager.getTask(repairTaskId)).not.toBeNull();
293
+ expect(seedCalls.length).toBe(1);
294
+ // approved 不得正常推进: commit 门 fail-closed (无 rollout 后继)
295
+ const commit = await orchestrator.commitNextTaskProposal(EVAL_ID);
296
+ expect(commit.decision).toBe('blocked_by_revision');
297
+ });
298
+ it('T4 evaluator validation drift: bearer 已 validated 后 LLM 返回 needs_revision 也不得漂移', async () => {
299
+ await seedLineage();
300
+ await craftEvaluatorCrashState('approved');
301
+ const spy = { llmCalls: 0 };
302
+ const seedCalls = [];
303
+ const evaluator = new EvaluatorRunner({
304
+ stateManager, runtimeAdapter: driftAdapter(evaluatorOutput('needs_revision'), spy),
305
+ eventEmitter: storeEmitter, artifactStore: new SqlitePIArtifactStore(new SqliteConnection(workspaceDir)),
306
+ validator: new DefaultEvaluatorValidator(),
307
+ isRepairLoopEnabled: () => true,
308
+ seedArtificerRepairTask: async () => { seedCalls.push(1); throw new Error('T4: seed must not be called'); },
309
+ }, { owner: 'drift', runtimeKind: 'test-double', pollIntervalMs: 5, timeoutMs: 5_000 });
310
+ const result = await evaluator.run(EVAL_ID);
311
+ expect(result.status).toBe('succeeded');
312
+ expect(spy.llmCalls).toBe(0);
313
+ expect((await stateManager.getTask(EVAL_ID))?.status).toBe('succeeded');
314
+ const pi = await readMeta(EVAL_ID);
315
+ expect(pi?.runnerDecision).toBe('approved'); // 原 intent 恢复
316
+ expect(pi?.completionIntent?.status).toBe('applied');
317
+ // 无 "validated + needs_revision" 矛盾: bearer 维持 validated,无 repair
318
+ expect(seedCalls).toEqual([]);
319
+ const conn = new SqliteConnection(workspaceDir);
320
+ const store = new SqlitePIArtifactStore(conn);
321
+ const bearer = await store.getArtifactById(SCRIBE_ART);
322
+ conn.close();
323
+ expect(bearer?.validationStatus).toBe('validated');
324
+ // approved 正常推进 (lineage 已预建 rollout 任务 → successor_exists 同为 ADVANCE)
325
+ const commit = await orchestrator.commitNextTaskProposal(EVAL_ID);
326
+ expect(commit.decision).not.toBe('blocked_by_revision');
327
+ expect(['successor_created', 'successor_exists']).toContain(commit.decision);
328
+ });
329
+ it('T5 new epoch: 真正 revision reopen 后允许不同 verdict (LLM 被调用)', async () => {
330
+ await seedLineage();
331
+ // 构造 r1 已 applied 且 rollout 被 reopen (revisionCount=1,intent/decision 已清空)
332
+ const raw = await stateManager.getTask(ROLLOUT_ID);
333
+ if (!raw)
334
+ throw new Error('T5: rollout missing');
335
+ const pi = hydratePITaskRecord(raw);
336
+ if (!pi)
337
+ throw new Error('T5: rollout not hydratable');
338
+ const payload = {
339
+ requiredChanges: ['x'], revisionIteration: 1, sourceRolloutTaskId: ROLLOUT_ID,
340
+ sourceArtifactId: SCRIBE_ART, targetTaskKind: 'scribe', status: 'applied',
341
+ };
342
+ await stateManager.updateTaskDiagnosticJson(ROLLOUT_ID, createPITaskDiagnosticJson(mergePITaskMetadata(pi, {
343
+ runnerDecision: undefined,
344
+ completionIntent: undefined,
345
+ revisionCount: 1,
346
+ rolloutRevisionPayload: payload,
347
+ })));
348
+ const spy = { llmCalls: 0 };
349
+ const dispatchCalls = [];
350
+ const runner = makeRolloutRunner(driftAdapter(rolloutOutput('approve_rollout'), spy), {
351
+ dispatch: async () => { dispatchCalls.push(1); return { decision: 'activated', activationId: 'act-t5' }; },
352
+ reopen: async () => { throw new Error('T5: reopen must not be called'); },
353
+ });
354
+ const result = await runner.run(ROLLOUT_ID);
355
+ expect(result.status).toBe('succeeded');
356
+ // 新 epoch 允许新 verdict: LLM 被调用,与上一轮 needs_revision 不同
357
+ expect(spy.llmCalls).toBe(1);
358
+ expect(dispatchCalls.length).toBe(1);
359
+ const finalPi = await readMeta(ROLLOUT_ID);
360
+ expect(finalPi?.runnerDecision).toBe('approve_rollout');
361
+ expect(finalPi?.completionIntent?.status).toBe('applied');
362
+ expect(finalPi?.completionIntent?.revisionEpoch).toBe(1);
363
+ });
364
+ it('T6 budget exhausted resume: crash before needs_human_review 写入,LLM 返回 approve 也不得漂移', async () => {
365
+ await seedLineage();
366
+ // 前置: r1/r2 已 materialized (applied iteration=2) — 新 verdict 需 budget
367
+ await craftRolloutCrashState('needs_revision', {
368
+ effect: 'needs_human_review',
369
+ rolloutRevisionPayload: {
370
+ requiredChanges: ['前两轮'], revisionIteration: 2, sourceRolloutTaskId: ROLLOUT_ID,
371
+ sourceArtifactId: SCRIBE_ART, targetTaskKind: 'scribe', status: 'applied',
372
+ },
373
+ });
374
+ const spy = { llmCalls: 0 };
375
+ const runner = makeRolloutRunner(driftAdapter(rolloutOutput('approve_rollout'), spy), {
376
+ dispatch: async () => { throw new Error('T6: dispatch must not be called'); },
377
+ reopen: async () => { throw new Error('T6: reopen must not be called'); },
378
+ });
379
+ const result = await runner.run(ROLLOUT_ID);
380
+ expect(result.status).toBe('succeeded');
381
+ // 同 epoch retry: resume needs_human_review 效果,LLM/activation/路由全禁
382
+ expect(spy.llmCalls).toBe(0);
383
+ expect((await stateManager.getTask(ROLLOUT_ID))?.status).toBe('needs_human_review');
384
+ const pi = await readMeta(ROLLOUT_ID);
385
+ expect(pi?.runnerDecision).toBe('needs_revision');
386
+ // budget 仍为 2 (未产生新修订轮)
387
+ expect(pi?.rolloutRevisionPayload?.revisionIteration).toBe(2);
388
+ expect(pi?.rolloutRevisionPayload?.status).toBe('applied');
389
+ expect(pi?.completionIntent?.status).toBe('applied');
390
+ expect(pi?.completionIntent?.effect).toBe('needs_human_review');
391
+ });
392
+ });
393
+ describe('P0-B verdict drift — rule assembly 纳入 completion effect', () => {
394
+ /** V2 (code-bearing) + adversarial passed 的 evaluator output */
395
+ function evaluatorV2Output() {
396
+ return {
397
+ taskId: EVAL_ID, sourceArtificerArtifactId: ARTIFICER_ART,
398
+ evaluation: { decision: 'approved', summary: 'v2', score: 0.9, strengths: [], concerns: [], requiredChanges: [] },
399
+ sourceTrace: { artificerArtifactId: ARTIFICER_ART, scribeArtifactId: SCRIBE_ART },
400
+ risks: [], generatedAt: new Date().toISOString(),
401
+ codeReview: {
402
+ intentConsistency: { aligned: true, explanation: 'ok' },
403
+ scopePrecision: { verdict: 'precise', explanation: 'ok' },
404
+ traceCoverage: { sufficient: true, gaps: [], explanation: 'ok' },
405
+ },
406
+ adversarialCases: [{ caseId: 'adv-1', attackType: 'boundary', toolName: 'write_file', params: { path: '/x' }, expectedDecision: 'block', rationale: 'r' }],
407
+ adversarialResult: { passed: true, failedCases: [] },
408
+ };
409
+ }
410
+ /** artificer 的 V2 产物 (implementationCode + ≥1 正例 + ≥1 负例 golden trace) */
411
+ async function seedArtificerV2Artifact() {
412
+ const conn = new SqliteConnection(workspaceDir);
413
+ const store = new SqlitePIArtifactStore(conn);
414
+ await store.upsertArtifact({
415
+ artifactId: ARTIFICER_ART, artifactKind: 'principle', sourceTaskId: ARTIFICER_ID,
416
+ lineageArtifactIds: [], validationStatus: 'validated',
417
+ contentJson: JSON.stringify({
418
+ taskId: ARTIFICER_ID, sourceScribeArtifactId: SCRIBE_ART,
419
+ implementationCode: 'function evaluate(input) { return { decision: "allow", matched: true, reason: "ok" }; }',
420
+ goldenTraceCases: [
421
+ { caseId: 'pos-1', kind: 'positive', toolName: 'write_file', params: { path: '/safe.txt' }, expectedDecision: 'allow' },
422
+ { caseId: 'neg-1', kind: 'negative', toolName: 'write_file', params: { path: '/etc/passwd' }, expectedDecision: 'block' },
423
+ ],
424
+ affectedTools: ['write_file'],
425
+ sourceTrace: { scribeArtifactId: SCRIBE_ART },
426
+ risks: [], generatedAt: new Date().toISOString(),
427
+ }),
428
+ createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
429
+ });
430
+ conn.close();
431
+ }
432
+ async function listRules() {
433
+ const conn = new SqliteConnection(workspaceDir);
434
+ const store = new SqlitePIArtifactStore(conn);
435
+ const all = await store.listBySourceTaskId(EVAL_ID);
436
+ conn.close();
437
+ return all.filter((a) => a.artifactKind === 'rule')
438
+ .map((a) => ({ artifactId: a.artifactId, validationStatus: a.validationStatus }));
439
+ }
440
+ function makeV2Evaluator(spy, payload, seedRepair) {
441
+ return new EvaluatorRunner({
442
+ stateManager, runtimeAdapter: driftAdapter(payload, spy),
443
+ eventEmitter: storeEmitter, artifactStore: new SqlitePIArtifactStore(new SqliteConnection(workspaceDir)),
444
+ validator: new DefaultEvaluatorValidator(),
445
+ isRepairLoopEnabled: () => true,
446
+ ...(seedRepair ? { seedArtificerRepairTask: seedRepair } : {}),
447
+ }, { owner: 'drift', runtimeKind: 'test-double', pollIntervalMs: 5, timeoutMs: 5_000 });
448
+ }
449
+ it('T7 assembly 完成后 crash before terminal: LLM 返回 needs_revision 也不得漂移,rule 恰一个', async () => {
450
+ await seedLineage();
451
+ await seedArtificerV2Artifact();
452
+ const runId = await craftEvaluatorCrashState('approved', evaluatorV2Output());
453
+ // run-1 的 durable 副作用: rule 已 assembled + validated,crash before terminal
454
+ const expectedRuleId = `pi-rule-${EVAL_ID}-${runId}`;
455
+ const conn = new SqliteConnection(workspaceDir);
456
+ const store = new SqlitePIArtifactStore(conn);
457
+ await store.upsertArtifact({
458
+ artifactId: expectedRuleId, artifactKind: 'rule', sourceTaskId: EVAL_ID,
459
+ sourcePrincipleId: 'd1-p', sourceRuleId: `rule-${EVAL_ID}`,
460
+ lineageArtifactIds: [], validationStatus: 'validated',
461
+ contentJson: JSON.stringify({ implementationCode: 'x', goldenTrace: { cases: [] }, goldenTraceCases: [], affectedTools: [], ruleHostGateDecision: 'accepted_shadow' }),
462
+ createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
463
+ });
464
+ conn.close();
465
+ const spy = { llmCalls: 0 };
466
+ const seedCalls = [];
467
+ const result = await makeV2Evaluator(spy, evaluatorV2Output(), async () => { seedCalls.push(1); throw new Error('T7: repair must not be seeded'); }).run(EVAL_ID);
468
+ expect(result.status).toBe('succeeded');
469
+ expect(spy.llmCalls).toBe(0); // LLM 未被调用
470
+ const pi = await readMeta(EVAL_ID);
471
+ expect(pi?.runnerDecision).toBe('approved'); // final verdict 不漂移
472
+ expect(pi?.completionIntent?.status).toBe('applied');
473
+ expect(seedCalls).toEqual([]); // 无 repair
474
+ // validated rule 恰好一个,id 与 fresh run 相同 (deterministic)
475
+ const rules = await listRules();
476
+ expect(rules.length).toBe(1);
477
+ expect(rules[0]?.artifactId).toBe(expectedRuleId);
478
+ expect(rules[0]?.validationStatus).toBe('validated');
479
+ });
480
+ it('T8 intent 持久化后、rule validated 前 crash: resume 自动恢复 assembly', async () => {
481
+ await seedLineage();
482
+ await seedArtificerV2Artifact();
483
+ const runId = await craftEvaluatorCrashState('approved', evaluatorV2Output());
484
+ // crash between intent and rule assembly — 无 rule 产物
485
+ expect((await listRules()).length).toBe(0);
486
+ const spy = { llmCalls: 0 };
487
+ const result = await makeV2Evaluator(spy, evaluatorV2Output()).run(EVAL_ID);
488
+ expect(result.status).toBe('succeeded');
489
+ expect(spy.llmCalls).toBe(0); // 不调用 LLM
490
+ // rule assembly 自动恢复: 最终 validated,evaluator succeeded
491
+ const rules = await listRules();
492
+ expect(rules.length).toBe(1);
493
+ expect(rules[0]?.artifactId).toBe(`pi-rule-${EVAL_ID}-${runId}`);
494
+ expect(rules[0]?.validationStatus).toBe('validated');
495
+ expect((await stateManager.getTask(EVAL_ID))?.status).toBe('succeeded');
496
+ const pi = await readMeta(EVAL_ID);
497
+ expect(pi?.runnerDecision).toBe('approved');
498
+ expect(pi?.completionIntent?.status).toBe('applied');
499
+ });
500
+ it('T8-reverse (approved V2 真实 fixture) intent 持久化前 crash: 无任何治理副作用落库', async () => {
501
+ await seedLineage();
502
+ await seedArtificerV2Artifact();
503
+ await stateManager.updateTask(EVAL_ID, { status: 'pending', attemptCount: 0 });
504
+ // seedLineage 为 rollout 链预置了 runnerDecision='approved' — 清空以证明
505
+ // "失败写不留痕" (assert 的 undefined 只能来自本次 abandoned attempt)
506
+ const preRaw = await stateManager.getTask(EVAL_ID);
507
+ if (!preRaw)
508
+ throw new Error('T8-reverse: evaluator missing');
509
+ const prePi = hydratePITaskRecord(preRaw);
510
+ if (!prePi)
511
+ throw new Error('T8-reverse: evaluator not hydratable');
512
+ await stateManager.updateTaskDiagnosticJson(EVAL_ID, createPITaskDiagnosticJson(mergePITaskMetadata(prePi, { runnerDecision: undefined, completionIntent: undefined })));
513
+ // bearer 起始 pending: 若 approved effects 在 intent 前执行过,会被翻 validated
514
+ const conn0 = new SqliteConnection(workspaceDir);
515
+ const store0 = new SqlitePIArtifactStore(conn0);
516
+ await store0.updateValidationStatus(SCRIBE_ART, 'pending');
517
+ conn0.close();
518
+ // 注入: completion intent 的 metadata 写失败 (crash-before-intent 等价)。
519
+ // fixture 是真实的 approved V2 + adversarial passed (PART B 修复: 此前
520
+ // 误用 needs_revision V1,从未证明 approved V2 ordering)。
521
+ const inner = stateManager;
522
+ const orig = inner.updateTaskDiagnosticJson;
523
+ inner.updateTaskDiagnosticJson = async (taskId, json) => {
524
+ if (taskId === EVAL_ID && json.includes('"completionIntent"')) {
525
+ throw new Error('injected completion intent write failure');
526
+ }
527
+ return orig.call(stateManager, taskId, json);
528
+ };
529
+ const spy = { llmCalls: 0 };
530
+ const seedCalls = [];
531
+ const result = await makeV2Evaluator(spy, evaluatorV2Output(), async () => {
532
+ seedCalls.push(1);
533
+ throw new Error('T8-reverse: repair must not be seeded');
534
+ }).run(EVAL_ID);
535
+ // evaluator 不得 succeeded
536
+ expect(result.status).not.toBe('succeeded');
537
+ const pi = await readMeta(EVAL_ID);
538
+ // 原子写失败 ⇒ decision + intent 均未落库,无 durable completion
539
+ expect(pi?.completionIntent).toBeUndefined();
540
+ expect(pi?.runnerDecision).toBeUndefined();
541
+ // validated rule 数量 = 0 (rule assembly 在 intent 后,intent 未落则绝不执行)
542
+ expect((await listRules()).filter((r) => r.validationStatus === 'validated')).toEqual([]);
543
+ // scribe bearer 不得因本次 abandoned completion 获得治理状态变化
544
+ const conn = new SqliteConnection(workspaceDir);
545
+ const store = new SqlitePIArtifactStore(conn);
546
+ const bearer = await store.getArtifactById(SCRIBE_ART);
547
+ conn.close();
548
+ expect(bearer?.validationStatus).toBe('pending');
549
+ // repair task = 0
550
+ expect(seedCalls).toEqual([]);
551
+ });
552
+ it('T6b needs_human_review materialization 失败: intent 不 applied,恢复后 resume 同一 effect (零 LLM)', async () => {
553
+ await seedLineage();
554
+ await craftRolloutCrashState('needs_revision', {
555
+ effect: 'needs_human_review',
556
+ rolloutRevisionPayload: {
557
+ requiredChanges: ['前两轮'], revisionIteration: 2, sourceRolloutTaskId: ROLLOUT_ID,
558
+ sourceArtifactId: SCRIBE_ART, targetTaskKind: 'scribe', status: 'applied',
559
+ },
560
+ });
561
+ // 注入: updateTask(status=needs_human_review) 一次性失败
562
+ const inner = stateManager;
563
+ const origUpdate = inner.updateTask;
564
+ let failuresLeft = 1;
565
+ inner.updateTask = async (taskId, patch) => {
566
+ if (taskId === ROLLOUT_ID && patch.status === 'needs_human_review' && failuresLeft > 0) {
567
+ failuresLeft -= 1;
568
+ throw new Error('injected needs_human_review write failure');
569
+ }
570
+ return origUpdate.call(stateManager, taskId, patch);
571
+ };
572
+ const spy = { llmCalls: 0 };
573
+ const runner1 = makeRolloutRunner(driftAdapter(rolloutOutput('approve_rollout'), spy), {
574
+ dispatch: async () => { throw new Error('T6b: dispatch must not be called'); },
575
+ reopen: async () => { throw new Error('T6b: reopen must not be called'); },
576
+ });
577
+ // 第一次 resume: materialize 失败 → 不正常完成
578
+ const r1 = await runner1.run(ROLLOUT_ID);
579
+ expect(r1.status).not.toBe('succeeded');
580
+ // intent 仍 pending (INV-2: effect 未 durable 不得 applied)
581
+ const pi1 = await readMeta(ROLLOUT_ID);
582
+ expect(pi1?.completionIntent?.status).toBe('pending');
583
+ expect((await stateManager.getTask(ROLLOUT_ID))?.status).not.toBe('needs_human_review');
584
+ expect(spy.llmCalls).toBe(0);
585
+ // 第二次 run: DB 恢复 → resume 同一 effect,LLM 总数仍 0
586
+ const runner2 = makeRolloutRunner(driftAdapter(rolloutOutput('approve_rollout'), spy), {
587
+ dispatch: async () => { throw new Error('T6b: dispatch must not be called'); },
588
+ reopen: async () => { throw new Error('T6b: reopen must not be called'); },
589
+ });
590
+ const r2 = await runner2.run(ROLLOUT_ID);
591
+ expect(r2.status).toBe('succeeded');
592
+ expect(spy.llmCalls).toBe(0);
593
+ expect((await stateManager.getTask(ROLLOUT_ID))?.status).toBe('needs_human_review');
594
+ const pi2 = await readMeta(ROLLOUT_ID);
595
+ expect(pi2?.runnerDecision).toBe('needs_revision');
596
+ expect(pi2?.completionIntent?.status).toBe('applied');
597
+ // revision budget 保持不变
598
+ expect(pi2?.rolloutRevisionPayload?.revisionIteration).toBe(2);
599
+ });
600
+ });
601
+ //# sourceMappingURL=verdict-drift-regressions.test.js.map