@principles/pd-cli 1.152.12 → 1.152.14

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 (305) hide show
  1. package/dist/commands/candidate.d.ts.map +1 -1
  2. package/dist/commands/candidate.js +34 -7
  3. package/dist/commands/candidate.js.map +1 -1
  4. package/dist/commands/diagnose.d.ts.map +1 -1
  5. package/dist/commands/diagnose.js +23 -7
  6. package/dist/commands/diagnose.js.map +1 -1
  7. package/dist/commands/pain-retry.d.ts.map +1 -1
  8. package/dist/commands/pain-retry.js +21 -7
  9. package/dist/commands/pain-retry.js.map +1 -1
  10. package/package.json +13 -7
  11. package/CHANGELOG.md +0 -23
  12. package/dist/commands/__tests__/intent-flag-wiring.test.d.ts +0 -9
  13. package/dist/commands/__tests__/intent-flag-wiring.test.d.ts.map +0 -1
  14. package/dist/commands/__tests__/intent-flag-wiring.test.js +0 -237
  15. package/dist/commands/__tests__/intent-flag-wiring.test.js.map +0 -1
  16. package/dist/commands/__tests__/legacy-cleanup.test.d.ts +0 -18
  17. package/dist/commands/__tests__/legacy-cleanup.test.d.ts.map +0 -1
  18. package/dist/commands/__tests__/legacy-cleanup.test.js +0 -467
  19. package/dist/commands/__tests__/legacy-cleanup.test.js.map +0 -1
  20. package/dist/commands/__tests__/mvp-smoke.test.d.ts +0 -15
  21. package/dist/commands/__tests__/mvp-smoke.test.d.ts.map +0 -1
  22. package/dist/commands/__tests__/mvp-smoke.test.js +0 -262
  23. package/dist/commands/__tests__/mvp-smoke.test.js.map +0 -1
  24. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts +0 -2
  25. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts.map +0 -1
  26. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js +0 -60
  27. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js.map +0 -1
  28. package/dist/commands/__tests__/rulecode-flag-wiring.test.d.ts +0 -21
  29. package/dist/commands/__tests__/rulecode-flag-wiring.test.d.ts.map +0 -1
  30. package/dist/commands/__tests__/rulecode-flag-wiring.test.js +0 -186
  31. package/dist/commands/__tests__/rulecode-flag-wiring.test.js.map +0 -1
  32. package/dist/commands/__tests__/rulecode-handler.test.d.ts +0 -16
  33. package/dist/commands/__tests__/rulecode-handler.test.d.ts.map +0 -1
  34. package/dist/commands/__tests__/rulecode-handler.test.js +0 -285
  35. package/dist/commands/__tests__/rulecode-handler.test.js.map +0 -1
  36. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.d.ts +0 -24
  37. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.d.ts.map +0 -1
  38. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js +0 -232
  39. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js.map +0 -1
  40. package/dist/commands/__tests__/runtime-activation-nextaction.test.d.ts +0 -15
  41. package/dist/commands/__tests__/runtime-activation-nextaction.test.d.ts.map +0 -1
  42. package/dist/commands/__tests__/runtime-activation-nextaction.test.js +0 -73
  43. package/dist/commands/__tests__/runtime-activation-nextaction.test.js.map +0 -1
  44. package/dist/commands/__tests__/runtime-probe-config.test.d.ts +0 -20
  45. package/dist/commands/__tests__/runtime-probe-config.test.d.ts.map +0 -1
  46. package/dist/commands/__tests__/runtime-probe-config.test.js +0 -389
  47. package/dist/commands/__tests__/runtime-probe-config.test.js.map +0 -1
  48. package/dist/commands/__tests__/telemetry-flag-wiring.test.d.ts +0 -9
  49. package/dist/commands/__tests__/telemetry-flag-wiring.test.d.ts.map +0 -1
  50. package/dist/commands/__tests__/telemetry-flag-wiring.test.js +0 -76
  51. package/dist/commands/__tests__/telemetry-flag-wiring.test.js.map +0 -1
  52. package/dist/commands/__tests__/version.test.d.ts +0 -2
  53. package/dist/commands/__tests__/version.test.d.ts.map +0 -1
  54. package/dist/commands/__tests__/version.test.js +0 -238
  55. package/dist/commands/__tests__/version.test.js.map +0 -1
  56. package/dist/commands/runtime-uat.guard.test.d.ts +0 -2
  57. package/dist/commands/runtime-uat.guard.test.d.ts.map +0 -1
  58. package/dist/commands/runtime-uat.guard.test.js +0 -153
  59. package/dist/commands/runtime-uat.guard.test.js.map +0 -1
  60. package/dist/services/__tests__/cli-output.test.d.ts +0 -18
  61. package/dist/services/__tests__/cli-output.test.d.ts.map +0 -1
  62. package/dist/services/__tests__/cli-output.test.js +0 -103
  63. package/dist/services/__tests__/cli-output.test.js.map +0 -1
  64. package/dist/services/__tests__/evaluator-runner-deps.test.d.ts +0 -2
  65. package/dist/services/__tests__/evaluator-runner-deps.test.d.ts.map +0 -1
  66. package/dist/services/__tests__/evaluator-runner-deps.test.js +0 -343
  67. package/dist/services/__tests__/evaluator-runner-deps.test.js.map +0 -1
  68. package/dist/services/__tests__/rulehost-readiness.test.d.ts +0 -2
  69. package/dist/services/__tests__/rulehost-readiness.test.d.ts.map +0 -1
  70. package/dist/services/__tests__/rulehost-readiness.test.js +0 -311
  71. package/dist/services/__tests__/rulehost-readiness.test.js.map +0 -1
  72. package/dist/services/__tests__/runtime-adapter-resolver.test.d.ts +0 -18
  73. package/dist/services/__tests__/runtime-adapter-resolver.test.d.ts.map +0 -1
  74. package/dist/services/__tests__/runtime-adapter-resolver.test.js +0 -673
  75. package/dist/services/__tests__/runtime-adapter-resolver.test.js.map +0 -1
  76. package/dist/utils/production-workspace-guard.test.d.ts +0 -2
  77. package/dist/utils/production-workspace-guard.test.d.ts.map +0 -1
  78. package/dist/utils/production-workspace-guard.test.js +0 -137
  79. package/dist/utils/production-workspace-guard.test.js.map +0 -1
  80. package/scripts/llm-dogfood.ts +0 -456
  81. package/scripts/migrate-illegal-expected-decision.ts +0 -252
  82. package/src/commands/__tests__/intent-flag-wiring.test.ts +0 -282
  83. package/src/commands/__tests__/legacy-cleanup.test.ts +0 -602
  84. package/src/commands/__tests__/mvp-smoke.test.ts +0 -300
  85. package/src/commands/__tests__/pain-retry-maxTokens-flag.test.ts +0 -73
  86. package/src/commands/__tests__/rulecode-flag-wiring.test.ts +0 -237
  87. package/src/commands/__tests__/rulecode-handler.test.ts +0 -369
  88. package/src/commands/__tests__/run-rulehost-flag-wiring.test.ts +0 -292
  89. package/src/commands/__tests__/runtime-activation-nextaction.test.ts +0 -81
  90. package/src/commands/__tests__/runtime-probe-config.test.ts +0 -432
  91. package/src/commands/__tests__/telemetry-flag-wiring.test.ts +0 -84
  92. package/src/commands/__tests__/version.test.ts +0 -251
  93. package/src/commands/admission-gate.ts +0 -30
  94. package/src/commands/artifact.ts +0 -82
  95. package/src/commands/build-trajectory-evidence.ts +0 -298
  96. package/src/commands/candidate.ts +0 -1250
  97. package/src/commands/codex-ingest-catchup.ts +0 -114
  98. package/src/commands/codex-ingest-quarantine.ts +0 -127
  99. package/src/commands/codex-reconcile.ts +0 -84
  100. package/src/commands/codex-setup.ts +0 -476
  101. package/src/commands/codex-worker.ts +0 -185
  102. package/src/commands/command-helpers.ts +0 -24
  103. package/src/commands/config-doctor.ts +0 -193
  104. package/src/commands/console.ts +0 -797
  105. package/src/commands/context.ts +0 -72
  106. package/src/commands/demo-story-a.ts +0 -241
  107. package/src/commands/diagnose.ts +0 -756
  108. package/src/commands/errors-list.ts +0 -441
  109. package/src/commands/evolution-tasks-list.ts +0 -48
  110. package/src/commands/evolution-tasks-show.ts +0 -62
  111. package/src/commands/flow.ts +0 -60
  112. package/src/commands/health-codex.ts +0 -576
  113. package/src/commands/health.ts +0 -315
  114. package/src/commands/history.ts +0 -63
  115. package/src/commands/intent.ts +0 -457
  116. package/src/commands/legacy-cleanup.ts +0 -531
  117. package/src/commands/legacy-import.ts +0 -104
  118. package/src/commands/mvp-smoke.ts +0 -160
  119. package/src/commands/pain-evidence.ts +0 -226
  120. package/src/commands/pain-list.ts +0 -239
  121. package/src/commands/pain-record.ts +0 -612
  122. package/src/commands/pain-retry.ts +0 -916
  123. package/src/commands/principles-stats.ts +0 -700
  124. package/src/commands/proven-channel-baseline.ts +0 -115
  125. package/src/commands/quality-scorecard.ts +0 -272
  126. package/src/commands/remediation-output.ts +0 -66
  127. package/src/commands/rulecode.ts +0 -465
  128. package/src/commands/run.ts +0 -89
  129. package/src/commands/runtime-activation.ts +0 -1629
  130. package/src/commands/runtime-artifact-repair.ts +0 -524
  131. package/src/commands/runtime-canary.ts +0 -395
  132. package/src/commands/runtime-compatibility-scan.ts +0 -105
  133. package/src/commands/runtime-diagnostics-export.ts +0 -229
  134. package/src/commands/runtime-features.ts +0 -153
  135. package/src/commands/runtime-gfi-snapshot.ts +0 -135
  136. package/src/commands/runtime-health-snapshot.ts +0 -108
  137. package/src/commands/runtime-init.ts +0 -429
  138. package/src/commands/runtime-internalization-enqueue-successors.ts +0 -530
  139. package/src/commands/runtime-internalization-integrity-repair.ts +0 -79
  140. package/src/commands/runtime-internalization-integrity.ts +0 -107
  141. package/src/commands/runtime-internalization-queue.ts +0 -135
  142. package/src/commands/runtime-internalization-retry.ts +0 -191
  143. package/src/commands/runtime-internalization-run-once.ts +0 -791
  144. package/src/commands/runtime-internalization-run-rulehost.ts +0 -749
  145. package/src/commands/runtime-internalization-wake-once.ts +0 -87
  146. package/src/commands/runtime-pain-flood-simulation.ts +0 -121
  147. package/src/commands/runtime-pruning.ts +0 -438
  148. package/src/commands/runtime-recovery-failed-tasks.ts +0 -258
  149. package/src/commands/runtime-recovery.ts +0 -109
  150. package/src/commands/runtime-synthetic-baseline.ts +0 -70
  151. package/src/commands/runtime-uat.guard.test.ts +0 -175
  152. package/src/commands/runtime-uat.ts +0 -380
  153. package/src/commands/runtime.ts +0 -473
  154. package/src/commands/samples-list.ts +0 -39
  155. package/src/commands/samples-review.ts +0 -36
  156. package/src/commands/task.ts +0 -271
  157. package/src/commands/telemetry.ts +0 -349
  158. package/src/commands/trace.ts +0 -174
  159. package/src/commands/trajectory-db-unavailable.ts +0 -19
  160. package/src/commands/trajectory.ts +0 -64
  161. package/src/commands/version.ts +0 -49
  162. package/src/config-reader.ts +0 -122
  163. package/src/index.ts +0 -1276
  164. package/src/legacy/legacy-import.ts +0 -179
  165. package/src/legacy/session-history-import.ts +0 -203
  166. package/src/resolve-workspace.ts +0 -106
  167. package/src/services/__tests__/cli-output.test.ts +0 -130
  168. package/src/services/__tests__/evaluator-runner-deps.test.ts +0 -382
  169. package/src/services/__tests__/rulehost-readiness.test.ts +0 -363
  170. package/src/services/__tests__/runtime-adapter-resolver.test.ts +0 -793
  171. package/src/services/cli-output.ts +0 -95
  172. package/src/services/config-doctor.ts +0 -697
  173. package/src/services/console-launcher.ts +0 -537
  174. package/src/services/demo-rule-compiler.ts +0 -40
  175. package/src/services/demo-story-a-runner.ts +0 -471
  176. package/src/services/mainline-snapshot-assembler.ts +0 -579
  177. package/src/services/pain-flood-simulation-runner.ts +0 -462
  178. package/src/services/pd-config-loader.ts +0 -433
  179. package/src/services/proven-channel-baseline-runner.ts +0 -157
  180. package/src/services/quality-scorecard/data-extractor.ts +0 -150
  181. package/src/services/quality-scorecard/local-evaluator.ts +0 -142
  182. package/src/services/quality-scorecard/strong-model-gate.ts +0 -202
  183. package/src/services/resolve-runtime-from-pd-config.ts +0 -230
  184. package/src/services/rulehost-pipeline-runner.ts +0 -1214
  185. package/src/services/rulehost-readiness.ts +0 -326
  186. package/src/services/runtime-adapter-resolver.ts +0 -364
  187. package/src/services/synthetic-baseline-runner.ts +0 -400
  188. package/src/services/version-report.ts +0 -253
  189. package/src/services/workspace-tool-semantics.ts +0 -26
  190. package/src/utils/path-security.ts +0 -96
  191. package/src/utils/production-workspace-guard.test.ts +0 -157
  192. package/src/utils/production-workspace-guard.ts +0 -210
  193. package/tests/bdd/cli-contract.steps.ts +0 -346
  194. package/tests/bdd/principles-stats.steps.ts +0 -211
  195. package/tests/bdd/support/gherkin-loader.ts +0 -147
  196. package/tests/bdd/support/repo-root.ts +0 -62
  197. package/tests/bdd/support/vitest-bdd.ts +0 -179
  198. package/tests/commands/admission-gate-coverage.test.ts +0 -79
  199. package/tests/commands/build-trajectory-evidence.test.ts +0 -508
  200. package/tests/commands/candidate-audit-repair.test.ts +0 -421
  201. package/tests/commands/candidate-intake.test.ts +0 -732
  202. package/tests/commands/candidate-internalization-backfill.test.ts +0 -548
  203. package/tests/commands/candidate-internalize-lineage.test.ts +0 -628
  204. package/tests/commands/candidate-internalize.test.ts +0 -299
  205. package/tests/commands/candidate-route.test.ts +0 -329
  206. package/tests/commands/candidate-show.test.ts +0 -95
  207. package/tests/commands/cli-command-tree.test.ts +0 -144
  208. package/tests/commands/cli-help-snapshot.test.ts +0 -155
  209. package/tests/commands/cli-skill-contract.test.ts +0 -121
  210. package/tests/commands/codex-consent-reversibility.steps.test.ts +0 -299
  211. package/tests/commands/codex-ingest-quarantine.test.ts +0 -136
  212. package/tests/commands/codex-setup.test.ts +0 -297
  213. package/tests/commands/codex-worker-daemon.test.ts +0 -95
  214. package/tests/commands/codex-worker-registration.test.ts +0 -207
  215. package/tests/commands/config-doctor.test.ts +0 -336
  216. package/tests/commands/console-launcher-edge-cases.test.ts +0 -388
  217. package/tests/commands/console-open.test.ts +0 -1381
  218. package/tests/commands/context.test.ts +0 -114
  219. package/tests/commands/demo-story-a.test.ts +0 -318
  220. package/tests/commands/diagnose.test.ts +0 -1803
  221. package/tests/commands/errors-list.test.ts +0 -679
  222. package/tests/commands/evolution-tasks-list.test.ts +0 -41
  223. package/tests/commands/evolution-tasks-show.test.ts +0 -46
  224. package/tests/commands/health-codex.test.ts +0 -349
  225. package/tests/commands/health-host-wiring.test.ts +0 -76
  226. package/tests/commands/health-reliability.test.ts +0 -99
  227. package/tests/commands/health.test.ts +0 -413
  228. package/tests/commands/intent.test.ts +0 -321
  229. package/tests/commands/legacy-cleanup.test.ts +0 -148
  230. package/tests/commands/pain-evidence.test.ts +0 -518
  231. package/tests/commands/pain-list.test.ts +0 -219
  232. package/tests/commands/pain-record-async.test.ts +0 -335
  233. package/tests/commands/pain-record-session-parser.test.ts +0 -228
  234. package/tests/commands/pain-record.test.ts +0 -851
  235. package/tests/commands/pain-retry.test.ts +0 -1121
  236. package/tests/commands/plugin-config-resolution-cutover.test.ts +0 -229
  237. package/tests/commands/pri-393-runtime-config-unification.test.ts +0 -443
  238. package/tests/commands/principles-stats-wiring.test.ts +0 -135
  239. package/tests/commands/principles-stats.test.ts +0 -274
  240. package/tests/commands/product-path-regression.test.ts +0 -86
  241. package/tests/commands/proven-channel-baseline.test.ts +0 -441
  242. package/tests/commands/rulecode.test.ts +0 -177
  243. package/tests/commands/run-rulehost-handler.test.ts +0 -908
  244. package/tests/commands/runtime-activation-approve-flag-wiring.test.ts +0 -227
  245. package/tests/commands/runtime-activation-deactivate-flag-wiring.test.ts +0 -167
  246. package/tests/commands/runtime-activation-dispatch-flag-wiring.test.ts +0 -262
  247. package/tests/commands/runtime-activation-governance-audit.test.ts +0 -95
  248. package/tests/commands/runtime-activation-list-flag-wiring.test.ts +0 -193
  249. package/tests/commands/runtime-activation-promote-flag-wiring.test.ts +0 -248
  250. package/tests/commands/runtime-activation-shadow-telemetry.test.ts +0 -130
  251. package/tests/commands/runtime-activation.test.ts +0 -2052
  252. package/tests/commands/runtime-artifact-repair-registration.test.ts +0 -74
  253. package/tests/commands/runtime-artifact-repair.test.ts +0 -404
  254. package/tests/commands/runtime-canary.test.ts +0 -430
  255. package/tests/commands/runtime-compatibility-scan.test.ts +0 -146
  256. package/tests/commands/runtime-diagnostics-export.test.ts +0 -175
  257. package/tests/commands/runtime-features.test.ts +0 -249
  258. package/tests/commands/runtime-health-snapshot.test.ts +0 -358
  259. package/tests/commands/runtime-init-empty-workspace.test.ts +0 -252
  260. package/tests/commands/runtime-init.test.ts +0 -420
  261. package/tests/commands/runtime-internalization-enqueue-successors.test.ts +0 -782
  262. package/tests/commands/runtime-internalization-integrity-repair.test.ts +0 -239
  263. package/tests/commands/runtime-internalization-integrity.test.ts +0 -140
  264. package/tests/commands/runtime-internalization-queue.test.ts +0 -380
  265. package/tests/commands/runtime-internalization-retry-owner-authority.test.ts +0 -431
  266. package/tests/commands/runtime-internalization-run-once-evaluator-parity.test.ts +0 -269
  267. package/tests/commands/runtime-internalization-run-once-language.test.ts +0 -303
  268. package/tests/commands/runtime-internalization-run-once-real-config.test.ts +0 -237
  269. package/tests/commands/runtime-internalization-run-once-rollout-parity.test.ts +0 -706
  270. package/tests/commands/runtime-internalization-run-once.test.ts +0 -1791
  271. package/tests/commands/runtime-internalization-wake-once.test.ts +0 -171
  272. package/tests/commands/runtime-internalization.test.ts +0 -53
  273. package/tests/commands/runtime-pain-flood-simulation.test.ts +0 -418
  274. package/tests/commands/runtime-pruning.test.ts +0 -694
  275. package/tests/commands/runtime-recovery-failed-tasks.test.ts +0 -328
  276. package/tests/commands/runtime-recovery.test.ts +0 -120
  277. package/tests/commands/runtime-synthetic-baseline.test.ts +0 -249
  278. package/tests/commands/runtime-uat.test.ts +0 -397
  279. package/tests/commands/runtime.test.ts +0 -392
  280. package/tests/commands/samples-list.test.ts +0 -43
  281. package/tests/commands/samples-review.test.ts +0 -41
  282. package/tests/commands/task.test.ts +0 -153
  283. package/tests/commands/telemetry.test.ts +0 -190
  284. package/tests/commands/trace.test.ts +0 -316
  285. package/tests/config-reader.test.ts +0 -143
  286. package/tests/e2e/candidate-intake-e2e.test.ts +0 -338
  287. package/tests/e2e/cli-full-flow.test.ts +0 -233
  288. package/tests/e2e/cross-package-acceptance.test.ts +0 -635
  289. package/tests/helpers/pd-cli-path.ts +0 -5
  290. package/tests/pd-cli-smoke.test.ts +0 -13
  291. package/tests/resolve-workspace.test.ts +0 -196
  292. package/tests/services/config-doctor-signal-health.test.ts +0 -109
  293. package/tests/services/console-launcher.test.ts +0 -114
  294. package/tests/services/demo-rule-compiler.test.ts +0 -242
  295. package/tests/services/mainline-snapshot-assembler.test.ts +0 -572
  296. package/tests/services/pd-config-loader.test.ts +0 -505
  297. package/tests/services/proven-channel-baseline-runner.test.ts +0 -30
  298. package/tests/services/quality-scorecard/strong-model-gate.test.ts +0 -133
  299. package/tests/services/resolve-runtime-from-pd-config.test.ts +0 -485
  300. package/tests/services/rulehost-pipeline-e2e.test.ts +0 -538
  301. package/tests/services/rulehost-pipeline-runner.test.ts +0 -1242
  302. package/tests/services/workspace-tool-semantics.test.ts +0 -76
  303. package/tests/utils/path-security.test.ts +0 -180
  304. package/tsconfig.json +0 -26
  305. 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
- }