@principles/pd-cli 1.73.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.
Files changed (298) hide show
  1. package/README.md +90 -0
  2. package/dist/commands/artifact.d.ts +14 -0
  3. package/dist/commands/artifact.d.ts.map +1 -0
  4. package/dist/commands/artifact.js +67 -0
  5. package/dist/commands/artifact.js.map +1 -0
  6. package/dist/commands/candidate.d.ts +83 -0
  7. package/dist/commands/candidate.d.ts.map +1 -0
  8. package/dist/commands/candidate.js +891 -0
  9. package/dist/commands/candidate.js.map +1 -0
  10. package/dist/commands/central-sync.d.ts +10 -0
  11. package/dist/commands/central-sync.d.ts.map +1 -0
  12. package/dist/commands/central-sync.js +32 -0
  13. package/dist/commands/central-sync.js.map +1 -0
  14. package/dist/commands/console.d.ts +9 -0
  15. package/dist/commands/console.d.ts.map +1 -0
  16. package/dist/commands/console.js +114 -0
  17. package/dist/commands/console.js.map +1 -0
  18. package/dist/commands/context.d.ts +7 -0
  19. package/dist/commands/context.d.ts.map +1 -0
  20. package/dist/commands/context.js +55 -0
  21. package/dist/commands/context.js.map +1 -0
  22. package/dist/commands/demo-story-a.d.ts +12 -0
  23. package/dist/commands/demo-story-a.d.ts.map +1 -0
  24. package/dist/commands/demo-story-a.js +175 -0
  25. package/dist/commands/demo-story-a.js.map +1 -0
  26. package/dist/commands/diagnose.d.ts +35 -0
  27. package/dist/commands/diagnose.d.ts.map +1 -0
  28. package/dist/commands/diagnose.js +390 -0
  29. package/dist/commands/diagnose.js.map +1 -0
  30. package/dist/commands/evolution-tasks-list.d.ts +15 -0
  31. package/dist/commands/evolution-tasks-list.d.ts.map +1 -0
  32. package/dist/commands/evolution-tasks-list.js +34 -0
  33. package/dist/commands/evolution-tasks-list.js.map +1 -0
  34. package/dist/commands/evolution-tasks-show.d.ts +14 -0
  35. package/dist/commands/evolution-tasks-show.d.ts.map +1 -0
  36. package/dist/commands/evolution-tasks-show.js +52 -0
  37. package/dist/commands/evolution-tasks-show.js.map +1 -0
  38. package/dist/commands/flow.d.ts +7 -0
  39. package/dist/commands/flow.d.ts.map +1 -0
  40. package/dist/commands/flow.js +57 -0
  41. package/dist/commands/flow.js.map +1 -0
  42. package/dist/commands/health.d.ts +16 -0
  43. package/dist/commands/health.d.ts.map +1 -0
  44. package/dist/commands/health.js +150 -0
  45. package/dist/commands/health.js.map +1 -0
  46. package/dist/commands/history.d.ts +11 -0
  47. package/dist/commands/history.d.ts.map +1 -0
  48. package/dist/commands/history.js +50 -0
  49. package/dist/commands/history.js.map +1 -0
  50. package/dist/commands/legacy-cleanup.d.ts +27 -0
  51. package/dist/commands/legacy-cleanup.d.ts.map +1 -0
  52. package/dist/commands/legacy-cleanup.js +171 -0
  53. package/dist/commands/legacy-cleanup.js.map +1 -0
  54. package/dist/commands/legacy-import.d.ts +7 -0
  55. package/dist/commands/legacy-import.d.ts.map +1 -0
  56. package/dist/commands/legacy-import.js +86 -0
  57. package/dist/commands/legacy-import.js.map +1 -0
  58. package/dist/commands/pain-record.d.ts +10 -0
  59. package/dist/commands/pain-record.d.ts.map +1 -0
  60. package/dist/commands/pain-record.js +162 -0
  61. package/dist/commands/pain-record.js.map +1 -0
  62. package/dist/commands/proven-channel-baseline.d.ts +12 -0
  63. package/dist/commands/proven-channel-baseline.d.ts.map +1 -0
  64. package/dist/commands/proven-channel-baseline.js +97 -0
  65. package/dist/commands/proven-channel-baseline.js.map +1 -0
  66. package/dist/commands/remediation-output.d.ts +40 -0
  67. package/dist/commands/remediation-output.d.ts.map +1 -0
  68. package/dist/commands/remediation-output.js +23 -0
  69. package/dist/commands/remediation-output.js.map +1 -0
  70. package/dist/commands/run.d.ts +10 -0
  71. package/dist/commands/run.d.ts.map +1 -0
  72. package/dist/commands/run.js +68 -0
  73. package/dist/commands/run.js.map +1 -0
  74. package/dist/commands/runtime-activation.d.ts +11 -0
  75. package/dist/commands/runtime-activation.d.ts.map +1 -0
  76. package/dist/commands/runtime-activation.js +150 -0
  77. package/dist/commands/runtime-activation.js.map +1 -0
  78. package/dist/commands/runtime-canary.d.ts +30 -0
  79. package/dist/commands/runtime-canary.d.ts.map +1 -0
  80. package/dist/commands/runtime-canary.js +343 -0
  81. package/dist/commands/runtime-canary.js.map +1 -0
  82. package/dist/commands/runtime-diagnostics-export.d.ts +20 -0
  83. package/dist/commands/runtime-diagnostics-export.d.ts.map +1 -0
  84. package/dist/commands/runtime-diagnostics-export.js +177 -0
  85. package/dist/commands/runtime-diagnostics-export.js.map +1 -0
  86. package/dist/commands/runtime-features.d.ts +26 -0
  87. package/dist/commands/runtime-features.d.ts.map +1 -0
  88. package/dist/commands/runtime-features.js +70 -0
  89. package/dist/commands/runtime-features.js.map +1 -0
  90. package/dist/commands/runtime-gfi-snapshot.d.ts +7 -0
  91. package/dist/commands/runtime-gfi-snapshot.d.ts.map +1 -0
  92. package/dist/commands/runtime-gfi-snapshot.js +101 -0
  93. package/dist/commands/runtime-gfi-snapshot.js.map +1 -0
  94. package/dist/commands/runtime-health-snapshot.d.ts +7 -0
  95. package/dist/commands/runtime-health-snapshot.d.ts.map +1 -0
  96. package/dist/commands/runtime-health-snapshot.js +93 -0
  97. package/dist/commands/runtime-health-snapshot.js.map +1 -0
  98. package/dist/commands/runtime-idle-trigger.d.ts +12 -0
  99. package/dist/commands/runtime-idle-trigger.d.ts.map +1 -0
  100. package/dist/commands/runtime-idle-trigger.js +102 -0
  101. package/dist/commands/runtime-idle-trigger.js.map +1 -0
  102. package/dist/commands/runtime-internalization-enqueue-successors.d.ts +9 -0
  103. package/dist/commands/runtime-internalization-enqueue-successors.d.ts.map +1 -0
  104. package/dist/commands/runtime-internalization-enqueue-successors.js +393 -0
  105. package/dist/commands/runtime-internalization-enqueue-successors.js.map +1 -0
  106. package/dist/commands/runtime-internalization-integrity-repair.d.ts +9 -0
  107. package/dist/commands/runtime-internalization-integrity-repair.d.ts.map +1 -0
  108. package/dist/commands/runtime-internalization-integrity-repair.js +54 -0
  109. package/dist/commands/runtime-internalization-integrity-repair.js.map +1 -0
  110. package/dist/commands/runtime-internalization-integrity.d.ts +7 -0
  111. package/dist/commands/runtime-internalization-integrity.d.ts.map +1 -0
  112. package/dist/commands/runtime-internalization-integrity.js +53 -0
  113. package/dist/commands/runtime-internalization-integrity.js.map +1 -0
  114. package/dist/commands/runtime-internalization-queue.d.ts +7 -0
  115. package/dist/commands/runtime-internalization-queue.d.ts.map +1 -0
  116. package/dist/commands/runtime-internalization-queue.js +85 -0
  117. package/dist/commands/runtime-internalization-queue.js.map +1 -0
  118. package/dist/commands/runtime-internalization-run-once.d.ts +12 -0
  119. package/dist/commands/runtime-internalization-run-once.d.ts.map +1 -0
  120. package/dist/commands/runtime-internalization-run-once.js +546 -0
  121. package/dist/commands/runtime-internalization-run-once.js.map +1 -0
  122. package/dist/commands/runtime-internalization-wake-once.d.ts +8 -0
  123. package/dist/commands/runtime-internalization-wake-once.d.ts.map +1 -0
  124. package/dist/commands/runtime-internalization-wake-once.js +72 -0
  125. package/dist/commands/runtime-internalization-wake-once.js.map +1 -0
  126. package/dist/commands/runtime-pain-flood-simulation.d.ts +10 -0
  127. package/dist/commands/runtime-pain-flood-simulation.d.ts.map +1 -0
  128. package/dist/commands/runtime-pain-flood-simulation.js +104 -0
  129. package/dist/commands/runtime-pain-flood-simulation.js.map +1 -0
  130. package/dist/commands/runtime-pruning.d.ts +45 -0
  131. package/dist/commands/runtime-pruning.d.ts.map +1 -0
  132. package/dist/commands/runtime-pruning.js +355 -0
  133. package/dist/commands/runtime-pruning.js.map +1 -0
  134. package/dist/commands/runtime-recovery.d.ts +9 -0
  135. package/dist/commands/runtime-recovery.d.ts.map +1 -0
  136. package/dist/commands/runtime-recovery.js +94 -0
  137. package/dist/commands/runtime-recovery.js.map +1 -0
  138. package/dist/commands/runtime-synthetic-baseline.d.ts +7 -0
  139. package/dist/commands/runtime-synthetic-baseline.d.ts.map +1 -0
  140. package/dist/commands/runtime-synthetic-baseline.js +59 -0
  141. package/dist/commands/runtime-synthetic-baseline.js.map +1 -0
  142. package/dist/commands/runtime-uat.d.ts +52 -0
  143. package/dist/commands/runtime-uat.d.ts.map +1 -0
  144. package/dist/commands/runtime-uat.js +274 -0
  145. package/dist/commands/runtime-uat.js.map +1 -0
  146. package/dist/commands/runtime.d.ts +20 -0
  147. package/dist/commands/runtime.d.ts.map +1 -0
  148. package/dist/commands/runtime.js +256 -0
  149. package/dist/commands/runtime.js.map +1 -0
  150. package/dist/commands/samples-list.d.ts +11 -0
  151. package/dist/commands/samples-list.d.ts.map +1 -0
  152. package/dist/commands/samples-list.js +37 -0
  153. package/dist/commands/samples-list.js.map +1 -0
  154. package/dist/commands/samples-review.d.ts +14 -0
  155. package/dist/commands/samples-review.d.ts.map +1 -0
  156. package/dist/commands/samples-review.js +22 -0
  157. package/dist/commands/samples-review.js.map +1 -0
  158. package/dist/commands/task.d.ts +14 -0
  159. package/dist/commands/task.d.ts.map +1 -0
  160. package/dist/commands/task.js +92 -0
  161. package/dist/commands/task.js.map +1 -0
  162. package/dist/commands/trace.d.ts +19 -0
  163. package/dist/commands/trace.d.ts.map +1 -0
  164. package/dist/commands/trace.js +154 -0
  165. package/dist/commands/trace.js.map +1 -0
  166. package/dist/commands/trajectory.d.ts +11 -0
  167. package/dist/commands/trajectory.d.ts.map +1 -0
  168. package/dist/commands/trajectory.js +47 -0
  169. package/dist/commands/trajectory.js.map +1 -0
  170. package/dist/index.d.ts +9 -0
  171. package/dist/index.d.ts.map +1 -0
  172. package/dist/index.js +736 -0
  173. package/dist/index.js.map +1 -0
  174. package/dist/legacy/legacy-import.d.ts +15 -0
  175. package/dist/legacy/legacy-import.d.ts.map +1 -0
  176. package/dist/legacy/legacy-import.js +141 -0
  177. package/dist/legacy/legacy-import.js.map +1 -0
  178. package/dist/legacy/session-history-import.d.ts +26 -0
  179. package/dist/legacy/session-history-import.d.ts.map +1 -0
  180. package/dist/legacy/session-history-import.js +151 -0
  181. package/dist/legacy/session-history-import.js.map +1 -0
  182. package/dist/principle-tree-ledger-adapter.d.ts +12 -0
  183. package/dist/principle-tree-ledger-adapter.d.ts.map +1 -0
  184. package/dist/principle-tree-ledger-adapter.js +12 -0
  185. package/dist/principle-tree-ledger-adapter.js.map +1 -0
  186. package/dist/resolve-workspace.d.ts +12 -0
  187. package/dist/resolve-workspace.d.ts.map +1 -0
  188. package/dist/resolve-workspace.js +20 -0
  189. package/dist/resolve-workspace.js.map +1 -0
  190. package/dist/services/demo-story-a-runner.d.ts +8 -0
  191. package/dist/services/demo-story-a-runner.d.ts.map +1 -0
  192. package/dist/services/demo-story-a-runner.js +369 -0
  193. package/dist/services/demo-story-a-runner.js.map +1 -0
  194. package/dist/services/feature-flag-loader.d.ts +6 -0
  195. package/dist/services/feature-flag-loader.d.ts.map +1 -0
  196. package/dist/services/feature-flag-loader.js +54 -0
  197. package/dist/services/feature-flag-loader.js.map +1 -0
  198. package/dist/services/pain-flood-simulation-runner.d.ts +10 -0
  199. package/dist/services/pain-flood-simulation-runner.d.ts.map +1 -0
  200. package/dist/services/pain-flood-simulation-runner.js +289 -0
  201. package/dist/services/pain-flood-simulation-runner.js.map +1 -0
  202. package/dist/services/proven-channel-baseline-runner.d.ts +12 -0
  203. package/dist/services/proven-channel-baseline-runner.d.ts.map +1 -0
  204. package/dist/services/proven-channel-baseline-runner.js +114 -0
  205. package/dist/services/proven-channel-baseline-runner.js.map +1 -0
  206. package/dist/services/synthetic-baseline-runner.d.ts +8 -0
  207. package/dist/services/synthetic-baseline-runner.d.ts.map +1 -0
  208. package/dist/services/synthetic-baseline-runner.js +251 -0
  209. package/dist/services/synthetic-baseline-runner.js.map +1 -0
  210. package/package.json +35 -0
  211. package/src/commands/artifact.ts +82 -0
  212. package/src/commands/candidate.ts +1117 -0
  213. package/src/commands/central-sync.ts +44 -0
  214. package/src/commands/console.ts +121 -0
  215. package/src/commands/context.ts +72 -0
  216. package/src/commands/demo-story-a.ts +195 -0
  217. package/src/commands/diagnose.ts +452 -0
  218. package/src/commands/evolution-tasks-list.ts +44 -0
  219. package/src/commands/evolution-tasks-show.ts +60 -0
  220. package/src/commands/flow.ts +60 -0
  221. package/src/commands/health.ts +189 -0
  222. package/src/commands/history.ts +63 -0
  223. package/src/commands/legacy-cleanup.ts +206 -0
  224. package/src/commands/legacy-import.ts +104 -0
  225. package/src/commands/pain-record.ts +167 -0
  226. package/src/commands/proven-channel-baseline.ts +113 -0
  227. package/src/commands/remediation-output.ts +66 -0
  228. package/src/commands/run.ts +89 -0
  229. package/src/commands/runtime-activation.ts +176 -0
  230. package/src/commands/runtime-canary.ts +371 -0
  231. package/src/commands/runtime-diagnostics-export.ts +229 -0
  232. package/src/commands/runtime-features.ts +103 -0
  233. package/src/commands/runtime-gfi-snapshot.ts +135 -0
  234. package/src/commands/runtime-health-snapshot.ts +106 -0
  235. package/src/commands/runtime-internalization-enqueue-successors.ts +479 -0
  236. package/src/commands/runtime-internalization-integrity-repair.ts +69 -0
  237. package/src/commands/runtime-internalization-integrity.ts +63 -0
  238. package/src/commands/runtime-internalization-queue.ts +106 -0
  239. package/src/commands/runtime-internalization-run-once.ts +658 -0
  240. package/src/commands/runtime-internalization-wake-once.ts +87 -0
  241. package/src/commands/runtime-pain-flood-simulation.ts +121 -0
  242. package/src/commands/runtime-pruning.ts +438 -0
  243. package/src/commands/runtime-recovery.ts +107 -0
  244. package/src/commands/runtime-synthetic-baseline.ts +70 -0
  245. package/src/commands/runtime-uat.ts +339 -0
  246. package/src/commands/runtime.ts +281 -0
  247. package/src/commands/samples-list.ts +43 -0
  248. package/src/commands/samples-review.ts +32 -0
  249. package/src/commands/task.ts +130 -0
  250. package/src/commands/trace.ts +174 -0
  251. package/src/commands/trajectory.ts +64 -0
  252. package/src/index.ts +829 -0
  253. package/src/legacy/legacy-import.ts +179 -0
  254. package/src/legacy/session-history-import.ts +231 -0
  255. package/src/principle-tree-ledger-adapter.ts +13 -0
  256. package/src/resolve-workspace.ts +20 -0
  257. package/src/services/demo-story-a-runner.ts +472 -0
  258. package/src/services/feature-flag-loader.ts +73 -0
  259. package/src/services/pain-flood-simulation-runner.ts +354 -0
  260. package/src/services/proven-channel-baseline-runner.ts +150 -0
  261. package/src/services/synthetic-baseline-runner.ts +291 -0
  262. package/tests/commands/candidate-audit-repair.test.ts +338 -0
  263. package/tests/commands/candidate-intake.test.ts +589 -0
  264. package/tests/commands/candidate-internalization-backfill.test.ts +480 -0
  265. package/tests/commands/candidate-internalize.test.ts +272 -0
  266. package/tests/commands/candidate-route.test.ts +328 -0
  267. package/tests/commands/candidate-show.test.ts +95 -0
  268. package/tests/commands/cli-command-tree.test.ts +64 -0
  269. package/tests/commands/context.test.ts +114 -0
  270. package/tests/commands/demo-story-a.test.ts +255 -0
  271. package/tests/commands/diagnose.test.ts +792 -0
  272. package/tests/commands/health.test.ts +330 -0
  273. package/tests/commands/pain-record.test.ts +316 -0
  274. package/tests/commands/plugin-config-resolution-cutover.test.ts +220 -0
  275. package/tests/commands/proven-channel-baseline.test.ts +441 -0
  276. package/tests/commands/runtime-activation.test.ts +168 -0
  277. package/tests/commands/runtime-canary.test.ts +369 -0
  278. package/tests/commands/runtime-diagnostics-export.test.ts +170 -0
  279. package/tests/commands/runtime-features.test.ts +114 -0
  280. package/tests/commands/runtime-health-snapshot.test.ts +357 -0
  281. package/tests/commands/runtime-internalization-enqueue-successors.test.ts +803 -0
  282. package/tests/commands/runtime-internalization-integrity-repair.test.ts +169 -0
  283. package/tests/commands/runtime-internalization-integrity.test.ts +102 -0
  284. package/tests/commands/runtime-internalization-queue.test.ts +252 -0
  285. package/tests/commands/runtime-internalization-run-once.test.ts +1318 -0
  286. package/tests/commands/runtime-internalization-wake-once.test.ts +170 -0
  287. package/tests/commands/runtime-internalization.test.ts +52 -0
  288. package/tests/commands/runtime-pain-flood-simulation.test.ts +418 -0
  289. package/tests/commands/runtime-pruning.test.ts +693 -0
  290. package/tests/commands/runtime-recovery.test.ts +96 -0
  291. package/tests/commands/runtime-synthetic-baseline.test.ts +249 -0
  292. package/tests/commands/runtime-uat.test.ts +397 -0
  293. package/tests/commands/runtime.test.ts +262 -0
  294. package/tests/commands/trace.test.ts +314 -0
  295. package/tests/e2e/candidate-intake-e2e.test.ts +316 -0
  296. package/tests/services/feature-flag-loader.test.ts +207 -0
  297. package/tests/services/proven-channel-baseline-runner.test.ts +30 -0
  298. package/tsconfig.json +26 -0
