@principles/pd-cli 1.152.12 → 1.152.13

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 (296) hide show
  1. package/package.json +13 -7
  2. package/CHANGELOG.md +0 -23
  3. package/dist/commands/__tests__/intent-flag-wiring.test.d.ts +0 -9
  4. package/dist/commands/__tests__/intent-flag-wiring.test.d.ts.map +0 -1
  5. package/dist/commands/__tests__/intent-flag-wiring.test.js +0 -237
  6. package/dist/commands/__tests__/intent-flag-wiring.test.js.map +0 -1
  7. package/dist/commands/__tests__/legacy-cleanup.test.d.ts +0 -18
  8. package/dist/commands/__tests__/legacy-cleanup.test.d.ts.map +0 -1
  9. package/dist/commands/__tests__/legacy-cleanup.test.js +0 -467
  10. package/dist/commands/__tests__/legacy-cleanup.test.js.map +0 -1
  11. package/dist/commands/__tests__/mvp-smoke.test.d.ts +0 -15
  12. package/dist/commands/__tests__/mvp-smoke.test.d.ts.map +0 -1
  13. package/dist/commands/__tests__/mvp-smoke.test.js +0 -262
  14. package/dist/commands/__tests__/mvp-smoke.test.js.map +0 -1
  15. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts +0 -2
  16. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts.map +0 -1
  17. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js +0 -60
  18. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js.map +0 -1
  19. package/dist/commands/__tests__/rulecode-flag-wiring.test.d.ts +0 -21
  20. package/dist/commands/__tests__/rulecode-flag-wiring.test.d.ts.map +0 -1
  21. package/dist/commands/__tests__/rulecode-flag-wiring.test.js +0 -186
  22. package/dist/commands/__tests__/rulecode-flag-wiring.test.js.map +0 -1
  23. package/dist/commands/__tests__/rulecode-handler.test.d.ts +0 -16
  24. package/dist/commands/__tests__/rulecode-handler.test.d.ts.map +0 -1
  25. package/dist/commands/__tests__/rulecode-handler.test.js +0 -285
  26. package/dist/commands/__tests__/rulecode-handler.test.js.map +0 -1
  27. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.d.ts +0 -24
  28. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.d.ts.map +0 -1
  29. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js +0 -232
  30. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js.map +0 -1
  31. package/dist/commands/__tests__/runtime-activation-nextaction.test.d.ts +0 -15
  32. package/dist/commands/__tests__/runtime-activation-nextaction.test.d.ts.map +0 -1
  33. package/dist/commands/__tests__/runtime-activation-nextaction.test.js +0 -73
  34. package/dist/commands/__tests__/runtime-activation-nextaction.test.js.map +0 -1
  35. package/dist/commands/__tests__/runtime-probe-config.test.d.ts +0 -20
  36. package/dist/commands/__tests__/runtime-probe-config.test.d.ts.map +0 -1
  37. package/dist/commands/__tests__/runtime-probe-config.test.js +0 -389
  38. package/dist/commands/__tests__/runtime-probe-config.test.js.map +0 -1
  39. package/dist/commands/__tests__/telemetry-flag-wiring.test.d.ts +0 -9
  40. package/dist/commands/__tests__/telemetry-flag-wiring.test.d.ts.map +0 -1
  41. package/dist/commands/__tests__/telemetry-flag-wiring.test.js +0 -76
  42. package/dist/commands/__tests__/telemetry-flag-wiring.test.js.map +0 -1
  43. package/dist/commands/__tests__/version.test.d.ts +0 -2
  44. package/dist/commands/__tests__/version.test.d.ts.map +0 -1
  45. package/dist/commands/__tests__/version.test.js +0 -238
  46. package/dist/commands/__tests__/version.test.js.map +0 -1
  47. package/dist/commands/runtime-uat.guard.test.d.ts +0 -2
  48. package/dist/commands/runtime-uat.guard.test.d.ts.map +0 -1
  49. package/dist/commands/runtime-uat.guard.test.js +0 -153
  50. package/dist/commands/runtime-uat.guard.test.js.map +0 -1
  51. package/dist/services/__tests__/cli-output.test.d.ts +0 -18
  52. package/dist/services/__tests__/cli-output.test.d.ts.map +0 -1
  53. package/dist/services/__tests__/cli-output.test.js +0 -103
  54. package/dist/services/__tests__/cli-output.test.js.map +0 -1
  55. package/dist/services/__tests__/evaluator-runner-deps.test.d.ts +0 -2
  56. package/dist/services/__tests__/evaluator-runner-deps.test.d.ts.map +0 -1
  57. package/dist/services/__tests__/evaluator-runner-deps.test.js +0 -343
  58. package/dist/services/__tests__/evaluator-runner-deps.test.js.map +0 -1
  59. package/dist/services/__tests__/rulehost-readiness.test.d.ts +0 -2
  60. package/dist/services/__tests__/rulehost-readiness.test.d.ts.map +0 -1
  61. package/dist/services/__tests__/rulehost-readiness.test.js +0 -311
  62. package/dist/services/__tests__/rulehost-readiness.test.js.map +0 -1
  63. package/dist/services/__tests__/runtime-adapter-resolver.test.d.ts +0 -18
  64. package/dist/services/__tests__/runtime-adapter-resolver.test.d.ts.map +0 -1
  65. package/dist/services/__tests__/runtime-adapter-resolver.test.js +0 -673
  66. package/dist/services/__tests__/runtime-adapter-resolver.test.js.map +0 -1
  67. package/dist/utils/production-workspace-guard.test.d.ts +0 -2
  68. package/dist/utils/production-workspace-guard.test.d.ts.map +0 -1
  69. package/dist/utils/production-workspace-guard.test.js +0 -137
  70. package/dist/utils/production-workspace-guard.test.js.map +0 -1
  71. package/scripts/llm-dogfood.ts +0 -456
  72. package/scripts/migrate-illegal-expected-decision.ts +0 -252
  73. package/src/commands/__tests__/intent-flag-wiring.test.ts +0 -282
  74. package/src/commands/__tests__/legacy-cleanup.test.ts +0 -602
  75. package/src/commands/__tests__/mvp-smoke.test.ts +0 -300
  76. package/src/commands/__tests__/pain-retry-maxTokens-flag.test.ts +0 -73
  77. package/src/commands/__tests__/rulecode-flag-wiring.test.ts +0 -237
  78. package/src/commands/__tests__/rulecode-handler.test.ts +0 -369
  79. package/src/commands/__tests__/run-rulehost-flag-wiring.test.ts +0 -292
  80. package/src/commands/__tests__/runtime-activation-nextaction.test.ts +0 -81
  81. package/src/commands/__tests__/runtime-probe-config.test.ts +0 -432
  82. package/src/commands/__tests__/telemetry-flag-wiring.test.ts +0 -84
  83. package/src/commands/__tests__/version.test.ts +0 -251
  84. package/src/commands/admission-gate.ts +0 -30
  85. package/src/commands/artifact.ts +0 -82
  86. package/src/commands/build-trajectory-evidence.ts +0 -298
  87. package/src/commands/candidate.ts +0 -1250
  88. package/src/commands/codex-ingest-catchup.ts +0 -114
  89. package/src/commands/codex-ingest-quarantine.ts +0 -127
  90. package/src/commands/codex-reconcile.ts +0 -84
  91. package/src/commands/codex-setup.ts +0 -476
  92. package/src/commands/codex-worker.ts +0 -185
  93. package/src/commands/command-helpers.ts +0 -24
  94. package/src/commands/config-doctor.ts +0 -193
  95. package/src/commands/console.ts +0 -797
  96. package/src/commands/context.ts +0 -72
  97. package/src/commands/demo-story-a.ts +0 -241
  98. package/src/commands/diagnose.ts +0 -756
  99. package/src/commands/errors-list.ts +0 -441
  100. package/src/commands/evolution-tasks-list.ts +0 -48
  101. package/src/commands/evolution-tasks-show.ts +0 -62
  102. package/src/commands/flow.ts +0 -60
  103. package/src/commands/health-codex.ts +0 -576
  104. package/src/commands/health.ts +0 -315
  105. package/src/commands/history.ts +0 -63
  106. package/src/commands/intent.ts +0 -457
  107. package/src/commands/legacy-cleanup.ts +0 -531
  108. package/src/commands/legacy-import.ts +0 -104
  109. package/src/commands/mvp-smoke.ts +0 -160
  110. package/src/commands/pain-evidence.ts +0 -226
  111. package/src/commands/pain-list.ts +0 -239
  112. package/src/commands/pain-record.ts +0 -612
  113. package/src/commands/pain-retry.ts +0 -916
  114. package/src/commands/principles-stats.ts +0 -700
  115. package/src/commands/proven-channel-baseline.ts +0 -115
  116. package/src/commands/quality-scorecard.ts +0 -272
  117. package/src/commands/remediation-output.ts +0 -66
  118. package/src/commands/rulecode.ts +0 -465
  119. package/src/commands/run.ts +0 -89
  120. package/src/commands/runtime-activation.ts +0 -1629
  121. package/src/commands/runtime-artifact-repair.ts +0 -524
  122. package/src/commands/runtime-canary.ts +0 -395
  123. package/src/commands/runtime-compatibility-scan.ts +0 -105
  124. package/src/commands/runtime-diagnostics-export.ts +0 -229
  125. package/src/commands/runtime-features.ts +0 -153
  126. package/src/commands/runtime-gfi-snapshot.ts +0 -135
  127. package/src/commands/runtime-health-snapshot.ts +0 -108
  128. package/src/commands/runtime-init.ts +0 -429
  129. package/src/commands/runtime-internalization-enqueue-successors.ts +0 -530
  130. package/src/commands/runtime-internalization-integrity-repair.ts +0 -79
  131. package/src/commands/runtime-internalization-integrity.ts +0 -107
  132. package/src/commands/runtime-internalization-queue.ts +0 -135
  133. package/src/commands/runtime-internalization-retry.ts +0 -191
  134. package/src/commands/runtime-internalization-run-once.ts +0 -791
  135. package/src/commands/runtime-internalization-run-rulehost.ts +0 -749
  136. package/src/commands/runtime-internalization-wake-once.ts +0 -87
  137. package/src/commands/runtime-pain-flood-simulation.ts +0 -121
  138. package/src/commands/runtime-pruning.ts +0 -438
  139. package/src/commands/runtime-recovery-failed-tasks.ts +0 -258
  140. package/src/commands/runtime-recovery.ts +0 -109
  141. package/src/commands/runtime-synthetic-baseline.ts +0 -70
  142. package/src/commands/runtime-uat.guard.test.ts +0 -175
  143. package/src/commands/runtime-uat.ts +0 -380
  144. package/src/commands/runtime.ts +0 -473
  145. package/src/commands/samples-list.ts +0 -39
  146. package/src/commands/samples-review.ts +0 -36
  147. package/src/commands/task.ts +0 -271
  148. package/src/commands/telemetry.ts +0 -349
  149. package/src/commands/trace.ts +0 -174
  150. package/src/commands/trajectory-db-unavailable.ts +0 -19
  151. package/src/commands/trajectory.ts +0 -64
  152. package/src/commands/version.ts +0 -49
  153. package/src/config-reader.ts +0 -122
  154. package/src/index.ts +0 -1276
  155. package/src/legacy/legacy-import.ts +0 -179
  156. package/src/legacy/session-history-import.ts +0 -203
  157. package/src/resolve-workspace.ts +0 -106
  158. package/src/services/__tests__/cli-output.test.ts +0 -130
  159. package/src/services/__tests__/evaluator-runner-deps.test.ts +0 -382
  160. package/src/services/__tests__/rulehost-readiness.test.ts +0 -363
  161. package/src/services/__tests__/runtime-adapter-resolver.test.ts +0 -793
  162. package/src/services/cli-output.ts +0 -95
  163. package/src/services/config-doctor.ts +0 -697
  164. package/src/services/console-launcher.ts +0 -537
  165. package/src/services/demo-rule-compiler.ts +0 -40
  166. package/src/services/demo-story-a-runner.ts +0 -471
  167. package/src/services/mainline-snapshot-assembler.ts +0 -579
  168. package/src/services/pain-flood-simulation-runner.ts +0 -462
  169. package/src/services/pd-config-loader.ts +0 -433
  170. package/src/services/proven-channel-baseline-runner.ts +0 -157
  171. package/src/services/quality-scorecard/data-extractor.ts +0 -150
  172. package/src/services/quality-scorecard/local-evaluator.ts +0 -142
  173. package/src/services/quality-scorecard/strong-model-gate.ts +0 -202
  174. package/src/services/resolve-runtime-from-pd-config.ts +0 -230
  175. package/src/services/rulehost-pipeline-runner.ts +0 -1214
  176. package/src/services/rulehost-readiness.ts +0 -326
  177. package/src/services/runtime-adapter-resolver.ts +0 -364
  178. package/src/services/synthetic-baseline-runner.ts +0 -400
  179. package/src/services/version-report.ts +0 -253
  180. package/src/services/workspace-tool-semantics.ts +0 -26
  181. package/src/utils/path-security.ts +0 -96
  182. package/src/utils/production-workspace-guard.test.ts +0 -157
  183. package/src/utils/production-workspace-guard.ts +0 -210
  184. package/tests/bdd/cli-contract.steps.ts +0 -346
  185. package/tests/bdd/principles-stats.steps.ts +0 -211
  186. package/tests/bdd/support/gherkin-loader.ts +0 -147
  187. package/tests/bdd/support/repo-root.ts +0 -62
  188. package/tests/bdd/support/vitest-bdd.ts +0 -179
  189. package/tests/commands/admission-gate-coverage.test.ts +0 -79
  190. package/tests/commands/build-trajectory-evidence.test.ts +0 -508
  191. package/tests/commands/candidate-audit-repair.test.ts +0 -421
  192. package/tests/commands/candidate-intake.test.ts +0 -732
  193. package/tests/commands/candidate-internalization-backfill.test.ts +0 -548
  194. package/tests/commands/candidate-internalize-lineage.test.ts +0 -628
  195. package/tests/commands/candidate-internalize.test.ts +0 -299
  196. package/tests/commands/candidate-route.test.ts +0 -329
  197. package/tests/commands/candidate-show.test.ts +0 -95
  198. package/tests/commands/cli-command-tree.test.ts +0 -144
  199. package/tests/commands/cli-help-snapshot.test.ts +0 -155
  200. package/tests/commands/cli-skill-contract.test.ts +0 -121
  201. package/tests/commands/codex-consent-reversibility.steps.test.ts +0 -299
  202. package/tests/commands/codex-ingest-quarantine.test.ts +0 -136
  203. package/tests/commands/codex-setup.test.ts +0 -297
  204. package/tests/commands/codex-worker-daemon.test.ts +0 -95
  205. package/tests/commands/codex-worker-registration.test.ts +0 -207
  206. package/tests/commands/config-doctor.test.ts +0 -336
  207. package/tests/commands/console-launcher-edge-cases.test.ts +0 -388
  208. package/tests/commands/console-open.test.ts +0 -1381
  209. package/tests/commands/context.test.ts +0 -114
  210. package/tests/commands/demo-story-a.test.ts +0 -318
  211. package/tests/commands/diagnose.test.ts +0 -1803
  212. package/tests/commands/errors-list.test.ts +0 -679
  213. package/tests/commands/evolution-tasks-list.test.ts +0 -41
  214. package/tests/commands/evolution-tasks-show.test.ts +0 -46
  215. package/tests/commands/health-codex.test.ts +0 -349
  216. package/tests/commands/health-host-wiring.test.ts +0 -76
  217. package/tests/commands/health-reliability.test.ts +0 -99
  218. package/tests/commands/health.test.ts +0 -413
  219. package/tests/commands/intent.test.ts +0 -321
  220. package/tests/commands/legacy-cleanup.test.ts +0 -148
  221. package/tests/commands/pain-evidence.test.ts +0 -518
  222. package/tests/commands/pain-list.test.ts +0 -219
  223. package/tests/commands/pain-record-async.test.ts +0 -335
  224. package/tests/commands/pain-record-session-parser.test.ts +0 -228
  225. package/tests/commands/pain-record.test.ts +0 -851
  226. package/tests/commands/pain-retry.test.ts +0 -1121
  227. package/tests/commands/plugin-config-resolution-cutover.test.ts +0 -229
  228. package/tests/commands/pri-393-runtime-config-unification.test.ts +0 -443
  229. package/tests/commands/principles-stats-wiring.test.ts +0 -135
  230. package/tests/commands/principles-stats.test.ts +0 -274
  231. package/tests/commands/product-path-regression.test.ts +0 -86
  232. package/tests/commands/proven-channel-baseline.test.ts +0 -441
  233. package/tests/commands/rulecode.test.ts +0 -177
  234. package/tests/commands/run-rulehost-handler.test.ts +0 -908
  235. package/tests/commands/runtime-activation-approve-flag-wiring.test.ts +0 -227
  236. package/tests/commands/runtime-activation-deactivate-flag-wiring.test.ts +0 -167
  237. package/tests/commands/runtime-activation-dispatch-flag-wiring.test.ts +0 -262
  238. package/tests/commands/runtime-activation-governance-audit.test.ts +0 -95
  239. package/tests/commands/runtime-activation-list-flag-wiring.test.ts +0 -193
  240. package/tests/commands/runtime-activation-promote-flag-wiring.test.ts +0 -248
  241. package/tests/commands/runtime-activation-shadow-telemetry.test.ts +0 -130
  242. package/tests/commands/runtime-activation.test.ts +0 -2052
  243. package/tests/commands/runtime-artifact-repair-registration.test.ts +0 -74
  244. package/tests/commands/runtime-artifact-repair.test.ts +0 -404
  245. package/tests/commands/runtime-canary.test.ts +0 -430
  246. package/tests/commands/runtime-compatibility-scan.test.ts +0 -146
  247. package/tests/commands/runtime-diagnostics-export.test.ts +0 -175
  248. package/tests/commands/runtime-features.test.ts +0 -249
  249. package/tests/commands/runtime-health-snapshot.test.ts +0 -358
  250. package/tests/commands/runtime-init-empty-workspace.test.ts +0 -252
  251. package/tests/commands/runtime-init.test.ts +0 -420
  252. package/tests/commands/runtime-internalization-enqueue-successors.test.ts +0 -782
  253. package/tests/commands/runtime-internalization-integrity-repair.test.ts +0 -239
  254. package/tests/commands/runtime-internalization-integrity.test.ts +0 -140
  255. package/tests/commands/runtime-internalization-queue.test.ts +0 -380
  256. package/tests/commands/runtime-internalization-retry-owner-authority.test.ts +0 -431
  257. package/tests/commands/runtime-internalization-run-once-evaluator-parity.test.ts +0 -269
  258. package/tests/commands/runtime-internalization-run-once-language.test.ts +0 -303
  259. package/tests/commands/runtime-internalization-run-once-real-config.test.ts +0 -237
  260. package/tests/commands/runtime-internalization-run-once-rollout-parity.test.ts +0 -706
  261. package/tests/commands/runtime-internalization-run-once.test.ts +0 -1791
  262. package/tests/commands/runtime-internalization-wake-once.test.ts +0 -171
  263. package/tests/commands/runtime-internalization.test.ts +0 -53
  264. package/tests/commands/runtime-pain-flood-simulation.test.ts +0 -418
  265. package/tests/commands/runtime-pruning.test.ts +0 -694
  266. package/tests/commands/runtime-recovery-failed-tasks.test.ts +0 -328
  267. package/tests/commands/runtime-recovery.test.ts +0 -120
  268. package/tests/commands/runtime-synthetic-baseline.test.ts +0 -249
  269. package/tests/commands/runtime-uat.test.ts +0 -397
  270. package/tests/commands/runtime.test.ts +0 -392
  271. package/tests/commands/samples-list.test.ts +0 -43
  272. package/tests/commands/samples-review.test.ts +0 -41
  273. package/tests/commands/task.test.ts +0 -153
  274. package/tests/commands/telemetry.test.ts +0 -190
  275. package/tests/commands/trace.test.ts +0 -316
  276. package/tests/config-reader.test.ts +0 -143
  277. package/tests/e2e/candidate-intake-e2e.test.ts +0 -338
  278. package/tests/e2e/cli-full-flow.test.ts +0 -233
  279. package/tests/e2e/cross-package-acceptance.test.ts +0 -635
  280. package/tests/helpers/pd-cli-path.ts +0 -5
  281. package/tests/pd-cli-smoke.test.ts +0 -13
  282. package/tests/resolve-workspace.test.ts +0 -196
  283. package/tests/services/config-doctor-signal-health.test.ts +0 -109
  284. package/tests/services/console-launcher.test.ts +0 -114
  285. package/tests/services/demo-rule-compiler.test.ts +0 -242
  286. package/tests/services/mainline-snapshot-assembler.test.ts +0 -572
  287. package/tests/services/pd-config-loader.test.ts +0 -505
  288. package/tests/services/proven-channel-baseline-runner.test.ts +0 -30
  289. package/tests/services/quality-scorecard/strong-model-gate.test.ts +0 -133
  290. package/tests/services/resolve-runtime-from-pd-config.test.ts +0 -485
  291. package/tests/services/rulehost-pipeline-e2e.test.ts +0 -538
  292. package/tests/services/rulehost-pipeline-runner.test.ts +0 -1242
  293. package/tests/services/workspace-tool-semantics.test.ts +0 -76
  294. package/tests/utils/path-security.test.ts +0 -180
  295. package/tsconfig.json +0 -26
  296. package/vitest.config.ts +0 -10
