principles-disciple 1.72.0 → 1.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/INSTALL.md +1 -3
  2. package/openclaw.plugin.json +10 -5
  3. package/package.json +17 -19
  4. package/scripts/acceptance-test.mjs +16 -73
  5. package/scripts/sync-plugin.mjs +382 -77
  6. package/src/commands/archive-impl.ts +2 -1
  7. package/src/commands/capabilities.ts +2 -2
  8. package/src/commands/context.ts +2 -2
  9. package/src/commands/disable-impl.ts +2 -1
  10. package/src/commands/evolution-status.ts +16 -16
  11. package/src/commands/export.ts +12 -67
  12. package/src/commands/pain.ts +91 -1
  13. package/src/commands/principle-rollback.ts +2 -1
  14. package/src/commands/promote-impl.ts +7 -43
  15. package/src/commands/rollback-impl.ts +2 -1
  16. package/src/commands/rollback.ts +2 -1
  17. package/src/commands/samples.ts +2 -1
  18. package/src/commands/thinking-os.ts +2 -1
  19. package/src/config/errors.ts +18 -2
  20. package/src/constants/diagnostician.ts +2 -2
  21. package/src/constants/tools.ts +2 -1
  22. package/src/core/__tests__/focus-history.test.ts +210 -0
  23. package/src/core/config.ts +1 -1
  24. package/src/core/correction-cue-learner.ts +2 -136
  25. package/src/core/correction-types.ts +16 -88
  26. package/src/core/dictionary.ts +19 -20
  27. package/src/core/empathy-keyword-matcher.ts +17 -289
  28. package/src/core/empathy-types.ts +18 -229
  29. package/src/core/event-log.ts +29 -132
  30. package/src/core/evolution-reducer.ts +21 -2
  31. package/src/core/evolution-types.ts +76 -464
  32. package/src/core/file-store.ts +80 -0
  33. package/src/core/focus-history.ts +228 -955
  34. package/src/core/local-worker-routing.ts +34 -314
  35. package/src/core/merge-gate-audit.ts +0 -195
  36. package/src/core/migration.ts +0 -1
  37. package/src/core/pain-diagnostic-gate.ts +154 -0
  38. package/src/core/pain-signal.ts +21 -138
  39. package/src/core/pain.ts +15 -88
  40. package/src/core/path-resolver.ts +0 -1
  41. package/src/core/paths.ts +0 -1
  42. package/src/core/pd-task-reconciler.ts +26 -115
  43. package/src/core/pd-task-service.ts +9 -9
  44. package/src/core/pd-task-types.ts +23 -127
  45. package/src/core/principle-compiler/__tests__/compiler-replay-gate.test.ts +174 -0
  46. package/src/core/principle-compiler/code-validator.ts +15 -42
  47. package/src/core/principle-compiler/compiler.ts +100 -15
  48. package/src/core/principle-compiler/index.ts +5 -2
  49. package/src/core/principle-compiler/template-generator.ts +4 -104
  50. package/src/core/principle-injection.ts +10 -202
  51. package/src/core/principle-internalization/filesystem-lifecycle-datasource.ts +42 -0
  52. package/src/core/principle-internalization/lifecycle-read-model.ts +39 -242
  53. package/src/core/principle-internalization/principle-lifecycle-service.ts +12 -10
  54. package/src/core/principle-tree-ledger-adapter.ts +145 -0
  55. package/src/core/principle-tree-ledger.ts +8 -6
  56. package/src/core/reflection/reflection-context.ts +14 -109
  57. package/src/core/replay-engine.ts +8 -500
  58. package/src/core/rule-host-helpers.ts +5 -35
  59. package/src/core/rule-host-types.ts +10 -82
  60. package/src/core/rule-host.ts +6 -63
  61. package/src/core/runtime-v2-prompt-activation-reader.ts +231 -0
  62. package/src/core/session-tracker.ts +87 -101
  63. package/src/core/shadow-observation-registry.ts +19 -48
  64. package/src/core/trajectory.ts +3 -1
  65. package/src/core/workflow-funnel-loader.ts +62 -68
  66. package/src/core/workspace-context.ts +46 -0
  67. package/src/core/workspace-dir-service.ts +1 -1
  68. package/src/core/workspace-dir-validation.ts +18 -9
  69. package/src/hooks/AGENTS.md +1 -1
  70. package/src/hooks/gate-block-helper.ts +71 -64
  71. package/src/hooks/gate.ts +183 -31
  72. package/src/hooks/lifecycle.ts +30 -32
  73. package/src/hooks/llm.ts +60 -32
  74. package/src/hooks/pain.ts +297 -103
  75. package/src/hooks/prompt.ts +400 -440
  76. package/src/hooks/subagent.ts +2 -29
  77. package/src/i18n/commands.ts +2 -10
  78. package/src/index.ts +95 -85
  79. package/src/openclaw-sdk.ts +311 -0
  80. package/src/service/central-database.ts +8 -4
  81. package/src/service/evolution-queue-migration.ts +2 -1
  82. package/src/service/evolution-worker.ts +163 -1786
  83. package/src/service/internalization-trigger-adapter.ts +302 -0
  84. package/src/service/keyword-optimization-service.ts +4 -4
  85. package/src/service/monitoring-query-service.ts +1 -215
  86. package/src/service/queue-io.ts +60 -331
  87. package/src/service/runtime-summary-service.ts +59 -16
  88. package/src/service/subagent-workflow/index.ts +0 -41
  89. package/src/service/subagent-workflow/types.ts +9 -120
  90. package/src/service/subagent-workflow/workflow-store.ts +2 -119
  91. package/src/service/workflow-watchdog.ts +0 -43
  92. package/src/types/event-payload.ts +16 -74
  93. package/src/types/event-types.ts +38 -547
  94. package/src/types/hygiene-types.ts +7 -30
  95. package/src/types/principle-tree-schema.ts +20 -222
  96. package/src/types/queue.ts +15 -70
  97. package/src/types/runtime-summary.ts +5 -49
  98. package/src/utils/io.ts +8 -20
  99. package/src/utils/retry.ts +1 -1
  100. package/src/utils/shadow-fingerprint.ts +2 -2
  101. package/src/utils/workspace-resolver.ts +50 -0
  102. package/templates/langs/en/core/AGENTS.md +7 -7
  103. package/templates/langs/en/core/BOOT.md +1 -1
  104. package/templates/langs/en/core/HEARTBEAT.md +2 -2
  105. package/templates/langs/en/principles/THINKING_OS.md +3 -2
  106. package/templates/langs/en/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  107. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  108. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  109. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  110. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  111. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  112. package/templates/langs/en/skills/evolve-task/SKILL.md +3 -3
  113. package/templates/langs/en/skills/pd-cli-operator/SKILL.md +67 -0
  114. package/templates/langs/en/skills/pd-diagnostician/SKILL.md +1 -1
  115. package/templates/langs/en/skills/pd-mentor/SKILL.md +2 -3
  116. package/templates/langs/en/skills/pd-pain-signal/SKILL.md +17 -39
  117. package/templates/langs/en/skills/pd-runtime-v2/SKILL.md +61 -0
  118. package/templates/langs/zh/core/AGENTS.md +7 -7
  119. package/templates/langs/zh/core/BOOT.md +1 -1
  120. package/templates/langs/zh/core/HEARTBEAT.md +2 -2
  121. package/templates/langs/zh/principles/THINKING_OS.md +3 -2
  122. package/templates/langs/zh/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  123. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  124. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  125. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +8 -8
  126. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  127. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  128. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  129. package/templates/langs/zh/skills/ai-sprint-orchestration/test/run.test.mjs +21 -5
  130. package/templates/langs/zh/skills/evolve-task/SKILL.md +4 -4
  131. package/templates/langs/zh/skills/pd-cli-operator/SKILL.md +67 -0
  132. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +1 -1
  133. package/templates/langs/zh/skills/pd-mentor/SKILL.md +2 -3
  134. package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +17 -38
  135. package/templates/langs/zh/skills/pd-runtime-v2/SKILL.md +61 -0
  136. package/tests/build-artifacts.test.ts +1 -3
  137. package/tests/commands/evolution-status.test.ts +0 -118
  138. package/tests/core/bootstrap-rules.test.ts +1 -1
  139. package/tests/core/config.test.ts +1 -1
  140. package/tests/core/event-log.test.ts +35 -0
  141. package/tests/core/evolution-engine.test.ts +610 -0
  142. package/tests/core/file-store.test.ts +102 -0
  143. package/tests/core/focus-history.test.ts +203 -11
  144. package/tests/core/merge-gate-audit.test.ts +2 -169
  145. package/tests/core/migration.test.ts +7 -7
  146. package/tests/core/model-deployment-registry.test.ts +7 -1
  147. package/tests/core/model-training-registry.test.ts +19 -0
  148. package/tests/core/observability.test.ts +0 -1
  149. package/tests/core/pain-diagnostic-gate.test.ts +498 -0
  150. package/tests/core/pain.test.ts +0 -1
  151. package/tests/core/path-resolver.test.ts +1 -1
  152. package/tests/core/paths-refactor.test.ts +0 -22
  153. package/tests/core/principle-internalization/deprecated-readiness.test.ts +2 -2
  154. package/tests/core/principle-internalization/lifecycle-metrics.test.ts +2 -2
  155. package/tests/core/principle-internalization/{internalization-routing-policy.test.ts → lifecycle-routing-policy.test.ts} +6 -6
  156. package/tests/core/principle-internalization/lineage-source-retired.test.ts +56 -0
  157. package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +1 -23
  158. package/tests/core/principle-tree-ledger-adapter.test.ts +253 -0
  159. package/tests/core/reflection-context.test.ts +0 -14
  160. package/tests/core/replay-engine.test.ts +127 -215
  161. package/tests/core/rule-host-helpers.test.ts +2 -2
  162. package/tests/core/rule-implementation-runtime.test.ts +0 -27
  163. package/tests/core/workflow-funnel-loader.test.ts +162 -0
  164. package/tests/core/workspace-context.test.ts +2 -2
  165. package/tests/core/workspace-dir-validation.test.ts +8 -1
  166. package/tests/core-anti-growth.test.ts +191 -0
  167. package/tests/hook-workspace-nextaction-contract.test.ts +42 -0
  168. package/tests/hooks/confirm-first-removal.test.ts +188 -0
  169. package/tests/hooks/gate-auto-correct-shadow.test.ts +310 -0
  170. package/tests/hooks/gate-auto-correct.test.ts +665 -0
  171. package/tests/hooks/gate-no-path-write-tool.test.ts +172 -0
  172. package/tests/hooks/gate-rule-host-pipeline.test.ts +2 -1
  173. package/tests/hooks/pain.test.ts +269 -12
  174. package/tests/hooks/prompt-characterization.test.ts +500 -0
  175. package/tests/hooks/prompt-size-guard.test.ts +32 -17
  176. package/tests/hooks/runtime-v2-prompt-activation.test.ts +869 -0
  177. package/tests/index.test.ts +94 -1
  178. package/tests/integration/auto-entry-gate.test.ts +248 -0
  179. package/tests/integration/internalization-trigger-guard.test.ts +69 -0
  180. package/tests/integration/m8-legacy-paths.test.ts +63 -0
  181. package/tests/integration/runtime-v2-pain-guard.test.ts +125 -0
  182. package/tests/plugin-config-resolution-cutover.test.ts +359 -0
  183. package/tests/runtime-v2-discovery-guard.test.ts +154 -0
  184. package/tests/service/central-database.test.ts +457 -0
  185. package/tests/service/evolution-worker.correction-observer.test.ts +173 -0
  186. package/tests/service/evolution-worker.timeout.test.ts +11 -129
  187. package/tests/service/internalization-trigger-adapter.test.ts +251 -0
  188. package/tests/service/monitoring-query-service.test.ts +1 -47
  189. package/tests/service/queue-io.test.ts +1 -62
  190. package/tests/service/runtime-summary-service.test.ts +3 -1
  191. package/tests/service/workflow-watchdog.test.ts +0 -91
  192. package/tests/utils/file-lock.test.ts +5 -3
  193. package/tests/utils/session-key.test.ts +52 -0
  194. package/tests/utils/subagent-probe.test.ts +48 -1
  195. package/vitest.config.ts +4 -11
  196. package/.planning/codebase/ARCHITECTURE.md +0 -157
  197. package/.planning/codebase/CONCERNS.md +0 -145
  198. package/.planning/codebase/CONVENTIONS.md +0 -148
  199. package/.planning/codebase/INTEGRATIONS.md +0 -81
  200. package/.planning/codebase/STACK.md +0 -87
  201. package/.planning/codebase/STRUCTURE.md +0 -193
  202. package/.planning/codebase/TESTING.md +0 -243
  203. package/.planning/phases/01-basic-visualization/01-GAP-CLOSURE-VERIFICATION.md +0 -113
  204. package/docs/COMMAND_REFERENCE.md +0 -76
  205. package/docs/COMMAND_REFERENCE_EN.md +0 -79
  206. package/scripts/build-web.mjs +0 -46
  207. package/scripts/diagnose-nocturnal.mjs +0 -537
  208. package/scripts/seed-nocturnal-scenarios.mjs +0 -384
  209. package/src/commands/nocturnal-review.ts +0 -322
  210. package/src/commands/nocturnal-rollout.ts +0 -790
  211. package/src/commands/nocturnal-train.ts +0 -986
  212. package/src/commands/pd-reflect.ts +0 -88
  213. package/src/core/adaptive-thresholds.ts +0 -478
  214. package/src/core/diagnostician-task-store.ts +0 -192
  215. package/src/core/nocturnal-arbiter.ts +0 -715
  216. package/src/core/nocturnal-artifact-lineage.ts +0 -116
  217. package/src/core/nocturnal-artificer.ts +0 -257
  218. package/src/core/nocturnal-candidate-scoring.ts +0 -530
  219. package/src/core/nocturnal-compliance.ts +0 -1146
  220. package/src/core/nocturnal-dataset.ts +0 -763
  221. package/src/core/nocturnal-executability.ts +0 -428
  222. package/src/core/nocturnal-export.ts +0 -499
  223. package/src/core/nocturnal-paths.ts +0 -240
  224. package/src/core/nocturnal-reasoning-deriver.ts +0 -343
  225. package/src/core/nocturnal-rule-implementation-validator.ts +0 -246
  226. package/src/core/nocturnal-snapshot-contract.ts +0 -99
  227. package/src/core/nocturnal-trajectory-extractor.ts +0 -512
  228. package/src/core/nocturnal-trinity-types.ts +0 -218
  229. package/src/core/nocturnal-trinity.ts +0 -2680
  230. package/src/core/principle-internalization/deprecated-readiness.ts +0 -93
  231. package/src/core/principle-internalization/internalization-routing-policy.ts +0 -208
  232. package/src/core/principle-internalization/lifecycle-metrics.ts +0 -152
  233. package/src/http/principles-console-route.ts +0 -709
  234. package/src/service/central-health-service.ts +0 -49
  235. package/src/service/central-overview-service.ts +0 -138
  236. package/src/service/control-ui-query-service.ts +0 -900
  237. package/src/service/cooldown-strategy.ts +0 -97
  238. package/src/service/evolution-pain-context.ts +0 -79
  239. package/src/service/evolution-query-service.ts +0 -407
  240. package/src/service/health-query-service.ts +0 -1038
  241. package/src/service/nocturnal-config.ts +0 -214
  242. package/src/service/nocturnal-runtime.ts +0 -734
  243. package/src/service/nocturnal-service.ts +0 -1605
  244. package/src/service/nocturnal-target-selector.ts +0 -545
  245. package/src/service/sleep-cycle.ts +0 -157
  246. package/src/service/startup-reconciler.ts +0 -112
  247. package/src/service/subagent-workflow/correction-observer-types.ts +0 -82
  248. package/src/service/subagent-workflow/correction-observer-workflow-manager.ts +0 -250
  249. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +0 -1
  250. package/src/service/subagent-workflow/dynamic-timeout.ts +0 -30
  251. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +0 -268
  252. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +0 -795
  253. package/src/service/subagent-workflow/runtime-direct-driver.ts +0 -268
  254. package/src/service/subagent-workflow/workflow-manager-base.ts +0 -580
  255. package/src/tools/write-pain-flag.ts +0 -215
  256. package/templates/langs/en/skills/plan-script/SKILL.md +0 -32
  257. package/templates/langs/zh/skills/plan-script/SKILL.md +0 -32
  258. package/tests/commands/nocturnal-review.test.ts +0 -448
  259. package/tests/commands/nocturnal-train.test.ts +0 -97
  260. package/tests/commands/pd-reflect.test.ts +0 -49
  261. package/tests/core/adaptive-thresholds.test.ts +0 -261
  262. package/tests/core/nocturnal-arbiter.test.ts +0 -559
  263. package/tests/core/nocturnal-artifact-lineage.test.ts +0 -53
  264. package/tests/core/nocturnal-artificer.test.ts +0 -241
  265. package/tests/core/nocturnal-candidate-scoring.test.ts +0 -532
  266. package/tests/core/nocturnal-compliance-p-principles.test.ts +0 -133
  267. package/tests/core/nocturnal-compliance.test.ts +0 -646
  268. package/tests/core/nocturnal-dataset.test.ts +0 -892
  269. package/tests/core/nocturnal-e2e.test.ts +0 -234
  270. package/tests/core/nocturnal-executability.test.ts +0 -357
  271. package/tests/core/nocturnal-export.test.ts +0 -517
  272. package/tests/core/nocturnal-reasoning-deriver.test.ts +0 -372
  273. package/tests/core/nocturnal-reviewed-subset-comparison.test.ts +0 -428
  274. package/tests/core/nocturnal-rule-implementation-validator.test.ts +0 -127
  275. package/tests/core/nocturnal-snapshot-contract.test.ts +0 -121
  276. package/tests/core/nocturnal-trajectory-extractor.test.ts +0 -634
  277. package/tests/core/nocturnal-trinity.test.ts +0 -2053
  278. package/tests/core/pain-auto-repair.test.ts +0 -96
  279. package/tests/core/pain-integration.test.ts +0 -510
  280. package/tests/fixtures/nocturnal-reviewed-subset.json +0 -183
  281. package/tests/http/principles-console-route.test.ts +0 -162
  282. package/tests/integration/chaos-resilience.test.ts +0 -348
  283. package/tests/integration/empathy-workflow-integration.test.ts +0 -626
  284. package/tests/integration/pain-diagnostician-loop.e2e.test.ts +0 -380
  285. package/tests/service/control-ui-query-service.test.ts +0 -121
  286. package/tests/service/cooldown-strategy.test.ts +0 -164
  287. package/tests/service/data-endpoints-regression.test.ts +0 -834
  288. package/tests/service/empathy-observer-workflow-manager.test.ts +0 -175
  289. package/tests/service/evolution-worker.nocturnal.test.ts +0 -601
  290. package/tests/service/nocturnal-runtime-hardening.test.ts +0 -118
  291. package/tests/service/nocturnal-runtime.test.ts +0 -473
  292. package/tests/service/nocturnal-service-code-candidate.test.ts +0 -330
  293. package/tests/service/nocturnal-target-selector.test.ts +0 -615
  294. package/tests/service/startup-reconciler.test.ts +0 -148
  295. package/tests/tools/write-pain-flag.test.ts +0 -358
  296. package/ui/src/App.tsx +0 -45
  297. package/ui/src/api.ts +0 -220
  298. package/ui/src/charts.tsx +0 -955
  299. package/ui/src/components/ErrorState.tsx +0 -6
  300. package/ui/src/components/Loading.tsx +0 -13
  301. package/ui/src/components/ProtectedRoute.tsx +0 -12
  302. package/ui/src/components/Shell.tsx +0 -91
  303. package/ui/src/components/WorkspaceConfig.tsx +0 -178
  304. package/ui/src/components/index.ts +0 -5
  305. package/ui/src/context/auth.tsx +0 -80
  306. package/ui/src/context/theme.tsx +0 -66
  307. package/ui/src/hooks/useAutoRefresh.ts +0 -39
  308. package/ui/src/i18n/ui.ts +0 -473
  309. package/ui/src/main.tsx +0 -16
  310. package/ui/src/pages/EvolutionPage.tsx +0 -333
  311. package/ui/src/pages/FeedbackPage.tsx +0 -138
  312. package/ui/src/pages/GateMonitorPage.tsx +0 -136
  313. package/ui/src/pages/LoginPage.tsx +0 -89
  314. package/ui/src/pages/OverviewPage.tsx +0 -599
  315. package/ui/src/pages/SamplesPage.tsx +0 -174
  316. package/ui/src/pages/ThinkingModelsPage.tsx +0 -702
  317. package/ui/src/styles.css +0 -2020
  318. package/ui/src/types.ts +0 -384
  319. package/ui/src/utils/format.ts +0 -15