@@ -0,0 +1,291 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { RuntimeStateManager } from '@principles/core/runtime-v2';
4
+ import { SqliteContextAssembler } from '@principles/core/runtime-v2';
5
+ import { SqliteHistoryQuery } from '@principles/core/runtime-v2';
6
+ import { StoreEventEmitter } from '@principles/core/runtime-v2';
7
+ import { DiagnosticianRunner } from '@principles/core/runtime-v2';
8
+ import { PassThroughValidator } from '@principles/core/runtime-v2';
9
+ import { SqliteDiagnosticianCommitter } from '@principles/core/runtime-v2';
10
+ import { TestDoubleRuntimeAdapter } from '@principles/core/runtime-v2';
11
+ import { PainSignalBridge } from '@principles/core/runtime-v2';
12
+ import { CandidateIntakeService } from '@principles/core/runtime-v2';
13
+ import { PrincipleTreeLedgerAdapter } from '@principles/core/runtime-v2';
14
+ import { auditCandidateLedgerConsistency } from '@principles/core/runtime-v2';
15
+ import { OperatorHealthReadModel } from '@principles/core/runtime-v2';
16
+ import { createInternalizationQueueReadModel } from '@principles/core/runtime-v2';
17
+ import { createPITaskDiagnosticJson } from '@principles/core/runtime-v2';
18
+ import {
19
+ computeOverallStatus,
20
+ boundedEvidence,
21
+ truncateReason,
22
+ recommendNextIssue,
23
+ makeDeterministicDiagnosticianOutput,
24
+ } from '@principles/core/runtime-v2';
25
+ import type {
26
+ SyntheticBaselineSummary,
27
+ SyntheticBaselineStage,
28
+ SyntheticBaselineStageName,
29
+ SyntheticBaselineFailStage,
30
+ } from '@principles/core/runtime-v2';
31
+
32
+ export interface SyntheticBaselineRunnerOptions {
33
+ workspaceDir: string;
34
+ workspaceMode: 'temp' | 'explicit_workspace';
35
+ failAfterStage?: SyntheticBaselineFailStage;
36
+ }
37
+
38
+ export async function runSyntheticBaseline(opts: SyntheticBaselineRunnerOptions): Promise<SyntheticBaselineSummary> {
39
+ const { workspaceDir, workspaceMode, failAfterStage } = opts;
40
+ const stages: SyntheticBaselineStage[] = [];
41
+ const generatedAt = new Date().toISOString();
42
+
43
+ const painId = `synth-pain-${Date.now()}`;
44
+ const diagnosticianOutput = makeDeterministicDiagnosticianOutput(painId);
45
+
46
+ let stateManager: RuntimeStateManager | null = null;
47
+
48
+ try {
49
+ const pdDir = path.join(workspaceDir, '.pd');
50
+ const stateDir = path.join(workspaceDir, '.state');
51
+ await fs.promises.mkdir(pdDir, { recursive: true });
52
+ await fs.promises.mkdir(stateDir, { recursive: true });
53
+
54
+ stateManager = new RuntimeStateManager({ workspaceDir });
55
+ await stateManager.initialize();
56
+
57
+ if (failAfterStage === 'before_pain_intake') {
58
+ stages.push({
59
+ name: 'pain_intake',
60
+ status: 'failed',
61
+ reason: truncateReason('Injected failure: pain intake stage forced to fail for testing'),
62
+ });
63
+ for (const name of ['diagnostician_task_created', 'candidate_created', 'ledger_consistent', 'internalization_queue_ready', 'canary_health'] as SyntheticBaselineStageName[]) {
64
+ stages.push({ name, status: 'failed', reason: 'Prerequisite stage pain_intake failed' });
65
+ }
66
+ const status = computeOverallStatus(stages);
67
+ return { status, workspaceMode, generatedAt, stages, recommendedNextIssue: recommendNextIssue(stages) };
68
+ }
69
+
70
+ const { connection: sqliteConn, taskStore, runStore } = stateManager;
71
+ const historyQuery = new SqliteHistoryQuery(sqliteConn);
72
+ const contextAssembler = new SqliteContextAssembler(taskStore, historyQuery, runStore);
73
+ const eventEmitter = new StoreEventEmitter();
74
+ const committer = new SqliteDiagnosticianCommitter(sqliteConn);
75
+ const validator = new PassThroughValidator();
76
+
77
+ const runtimeAdapter = new TestDoubleRuntimeAdapter(
78
+ { onFetchOutput: () => ({ runId: `synth-${painId}`, payload: diagnosticianOutput }) },
79
+ `diagnosis_${painId}`,
80
+ );
81
+
82
+ const runner = new DiagnosticianRunner(
83
+ {
84
+ stateManager,
85
+ contextAssembler,
86
+ runtimeAdapter,
87
+ eventEmitter,
88
+ validator,
89
+ committer,
90
+ },
91
+ {
92
+ owner: 'synthetic-baseline',
93
+ runtimeKind: 'test-double',
94
+ pollIntervalMs: 50,
95
+ timeoutMs: 10000,
96
+ },
97
+ );
98
+
99
+ const ledgerAdapter = new PrincipleTreeLedgerAdapter({ stateDir });
100
+ const intakeService = new CandidateIntakeService({ stateManager, ledgerAdapter });
101
+
102
+ const bridge = new PainSignalBridge({
103
+ stateManager,
104
+ runner,
105
+ intakeService,
106
+ ledgerAdapter,
107
+ autoIntakeEnabled: true,
108
+ });
109
+
110
+ const bridgeResult = await bridge.onPainDetected({
111
+ painId,
112
+ painType: 'tool_failure',
113
+ source: 'synthetic-baseline',
114
+ reason: 'Synthetic baseline deterministic pain signal',
115
+ });
116
+
117
+ const painPassed = bridgeResult.status === 'succeeded';
118
+ stages.push({
119
+ name: 'pain_intake',
120
+ status: painPassed ? 'passed' : 'failed',
121
+ ...(painPassed ? {} : { reason: truncateReason(bridgeResult.message ?? `PainSignalBridge returned status=${bridgeResult.status}`) }),
122
+ evidence: boundedEvidence({
123
+ painId: bridgeResult.painId,
124
+ bridgeStatus: bridgeResult.status,
125
+ }),
126
+ });
127
+
128
+ if (failAfterStage === 'after_pain_intake') {
129
+ stages.push({ name: 'diagnostician_task_created', status: 'failed', reason: 'Injected failure: forced fail after pain_intake' });
130
+ stages.push({ name: 'candidate_created', status: 'failed', reason: 'Prerequisite stage diagnostician_task_created failed' });
131
+ stages.push({ name: 'ledger_consistent', status: 'failed', reason: 'Prerequisite stage candidate_created failed' });
132
+ stages.push({ name: 'internalization_queue_ready', status: 'failed', reason: 'Prerequisite stage ledger_consistent failed' });
133
+ stages.push({ name: 'canary_health', status: 'failed', reason: 'Prerequisite stage internalization_queue_ready failed' });
134
+ const status = computeOverallStatus(stages);
135
+ return { status, workspaceMode, generatedAt, stages, recommendedNextIssue: recommendNextIssue(stages) };
136
+ }
137
+
138
+ const { taskId } = bridgeResult;
139
+ const task = await stateManager.getTask(taskId);
140
+ const taskPassed = task !== null && task.status === 'succeeded';
141
+ stages.push({
142
+ name: 'diagnostician_task_created',
143
+ status: taskPassed ? 'passed' : 'failed',
144
+ ...(taskPassed ? {} : { reason: truncateReason(`Task ${taskId} not found or not succeeded. task=${task ? task.status : 'null'}`) }),
145
+ evidence: boundedEvidence({
146
+ taskId,
147
+ taskStatus: task?.status ?? 'not_found',
148
+ }),
149
+ });
150
+
151
+ const candidates = await stateManager.getCandidatesByTaskId(taskId);
152
+ const candidatePassed = candidates.length > 0;
153
+ stages.push({
154
+ name: 'candidate_created',
155
+ status: candidatePassed ? 'passed' : 'failed',
156
+ ...(candidatePassed ? {} : { reason: truncateReason(`No candidates found for task ${taskId}`) }),
157
+ evidence: boundedEvidence({
158
+ candidateCount: candidates.length,
159
+ candidateIds: candidates.map(c => c.candidateId).slice(0, 5),
160
+ }),
161
+ });
162
+
163
+ if (failAfterStage === 'after_candidate_created') {
164
+ stages.push({ name: 'ledger_consistent', status: 'failed', reason: 'Injected failure: forced fail after candidate_created' });
165
+ stages.push({ name: 'internalization_queue_ready', status: 'failed', reason: 'Prerequisite stage ledger_consistent failed' });
166
+ stages.push({ name: 'canary_health', status: 'failed', reason: 'Prerequisite stage internalization_queue_ready failed' });
167
+ const status = computeOverallStatus(stages);
168
+ return { status, workspaceMode, generatedAt, stages, recommendedNextIssue: recommendNextIssue(stages) };
169
+ }
170
+
171
+ const auditResult = await auditCandidateLedgerConsistency(workspaceDir);
172
+ const ledgerPassed = auditResult.status === 'ok';
173
+ stages.push({
174
+ name: 'ledger_consistent',
175
+ status: ledgerPassed ? 'passed' : 'failed',
176
+ ...(ledgerPassed ? {} : { reason: truncateReason(`Ledger audit status=${auditResult.status}. orphanCandidates=${auditResult.orphanCandidateCount} missingLedger=${auditResult.missingLedgerCount}`) }),
177
+ evidence: boundedEvidence({
178
+ auditStatus: auditResult.status,
179
+ consumedCount: auditResult.consumedCount,
180
+ orphanCandidateCount: auditResult.orphanCandidateCount,
181
+ missingLedgerCount: auditResult.missingLedgerCount,
182
+ }),
183
+ });
184
+
185
+ if (failAfterStage === 'after_ledger_consistent') {
186
+ stages.push({ name: 'internalization_queue_ready', status: 'failed', reason: 'Injected failure: forced fail after ledger_consistent' });
187
+ stages.push({ name: 'canary_health', status: 'failed', reason: 'Prerequisite stage internalization_queue_ready failed' });
188
+ const status = computeOverallStatus(stages);
189
+ return { status, workspaceMode, generatedAt, stages, recommendedNextIssue: recommendNextIssue(stages) };
190
+ }
191
+
192
+ const piTaskId = `synth-dreamer-${Date.now()}`;
193
+ await stateManager.createTask({
194
+ taskId: piTaskId,
195
+ taskKind: 'dreamer',
196
+ status: 'pending',
197
+ attemptCount: 0,
198
+ maxAttempts: 3,
199
+ diagnosticJson: createPITaskDiagnosticJson({
200
+ dependencyTaskIds: [],
201
+ channel: 'prompt',
202
+ timeoutMs: 300_000,
203
+ inputArtifactRefs: [],
204
+ outputArtifactRefs: [],
205
+ }),
206
+ });
207
+
208
+ const { readModel: queueReadModel, close: queueClose } = await createInternalizationQueueReadModel({
209
+ workspaceDir,
210
+ readonly: true,
211
+ });
212
+ try {
213
+ const queueSnapshot = await queueReadModel.getSnapshot();
214
+ const queueReady = queueSnapshot.readyTasks.length > 0;
215
+ stages.push({
216
+ name: 'internalization_queue_ready',
217
+ status: queueReady ? 'passed' : 'failed',
218
+ ...(queueReady ? {} : { reason: truncateReason(`Queue has no ready tasks. pendingCount=${queueSnapshot.pendingCount} noReadyTasks=${queueSnapshot.noReadyTasks?.reason ?? 'n/a'}`) }),
219
+ evidence: boundedEvidence({
220
+ readyCount: queueSnapshot.readyTasks.length,
221
+ pendingCount: queueSnapshot.pendingCount,
222
+ noReadyReason: queueSnapshot.noReadyTasks?.reason ?? null,
223
+ }),
224
+ });
225
+ } catch (err) {
226
+ stages.push({
227
+ name: 'internalization_queue_ready',
228
+ status: 'failed',
229
+ reason: truncateReason(`Queue read model failed: ${err instanceof Error ? err.message : String(err)}`),
230
+ });
231
+ } finally {
232
+ await queueClose();
233
+ }
234
+
235
+ const healthModel = new OperatorHealthReadModel({ workspaceDir });
236
+ try {
237
+ const healthSnapshot = await healthModel.getSnapshot();
238
+ const healthPassed = healthSnapshot.overallStatus === 'healthy' || healthSnapshot.overallStatus === 'degraded';
239
+ stages.push({
240
+ name: 'canary_health',
241
+ status: healthPassed ? 'passed' : 'failed',
242
+ ...(healthPassed ? {} : { reason: truncateReason(`Operator health: ${healthSnapshot.overallStatus}. Actions: ${healthSnapshot.recommendedActions.join('; ')}`) }),
243
+ evidence: boundedEvidence({
244
+ overallStatus: healthSnapshot.overallStatus,
245
+ recommendedActions: healthSnapshot.recommendedActions.slice(0, 3),
246
+ }),
247
+ });
248
+ } catch (err) {
249
+ stages.push({
250
+ name: 'canary_health',
251
+ status: 'failed',
252
+ reason: truncateReason(`Health read model failed: ${err instanceof Error ? err.message : String(err)}`),
253
+ });
254
+ } finally {
255
+ await healthModel.close();
256
+ }
257
+ } catch (err) {
258
+ const errorMessage = err instanceof Error ? err.message : String(err);
259
+ const stageNames: SyntheticBaselineStageName[] = [
260
+ 'pain_intake',
261
+ 'diagnostician_task_created',
262
+ 'candidate_created',
263
+ 'ledger_consistent',
264
+ 'internalization_queue_ready',
265
+ 'canary_health',
266
+ ];
267
+ const existingNames = new Set(stages.map(s => s.name));
268
+ for (const name of stageNames) {
269
+ if (!existingNames.has(name)) {
270
+ stages.push({
271
+ name,
272
+ status: 'failed',
273
+ reason: truncateReason(`Unexpected error: ${errorMessage}`),
274
+ });
275
+ }
276
+ }
277
+ } finally {
278
+ if (stateManager) {
279
+ await stateManager.close();
280
+ }
281
+ }
282
+
283
+ const status = computeOverallStatus(stages);
284
+ return {
285
+ status,
286
+ workspaceMode,
287
+ generatedAt,
288
+ stages,
289
+ recommendedNextIssue: recommendNextIssue(stages),
290
+ };
291
+ }
@@ -0,0 +1,338 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { handleCandidateAudit, handleCandidateRepair } from '../../src/commands/candidate.js';
3
+
4
+ // Use vi.hoisted to define mocks
5
+ const { mockStateManager, mockAdapter, mockService, mockDb, mockLoadLedger, mockGetLedgerFilePath, MockRuntimeStateManager, MockCandidateIntakeService, MockPrincipleTreeLedgerAdapter } = vi.hoisted(() => {
6
+ const mockDbRows: Record<string, unknown[]> = {};
7
+ // Track all run() calls: { sql, args }[]
8
+ const runCalls: { sql: string; args: unknown[] }[] = [];
9
+
10
+ const mockDb = {
11
+ getDb: () => ({
12
+ prepare: (sql: string) => {
13
+ const key = sql.trim();
14
+ const rows = mockDbRows[key] ?? [];
15
+ return {
16
+ get: (..._args: unknown[]) => rows[0] ?? undefined,
17
+ all: () => rows,
18
+ run: (...args: unknown[]) => {
19
+ runCalls.push({ sql: key, args });
20
+ },
21
+ };
22
+ },
23
+ }),
24
+ setRows: (sql: string, rows: unknown[]) => {
25
+ mockDbRows[sql.trim()] = rows;
26
+ },
27
+ clearRows: () => {
28
+ for (const key of Object.keys(mockDbRows)) {
29
+ delete mockDbRows[key];
30
+ }
31
+ runCalls.length = 0;
32
+ },
33
+ runCalls,
34
+ };
35
+
36
+ const mockStateManager = {
37
+ initialize: vi.fn().mockResolvedValue(undefined),
38
+ getCandidate: vi.fn(),
39
+ close: vi.fn().mockResolvedValue(undefined),
40
+ connection: mockDb,
41
+ };
42
+
43
+ const mockAdapter = {
44
+ writeProbationEntry: vi.fn(),
45
+ existsForCandidate: vi.fn().mockReturnValue(null),
46
+ };
47
+
48
+ const mockService = {
49
+ intake: vi.fn(),
50
+ };
51
+
52
+ function MockRuntimeStateManager(this: unknown) {
53
+ return mockStateManager;
54
+ }
55
+ MockRuntimeStateManager.prototype = {};
56
+
57
+ function MockCandidateIntakeService(this: unknown) {
58
+ return mockService;
59
+ }
60
+ MockCandidateIntakeService.prototype = {};
61
+
62
+ function MockPrincipleTreeLedgerAdapter(this: unknown) {
63
+ return mockAdapter;
64
+ }
65
+ MockPrincipleTreeLedgerAdapter.prototype = {};
66
+
67
+ const mockLoadLedger = vi.fn();
68
+ const mockGetLedgerFilePath = vi.fn().mockReturnValue('/tmp/test-workspace/.state/principle_training_state.json');
69
+
70
+ return {
71
+ mockStateManager,
72
+ mockAdapter,
73
+ mockService,
74
+ mockDb,
75
+ mockLoadLedger,
76
+ mockGetLedgerFilePath,
77
+ MockRuntimeStateManager,
78
+ MockCandidateIntakeService,
79
+ MockPrincipleTreeLedgerAdapter,
80
+ };
81
+ });
82
+
83
+ vi.mock('@principles/core/runtime-v2', () => ({
84
+ CandidateIntakeService: MockCandidateIntakeService,
85
+ CandidateIntakeError: class CandidateIntakeError extends Error {
86
+ code: string;
87
+ constructor(message: string, code: string) {
88
+ super(message);
89
+ this.name = 'CandidateIntakeError';
90
+ this.code = code;
91
+ }
92
+ },
93
+ RuntimeStateManager: MockRuntimeStateManager,
94
+ loadLedger: mockLoadLedger,
95
+ getLedgerFilePathPublic: mockGetLedgerFilePath,
96
+ }));
97
+
98
+ vi.mock('../../src/principle-tree-ledger-adapter.js', () => ({
99
+ PrincipleTreeLedgerAdapter: MockPrincipleTreeLedgerAdapter,
100
+ }));
101
+
102
+ vi.mock('../../src/resolve-workspace.js', () => ({
103
+ resolveWorkspaceDir: vi.fn().mockReturnValue('/tmp/test-workspace'),
104
+ }));
105
+
106
+ describe('pd candidate audit', () => {
107
+ let consoleLogSpy: ReturnType<typeof vi.spyOn>;
108
+ let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
109
+ let exitSpy: ReturnType<typeof vi.spyOn>;
110
+
111
+ beforeEach(() => {
112
+ vi.clearAllMocks();
113
+ mockStateManager.initialize.mockResolvedValue(undefined);
114
+ mockStateManager.close.mockResolvedValue(undefined);
115
+ mockLoadLedger.mockReset();
116
+ mockGetLedgerFilePath.mockReturnValue('/tmp/test-workspace/.state/principle_training_state.json');
117
+ mockDb.clearRows();
118
+
119
+ consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
120
+ consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
121
+ exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
122
+ });
123
+
124
+ afterEach(() => {
125
+ consoleLogSpy.mockRestore();
126
+ consoleErrorSpy.mockRestore();
127
+ exitSpy.mockRestore();
128
+ });
129
+
130
+ it('audit ok: all consumed candidates have ledger entries', async () => {
131
+ // DB returns one consumed candidate
132
+ mockDb.setRows(
133
+ "SELECT candidate_id FROM principle_candidates WHERE status = 'consumed'",
134
+ [{ candidate_id: 'c1' }],
135
+ );
136
+
137
+ // Ledger contains a principle with derivedFromPainIds matching c1
138
+ mockLoadLedger.mockReturnValue({
139
+ tree: {
140
+ principles: {
141
+ p1: { id: 'p1', derivedFromPainIds: ['c1'] },
142
+ },
143
+ },
144
+ });
145
+
146
+ await handleCandidateAudit({ workspace: '/tmp/test-workspace', json: true });
147
+
148
+ const jsonOutput = consoleLogSpy.mock.calls.find((call) => {
149
+ try {
150
+ const parsed = JSON.parse(call[0] as string);
151
+ return parsed.status === 'ok';
152
+ } catch {
153
+ return false;
154
+ }
155
+ });
156
+ expect(jsonOutput).toBeDefined();
157
+ const parsed = JSON.parse((jsonOutput as [string])[0]);
158
+ expect(parsed.status).toBe('ok');
159
+ expect(parsed.consumedCount).toBe(1);
160
+ expect(parsed.missingLedgerEntryIds).toEqual([]);
161
+
162
+ // ok audit should NOT call process.exit(1)
163
+ expect(exitSpy).not.toHaveBeenCalledWith(1);
164
+ });
165
+
166
+ it('audit degraded: consumed candidate missing from ledger exits 1', async () => {
167
+ mockDb.setRows(
168
+ "SELECT candidate_id FROM principle_candidates WHERE status = 'consumed'",
169
+ [{ candidate_id: 'c1' }],
170
+ );
171
+
172
+ // Ledger has no matching derivedFromPainIds
173
+ mockLoadLedger.mockReturnValue({
174
+ tree: {
175
+ principles: {
176
+ p1: { id: 'p1', derivedFromPainIds: ['other-candidate'] },
177
+ },
178
+ },
179
+ });
180
+
181
+ await handleCandidateAudit({ workspace: '/tmp/test-workspace', json: true });
182
+
183
+ const jsonOutput = consoleLogSpy.mock.calls.find((call) => {
184
+ try {
185
+ const parsed = JSON.parse(call[0] as string);
186
+ return parsed.status === 'degraded';
187
+ } catch {
188
+ return false;
189
+ }
190
+ });
191
+ expect(jsonOutput).toBeDefined();
192
+ const parsed = JSON.parse((jsonOutput as [string])[0]);
193
+ expect(parsed.status).toBe('degraded');
194
+ expect(parsed.missingLedgerEntryIds).toEqual(['c1']);
195
+
196
+ // degraded audit must exit 1
197
+ expect(exitSpy).toHaveBeenCalledWith(1);
198
+ });
199
+ });
200
+
201
+ describe('pd candidate repair', () => {
202
+ let consoleLogSpy: ReturnType<typeof vi.spyOn>;
203
+ let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
204
+ let exitSpy: ReturnType<typeof vi.spyOn>;
205
+
206
+ beforeEach(() => {
207
+ vi.clearAllMocks();
208
+ mockStateManager.initialize.mockResolvedValue(undefined);
209
+ mockStateManager.close.mockResolvedValue(undefined);
210
+ mockStateManager.getCandidate.mockReset();
211
+ mockAdapter.existsForCandidate.mockReset();
212
+ mockService.intake.mockReset();
213
+ mockDb.clearRows();
214
+
215
+ consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
216
+ consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
217
+ exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
218
+ });
219
+
220
+ afterEach(() => {
221
+ consoleLogSpy.mockRestore();
222
+ consoleErrorSpy.mockRestore();
223
+ exitSpy.mockRestore();
224
+ });
225
+
226
+ it('repair already_consistent with null consumed_at: sets consumed_at', async () => {
227
+ mockStateManager.getCandidate.mockResolvedValue({
228
+ candidateId: 'c1',
229
+ status: 'consumed',
230
+ artifactId: 'a1',
231
+ title: 'Test',
232
+ description: 'desc',
233
+ });
234
+
235
+ // Ledger entry exists
236
+ mockAdapter.existsForCandidate.mockReturnValue({ id: 'ledger-1', derivedFromPainIds: ['c1'] });
237
+
238
+ // consumed_at is null in DB
239
+ mockDb.setRows(
240
+ 'SELECT consumed_at FROM principle_candidates WHERE candidate_id = ?',
241
+ [{ consumed_at: null }],
242
+ );
243
+
244
+ await handleCandidateRepair({ candidateId: 'c1', workspace: '/tmp/test-workspace', json: true });
245
+
246
+ const jsonOutput = consoleLogSpy.mock.calls.find((call) => {
247
+ try {
248
+ const parsed = JSON.parse(call[0] as string);
249
+ return parsed.status === 'already_consistent';
250
+ } catch {
251
+ return false;
252
+ }
253
+ });
254
+ expect(jsonOutput).toBeDefined();
255
+ const parsed = JSON.parse((jsonOutput as [string])[0]);
256
+ expect(parsed.status).toBe('already_consistent');
257
+ expect(parsed.ledgerEntryId).toBe('ledger-1');
258
+ // consumedAt should be a freshly written ISO timestamp
259
+ expect(parsed.consumedAt).toBeDefined();
260
+ expect(typeof parsed.consumedAt).toBe('string');
261
+ // Directly assert SQL UPDATE was executed
262
+ const updateCall = mockDb.runCalls.find((c) => c.sql.startsWith('UPDATE principle_candidates SET consumed_at'));
263
+ expect(updateCall).toBeDefined();
264
+ expect(updateCall!.args[0]).toBe(parsed.consumedAt); // timestamp arg
265
+ expect(updateCall!.args[1]).toBe('c1'); // candidate_id arg
266
+ // Should not exit 1
267
+ expect(exitSpy).not.toHaveBeenCalledWith(1);
268
+ });
269
+
270
+ it('repair missing ledger: re-intakes and sets consumed_at', async () => {
271
+ mockStateManager.getCandidate.mockResolvedValue({
272
+ candidateId: 'c2',
273
+ status: 'consumed',
274
+ artifactId: 'a2',
275
+ title: 'Test 2',
276
+ description: 'desc 2',
277
+ });
278
+
279
+ // No ledger entry exists
280
+ mockAdapter.existsForCandidate.mockReturnValue(null);
281
+
282
+ // intake returns new entry
283
+ mockService.intake.mockResolvedValue({
284
+ id: 'new-ledger-entry',
285
+ title: 'Test 2',
286
+ text: 'text',
287
+ status: 'probation',
288
+ });
289
+
290
+ // consumed_at is null
291
+ mockDb.setRows(
292
+ 'SELECT consumed_at FROM principle_candidates WHERE candidate_id = ?',
293
+ [{ consumed_at: null }],
294
+ );
295
+
296
+ await handleCandidateRepair({ candidateId: 'c2', workspace: '/tmp/test-workspace', json: true });
297
+
298
+ // intake was called to restore ledger
299
+ expect(mockService.intake).toHaveBeenCalledWith('c2');
300
+
301
+ const jsonOutput = consoleLogSpy.mock.calls.find((call) => {
302
+ try {
303
+ const parsed = JSON.parse(call[0] as string);
304
+ return parsed.status === 'repaired';
305
+ } catch {
306
+ return false;
307
+ }
308
+ });
309
+ expect(jsonOutput).toBeDefined();
310
+ const parsed = JSON.parse((jsonOutput as [string])[0]);
311
+ expect(parsed.status).toBe('repaired');
312
+ expect(parsed.ledgerEntryId).toBe('new-ledger-entry');
313
+ expect(parsed.consumedAt).toBeDefined();
314
+ // Directly assert SQL UPDATE was executed
315
+ const updateCall = mockDb.runCalls.find((c) => c.sql.startsWith('UPDATE principle_candidates SET consumed_at'));
316
+ expect(updateCall).toBeDefined();
317
+ expect(updateCall!.args[0]).toBe(parsed.consumedAt);
318
+ expect(updateCall!.args[1]).toBe('c2');
319
+ expect(exitSpy).not.toHaveBeenCalledWith(1);
320
+ });
321
+
322
+ it('repair non-consumed candidate exits 1', async () => {
323
+ mockStateManager.getCandidate.mockResolvedValue({
324
+ candidateId: 'c3',
325
+ status: 'pending',
326
+ artifactId: 'a3',
327
+ title: 'Test 3',
328
+ description: 'desc 3',
329
+ });
330
+
331
+ await handleCandidateRepair({ candidateId: 'c3', workspace: '/tmp/test-workspace', json: true });
332
+
333
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
334
+ expect.stringContaining('is not consumed'),
335
+ );
336
+ expect(exitSpy).toHaveBeenCalledWith(1);
337
+ });
338
+ });