@@ -1,700 +0,0 @@
1
- /**
2
- * pd principles stats — Principle injection observability (PRI-562 Phase 0).
3
- *
4
- * Usage:
5
- * pd principles stats [--json] [--days <n>] [--workspace <path>]
6
- *
7
- * Read-only aggregation over two independent sources, answering "what does
8
- * principle injection actually look like right now?" before any Working Set
9
- * work changes selection behavior (SPEC v0.2 §6 Phase 0):
10
- *
11
- * 1. Event JSONL {workspace}/.state/logs/events_YYYY-MM-DD.jsonl —
12
- * per-prompt-build records of type 'runtime_v2_prompt_activations_injected'
13
- * (retention ~7 days; per-turn fidelity incl. chars/truncation/cross-block
14
- * duplicates from the PRI-562 enriched fields).
15
- * 2. Receipt ledger {workspace}/.pd/state.db table principle_applications —
16
- * session×principle presence rows + effect rows (90-day retention;
17
- * written only when features.principle_receipt_ledger is enabled).
18
- *
19
- * Coverage honesty (rc-9): per-turn precision (chars/truncation/per-turn
20
- * duplicates) is only as old as the event log (~7 days). Counts and
21
- * application correlation span the full --days window via the ledger when it
22
- * has data. Each metric reports which source produced it.
23
- *
24
- * CLI gate compliance:
25
- * - cli-1-strict-json: --json stdout is exactly one parseable JSON object.
26
- * - cli-2-exit-stops: failure paths set process.exitCode = 1 then return.
27
- * - cli-4-dry-run-confirm-mutex: N/A (read-only command, no mutation flags).
28
- * - cli-5-failure-no-mutation: read-only by construction (readonly SQLite
29
- * connection, bootstrapIfMissing false, no writes anywhere).
30
- * - cli-6-output-next-action: degraded/empty results carry nextAction.
31
- *
32
- * Runtime contract compliance:
33
- * - rc-1-treat-as-unknown: JSONL lines and SQL rows start as unknown.
34
- * - rc-2-no-as-bypass: runtime guards (isRecord/typeof), no casts.
35
- * - rc-3-fail-loud-missing: required event fields validated; malformed
36
- * lines are skipped but COUNTED into warnings (not silently dropped).
37
- * - rc-4-validate-array-elements: principleIds elements validated.
38
- * - rc-5-object-hasown-not-in: Object.hasOwn for untrusted keys.
39
- * - rc-8-safe-serialization: output built from validated primitives only.
40
- * - rc-9-no-silent-fallback: every degraded source carries a reason/note.
41
- */
42
- import * as path from 'path';
43
- import * as fs from 'fs';
44
- import type { Command } from 'commander';
45
- import { SqliteConnection } from '@principles/core';
46
- import { resolveWorkspaceDir } from '../resolve-workspace.js';
47
- import { loadPdConfig, computeFlagsFromLoadResult } from '../services/pd-config-loader.js';
48
-
49
- const EVENT_TYPE = 'runtime_v2_prompt_activations_injected';
50
- const DEFAULT_DAYS = 14;
51
- const MAX_DAYS = 90;
52
- /** Event log filename convention: events_YYYY-MM-DD.jsonl */
53
- const EVENTS_FILE_PATTERN = /^events_(\d{4}-\d{2}-\d{2})\.jsonl$/;
54
-
55
- export interface PrinciplesStatsOptions {
56
- workspace?: string;
57
- json?: boolean;
58
- days?: number;
59
- }
60
-
61
- /** One validated per-build injection record from the event JSONL source. */
62
- interface InjectionTurnRecord {
63
- sessionId: string;
64
- dateStr: string;
65
- principleIds: string[];
66
- injectedCharCount: number;
67
- legacySelectedCount: number | null;
68
- legacyTotalChars: number | null;
69
- legacyTruncated: boolean | null;
70
- v2Truncated: boolean | null;
71
- crossBlockDuplicateIds: string[];
72
- }
73
-
74
- interface LedgerAggregate {
75
- available: boolean;
76
- unavailableReason?: string;
77
- sessionsWithData: number;
78
- avgDistinctPerSession: number | null;
79
- presenceRows: number;
80
- effectRows: number;
81
- topPrinciples: { principleId: string; presenceCount: number; effectCount: number }[];
82
- }
83
-
84
- /** Inclusive local-date window (YYYY-MM-DD strings) for event-file selection. */
85
- interface EventDateWindow {
86
- cutoffDateStr: string;
87
- todayStr: string;
88
- }
89
-
90
- export interface PrinciplesStatsResult {
91
- ok: true;
92
- status: 'ok' | 'degraded';
93
- workspaceDir: string;
94
- windowDays: number;
95
- coverage: {
96
- eventsDaysFound: string[];
97
- eventsTurns: number;
98
- ledgerAvailable: boolean;
99
- receiptLedgerFlagEnabled: boolean | null;
100
- receiptSelfReportFlagEnabled: boolean | null;
101
- notes: string[];
102
- };
103
- sessions: number;
104
- injections: {
105
- avgDistinctPerSession: number | null;
106
- avgPerTurn: number | null;
107
- distinctPrinciples: number;
108
- source: 'ledger' | 'events' | 'none';
109
- };
110
- chars: {
111
- avgV2PerTurn: number | null;
112
- avgLegacyPerTurn: number | null;
113
- truncationRate: number | null;
114
- v2TruncatedTurns: number;
115
- legacyTruncatedTurns: number;
116
- turnsReporting: number;
117
- };
118
- duplicates: {
119
- crossBlockTotal: number;
120
- crossBlockTop: { principleId: string; count: number }[];
121
- intraSessionRepeatShare: number | null;
122
- };
123
- applicationCorrelation: {
124
- presenceRows: number;
125
- effectRows: number;
126
- correlatedPrinciples: number;
127
- top: { principleId: string; presenceCount: number; effectCount: number }[];
128
- };
129
- warnings: string[];
130
- nextAction?: string;
131
- }
132
-
133
- function isRecord(value: unknown): value is Record<string, unknown> {
134
- return value !== null && typeof value === 'object' && !Array.isArray(value);
135
- }
136
-
137
- function asString(v: unknown): string | null {
138
- return typeof v === 'string' ? v : null;
139
- }
140
-
141
- function asNumber(v: unknown): number | null {
142
- return typeof v === 'number' && Number.isFinite(v) ? v : null;
143
- }
144
-
145
- function asBoolean(v: unknown): boolean | null {
146
- return typeof v === 'boolean' ? v : null;
147
- }
148
-
149
- /** rc-4: validate an untrusted array of ids down to its string elements. */
150
- function asStringArray(v: unknown): string[] | null {
151
- if (!Array.isArray(v)) return null;
152
- return v.filter((e): e is string => typeof e === 'string');
153
- }
154
-
155
- /** Event log files are named by UTC date (event-log.ts uses toISOString). */
156
- function utcDateString(d: Date): string {
157
- return d.toISOString().slice(0, 10);
158
- }
159
-
160
- /**
161
- * Read and validate injection events from the daily JSONL files inside the
162
- * --days window. Malformed lines are skipped but counted into warnings
163
- * (rc-3/rc-9). Missing logs directory is not an error — returns empty with a
164
- * note so the caller can degrade gracefully.
165
- */
166
- function readInjectionEvents(
167
- workspaceDir: string,
168
- window: EventDateWindow,
169
- warnings: string[],
170
- ): { turns: InjectionTurnRecord[]; daysFound: string[] } {
171
- const logsDir = path.join(workspaceDir, '.state', 'logs');
172
- if (!fs.existsSync(logsDir)) {
173
- warnings.push(`event logs directory not found: ${logsDir}`);
174
- return { turns: [], daysFound: [] };
175
- }
176
- let entries: string[];
177
- try {
178
- entries = fs.readdirSync(logsDir);
179
- } catch (err) {
180
- warnings.push(`failed to list ${logsDir}: ${err instanceof Error ? err.message : String(err)}`);
181
- return { turns: [], daysFound: [] };
182
- }
183
- const daysFound: string[] = [];
184
- const turns: InjectionTurnRecord[] = [];
185
- let malformedLines = 0;
186
- for (const entry of entries) {
187
- // String.match returns the same capture groups as RegExp matching; kept
188
- // off RegExp#exec because the Mimosa write-gate flags every `.exec(`
189
- // call site as shell-command injection (false positive).
190
- // eslint-disable-next-line @typescript-eslint/prefer-regexp-exec
191
- const match = entry.match(EVENTS_FILE_PATTERN);
192
- const dateStr = match?.[1];
193
- if (dateStr === undefined) continue;
194
- // String compare works: ISO dates sort lexicographically.
195
- if (dateStr < window.cutoffDateStr || dateStr > window.todayStr) continue;
196
- daysFound.push(dateStr);
197
- let raw: string;
198
- try {
199
- raw = fs.readFileSync(path.join(logsDir, entry), 'utf8');
200
- } catch (err) {
201
- warnings.push(`failed to read ${entry}: ${err instanceof Error ? err.message : String(err)}`);
202
- continue;
203
- }
204
- for (const line of raw.split('\n')) {
205
- const trimmed = line.trim();
206
- if (trimmed.length === 0) continue;
207
- // rc-1: each line is untrusted until validated below.
208
- let parsed: unknown;
209
- try {
210
- parsed = JSON.parse(trimmed);
211
- } catch {
212
- malformedLines++;
213
- continue;
214
- }
215
- if (!isRecord(parsed)) {
216
- malformedLines++;
217
- continue;
218
- }
219
- if (asString(parsed.type) !== EVENT_TYPE) continue;
220
- // rc-5: Object.hasOwn for untrusted key checks; rc-3: required fields
221
- // must be present and well-typed or the line is skipped (counted).
222
- const sessionId = Object.hasOwn(parsed, 'sessionId') && typeof parsed.sessionId === 'string'
223
- ? parsed.sessionId
224
- : null;
225
- const dataField = Object.hasOwn(parsed, 'data') && isRecord(parsed.data) ? parsed.data : null;
226
- const source: Record<string, unknown> = dataField ?? parsed;
227
- const principleIdsRaw = Object.hasOwn(source, 'principleIds') ? source.principleIds : undefined;
228
- const principleIds = asStringArray(principleIdsRaw);
229
- const injectedCharCount = asNumber(Object.hasOwn(source, 'injectedCharCount') ? source.injectedCharCount : undefined);
230
- if (sessionId === null || principleIds === null || injectedCharCount === null) {
231
- malformedLines++;
232
- continue;
233
- }
234
- const legacySelectedCount = asNumber(
235
- Object.hasOwn(source, 'legacySelectedCount') ? source.legacySelectedCount : undefined,
236
- );
237
- const legacyTotalChars = asNumber(
238
- Object.hasOwn(source, 'legacyTotalChars') ? source.legacyTotalChars : undefined,
239
- );
240
- const legacyTruncated = asBoolean(
241
- Object.hasOwn(source, 'legacyTruncated') ? source.legacyTruncated : undefined,
242
- );
243
- const v2Truncated = asBoolean(Object.hasOwn(source, 'v2Truncated') ? source.v2Truncated : undefined);
244
- const crossBlockDuplicateIds =
245
- asStringArray(Object.hasOwn(source, 'crossBlockDuplicateIds') ? source.crossBlockDuplicateIds : undefined) ?? [];
246
- turns.push({
247
- sessionId,
248
- dateStr,
249
- principleIds,
250
- injectedCharCount,
251
- legacySelectedCount,
252
- legacyTotalChars,
253
- legacyTruncated,
254
- v2Truncated,
255
- crossBlockDuplicateIds,
256
- });
257
- }
258
- }
259
- if (malformedLines > 0) {
260
- warnings.push(`skipped ${malformedLines} malformed event log line(s)`);
261
- }
262
- return { turns, daysFound };
263
- }
264
-
265
- /**
266
- * Aggregate the receipt ledger (principle_applications) for the window.
267
- * Degrades with a reason when the DB/table is missing (rc-9) — the events
268
- * source stays usable either way.
269
- */
270
- function readLedgerAggregate(workspaceDir: string, cutoffIso: string, warnings: string[]): LedgerAggregate {
271
- const dbPath = path.join(workspaceDir, '.pd', 'state.db');
272
- if (!fs.existsSync(dbPath)) {
273
- return {
274
- available: false,
275
- unavailableReason: `state.db not found at ${dbPath}`,
276
- sessionsWithData: 0,
277
- avgDistinctPerSession: null,
278
- presenceRows: 0,
279
- effectRows: 0,
280
- topPrinciples: [],
281
- };
282
- }
283
- let connection: SqliteConnection;
284
- try {
285
- connection = new SqliteConnection({ workspaceDir, readonly: true, bootstrapIfMissing: false });
286
- } catch (err) {
287
- return {
288
- available: false,
289
- unavailableReason: `failed to open state.db readonly: ${err instanceof Error ? err.message : String(err)}`,
290
- sessionsWithData: 0,
291
- avgDistinctPerSession: null,
292
- presenceRows: 0,
293
- effectRows: 0,
294
- topPrinciples: [],
295
- };
296
- }
297
- try {
298
- const db = connection.getDb();
299
- const sessionRowsUnknown: unknown = db
300
- .prepare(
301
- `SELECT session_id, COUNT(DISTINCT principle_id) AS n
302
- FROM principle_applications
303
- WHERE kind = 'prompt_injected' AND session_id IS NOT NULL AND created_at >= ?
304
- GROUP BY session_id`,
305
- )
306
- .all(cutoffIso);
307
- const totalsRowUnknown: unknown = db
308
- .prepare(
309
- `SELECT
310
- SUM(CASE WHEN level = 'presence' THEN 1 ELSE 0 END) AS presence_rows,
311
- SUM(CASE WHEN level = 'effect' THEN 1 ELSE 0 END) AS effect_rows
312
- FROM principle_applications WHERE created_at >= ?`,
313
- )
314
- .get(cutoffIso);
315
- const topRowsUnknown: unknown = db
316
- .prepare(
317
- `SELECT principle_id,
318
- SUM(CASE WHEN level = 'presence' THEN 1 ELSE 0 END) AS presence_count,
319
- SUM(CASE WHEN level = 'effect' THEN 1 ELSE 0 END) AS effect_count
320
- FROM principle_applications WHERE created_at >= ?
321
- GROUP BY principle_id
322
- ORDER BY effect_count DESC, presence_count DESC LIMIT 10`,
323
- )
324
- .all(cutoffIso);
325
-
326
- // rc-1/rc-2: rows arrive unknown; validate element-wise into primitives.
327
- const perSession: number[] = [];
328
- for (const row of Array.isArray(sessionRowsUnknown) ? sessionRowsUnknown : []) {
329
- if (!isRecord(row)) continue;
330
- const n = asNumber(row.n);
331
- if (n !== null) perSession.push(n);
332
- }
333
- let presenceRows = 0;
334
- let effectRows = 0;
335
- if (isRecord(totalsRowUnknown)) {
336
- presenceRows = asNumber(totalsRowUnknown.presence_rows) ?? 0;
337
- effectRows = asNumber(totalsRowUnknown.effect_rows) ?? 0;
338
- }
339
- const top: { principleId: string; presenceCount: number; effectCount: number }[] = [];
340
- for (const row of Array.isArray(topRowsUnknown) ? topRowsUnknown : []) {
341
- if (!isRecord(row)) continue;
342
- const principleId = asString(row.principle_id);
343
- const p = asNumber(row.presence_count) ?? 0;
344
- const e = asNumber(row.effect_count) ?? 0;
345
- if (principleId !== null) top.push({ principleId, presenceCount: p, effectCount: e });
346
- }
347
- const avg = perSession.length > 0 ? perSession.reduce((a, b) => a + b, 0) / perSession.length : null;
348
- return {
349
- available: true,
350
- sessionsWithData: perSession.length,
351
- avgDistinctPerSession: avg,
352
- presenceRows,
353
- effectRows,
354
- topPrinciples: top,
355
- };
356
- } catch (err) {
357
- // Most commonly: pre-receipt state.db without the principle_applications table.
358
- const message = err instanceof Error ? err.message : String(err);
359
- warnings.push(`ledger query failed (${message})`);
360
- return {
361
- available: false,
362
- unavailableReason: `ledger query failed: ${message}`,
363
- sessionsWithData: 0,
364
- avgDistinctPerSession: null,
365
- presenceRows: 0,
366
- effectRows: 0,
367
- topPrinciples: [],
368
- };
369
- } finally {
370
- connection.close();
371
- }
372
- }
373
-
374
- interface BuildResultInput {
375
- workspaceDir: string;
376
- windowDays: number;
377
- turns: InjectionTurnRecord[];
378
- daysFound: string[];
379
- ledger: LedgerAggregate;
380
- receiptLedgerFlagEnabled: boolean | null;
381
- receiptSelfReportFlagEnabled: boolean | null;
382
- warnings: string[];
383
- }
384
-
385
- function buildResult(input: BuildResultInput): PrinciplesStatsResult {
386
- const {
387
- workspaceDir,
388
- windowDays,
389
- turns,
390
- daysFound,
391
- ledger,
392
- receiptLedgerFlagEnabled,
393
- receiptSelfReportFlagEnabled,
394
- warnings,
395
- } = input;
396
- const notes: string[] = [];
397
- notes.push('per-turn precision (chars/truncation/duplicates) covers only the retained event window (~7 days)');
398
- if (!ledger.available && ledger.unavailableReason) {
399
- notes.push(`receipt ledger unavailable: ${ledger.unavailableReason}`);
400
- }
401
-
402
- // ── Sessions & counts ──
403
- const eventSessions = new Set(turns.map((t) => t.sessionId));
404
- // Ledger session ids are aggregated away by SQL, so the headline session
405
- // count takes the larger of the two source-side session counts.
406
- const sessions = Math.max(eventSessions.size, ledger.sessionsWithData);
407
-
408
- const totalTurnIds = turns.reduce((acc, t) => acc + t.principleIds.length, 0);
409
- const distinctPrinciples = new Set(turns.flatMap((t) => t.principleIds)).size;
410
- const avgPerTurn = turns.length > 0 ? totalTurnIds / turns.length : null;
411
-
412
- // Prefer the ledger's session-level distinct counts (90-day, deduped by the
413
- // unique index); fall back to the events-window union-per-session estimate.
414
- let { avgDistinctPerSession } = ledger;
415
- let injectionsSource: 'ledger' | 'events' | 'none' = 'none';
416
- if (avgDistinctPerSession !== null) {
417
- injectionsSource = 'ledger';
418
- } else if (turns.length > 0) {
419
- const perSessionUnion = new Map<string, Set<string>>();
420
- for (const t of turns) {
421
- let set = perSessionUnion.get(t.sessionId);
422
- if (!set) {
423
- set = new Set<string>();
424
- perSessionUnion.set(t.sessionId, set);
425
- }
426
- for (const id of t.principleIds) set.add(id);
427
- }
428
- avgDistinctPerSession =
429
- perSessionUnion.size > 0
430
- ? [...perSessionUnion.values()].reduce((acc, s) => acc + s.size, 0) / perSessionUnion.size
431
- : null;
432
- if (avgDistinctPerSession !== null) injectionsSource = 'events';
433
- }
434
-
435
- // ── Chars / truncation (events only) ──
436
- const v2CharTurns = turns.map((t) => t.injectedCharCount);
437
- const legacyCharTurns = turns.map((t) => t.legacyTotalChars).filter((c): c is number => c !== null);
438
- const reporting = turns.filter((t) => t.v2Truncated !== null || t.legacyTruncated !== null);
439
- const v2TruncatedTurns = turns.filter((t) => t.v2Truncated === true).length;
440
- const legacyTruncatedTurns = turns.filter((t) => t.legacyTruncated === true).length;
441
- const truncationRate =
442
- reporting.length > 0 ? (v2TruncatedTurns + legacyTruncatedTurns) / reporting.length : null;
443
-
444
- // ── Duplicates ──
445
- const crossBlockTotal = turns.reduce((acc, t) => acc + t.crossBlockDuplicateIds.length, 0);
446
- const crossBlockCounts = new Map<string, number>();
447
- for (const t of turns) {
448
- for (const id of t.crossBlockDuplicateIds) {
449
- crossBlockCounts.set(id, (crossBlockCounts.get(id) ?? 0) + 1);
450
- }
451
- }
452
- const crossBlockTop = [...crossBlockCounts.entries()]
453
- .map(([principleId, count]) => ({ principleId, count }))
454
- .sort((a, b) => b.count - a.count)
455
- .slice(0, 5);
456
- // Intra-session repeat: principles seen in >1 turn within one session,
457
- // relative to all distinct principles observed in the window.
458
- const perSessionIdTurns = new Map<string, Map<string, number>>();
459
- for (const t of turns) {
460
- let inner = perSessionIdTurns.get(t.sessionId);
461
- if (!inner) {
462
- inner = new Map<string, number>();
463
- perSessionIdTurns.set(t.sessionId, inner);
464
- }
465
- for (const id of t.principleIds) {
466
- inner.set(id, (inner.get(id) ?? 0) + 1);
467
- }
468
- }
469
- let repeatedIds = 0;
470
- for (const inner of perSessionIdTurns.values()) {
471
- for (const n of inner.values()) {
472
- if (n > 1) repeatedIds++;
473
- }
474
- }
475
- const intraSessionRepeatShare = distinctPrinciples > 0 ? repeatedIds / distinctPrinciples : null;
476
-
477
- const correlated = ledger.topPrinciples.filter((p) => p.effectCount > 0);
478
-
479
- const result: PrinciplesStatsResult = {
480
- ok: true,
481
- status: turns.length === 0 && !ledger.available ? 'degraded' : 'ok',
482
- workspaceDir,
483
- windowDays,
484
- coverage: {
485
- eventsDaysFound: daysFound,
486
- eventsTurns: turns.length,
487
- ledgerAvailable: ledger.available,
488
- receiptLedgerFlagEnabled,
489
- receiptSelfReportFlagEnabled,
490
- notes,
491
- },
492
- sessions,
493
- injections: {
494
- avgDistinctPerSession,
495
- avgPerTurn,
496
- distinctPrinciples,
497
- source: injectionsSource,
498
- },
499
- chars: {
500
- avgV2PerTurn: v2CharTurns.length > 0 ? v2CharTurns.reduce((a, b) => a + b, 0) / v2CharTurns.length : null,
501
- avgLegacyPerTurn:
502
- legacyCharTurns.length > 0 ? legacyCharTurns.reduce((a, b) => a + b, 0) / legacyCharTurns.length : null,
503
- truncationRate,
504
- v2TruncatedTurns,
505
- legacyTruncatedTurns,
506
- turnsReporting: reporting.length,
507
- },
508
- duplicates: {
509
- crossBlockTotal,
510
- crossBlockTop,
511
- intraSessionRepeatShare,
512
- },
513
- applicationCorrelation: {
514
- presenceRows: ledger.presenceRows,
515
- effectRows: ledger.effectRows,
516
- correlatedPrinciples: correlated.length,
517
- top: ledger.topPrinciples,
518
- },
519
- warnings,
520
- };
521
-
522
- if (result.status === 'degraded') {
523
- const flagHint =
524
- receiptLedgerFlagEnabled === false
525
- ? 'Enable features.principle_receipt_ledger and features.principle_receipt_self_report in <workspace>/.pd/config.yaml to start capturing durable receipts.'
526
- : 'Run PD with an agent session first — stats appear after prompt builds occur.';
527
- result.nextAction = `No injection data found in the last ${windowDays} day(s). ${flagHint}`;
528
- }
529
- return result;
530
- }
531
-
532
- function formatTextOutput(r: PrinciplesStatsResult): string {
533
- const pct = (v: number | null): string => (v === null ? 'n/a' : `${(v * 100).toFixed(1)}%`);
534
- const num = (v: number | null): string => (v === null ? 'n/a' : v.toFixed(2));
535
- const lines: string[] = [];
536
- lines.push('PD Principles Injection Stats');
537
- lines.push(` workspace: ${r.workspaceDir}`);
538
- lines.push(` window: last ${r.windowDays} day(s) [status: ${r.status}]`);
539
- lines.push(` sessions: ${r.sessions}`);
540
- lines.push('');
541
- lines.push('Injections');
542
- lines.push(` avg distinct/session: ${num(r.injections.avgDistinctPerSession)} (source: ${r.injections.source})`);
543
- lines.push(` avg per turn: ${num(r.injections.avgPerTurn)} (${r.coverage.eventsTurns} turns in event window)`);
544
- lines.push(` distinct principles: ${r.injections.distinctPrinciples}`);
545
- lines.push('');
546
- lines.push('Context cost (chars)');
547
- lines.push(` avg runtime-v2 block: ${num(r.chars.avgV2PerTurn)}`);
548
- lines.push(` avg legacy block: ${num(r.chars.avgLegacyPerTurn)}`);
549
- lines.push(` truncation rate: ${pct(r.chars.truncationRate)} (v2:${r.chars.v2TruncatedTurns} legacy:${r.chars.legacyTruncatedTurns} of ${r.chars.turnsReporting} reporting turns)`);
550
- lines.push('');
551
- lines.push('Duplicates');
552
- lines.push(` cross-block dup pressure (v2 ids suppressed, legacy already carries them): ${r.duplicates.crossBlockTotal}`);
553
- for (const d of r.duplicates.crossBlockTop) {
554
- lines.push(` - ${d.principleId}: ${d.count} turn(s)`);
555
- }
556
- lines.push(` per-turn re-injection share (stability): ${pct(r.duplicates.intraSessionRepeatShare)}`);
557
- lines.push('');
558
- lines.push('Application correlation (receipt ledger)');
559
- lines.push(` ledger available: ${r.coverage.ledgerAvailable ? 'yes' : 'no'}`);
560
- lines.push(` presence/effect rows: ${r.applicationCorrelation.presenceRows} / ${r.applicationCorrelation.effectRows}`);
561
- for (const t of r.applicationCorrelation.top.slice(0, 5)) {
562
- lines.push(` - ${t.principleId}: presence=${t.presenceCount} effect=${t.effectCount}`);
563
- }
564
- lines.push('');
565
- lines.push('Coverage');
566
- lines.push(` event days found: ${r.coverage.eventsDaysFound.length > 0 ? r.coverage.eventsDaysFound.join(', ') : '(none)'}`);
567
- lines.push(` receipt flags: ledger=${String(r.coverage.receiptLedgerFlagEnabled)} self_report=${String(r.coverage.receiptSelfReportFlagEnabled)}`);
568
- for (const n of r.coverage.notes) lines.push(` note: ${n}`);
569
- for (const w of r.warnings) lines.push(` warning: ${w}`);
570
- if (r.nextAction) {
571
- lines.push('');
572
- lines.push(`Next action: ${r.nextAction}`);
573
- }
574
- return lines.join('\n');
575
- }
576
-
577
- export async function handlePrinciplesStats(opts: PrinciplesStatsOptions): Promise<void> {
578
- // Resolve --days first so arg errors never touch the filesystem.
579
- let windowDays = DEFAULT_DAYS;
580
- if (opts.days !== undefined) {
581
- if (!Number.isFinite(opts.days) || opts.days < 1 || opts.days > MAX_DAYS || !Number.isInteger(opts.days)) {
582
- const msg = `Error: --days must be an integer between 1 and ${MAX_DAYS}, got ${opts.days}`;
583
- if (opts.json) {
584
- process.stderr.write(
585
- JSON.stringify(
586
- {
587
- ok: false,
588
- reason: msg,
589
- nextAction: `Pass --days as an integer in [1, ${MAX_DAYS}] (e.g. --days 14). Omit for the default (${DEFAULT_DAYS}).`,
590
- },
591
- null,
592
- 2,
593
- ) + '\n',
594
- );
595
- } else {
596
- process.stderr.write(msg + '\n');
597
- }
598
- process.exitCode = 1;
599
- return; // cli-2-exit-stops
600
- }
601
- windowDays = opts.days;
602
- }
603
-
604
- try {
605
- const workspaceDir = opts.workspace ? path.resolve(opts.workspace) : resolveWorkspaceDir();
606
-
607
- const now = new Date();
608
- // Event files are UTC-day based; the window is computed on the same basis
609
- // so day boundaries align with the file names (a local-date window would
610
- // drift by the timezone offset at day edges).
611
- const cutoffDateStr = utcDateString(new Date(now.getTime() - (windowDays - 1) * 24 * 3600 * 1000));
612
- const todayStr = utcDateString(now);
613
- const cutoffIso = new Date(now.getTime() - windowDays * 24 * 3600 * 1000).toISOString();
614
-
615
- const warnings: string[] = [];
616
- const { turns, daysFound } = readInjectionEvents(
617
- workspaceDir,
618
- { cutoffDateStr: cutoffDateStr, todayStr: todayStr },
619
- warnings,
620
- );
621
- const ledger = readLedgerAggregate(workspaceDir, cutoffIso, warnings);
622
-
623
- // Effective receipt flag states help interpret an empty ledger (flags off
624
- // vs genuinely no traffic). Best-effort: null when config cannot be loaded.
625
- let receiptLedgerFlagEnabled: boolean | null = null;
626
- let receiptSelfReportFlagEnabled: boolean | null = null;
627
- try {
628
- const loadResult = loadPdConfig(workspaceDir);
629
- const { flags } = computeFlagsFromLoadResult(loadResult);
630
- receiptLedgerFlagEnabled = flags.principle_receipt_ledger?.enabled ?? null;
631
- receiptSelfReportFlagEnabled = flags.principle_receipt_self_report?.enabled ?? null;
632
- } catch {
633
- warnings.push('feature flag states could not be determined (.pd/config.yaml unreadable?)');
634
- }
635
-
636
- const result = buildResult({
637
- workspaceDir,
638
- windowDays,
639
- turns,
640
- daysFound,
641
- ledger,
642
- receiptLedgerFlagEnabled,
643
- receiptSelfReportFlagEnabled,
644
- warnings,
645
- });
646
-
647
- if (opts.json) {
648
- // cli-1-strict-json: stdout is exactly one parseable JSON object.
649
- console.log(JSON.stringify(result, null, 2));
650
- } else {
651
- console.log(formatTextOutput(result));
652
- }
653
- } catch (err: unknown) {
654
- const message = err instanceof Error ? err.message : String(err);
655
- if (opts.json) {
656
- process.stderr.write(
657
- JSON.stringify(
658
- {
659
- ok: false,
660
- reason: `Failed to compute principle stats: ${message}`,
661
- nextAction: 'Check the workspace path (-w) and that .pd/config.yaml is readable, then retry.',
662
- },
663
- null,
664
- 2,
665
- ) + '\n',
666
- );
667
- } else {
668
- process.stderr.write(`Error: ${message}\n`);
669
- }
670
- process.exitCode = 1;
671
- return; // cli-2-exit-stops
672
- }
673
- }
674
-
675
- /**
676
- * Register the `principles` command group with `stats` subcommand on a
677
- * Commander program. Used by both production CLI (src/index.ts) and
678
- * parser-level tests (cli-7-test-wiring).
679
- */
680
- export function registerPrinciplesCommand(program: Command): Command {
681
- const principlesCmd = program
682
- .command('principles')
683
- .description('Owner-facing principle observability (injection stats, PRI-562 Phase 0)');
684
-
685
- principlesCmd
686
- .command('stats')
687
- .description('Summarize principle injection volume/cost/duplicates/application evidence')
688
- .option('-w, --workspace <path>', 'Workspace directory')
689
- .option('--json', 'Output raw JSON (cli-1-strict-json: stdout is exactly one JSON object)')
690
- .option('--days <n>', 'Window in days (default: 14, max: 90)', parseInt)
691
- .action(async (opts) => {
692
- await handlePrinciplesStats({
693
- workspace: opts.workspace,
694
- json: opts.json === true,
695
- days: opts.days,
696
- });
697
- });
698
-
699
- return principlesCmd;
700
- }