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.
- package/openclaw.plugin.json +10 -5
- package/package.json +17 -19
- package/scripts/acceptance-test.mjs +16 -73
- package/scripts/sync-plugin.mjs +382 -77
- package/src/commands/archive-impl.ts +2 -1
- package/src/commands/capabilities.ts +2 -2
- package/src/commands/context.ts +2 -2
- package/src/commands/disable-impl.ts +2 -1
- package/src/commands/evolution-status.ts +16 -16
- package/src/commands/export.ts +12 -67
- package/src/commands/pain.ts +91 -1
- package/src/commands/principle-rollback.ts +2 -1
- package/src/commands/promote-impl.ts +7 -43
- package/src/commands/rollback-impl.ts +2 -1
- package/src/commands/rollback.ts +2 -1
- package/src/commands/samples.ts +2 -1
- package/src/commands/thinking-os.ts +2 -1
- package/src/config/errors.ts +18 -2
- package/src/constants/diagnostician.ts +2 -2
- package/src/constants/tools.ts +2 -1
- package/src/core/__tests__/focus-history.test.ts +210 -0
- package/src/core/config.ts +1 -1
- package/src/core/confirm-first-gate.ts +255 -0
- package/src/core/correction-cue-learner.ts +2 -136
- package/src/core/correction-types.ts +16 -88
- package/src/core/dictionary.ts +19 -20
- package/src/core/empathy-keyword-matcher.ts +17 -289
- package/src/core/empathy-types.ts +18 -229
- package/src/core/event-log.ts +38 -132
- package/src/core/evolution-reducer.ts +21 -2
- package/src/core/evolution-types.ts +76 -464
- package/src/core/file-store.ts +80 -0
- package/src/core/focus-history.ts +228 -955
- package/src/core/local-worker-routing.ts +34 -314
- package/src/core/merge-gate-audit.ts +0 -195
- package/src/core/pain-diagnostic-gate.ts +154 -0
- package/src/core/pain-signal.ts +21 -138
- package/src/core/pain.ts +15 -88
- package/src/core/pd-task-reconciler.ts +26 -115
- package/src/core/pd-task-service.ts +9 -9
- package/src/core/pd-task-types.ts +23 -127
- package/src/core/principle-compiler/__tests__/compiler-replay-gate.test.ts +174 -0
- package/src/core/principle-compiler/code-validator.ts +15 -42
- package/src/core/principle-compiler/compiler.ts +100 -15
- package/src/core/principle-compiler/index.ts +5 -2
- package/src/core/principle-compiler/template-generator.ts +4 -104
- package/src/core/principle-injection.ts +10 -202
- package/src/core/principle-internalization/filesystem-lifecycle-datasource.ts +42 -0
- package/src/core/principle-internalization/lifecycle-read-model.ts +39 -242
- package/src/core/principle-internalization/principle-lifecycle-service.ts +12 -10
- package/src/core/principle-tree-ledger-adapter.ts +145 -0
- package/src/core/principle-tree-ledger.ts +8 -6
- package/src/core/reflection/reflection-context.ts +14 -109
- package/src/core/replay-engine.ts +8 -500
- package/src/core/rule-host-helpers.ts +5 -35
- package/src/core/rule-host-types.ts +10 -82
- package/src/core/rule-host.ts +6 -63
- package/src/core/runtime-v2-prompt-activation-reader.ts +231 -0
- package/src/core/session-tracker.ts +87 -101
- package/src/core/shadow-observation-registry.ts +19 -48
- package/src/core/trajectory.ts +3 -1
- package/src/core/workflow-funnel-loader.ts +62 -68
- package/src/core/workspace-context.ts +46 -0
- package/src/core/workspace-dir-service.ts +1 -1
- package/src/core/workspace-dir-validation.ts +18 -9
- package/src/hooks/AGENTS.md +1 -1
- package/src/hooks/gate-block-helper.ts +46 -44
- package/src/hooks/gate.ts +207 -7
- package/src/hooks/lifecycle.ts +30 -32
- package/src/hooks/llm.ts +60 -32
- package/src/hooks/pain.ts +297 -103
- package/src/hooks/prompt.ts +469 -339
- package/src/hooks/subagent.ts +2 -29
- package/src/i18n/commands.ts +2 -10
- package/src/index.ts +95 -85
- package/src/openclaw-sdk.ts +311 -0
- package/src/service/central-database.ts +8 -4
- package/src/service/evolution-queue-migration.ts +2 -1
- package/src/service/evolution-worker.ts +163 -1786
- package/src/service/internalization-trigger-adapter.ts +302 -0
- package/src/service/keyword-optimization-service.ts +4 -4
- package/src/service/monitoring-query-service.ts +1 -215
- package/src/service/queue-io.ts +60 -331
- package/src/service/runtime-summary-service.ts +115 -18
- package/src/service/subagent-workflow/index.ts +0 -41
- package/src/service/subagent-workflow/types.ts +9 -120
- package/src/service/subagent-workflow/workflow-store.ts +2 -119
- package/src/service/workflow-watchdog.ts +0 -43
- package/src/types/event-payload.ts +16 -74
- package/src/types/event-types.ts +39 -547
- package/src/types/hygiene-types.ts +7 -30
- package/src/types/principle-tree-schema.ts +20 -222
- package/src/types/queue.ts +15 -70
- package/src/types/runtime-summary.ts +5 -49
- package/src/utils/io.ts +10 -0
- package/src/utils/retry.ts +1 -1
- package/src/utils/shadow-fingerprint.ts +2 -2
- package/src/utils/workspace-resolver.ts +50 -0
- package/templates/langs/en/core/AGENTS.md +2 -2
- package/templates/langs/en/core/BOOT.md +1 -1
- package/templates/langs/en/core/HEARTBEAT.md +2 -2
- package/templates/langs/en/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
- package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
- package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
- package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
- package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
- package/templates/langs/en/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
- package/templates/langs/en/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
- package/templates/langs/en/skills/evolve-task/SKILL.md +1 -1
- package/templates/langs/en/skills/pd-cli-operator/SKILL.md +67 -0
- package/templates/langs/en/skills/pd-diagnostician/SKILL.md +1 -1
- package/templates/langs/en/skills/pd-mentor/SKILL.md +1 -1
- package/templates/langs/en/skills/pd-pain-signal/SKILL.md +17 -39
- package/templates/langs/en/skills/pd-runtime-v2/SKILL.md +61 -0
- package/templates/langs/zh/core/AGENTS.md +2 -2
- package/templates/langs/zh/core/BOOT.md +1 -1
- package/templates/langs/zh/core/HEARTBEAT.md +2 -2
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +8 -8
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
- package/templates/langs/zh/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
- package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
- package/templates/langs/zh/skills/ai-sprint-orchestration/test/run.test.mjs +21 -5
- package/templates/langs/zh/skills/evolve-task/SKILL.md +2 -2
- package/templates/langs/zh/skills/pd-cli-operator/SKILL.md +67 -0
- package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +1 -1
- package/templates/langs/zh/skills/pd-mentor/SKILL.md +1 -1
- package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +17 -38
- package/templates/langs/zh/skills/pd-runtime-v2/SKILL.md +61 -0
- package/tests/build-artifacts.test.ts +1 -3
- package/tests/commands/evolution-status.test.ts +0 -118
- package/tests/core/bootstrap-rules.test.ts +1 -1
- package/tests/core/config.test.ts +1 -1
- package/tests/core/event-log.test.ts +35 -0
- package/tests/core/evolution-engine.test.ts +610 -0
- package/tests/core/file-store.test.ts +102 -0
- package/tests/core/focus-history.test.ts +203 -11
- package/tests/core/merge-gate-audit.test.ts +2 -169
- package/tests/core/model-deployment-registry.test.ts +7 -1
- package/tests/core/model-training-registry.test.ts +19 -0
- package/tests/core/observability.test.ts +0 -1
- package/tests/core/pain-diagnostic-gate.test.ts +498 -0
- package/tests/core/pain.test.ts +0 -1
- package/tests/core/principle-internalization/deprecated-readiness.test.ts +2 -2
- package/tests/core/principle-internalization/lifecycle-metrics.test.ts +2 -2
- package/tests/core/principle-internalization/{internalization-routing-policy.test.ts → lifecycle-routing-policy.test.ts} +6 -6
- package/tests/core/principle-internalization/lineage-source-retired.test.ts +56 -0
- package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +1 -23
- package/tests/core/principle-tree-ledger-adapter.test.ts +253 -0
- package/tests/core/reflection-context.test.ts +0 -14
- package/tests/core/replay-engine.test.ts +127 -215
- package/tests/core/rule-host-helpers.test.ts +2 -2
- package/tests/core/rule-implementation-runtime.test.ts +0 -27
- package/tests/core/workflow-funnel-loader.test.ts +162 -0
- package/tests/core/workspace-dir-validation.test.ts +8 -1
- package/tests/core-anti-growth.test.ts +192 -0
- package/tests/hook-workspace-nextaction-contract.test.ts +42 -0
- package/tests/hooks/confirm-first-gate.test.ts +333 -0
- package/tests/hooks/gate-auto-correct-shadow.test.ts +310 -0
- package/tests/hooks/gate-auto-correct.test.ts +665 -0
- package/tests/hooks/gate-rule-host-pipeline.test.ts +2 -1
- package/tests/hooks/pain.test.ts +269 -12
- package/tests/hooks/prompt-characterization.test.ts +500 -0
- package/tests/hooks/prompt-size-guard.test.ts +329 -0
- package/tests/hooks/runtime-v2-prompt-activation.test.ts +869 -0
- package/tests/index.test.ts +94 -1
- package/tests/integration/auto-entry-gate.test.ts +248 -0
- package/tests/integration/internalization-trigger-guard.test.ts +69 -0
- package/tests/integration/m8-legacy-paths.test.ts +63 -0
- package/tests/integration/runtime-v2-pain-guard.test.ts +125 -0
- package/tests/plugin-config-resolution-cutover.test.ts +359 -0
- package/tests/runtime-v2-discovery-guard.test.ts +154 -0
- package/tests/service/central-database.test.ts +457 -0
- package/tests/service/evolution-worker.correction-observer.test.ts +173 -0
- package/tests/service/evolution-worker.timeout.test.ts +11 -129
- package/tests/service/internalization-trigger-adapter.test.ts +251 -0
- package/tests/service/monitoring-query-service.test.ts +1 -47
- package/tests/service/queue-io.test.ts +1 -62
- package/tests/service/runtime-summary-service.test.ts +184 -3
- package/tests/service/workflow-watchdog.test.ts +0 -91
- package/tests/utils/file-lock.test.ts +5 -3
- package/tests/utils/session-key.test.ts +52 -0
- package/tests/utils/subagent-probe.test.ts +48 -1
- package/vitest.config.ts +4 -11
- package/.planning/codebase/ARCHITECTURE.md +0 -157
- package/.planning/codebase/CONCERNS.md +0 -145
- package/.planning/codebase/CONVENTIONS.md +0 -148
- package/.planning/codebase/INTEGRATIONS.md +0 -81
- package/.planning/codebase/STACK.md +0 -87
- package/.planning/codebase/STRUCTURE.md +0 -193
- package/.planning/codebase/TESTING.md +0 -243
- package/.planning/phases/01-basic-visualization/01-GAP-CLOSURE-VERIFICATION.md +0 -113
- package/docs/COMMAND_REFERENCE.md +0 -76
- package/docs/COMMAND_REFERENCE_EN.md +0 -79
- package/scripts/build-web.mjs +0 -46
- package/scripts/diagnose-nocturnal.mjs +0 -537
- package/scripts/seed-nocturnal-scenarios.mjs +0 -384
- package/src/commands/nocturnal-review.ts +0 -322
- package/src/commands/nocturnal-rollout.ts +0 -790
- package/src/commands/nocturnal-train.ts +0 -986
- package/src/commands/pd-reflect.ts +0 -88
- package/src/core/adaptive-thresholds.ts +0 -478
- package/src/core/diagnostician-task-store.ts +0 -192
- package/src/core/nocturnal-arbiter.ts +0 -715
- package/src/core/nocturnal-artifact-lineage.ts +0 -116
- package/src/core/nocturnal-artificer.ts +0 -257
- package/src/core/nocturnal-candidate-scoring.ts +0 -530
- package/src/core/nocturnal-compliance.ts +0 -1146
- package/src/core/nocturnal-dataset.ts +0 -763
- package/src/core/nocturnal-executability.ts +0 -428
- package/src/core/nocturnal-export.ts +0 -499
- package/src/core/nocturnal-paths.ts +0 -240
- package/src/core/nocturnal-reasoning-deriver.ts +0 -343
- package/src/core/nocturnal-rule-implementation-validator.ts +0 -246
- package/src/core/nocturnal-snapshot-contract.ts +0 -99
- package/src/core/nocturnal-trajectory-extractor.ts +0 -512
- package/src/core/nocturnal-trinity-types.ts +0 -218
- package/src/core/nocturnal-trinity.ts +0 -2680
- package/src/core/principle-internalization/deprecated-readiness.ts +0 -93
- package/src/core/principle-internalization/internalization-routing-policy.ts +0 -208
- package/src/core/principle-internalization/lifecycle-metrics.ts +0 -152
- package/src/http/principles-console-route.ts +0 -709
- package/src/service/central-health-service.ts +0 -49
- package/src/service/central-overview-service.ts +0 -138
- package/src/service/control-ui-query-service.ts +0 -900
- package/src/service/cooldown-strategy.ts +0 -97
- package/src/service/evolution-pain-context.ts +0 -79
- package/src/service/evolution-query-service.ts +0 -407
- package/src/service/health-query-service.ts +0 -1038
- package/src/service/nocturnal-config.ts +0 -214
- package/src/service/nocturnal-runtime.ts +0 -734
- package/src/service/nocturnal-service.ts +0 -1605
- package/src/service/nocturnal-target-selector.ts +0 -545
- package/src/service/sleep-cycle.ts +0 -157
- package/src/service/startup-reconciler.ts +0 -112
- package/src/service/subagent-workflow/correction-observer-types.ts +0 -82
- package/src/service/subagent-workflow/correction-observer-workflow-manager.ts +0 -250
- package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +0 -1
- package/src/service/subagent-workflow/dynamic-timeout.ts +0 -30
- package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +0 -268
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +0 -795
- package/src/service/subagent-workflow/runtime-direct-driver.ts +0 -268
- package/src/service/subagent-workflow/workflow-manager-base.ts +0 -580
- package/src/tools/write-pain-flag.ts +0 -215
- package/tests/commands/nocturnal-review.test.ts +0 -448
- package/tests/commands/nocturnal-train.test.ts +0 -97
- package/tests/commands/pd-reflect.test.ts +0 -49
- package/tests/core/adaptive-thresholds.test.ts +0 -261
- package/tests/core/nocturnal-arbiter.test.ts +0 -559
- package/tests/core/nocturnal-artifact-lineage.test.ts +0 -53
- package/tests/core/nocturnal-artificer.test.ts +0 -241
- package/tests/core/nocturnal-candidate-scoring.test.ts +0 -532
- package/tests/core/nocturnal-compliance-p-principles.test.ts +0 -133
- package/tests/core/nocturnal-compliance.test.ts +0 -646
- package/tests/core/nocturnal-dataset.test.ts +0 -892
- package/tests/core/nocturnal-e2e.test.ts +0 -234
- package/tests/core/nocturnal-executability.test.ts +0 -357
- package/tests/core/nocturnal-export.test.ts +0 -517
- package/tests/core/nocturnal-reasoning-deriver.test.ts +0 -372
- package/tests/core/nocturnal-reviewed-subset-comparison.test.ts +0 -428
- package/tests/core/nocturnal-rule-implementation-validator.test.ts +0 -127
- package/tests/core/nocturnal-snapshot-contract.test.ts +0 -121
- package/tests/core/nocturnal-trajectory-extractor.test.ts +0 -634
- package/tests/core/nocturnal-trinity.test.ts +0 -2053
- package/tests/core/pain-auto-repair.test.ts +0 -96
- package/tests/core/pain-integration.test.ts +0 -510
- package/tests/fixtures/nocturnal-reviewed-subset.json +0 -183
- package/tests/http/principles-console-route.test.ts +0 -162
- package/tests/integration/chaos-resilience.test.ts +0 -348
- package/tests/integration/empathy-workflow-integration.test.ts +0 -626
- package/tests/integration/pain-diagnostician-loop.e2e.test.ts +0 -380
- package/tests/service/control-ui-query-service.test.ts +0 -121
- package/tests/service/cooldown-strategy.test.ts +0 -164
- package/tests/service/data-endpoints-regression.test.ts +0 -834
- package/tests/service/empathy-observer-workflow-manager.test.ts +0 -175
- package/tests/service/evolution-worker.nocturnal.test.ts +0 -601
- package/tests/service/nocturnal-runtime-hardening.test.ts +0 -118
- package/tests/service/nocturnal-runtime.test.ts +0 -473
- package/tests/service/nocturnal-service-code-candidate.test.ts +0 -330
- package/tests/service/nocturnal-target-selector.test.ts +0 -615
- package/tests/service/startup-reconciler.test.ts +0 -148
- package/tests/tools/write-pain-flag.test.ts +0 -358
- package/ui/src/App.tsx +0 -45
- package/ui/src/api.ts +0 -220
- package/ui/src/charts.tsx +0 -955
- package/ui/src/components/ErrorState.tsx +0 -6
- package/ui/src/components/Loading.tsx +0 -13
- package/ui/src/components/ProtectedRoute.tsx +0 -12
- package/ui/src/components/Shell.tsx +0 -91
- package/ui/src/components/WorkspaceConfig.tsx +0 -178
- package/ui/src/components/index.ts +0 -5
- package/ui/src/context/auth.tsx +0 -80
- package/ui/src/context/theme.tsx +0 -66
- package/ui/src/hooks/useAutoRefresh.ts +0 -39
- package/ui/src/i18n/ui.ts +0 -473
- package/ui/src/main.tsx +0 -16
- package/ui/src/pages/EvolutionPage.tsx +0 -333
- package/ui/src/pages/FeedbackPage.tsx +0 -138
- package/ui/src/pages/GateMonitorPage.tsx +0 -136
- package/ui/src/pages/LoginPage.tsx +0 -89
- package/ui/src/pages/OverviewPage.tsx +0 -599
- package/ui/src/pages/SamplesPage.tsx +0 -174
- package/ui/src/pages/ThinkingModelsPage.tsx +0 -702
- package/ui/src/styles.css +0 -2020
- package/ui/src/types.ts +0 -384
- 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
|
-
});
|