principles-disciple 1.71.0 → 1.73.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. package/openclaw.plugin.json +10 -5
  2. package/package.json +17 -19
  3. package/scripts/acceptance-test.mjs +16 -73
  4. package/scripts/sync-plugin.mjs +382 -77
  5. package/src/commands/archive-impl.ts +2 -1
  6. package/src/commands/capabilities.ts +2 -2
  7. package/src/commands/context.ts +2 -2
  8. package/src/commands/disable-impl.ts +2 -1
  9. package/src/commands/evolution-status.ts +16 -16
  10. package/src/commands/export.ts +12 -67
  11. package/src/commands/pain.ts +91 -1
  12. package/src/commands/principle-rollback.ts +2 -1
  13. package/src/commands/promote-impl.ts +7 -43
  14. package/src/commands/rollback-impl.ts +2 -1
  15. package/src/commands/rollback.ts +2 -1
  16. package/src/commands/samples.ts +2 -1
  17. package/src/commands/thinking-os.ts +2 -1
  18. package/src/config/errors.ts +18 -2
  19. package/src/constants/diagnostician.ts +2 -2
  20. package/src/constants/tools.ts +2 -1
  21. package/src/core/__tests__/focus-history.test.ts +210 -0
  22. package/src/core/config.ts +1 -1
  23. package/src/core/confirm-first-gate.ts +255 -0
  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 +38 -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/pain-diagnostic-gate.ts +154 -0
  37. package/src/core/pain-signal.ts +21 -138
  38. package/src/core/pain.ts +15 -88
  39. package/src/core/pd-task-reconciler.ts +26 -115
  40. package/src/core/pd-task-service.ts +9 -9
  41. package/src/core/pd-task-types.ts +23 -127
  42. package/src/core/principle-compiler/__tests__/compiler-replay-gate.test.ts +174 -0
  43. package/src/core/principle-compiler/code-validator.ts +15 -42
  44. package/src/core/principle-compiler/compiler.ts +100 -15
  45. package/src/core/principle-compiler/index.ts +5 -2
  46. package/src/core/principle-compiler/template-generator.ts +4 -104
  47. package/src/core/principle-injection.ts +10 -202
  48. package/src/core/principle-internalization/filesystem-lifecycle-datasource.ts +42 -0
  49. package/src/core/principle-internalization/lifecycle-read-model.ts +39 -242
  50. package/src/core/principle-internalization/principle-lifecycle-service.ts +12 -10
  51. package/src/core/principle-tree-ledger-adapter.ts +145 -0
  52. package/src/core/principle-tree-ledger.ts +8 -6
  53. package/src/core/reflection/reflection-context.ts +14 -109
  54. package/src/core/replay-engine.ts +8 -500
  55. package/src/core/rule-host-helpers.ts +5 -35
  56. package/src/core/rule-host-types.ts +10 -82
  57. package/src/core/rule-host.ts +6 -63
  58. package/src/core/runtime-v2-prompt-activation-reader.ts +231 -0
  59. package/src/core/session-tracker.ts +87 -101
  60. package/src/core/shadow-observation-registry.ts +19 -48
  61. package/src/core/trajectory.ts +3 -1
  62. package/src/core/workflow-funnel-loader.ts +62 -68
  63. package/src/core/workspace-context.ts +46 -0
  64. package/src/core/workspace-dir-service.ts +1 -1
  65. package/src/core/workspace-dir-validation.ts +18 -9
  66. package/src/hooks/AGENTS.md +1 -1
  67. package/src/hooks/gate-block-helper.ts +46 -44
  68. package/src/hooks/gate.ts +207 -7
  69. package/src/hooks/lifecycle.ts +30 -32
  70. package/src/hooks/llm.ts +60 -32
  71. package/src/hooks/pain.ts +297 -103
  72. package/src/hooks/prompt.ts +469 -339
  73. package/src/hooks/subagent.ts +2 -29
  74. package/src/i18n/commands.ts +2 -10
  75. package/src/index.ts +95 -85
  76. package/src/openclaw-sdk.ts +311 -0
  77. package/src/service/central-database.ts +8 -4
  78. package/src/service/evolution-queue-migration.ts +2 -1
  79. package/src/service/evolution-worker.ts +163 -1786
  80. package/src/service/internalization-trigger-adapter.ts +302 -0
  81. package/src/service/keyword-optimization-service.ts +4 -4
  82. package/src/service/monitoring-query-service.ts +1 -215
  83. package/src/service/queue-io.ts +60 -331
  84. package/src/service/runtime-summary-service.ts +115 -18
  85. package/src/service/subagent-workflow/index.ts +0 -41
  86. package/src/service/subagent-workflow/types.ts +9 -120
  87. package/src/service/subagent-workflow/workflow-store.ts +2 -119
  88. package/src/service/workflow-watchdog.ts +0 -43
  89. package/src/types/event-payload.ts +16 -74
  90. package/src/types/event-types.ts +39 -547
  91. package/src/types/hygiene-types.ts +7 -30
  92. package/src/types/principle-tree-schema.ts +20 -222
  93. package/src/types/queue.ts +15 -70
  94. package/src/types/runtime-summary.ts +5 -49
  95. package/src/utils/io.ts +10 -0
  96. package/src/utils/retry.ts +1 -1
  97. package/src/utils/shadow-fingerprint.ts +2 -2
  98. package/src/utils/workspace-resolver.ts +50 -0
  99. package/templates/langs/en/core/AGENTS.md +2 -2
  100. package/templates/langs/en/core/BOOT.md +1 -1
  101. package/templates/langs/en/core/HEARTBEAT.md +2 -2
  102. package/templates/langs/en/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  103. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  104. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  105. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  106. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  107. package/templates/langs/en/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
  108. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  109. package/templates/langs/en/skills/evolve-task/SKILL.md +1 -1
  110. package/templates/langs/en/skills/pd-cli-operator/SKILL.md +67 -0
  111. package/templates/langs/en/skills/pd-diagnostician/SKILL.md +1 -1
  112. package/templates/langs/en/skills/pd-mentor/SKILL.md +1 -1
  113. package/templates/langs/en/skills/pd-pain-signal/SKILL.md +17 -39
  114. package/templates/langs/en/skills/pd-runtime-v2/SKILL.md +61 -0
  115. package/templates/langs/zh/core/AGENTS.md +2 -2
  116. package/templates/langs/zh/core/BOOT.md +1 -1
  117. package/templates/langs/zh/core/HEARTBEAT.md +2 -2
  118. package/templates/langs/zh/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  119. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  120. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  121. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +8 -8
  122. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  123. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  124. package/templates/langs/zh/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
  125. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  126. package/templates/langs/zh/skills/ai-sprint-orchestration/test/run.test.mjs +21 -5
  127. package/templates/langs/zh/skills/evolve-task/SKILL.md +2 -2
  128. package/templates/langs/zh/skills/pd-cli-operator/SKILL.md +67 -0
  129. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +1 -1
  130. package/templates/langs/zh/skills/pd-mentor/SKILL.md +1 -1
  131. package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +17 -38
  132. package/templates/langs/zh/skills/pd-runtime-v2/SKILL.md +61 -0
  133. package/tests/build-artifacts.test.ts +1 -3
  134. package/tests/commands/evolution-status.test.ts +0 -118
  135. package/tests/core/bootstrap-rules.test.ts +1 -1
  136. package/tests/core/config.test.ts +1 -1
  137. package/tests/core/event-log.test.ts +35 -0
  138. package/tests/core/evolution-engine.test.ts +610 -0
  139. package/tests/core/file-store.test.ts +102 -0
  140. package/tests/core/focus-history.test.ts +203 -11
  141. package/tests/core/merge-gate-audit.test.ts +2 -169
  142. package/tests/core/model-deployment-registry.test.ts +7 -1
  143. package/tests/core/model-training-registry.test.ts +19 -0
  144. package/tests/core/observability.test.ts +0 -1
  145. package/tests/core/pain-diagnostic-gate.test.ts +498 -0
  146. package/tests/core/pain.test.ts +0 -1
  147. package/tests/core/principle-internalization/deprecated-readiness.test.ts +2 -2
  148. package/tests/core/principle-internalization/lifecycle-metrics.test.ts +2 -2
  149. package/tests/core/principle-internalization/{internalization-routing-policy.test.ts → lifecycle-routing-policy.test.ts} +6 -6
  150. package/tests/core/principle-internalization/lineage-source-retired.test.ts +56 -0
  151. package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +1 -23
  152. package/tests/core/principle-tree-ledger-adapter.test.ts +253 -0
  153. package/tests/core/reflection-context.test.ts +0 -14
  154. package/tests/core/replay-engine.test.ts +127 -215
  155. package/tests/core/rule-host-helpers.test.ts +2 -2
  156. package/tests/core/rule-implementation-runtime.test.ts +0 -27
  157. package/tests/core/workflow-funnel-loader.test.ts +162 -0
  158. package/tests/core/workspace-dir-validation.test.ts +8 -1
  159. package/tests/core-anti-growth.test.ts +192 -0
  160. package/tests/hook-workspace-nextaction-contract.test.ts +42 -0
  161. package/tests/hooks/confirm-first-gate.test.ts +333 -0
  162. package/tests/hooks/gate-auto-correct-shadow.test.ts +310 -0
  163. package/tests/hooks/gate-auto-correct.test.ts +665 -0
  164. package/tests/hooks/gate-rule-host-pipeline.test.ts +2 -1
  165. package/tests/hooks/pain.test.ts +269 -12
  166. package/tests/hooks/prompt-characterization.test.ts +500 -0
  167. package/tests/hooks/prompt-size-guard.test.ts +329 -0
  168. package/tests/hooks/runtime-v2-prompt-activation.test.ts +869 -0
  169. package/tests/index.test.ts +94 -1
  170. package/tests/integration/auto-entry-gate.test.ts +248 -0
  171. package/tests/integration/internalization-trigger-guard.test.ts +69 -0
  172. package/tests/integration/m8-legacy-paths.test.ts +63 -0
  173. package/tests/integration/runtime-v2-pain-guard.test.ts +125 -0
  174. package/tests/plugin-config-resolution-cutover.test.ts +359 -0
  175. package/tests/runtime-v2-discovery-guard.test.ts +154 -0
  176. package/tests/service/central-database.test.ts +457 -0
  177. package/tests/service/evolution-worker.correction-observer.test.ts +173 -0
  178. package/tests/service/evolution-worker.timeout.test.ts +11 -129
  179. package/tests/service/internalization-trigger-adapter.test.ts +251 -0
  180. package/tests/service/monitoring-query-service.test.ts +1 -47
  181. package/tests/service/queue-io.test.ts +1 -62
  182. package/tests/service/runtime-summary-service.test.ts +184 -3
  183. package/tests/service/workflow-watchdog.test.ts +0 -91
  184. package/tests/utils/file-lock.test.ts +5 -3
  185. package/tests/utils/session-key.test.ts +52 -0
  186. package/tests/utils/subagent-probe.test.ts +48 -1
  187. package/vitest.config.ts +4 -11
  188. package/.planning/codebase/ARCHITECTURE.md +0 -157
  189. package/.planning/codebase/CONCERNS.md +0 -145
  190. package/.planning/codebase/CONVENTIONS.md +0 -148
  191. package/.planning/codebase/INTEGRATIONS.md +0 -81
  192. package/.planning/codebase/STACK.md +0 -87
  193. package/.planning/codebase/STRUCTURE.md +0 -193
  194. package/.planning/codebase/TESTING.md +0 -243
  195. package/.planning/phases/01-basic-visualization/01-GAP-CLOSURE-VERIFICATION.md +0 -113
  196. package/docs/COMMAND_REFERENCE.md +0 -76
  197. package/docs/COMMAND_REFERENCE_EN.md +0 -79
  198. package/scripts/build-web.mjs +0 -46
  199. package/scripts/diagnose-nocturnal.mjs +0 -537
  200. package/scripts/seed-nocturnal-scenarios.mjs +0 -384
  201. package/src/commands/nocturnal-review.ts +0 -322
  202. package/src/commands/nocturnal-rollout.ts +0 -790
  203. package/src/commands/nocturnal-train.ts +0 -986
  204. package/src/commands/pd-reflect.ts +0 -88
  205. package/src/core/adaptive-thresholds.ts +0 -478
  206. package/src/core/diagnostician-task-store.ts +0 -192
  207. package/src/core/nocturnal-arbiter.ts +0 -715
  208. package/src/core/nocturnal-artifact-lineage.ts +0 -116
  209. package/src/core/nocturnal-artificer.ts +0 -257
  210. package/src/core/nocturnal-candidate-scoring.ts +0 -530
  211. package/src/core/nocturnal-compliance.ts +0 -1146
  212. package/src/core/nocturnal-dataset.ts +0 -763
  213. package/src/core/nocturnal-executability.ts +0 -428
  214. package/src/core/nocturnal-export.ts +0 -499
  215. package/src/core/nocturnal-paths.ts +0 -240
  216. package/src/core/nocturnal-reasoning-deriver.ts +0 -343
  217. package/src/core/nocturnal-rule-implementation-validator.ts +0 -246
  218. package/src/core/nocturnal-snapshot-contract.ts +0 -99
  219. package/src/core/nocturnal-trajectory-extractor.ts +0 -512
  220. package/src/core/nocturnal-trinity-types.ts +0 -218
  221. package/src/core/nocturnal-trinity.ts +0 -2680
  222. package/src/core/principle-internalization/deprecated-readiness.ts +0 -93
  223. package/src/core/principle-internalization/internalization-routing-policy.ts +0 -208
  224. package/src/core/principle-internalization/lifecycle-metrics.ts +0 -152
  225. package/src/http/principles-console-route.ts +0 -709
  226. package/src/service/central-health-service.ts +0 -49
  227. package/src/service/central-overview-service.ts +0 -138
  228. package/src/service/control-ui-query-service.ts +0 -900
  229. package/src/service/cooldown-strategy.ts +0 -97
  230. package/src/service/evolution-pain-context.ts +0 -79
  231. package/src/service/evolution-query-service.ts +0 -407
  232. package/src/service/health-query-service.ts +0 -1038
  233. package/src/service/nocturnal-config.ts +0 -214
  234. package/src/service/nocturnal-runtime.ts +0 -734
  235. package/src/service/nocturnal-service.ts +0 -1605
  236. package/src/service/nocturnal-target-selector.ts +0 -545
  237. package/src/service/sleep-cycle.ts +0 -157
  238. package/src/service/startup-reconciler.ts +0 -112
  239. package/src/service/subagent-workflow/correction-observer-types.ts +0 -82
  240. package/src/service/subagent-workflow/correction-observer-workflow-manager.ts +0 -250
  241. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +0 -1
  242. package/src/service/subagent-workflow/dynamic-timeout.ts +0 -30
  243. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +0 -268
  244. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +0 -795
  245. package/src/service/subagent-workflow/runtime-direct-driver.ts +0 -268
  246. package/src/service/subagent-workflow/workflow-manager-base.ts +0 -580
  247. package/src/tools/write-pain-flag.ts +0 -215
  248. package/tests/commands/nocturnal-review.test.ts +0 -448
  249. package/tests/commands/nocturnal-train.test.ts +0 -97
  250. package/tests/commands/pd-reflect.test.ts +0 -49
  251. package/tests/core/adaptive-thresholds.test.ts +0 -261
  252. package/tests/core/nocturnal-arbiter.test.ts +0 -559
  253. package/tests/core/nocturnal-artifact-lineage.test.ts +0 -53
  254. package/tests/core/nocturnal-artificer.test.ts +0 -241
  255. package/tests/core/nocturnal-candidate-scoring.test.ts +0 -532
  256. package/tests/core/nocturnal-compliance-p-principles.test.ts +0 -133
  257. package/tests/core/nocturnal-compliance.test.ts +0 -646
  258. package/tests/core/nocturnal-dataset.test.ts +0 -892
  259. package/tests/core/nocturnal-e2e.test.ts +0 -234
  260. package/tests/core/nocturnal-executability.test.ts +0 -357
  261. package/tests/core/nocturnal-export.test.ts +0 -517
  262. package/tests/core/nocturnal-reasoning-deriver.test.ts +0 -372
  263. package/tests/core/nocturnal-reviewed-subset-comparison.test.ts +0 -428
  264. package/tests/core/nocturnal-rule-implementation-validator.test.ts +0 -127
  265. package/tests/core/nocturnal-snapshot-contract.test.ts +0 -121
  266. package/tests/core/nocturnal-trajectory-extractor.test.ts +0 -634
  267. package/tests/core/nocturnal-trinity.test.ts +0 -2053
  268. package/tests/core/pain-auto-repair.test.ts +0 -96
  269. package/tests/core/pain-integration.test.ts +0 -510
  270. package/tests/fixtures/nocturnal-reviewed-subset.json +0 -183
  271. package/tests/http/principles-console-route.test.ts +0 -162
  272. package/tests/integration/chaos-resilience.test.ts +0 -348
  273. package/tests/integration/empathy-workflow-integration.test.ts +0 -626
  274. package/tests/integration/pain-diagnostician-loop.e2e.test.ts +0 -380
  275. package/tests/service/control-ui-query-service.test.ts +0 -121
  276. package/tests/service/cooldown-strategy.test.ts +0 -164
  277. package/tests/service/data-endpoints-regression.test.ts +0 -834
  278. package/tests/service/empathy-observer-workflow-manager.test.ts +0 -175
  279. package/tests/service/evolution-worker.nocturnal.test.ts +0 -601
  280. package/tests/service/nocturnal-runtime-hardening.test.ts +0 -118
  281. package/tests/service/nocturnal-runtime.test.ts +0 -473
  282. package/tests/service/nocturnal-service-code-candidate.test.ts +0 -330
  283. package/tests/service/nocturnal-target-selector.test.ts +0 -615
  284. package/tests/service/startup-reconciler.test.ts +0 -148
  285. package/tests/tools/write-pain-flag.test.ts +0 -358
  286. package/ui/src/App.tsx +0 -45
  287. package/ui/src/api.ts +0 -220
  288. package/ui/src/charts.tsx +0 -955
  289. package/ui/src/components/ErrorState.tsx +0 -6
  290. package/ui/src/components/Loading.tsx +0 -13
  291. package/ui/src/components/ProtectedRoute.tsx +0 -12
  292. package/ui/src/components/Shell.tsx +0 -91
  293. package/ui/src/components/WorkspaceConfig.tsx +0 -178
  294. package/ui/src/components/index.ts +0 -5
  295. package/ui/src/context/auth.tsx +0 -80
  296. package/ui/src/context/theme.tsx +0 -66
  297. package/ui/src/hooks/useAutoRefresh.ts +0 -39
  298. package/ui/src/i18n/ui.ts +0 -473
  299. package/ui/src/main.tsx +0 -16
  300. package/ui/src/pages/EvolutionPage.tsx +0 -333
  301. package/ui/src/pages/FeedbackPage.tsx +0 -138
  302. package/ui/src/pages/GateMonitorPage.tsx +0 -136
  303. package/ui/src/pages/LoginPage.tsx +0 -89
  304. package/ui/src/pages/OverviewPage.tsx +0 -599
  305. package/ui/src/pages/SamplesPage.tsx +0 -174
  306. package/ui/src/pages/ThinkingModelsPage.tsx +0 -702
  307. package/ui/src/styles.css +0 -2020
  308. package/ui/src/types.ts +0 -384
  309. 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
- });