@@ -1,646 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import {
3
- detectOpportunity,
4
- detectViolation,
5
- computeCompliance,
6
- computeAllCompliance,
7
- groupEventsIntoSessions,
8
- type SessionEvents,
9
- type RawEventEntry,
10
- } from '../../src/core/nocturnal-compliance.js';
11
-
12
- // ---------------------------------------------------------------------------
13
- // Test Utilities
14
- // ---------------------------------------------------------------------------
15
-
16
- function makeSession(overrides: Partial<SessionEvents> = {}): SessionEvents {
17
- return {
18
- sessionId: overrides.sessionId ?? 'session-1',
19
- toolCalls: overrides.toolCalls ?? [],
20
- painSignals: overrides.painSignals ?? [],
21
- gateBlocks: overrides.gateBlocks ?? [],
22
- userCorrections: overrides.userCorrections ?? [],
23
- planApprovals: overrides.planApprovals ?? [],
24
- };
25
- }
26
-
27
- // ---------------------------------------------------------------------------
28
- // detectOpportunity — T-01
29
- // ---------------------------------------------------------------------------
30
-
31
- describe('detectOpportunity — T-01', () => {
32
- it('returns applicable on edit operations', () => {
33
- const session = makeSession({
34
- toolCalls: [{ toolName: 'edit_file', filePath: 'src/main.ts', outcome: 'success' }],
35
- });
36
- const result = detectOpportunity('T-01', session);
37
- expect(result.applicable).toBe(true);
38
- });
39
-
40
- it('returns applicable on write_to_file', () => {
41
- const session = makeSession({
42
- toolCalls: [{ toolName: 'write_to_file', filePath: 'src/new.ts', outcome: 'success' }],
43
- });
44
- expect(detectOpportunity('T-01', session).applicable).toBe(true);
45
- });
46
-
47
- it('returns not applicable when only read operations', () => {
48
- const session = makeSession({
49
- toolCalls: [{ toolName: 'read_file', filePath: 'src/main.ts', outcome: 'success' }],
50
- });
51
- const result = detectOpportunity('T-01', session);
52
- expect(result.applicable).toBe(false);
53
- });
54
-
55
- it('returns not applicable on empty session', () => {
56
- const session = makeSession({ toolCalls: [] });
57
- expect(detectOpportunity('T-01', session).applicable).toBe(false);
58
- });
59
- });
60
-
61
- // ---------------------------------------------------------------------------
62
- // detectOpportunity — T-05
63
- // ---------------------------------------------------------------------------
64
-
65
- describe('detectOpportunity — T-05', () => {
66
- it('returns applicable when gate block fires', () => {
67
- const session = makeSession({
68
- gateBlocks: [{ toolName: 'delete_file', filePath: 'src/main.ts', reason: 'risky operation' }],
69
- });
70
- const result = detectOpportunity('T-05', session);
71
- expect(result.applicable).toBe(true);
72
- });
73
-
74
- it('returns applicable when risky tool is attempted', () => {
75
- const session = makeSession({
76
- toolCalls: [{ toolName: 'delete_file', outcome: 'blocked' }],
77
- });
78
- expect(detectOpportunity('T-05', session).applicable).toBe(true);
79
- });
80
-
81
- it('returns applicable for dangerous bash command', () => {
82
- const session = makeSession({
83
- toolCalls: [{ toolName: 'bash', outcome: 'failure', errorMessage: 'rm -rf /home' }],
84
- });
85
- expect(detectOpportunity('T-05', session).applicable).toBe(true);
86
- });
87
-
88
- it('returns not applicable when no risky operations', () => {
89
- const session = makeSession({
90
- toolCalls: [{ toolName: 'read_file', outcome: 'success' }],
91
- });
92
- const result = detectOpportunity('T-05', session);
93
- expect(result.applicable).toBe(false);
94
- });
95
- });
96
-
97
- // ---------------------------------------------------------------------------
98
- // detectOpportunity — T-09
99
- // ---------------------------------------------------------------------------
100
-
101
- describe('detectOpportunity — T-09', () => {
102
- it('returns applicable when session has 5+ tool calls', () => {
103
- const calls = Array.from({ length: 6 }, (_, i) => ({
104
- toolName: 'read_file' as const,
105
- filePath: `src/file${i}.ts`,
106
- outcome: 'success' as const,
107
- }));
108
- const session = makeSession({ toolCalls: calls });
109
- expect(detectOpportunity('T-09', session).applicable).toBe(true);
110
- });
111
-
112
- it('returns applicable when 3+ files touched', () => {
113
- const session = makeSession({
114
- toolCalls: [
115
- { toolName: 'read_file', filePath: 'src/a.ts', outcome: 'success' },
116
- { toolName: 'read_file', filePath: 'src/b.ts', outcome: 'success' },
117
- { toolName: 'edit_file', filePath: 'src/c.ts', outcome: 'success' },
118
- ],
119
- });
120
- expect(detectOpportunity('T-09', session).applicable).toBe(true);
121
- });
122
-
123
- it('returns applicable when pain present on complex task', () => {
124
- const session = makeSession({
125
- toolCalls: [
126
- { toolName: 'edit_file', filePath: 'src/main.ts', outcome: 'failure' },
127
- ],
128
- painSignals: [{ source: 'edit', score: 60 }],
129
- });
130
- expect(detectOpportunity('T-09', session).applicable).toBe(true);
131
- });
132
-
133
- it('returns not applicable for short sessions', () => {
134
- const session = makeSession({
135
- toolCalls: [
136
- { toolName: 'read_file', filePath: 'src/main.ts', outcome: 'success' },
137
- { toolName: 'edit_file', filePath: 'src/main.ts', outcome: 'success' },
138
- ],
139
- });
140
- expect(detectOpportunity('T-09', session).applicable).toBe(false);
141
- });
142
- });
143
-
144
- // ---------------------------------------------------------------------------
145
- // detectViolation — T-01
146
- // ---------------------------------------------------------------------------
147
-
148
- describe('detectViolation — T-01', () => {
149
- it('returns violated when editing unread file followed by pain', () => {
150
- const session = makeSession({
151
- toolCalls: [
152
- { toolName: 'edit_file', filePath: 'src/main.ts', outcome: 'failure' },
153
- ],
154
- painSignals: [
155
- { source: 'src/main.ts', score: 50, reason: 'edit without understanding structure' },
156
- ],
157
- });
158
- const result = detectViolation('T-01', session);
159
- expect(result.violated).toBe(true);
160
- });
161
-
162
- it('returns NOT violated when file was read before edit', () => {
163
- const session = makeSession({
164
- toolCalls: [
165
- { toolName: 'read_file', filePath: 'src/main.ts', outcome: 'success' },
166
- { toolName: 'edit_file', filePath: 'src/main.ts', outcome: 'success' },
167
- ],
168
- });
169
- const result = detectViolation('T-01', session);
170
- expect(result.violated).toBe(false);
171
- });
172
-
173
- it('returns violated when editing unread file followed by tool failure', () => {
174
- const session = makeSession({
175
- toolCalls: [
176
- { toolName: 'edit_file', filePath: 'src/main.ts', outcome: 'failure' },
177
- ],
178
- });
179
- const result = detectViolation('T-01', session);
180
- expect(result.violated).toBe(true);
181
- expect(result.reason).toContain('without understanding');
182
- });
183
-
184
- it('returns NOT violated when edit succeeds without prior read (but no pain)', () => {
185
- // No pain signal, no failure → can't confirm violation
186
- const session = makeSession({
187
- toolCalls: [
188
- { toolName: 'edit_file', filePath: 'src/main.ts', outcome: 'success' },
189
- ],
190
- });
191
- const result = detectViolation('T-01', session);
192
- expect(result.violated).toBe(false);
193
- });
194
- });
195
-
196
- // ---------------------------------------------------------------------------
197
- // detectViolation — T-05
198
- // ---------------------------------------------------------------------------
199
-
200
- describe('detectViolation — T-05', () => {
201
- it('returns violated when gate block fires on risky operation', () => {
202
- const session = makeSession({
203
- gateBlocks: [{ toolName: 'bash', reason: 'rm -rf attempted', filePath: '/' }],
204
- });
205
- const result = detectViolation('T-05', session);
206
- expect(result.violated).toBe(true);
207
- expect(result.reason).toContain('safety rail not');
208
- });
209
-
210
- it('returns violated when gate block fires on delete_file', () => {
211
- const session = makeSession({
212
- gateBlocks: [{ toolName: 'delete_file', reason: 'risky', filePath: 'src/old.ts' }],
213
- });
214
- expect(detectViolation('T-05', session).violated).toBe(true);
215
- });
216
-
217
- it('returns NOT violated when no gate blocks', () => {
218
- const session = makeSession({
219
- toolCalls: [{ toolName: 'delete_file', outcome: 'success' }],
220
- });
221
- expect(detectViolation('T-05', session).violated).toBe(false);
222
- });
223
- });
224
-
225
- // ---------------------------------------------------------------------------
226
- // detectViolation — T-09
227
- // ---------------------------------------------------------------------------
228
-
229
- describe('detectViolation — T-09', () => {
230
- it('returns violated on complex task with failure and no planning', () => {
231
- const calls = Array.from({ length: 6 }, (_, i) => ({
232
- toolName: 'edit_file' as const,
233
- filePath: `src/file${i}.ts`,
234
- outcome: 'failure' as const,
235
- }));
236
- const session = makeSession({ toolCalls: calls });
237
- const result = detectViolation('T-09', session);
238
- expect(result.violated).toBe(true);
239
- });
240
-
241
- it('returns NOT violated on complex task that has plan approval', () => {
242
- const calls = Array.from({ length: 6 }, (_, i) => ({
243
- toolName: 'edit_file' as const,
244
- filePath: `src/file${i}.ts`,
245
- outcome: 'failure' as const,
246
- }));
247
- const session = makeSession({
248
- toolCalls: calls,
249
- planApprovals: [{ toolName: 'edit_file', filePath: 'src/main.ts' }],
250
- });
251
- expect(detectViolation('T-09', session).violated).toBe(false);
252
- });
253
-
254
- it('returns NOT violated on non-complex session', () => {
255
- const session = makeSession({
256
- toolCalls: [
257
- { toolName: 'read_file', filePath: 'src/main.ts', outcome: 'success' },
258
- { toolName: 'edit_file', filePath: 'src/main.ts', outcome: 'success' },
259
- ],
260
- });
261
- expect(detectViolation('T-09', session).violated).toBe(false);
262
- });
263
- });
264
-
265
- // ---------------------------------------------------------------------------
266
- // computeCompliance — basic
267
- // ---------------------------------------------------------------------------
268
-
269
- describe('computeCompliance — basic', () => {
270
- it('returns zero compliance when no sessions provided', () => {
271
- const result = computeCompliance('T-01', []);
272
- expect(result.principleId).toBe('T-01');
273
- expect(result.applicableOpportunityCount).toBe(0);
274
- expect(result.observedViolationCount).toBe(0);
275
- expect(result.complianceRate).toBe(0);
276
- expect(result.violationTrend).toBe(0);
277
- });
278
-
279
- it('returns compliance 1.0 when all opportunities compliant', () => {
280
- // T-01 applicable (has edit) but no violation (file was read first)
281
- const session = makeSession({
282
- toolCalls: [
283
- { toolName: 'read_file', filePath: 'src/main.ts', outcome: 'success' },
284
- { toolName: 'edit_file', filePath: 'src/main.ts', outcome: 'success' },
285
- ],
286
- });
287
- const result = computeCompliance('T-01', [session]);
288
- expect(result.complianceRate).toBe(1.0);
289
- expect(result.applicableOpportunityCount).toBe(1);
290
- expect(result.observedViolationCount).toBe(0);
291
- });
292
-
293
- it('returns compliance 0.0 when all opportunities violated', () => {
294
- // T-05: gate block fires (applicable) AND violated
295
- const session = makeSession({
296
- gateBlocks: [{ toolName: 'delete_file', reason: 'risky', filePath: 'src/old.ts' }],
297
- });
298
- const result = computeCompliance('T-05', [session]);
299
- expect(result.complianceRate).toBe(0);
300
- expect(result.applicableOpportunityCount).toBe(1);
301
- expect(result.observedViolationCount).toBe(1);
302
- });
303
-
304
- it('computes partial compliance correctly', () => {
305
- const compliant = makeSession({
306
- toolCalls: [
307
- { toolName: 'read_file', filePath: 'src/main.ts', outcome: 'success' },
308
- { toolName: 'edit_file', filePath: 'src/main.ts', outcome: 'success' },
309
- ],
310
- });
311
- const violated = makeSession({
312
- sessionId: 'session-2',
313
- toolCalls: [
314
- { toolName: 'edit_file', filePath: 'src/main.ts', outcome: 'failure' },
315
- ],
316
- });
317
- const result = computeCompliance('T-01', [compliant, violated]);
318
- // 2 applicable, 1 violated → compliance = (2-1)/2 = 0.5
319
- expect(result.complianceRate).toBe(0.5);
320
- expect(result.applicableOpportunityCount).toBe(2);
321
- expect(result.observedViolationCount).toBe(1);
322
- });
323
- });
324
-
325
- // ---------------------------------------------------------------------------
326
- // computeCompliance — dilution prevention
327
- // ---------------------------------------------------------------------------
328
-
329
- describe('computeCompliance — dilution prevention', () => {
330
- /**
331
- * The dilution prevention scenario:
332
- * T-05 is a LOW-frequency, HIGH-severity principle.
333
- * If we compute compliance over ALL sessions (including ones with no risky ops),
334
- * the compliance rate would be inflated because non-applicable sessions
335
- * count as "compliant by default" — which is WRONG.
336
- *
337
- * Our engine ONLY counts sessions where T-05 was applicable.
338
- */
339
- it('T-05 compliance ignores sessions with no risky operations', () => {
340
- // Session A: T-05 violated (gate block on delete)
341
- const sessionA = makeSession({
342
- sessionId: 'A',
343
- gateBlocks: [{ toolName: 'delete_file', reason: 'risky', filePath: 'src/old.ts' }],
344
- });
345
-
346
- // Session B: No gate blocks, no risky ops — T-05 NOT APPLICABLE
347
- const sessionB = makeSession({
348
- sessionId: 'B',
349
- toolCalls: [{ toolName: 'read_file', outcome: 'success' }],
350
- });
351
-
352
- // Session C: Another non-applicable session (only read ops)
353
- const sessionC = makeSession({
354
- sessionId: 'C',
355
- toolCalls: [{ toolName: 'grep', outcome: 'success' }],
356
- });
357
-
358
- // WRONG approach (session-average): (1 + 0 + 0) / 3 = 33% compliance
359
- // CORRECT approach (opportunity-based): only session A counts
360
- // Session A: applicable + violated → 0% compliance
361
- const result = computeCompliance('T-05', [sessionA, sessionB, sessionC]);
362
-
363
- expect(result.applicableOpportunityCount).toBe(1); // Only session A
364
- expect(result.observedViolationCount).toBe(1); // Session A violated
365
- expect(result.complianceRate).toBe(0); // 0% — not diluted by B and C
366
-
367
- // Explanation must mention dilution prevention
368
- expect(result.explanation).toContain('applicable opportunities');
369
- });
370
-
371
- it('T-01 compliance ignores sessions with no edit operations', () => {
372
- // Session A: T-01 applicable + violated
373
- const sessionA = makeSession({
374
- sessionId: 'A',
375
- toolCalls: [{ toolName: 'edit_file', filePath: 'src/main.ts', outcome: 'failure' }],
376
- });
377
-
378
- // Session B: No edits — T-01 NOT APPLICABLE
379
- const sessionB = makeSession({
380
- sessionId: 'B',
381
- toolCalls: [{ toolName: 'read_file', outcome: 'success' }],
382
- });
383
-
384
- // T-01 compliance = 0% (1 applicable, 1 violated), session B doesn't dilute
385
- const result = computeCompliance('T-01', [sessionA, sessionB]);
386
-
387
- expect(result.applicableOpportunityCount).toBe(1);
388
- expect(result.observedViolationCount).toBe(1);
389
- expect(result.complianceRate).toBe(0);
390
- });
391
-
392
- it('high-frequency principle (T-01) still gets high opportunity count across diverse sessions', () => {
393
- // Sessions with edit ops — all T-01 applicable
394
- const sessions = [
395
- makeSession({ sessionId: '1', toolCalls: [{ toolName: 'edit_file', filePath: 'a.ts', outcome: 'success' }] }),
396
- makeSession({ sessionId: '2', toolCalls: [{ toolName: 'edit_file', filePath: 'b.ts', outcome: 'failure' }] }),
397
- makeSession({ sessionId: '3', toolCalls: [{ toolName: 'write_to_file', filePath: 'c.ts', outcome: 'success' }] }),
398
- ];
399
-
400
- // Session with no edits — T-01 not applicable
401
- const noEdit = makeSession({ sessionId: '4', toolCalls: [{ toolName: 'grep', outcome: 'success' }] });
402
-
403
- const result = computeCompliance('T-01', [...sessions, noEdit]);
404
- expect(result.applicableOpportunityCount).toBe(3); // Only sessions with edits
405
- });
406
- });
407
-
408
- // ---------------------------------------------------------------------------
409
- // computeCompliance — violationTrend
410
- // ---------------------------------------------------------------------------
411
-
412
- describe('computeCompliance — violationTrend', () => {
413
- function t01(opportunity: 'violated' | 'compliant'): SessionEvents {
414
- if (opportunity === 'violated') {
415
- return makeSession({
416
- sessionId: `s-${opportunity}`,
417
- toolCalls: [{ toolName: 'edit_file', filePath: 'src/main.ts', outcome: 'failure' }],
418
- });
419
- }
420
- return makeSession({
421
- sessionId: `s-${opportunity}`,
422
- toolCalls: [
423
- { toolName: 'read_file', filePath: 'src/main.ts', outcome: 'success' },
424
- { toolName: 'edit_file', filePath: 'src/main.ts', outcome: 'success' },
425
- ],
426
- });
427
- }
428
-
429
- it('returns trend = +1 (improving) when recent violations decrease', () => {
430
- // Most recent: compliant, compliant
431
- // Previous: violated, violated, violated
432
- // Recent rate = 0/2 = 0, Previous rate = 3/3 = 1
433
- // delta = 1 - 0 > 0.1 → improving (+1)
434
- const sessions = [
435
- t01('compliant'), // index 0 (most recent in input order)
436
- t01('compliant'), // index 1
437
- t01('violated'), // index 2
438
- t01('violated'), // index 3
439
- t01('violated'), // index 4
440
- ];
441
- const result = computeCompliance('T-01', sessions, { trendWindowSize: 2 });
442
- expect(result.violationTrend).toBe(1);
443
- });
444
-
445
- it('returns trend = -1 (worsening) when recent violations increase', () => {
446
- // Most recent: violated, violated
447
- // Previous: compliant, compliant
448
- // Recent rate = 2/2 = 1, Previous rate = 0/2 = 0
449
- // delta = 0 - 1 = -1 < -0.1 → worsening (-1)
450
- const sessions = [
451
- t01('violated'),
452
- t01('violated'),
453
- t01('compliant'),
454
- t01('compliant'),
455
- ];
456
- const result = computeCompliance('T-01', sessions, { trendWindowSize: 2 });
457
- expect(result.violationTrend).toBe(-1);
458
- });
459
-
460
- it('returns trend = 0 when stable', () => {
461
- // 2 compliant, then 2 compliant
462
- const sessions = [t01('compliant'), t01('compliant'), t01('compliant'), t01('compliant')];
463
- const result = computeCompliance('T-01', sessions, { trendWindowSize: 2 });
464
- expect(result.violationTrend).toBe(0);
465
- });
466
-
467
- it('returns trend = 0 when only 1 applicable session', () => {
468
- const sessions = [t01('violated')];
469
- const result = computeCompliance('T-01', sessions);
470
- expect(result.violationTrend).toBe(0);
471
- });
472
- });
473
-
474
- // ---------------------------------------------------------------------------
475
- // computeAllCompliance
476
- // ---------------------------------------------------------------------------
477
-
478
- describe('computeAllCompliance', () => {
479
- it('returns results for all T-01 through T-09', () => {
480
- const results = computeAllCompliance([]);
481
- const ids = results.map((r) => r.principleId);
482
- expect(ids).toEqual(['T-01', 'T-02', 'T-03', 'T-04', 'T-05', 'T-06', 'T-07', 'T-08', 'T-09']);
483
- });
484
-
485
- it('each result has all required fields', () => {
486
- const results = computeAllCompliance([]);
487
- for (const result of results) {
488
- expect(result.principleId).toBeDefined();
489
- expect(result.applicableOpportunityCount).toBe(0);
490
- expect(result.observedViolationCount).toBe(0);
491
- expect(result.complianceRate).toBe(0);
492
- expect(result.violationTrend).toBe(0);
493
- expect(result.explanation).toBeDefined();
494
- }
495
- });
496
- });
497
-
498
- // ---------------------------------------------------------------------------
499
- // groupEventsIntoSessions
500
- // ---------------------------------------------------------------------------
501
-
502
- describe('groupEventsIntoSessions', () => {
503
- function event(type: string, sessionId: string, data: Record<string, unknown> = {}): RawEventEntry {
504
- return { ts: '2026-03-27T12:00:00Z', type, sessionId, data };
505
- }
506
-
507
- it('groups events by sessionId', () => {
508
- const events: RawEventEntry[] = [
509
- event('tool_call', 's1', { toolName: 'read_file', filePath: 'a.ts' }),
510
- event('tool_call', 's1', { toolName: 'edit_file', filePath: 'b.ts' }),
511
- event('tool_call', 's2', { toolName: 'read_file', filePath: 'c.ts' }),
512
- ];
513
- const sessions = groupEventsIntoSessions(events);
514
- expect(sessions.get('s1')!.toolCalls).toHaveLength(2);
515
- expect(sessions.get('s2')!.toolCalls).toHaveLength(1);
516
- });
517
-
518
- it('maps pain_signal events', () => {
519
- const events: RawEventEntry[] = [
520
- event('pain_signal', 's1', { source: 'edit', score: 50, severity: 'moderate' }),
521
- ];
522
- const sessions = groupEventsIntoSessions(events);
523
- expect(sessions.get('s1')!.painSignals).toHaveLength(1);
524
- expect(sessions.get('s1')!.painSignals[0].source).toBe('edit');
525
- expect(sessions.get('s1')!.painSignals[0].severity).toBe('moderate');
526
- });
527
-
528
- it('maps gate_block events', () => {
529
- const events: RawEventEntry[] = [
530
- event('gate_block', 's1', { toolName: 'bash', reason: 'dangerous command' }),
531
- ];
532
- const sessions = groupEventsIntoSessions(events);
533
- expect(sessions.get('s1')!.gateBlocks).toHaveLength(1);
534
- expect(sessions.get('s1')!.gateBlocks[0].toolName).toBe('bash');
535
- });
536
-
537
- it('maps plan_approval events', () => {
538
- const events: RawEventEntry[] = [
539
- event('plan_approval', 's1', { toolName: 'edit_file', filePath: 'src/main.ts' }),
540
- ];
541
- const sessions = groupEventsIntoSessions(events);
542
- expect(sessions.get('s1')!.planApprovals).toHaveLength(1);
543
- });
544
-
545
- it('groups events without sessionId into "unknown"', () => {
546
- const events: RawEventEntry[] = [
547
- { ts: '2026-03-27T12:00:00Z', type: 'tool_call', data: { toolName: 'read_file' } },
548
- ];
549
- const sessions = groupEventsIntoSessions(events);
550
- expect(sessions.get('unknown')!.toolCalls).toHaveLength(1);
551
- });
552
-
553
- it('correctly maps error field to outcome', () => {
554
- const events: RawEventEntry[] = [
555
- event('tool_call', 's1', { toolName: 'edit_file', error: 'file not found' }),
556
- event('tool_call', 's2', { toolName: 'read_file' }), // no error → success
557
- ];
558
- const sessions = groupEventsIntoSessions(events);
559
- expect(sessions.get('s1')!.toolCalls[0].outcome).toBe('failure');
560
- expect(sessions.get('s2')!.toolCalls[0].outcome).toBe('success');
561
- });
562
- });
563
-
564
- // ---------------------------------------------------------------------------
565
- // Explanation is human-readable
566
- // ---------------------------------------------------------------------------
567
-
568
- describe('ComplianceResult — explanation', () => {
569
- it('explanation includes compliance rate and trend', () => {
570
- const session = makeSession({
571
- toolCalls: [
572
- { toolName: 'read_file', filePath: 'src/main.ts', outcome: 'success' },
573
- { toolName: 'edit_file', filePath: 'src/main.ts', outcome: 'success' },
574
- ],
575
- });
576
- const result = computeCompliance('T-01', [session]);
577
- expect(result.explanation).toContain('T-01');
578
- expect(result.explanation).toContain('applicable opportunities');
579
- expect(result.explanation).toContain('100.0%'); // compliance rate
580
- });
581
-
582
- it('explanation notes when no opportunities exist', () => {
583
- const result = computeCompliance('T-05', [
584
- makeSession({ toolCalls: [{ toolName: 'read_file', outcome: 'success' }] }),
585
- ]);
586
- expect(result.explanation).toContain('No applicable opportunities');
587
- });
588
-
589
- it('explanation includes sample violation reasons', () => {
590
- const session = makeSession({
591
- gateBlocks: [{ toolName: 'delete_file', reason: 'risky', filePath: 'src/old.ts' }],
592
- });
593
- const result = computeCompliance('T-05', [session]);
594
- expect(result.explanation).toContain('violation');
595
- expect(result.explanation).toContain('safety rail not');
596
- });
597
- });
598
-
599
- // ---------------------------------------------------------------------------
600
- // Integration: full session list
601
- // ---------------------------------------------------------------------------
602
-
603
- describe('Full session integration — T-05 dilution scenario', () => {
604
- /**
605
- * Real-world scenario: 20 sessions in a day.
606
- * Only 2 sessions involve risky operations (T-05 applicable).
607
- * Both had gate blocks (violations).
608
- * 18 sessions had no risky operations (T-05 not applicable).
609
- *
610
- * If we averaged all 20 sessions: 2 violations / 20 = 90% compliance (wrong!)
611
- * With opportunity-based: 2 applicable / 2 violated = 0% compliance (correct!)
612
- */
613
- it('does not dilute low-frequency high-severity principle compliance', () => {
614
- function gateBlockSession(id: string): SessionEvents {
615
- return makeSession({
616
- sessionId: id,
617
- gateBlocks: [{ toolName: 'bash', reason: 'rm -rf attempted', filePath: '/' }],
618
- });
619
- }
620
-
621
- function safeSession(id: string): SessionEvents {
622
- return makeSession({
623
- sessionId: id,
624
- toolCalls: [{ toolName: 'read_file', outcome: 'success' }],
625
- });
626
- }
627
-
628
- const sessions: SessionEvents[] = [
629
- // Only 2 sessions where T-05 is applicable
630
- gateBlockSession('risky-1'),
631
- gateBlockSession('risky-2'),
632
- // 18 sessions where T-05 is NOT applicable (safe operations)
633
- ...Array.from({ length: 18 }, (_, i) => safeSession(`safe-${i}`)),
634
- ];
635
-
636
- const result = computeCompliance('T-05', sessions);
637
-
638
- // Only 2 applicable opportunities
639
- expect(result.applicableOpportunityCount).toBe(2);
640
- // Both were violated
641
- expect(result.observedViolationCount).toBe(2);
642
- // Compliance = 0% — NOT 90%
643
- expect(result.complianceRate).toBe(0);
644
- expect(result.explanation).toContain('applicable opportunities');
645
- });
646
- });