principles-disciple 1.11.0 → 1.13.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/CHANGELOG.md +26 -0
- package/README.md +66 -0
- package/esbuild.config.js +1 -1
- package/openclaw.plugin.json +4 -4
- package/package.json +2 -3
- package/run-nocturnal.mjs +30 -0
- package/scripts/db-migrate.mjs +170 -0
- package/scripts/sync-plugin.mjs +250 -6
- package/src/commands/archive-impl.ts +136 -0
- package/src/commands/capabilities.ts +4 -2
- package/src/commands/context.ts +5 -1
- package/src/commands/disable-impl.ts +151 -0
- package/src/commands/evolution-status.ts +64 -19
- package/src/commands/export.ts +8 -6
- package/src/commands/focus.ts +8 -20
- package/src/commands/nocturnal-review.ts +5 -7
- package/src/commands/nocturnal-rollout.ts +1 -12
- package/src/commands/nocturnal-train.ts +21 -47
- package/src/commands/pain.ts +10 -5
- package/src/commands/principle-rollback.ts +4 -2
- package/src/commands/promote-impl.ts +274 -0
- package/src/commands/rollback-impl.ts +234 -0
- package/src/commands/rollback.ts +6 -3
- package/src/commands/samples.ts +2 -0
- package/src/commands/thinking-os.ts +3 -4
- package/src/commands/workflow-debug.ts +2 -1
- package/src/config/errors.ts +1 -0
- package/src/core/AGENTS.md +34 -0
- package/src/core/adaptive-thresholds.ts +4 -3
- package/src/core/code-implementation-storage.ts +241 -0
- package/src/core/config.ts +5 -2
- package/src/core/control-ui-db.ts +29 -10
- package/src/core/detection-funnel.ts +12 -7
- package/src/core/diagnostician-task-store.ts +156 -0
- package/src/core/dictionary.ts +4 -4
- package/src/core/empathy-keyword-matcher.ts +7 -3
- package/src/core/empathy-types.ts +13 -2
- package/src/core/event-log.ts +14 -6
- package/src/core/evolution-engine.ts +27 -31
- package/src/core/evolution-logger.ts +3 -2
- package/src/core/evolution-reducer.ts +110 -31
- package/src/core/evolution-types.ts +10 -0
- package/src/core/external-training-contract.ts +1 -0
- package/src/core/focus-history.ts +38 -24
- package/src/core/hygiene/tracker.ts +10 -6
- package/src/core/init.ts +5 -2
- package/src/core/migration.ts +3 -3
- package/src/core/model-deployment-registry.ts +6 -4
- package/src/core/model-training-registry.ts +5 -3
- package/src/core/nocturnal-arbiter.ts +13 -14
- package/src/core/nocturnal-artifact-lineage.ts +117 -0
- package/src/core/nocturnal-artificer.ts +257 -0
- package/src/core/nocturnal-candidate-scoring.ts +4 -2
- package/src/core/nocturnal-compliance.ts +67 -19
- package/src/core/nocturnal-dataset.ts +95 -2
- package/src/core/nocturnal-executability.ts +2 -3
- package/src/core/nocturnal-export.ts +6 -3
- package/src/core/nocturnal-rule-implementation-validator.ts +245 -0
- package/src/core/nocturnal-trajectory-extractor.ts +10 -3
- package/src/core/nocturnal-trinity.ts +319 -61
- package/src/core/pain-context-extractor.ts +29 -15
- package/src/core/pain.ts +7 -5
- package/src/core/path-resolver.ts +16 -15
- package/src/core/paths.ts +2 -1
- package/src/core/pd-task-reconciler.ts +463 -0
- package/src/core/pd-task-service.ts +42 -0
- package/src/core/pd-task-store.ts +77 -0
- package/src/core/pd-task-types.ts +128 -0
- package/src/core/principle-internalization/deprecated-readiness.ts +91 -0
- package/src/core/principle-internalization/internalization-routing-policy.ts +208 -0
- package/src/core/principle-internalization/lifecycle-metrics.ts +149 -0
- package/src/core/principle-internalization/lifecycle-read-model.ts +243 -0
- package/src/core/principle-internalization/lifecycle-refresh.ts +11 -0
- package/src/core/principle-internalization/principle-lifecycle-service.ts +167 -0
- package/src/core/principle-training-state.ts +95 -370
- package/src/core/principle-tree-ledger.ts +733 -0
- package/src/core/principle-tree-migration.ts +195 -0
- package/src/core/profile.ts +3 -1
- package/src/core/promotion-gate.ts +14 -18
- package/src/core/replay-engine.ts +562 -0
- package/src/core/risk-calculator.ts +6 -4
- package/src/core/rule-host-helpers.ts +39 -0
- package/src/core/rule-host-types.ts +82 -0
- package/src/core/rule-host.ts +245 -0
- package/src/core/rule-implementation-runtime.ts +38 -0
- package/src/core/schema/db-types.ts +16 -0
- package/src/core/schema/index.ts +26 -0
- package/src/core/schema/migration-runner.ts +207 -0
- package/src/core/schema/migrations/001-init-trajectory.ts +211 -0
- package/src/core/schema/migrations/002-init-central.ts +122 -0
- package/src/core/schema/migrations/003-init-workflow.ts +55 -0
- package/src/core/schema/migrations/004-add-thinking-and-gfi.ts +74 -0
- package/src/core/schema/migrations/index.ts +31 -0
- package/src/core/schema/schema-definitions.ts +650 -0
- package/src/core/session-tracker.ts +6 -4
- package/src/core/shadow-observation-registry.ts +6 -3
- package/src/core/system-logger.ts +2 -2
- package/src/core/thinking-models.ts +182 -46
- package/src/core/thinking-os-parser.ts +156 -0
- package/src/core/training-program.ts +7 -7
- package/src/core/trajectory.ts +42 -36
- package/src/core/workspace-context.ts +77 -11
- package/src/core/workspace-dir-validation.ts +152 -0
- package/src/hooks/AGENTS.md +31 -0
- package/src/hooks/bash-risk.ts +3 -1
- package/src/hooks/edit-verification.ts +9 -5
- package/src/hooks/gate-block-helper.ts +5 -1
- package/src/hooks/gate.ts +152 -5
- package/src/hooks/gfi-gate.ts +9 -2
- package/src/hooks/lifecycle-routing.ts +124 -0
- package/src/hooks/lifecycle.ts +12 -12
- package/src/hooks/llm.ts +17 -109
- package/src/hooks/message-sanitize.ts +5 -3
- package/src/hooks/pain.ts +19 -15
- package/src/hooks/progressive-trust-gate.ts +7 -1
- package/src/hooks/prompt.ts +169 -60
- package/src/hooks/subagent.ts +5 -4
- package/src/hooks/thinking-checkpoint.ts +2 -0
- package/src/hooks/trajectory-collector.ts +15 -12
- package/src/http/principles-console-route.ts +31 -68
- package/src/i18n/commands.ts +2 -2
- package/src/index.ts +130 -40
- package/src/service/central-database.ts +131 -43
- package/src/service/central-health-service.ts +47 -0
- package/src/service/central-overview-service.ts +135 -0
- package/src/service/central-sync-service.ts +87 -0
- package/src/service/control-ui-query-service.ts +46 -36
- package/src/service/event-log-auditor.ts +261 -0
- package/src/service/evolution-query-service.ts +23 -22
- package/src/service/evolution-worker.ts +565 -261
- package/src/service/health-query-service.ts +213 -36
- package/src/service/nocturnal-runtime.ts +8 -4
- package/src/service/nocturnal-service.ts +503 -59
- package/src/service/nocturnal-target-selector.ts +5 -7
- package/src/service/runtime-summary-service.ts +2 -1
- package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +25 -336
- package/src/service/subagent-workflow/dynamic-timeout.ts +30 -0
- package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +48 -386
- package/src/service/subagent-workflow/index.ts +2 -0
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +169 -284
- package/src/service/subagent-workflow/runtime-direct-driver.ts +114 -16
- package/src/service/subagent-workflow/subagent-error-utils.ts +25 -0
- package/src/service/subagent-workflow/types.ts +9 -4
- package/src/service/subagent-workflow/workflow-manager-base.ts +573 -0
- package/src/service/subagent-workflow/workflow-store.ts +71 -11
- package/src/service/trajectory-service.ts +2 -1
- package/src/tools/critique-prompt.ts +1 -1
- package/src/tools/deep-reflect.ts +175 -209
- package/src/tools/model-index.ts +2 -1
- package/src/types/event-types.ts +2 -2
- package/src/types/principle-tree-schema.ts +29 -23
- package/src/utils/file-lock.ts +5 -3
- package/src/utils/io.ts +5 -2
- package/src/utils/nlp.ts +5 -46
- package/src/utils/node-vm-polyfill.ts +11 -0
- package/src/utils/plugin-logger.ts +2 -0
- package/src/utils/retry.ts +572 -0
- package/src/utils/subagent-probe.ts +1 -1
- package/templates/langs/en/core/AGENTS.md +0 -13
- package/templates/langs/en/core/SOUL.md +1 -31
- package/templates/langs/en/core/TOOLS.md +0 -4
- package/templates/langs/en/principles/THINKING_OS.md +77 -0
- package/templates/langs/en/skills/admin/SKILL.md +0 -1
- package/templates/langs/en/skills/evolution-framework-update/SKILL.md +1 -1
- package/templates/langs/en/skills/pd-diagnostician/SKILL.md +18 -5
- package/templates/langs/zh/core/AGENTS.md +0 -22
- package/templates/langs/zh/core/SOUL.md +1 -31
- package/templates/langs/zh/core/TOOLS.md +0 -4
- package/templates/langs/zh/principles/THINKING_OS.md +77 -0
- package/templates/langs/zh/skills/admin/SKILL.md +0 -1
- package/templates/langs/zh/skills/evolution-framework-update/SKILL.md +1 -1
- package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +25 -4
- package/tests/commands/evolution-status.test.ts +119 -0
- package/tests/commands/implementation-lifecycle.test.ts +362 -0
- package/tests/core/code-implementation-storage.test.ts +398 -0
- package/tests/core/evolution-reducer.detector-metadata.test.ts +28 -28
- package/tests/core/nocturnal-artifact-lineage.test.ts +53 -0
- package/tests/core/nocturnal-artificer.test.ts +241 -0
- package/tests/core/nocturnal-compliance-p-principles.test.ts +133 -0
- package/tests/core/nocturnal-rule-implementation-validator.test.ts +127 -0
- package/tests/core/pd-task-store.test.ts +126 -0
- package/tests/core/principle-internalization/deprecated-readiness.test.ts +193 -0
- package/tests/core/principle-internalization/internalization-routing-policy.test.ts +212 -0
- package/tests/core/principle-internalization/lifecycle-metrics.test.ts +350 -0
- package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +211 -0
- package/tests/core/principle-training-state.test.ts +228 -1
- package/tests/core/principle-tree-ledger.test.ts +423 -0
- package/tests/core/regression-v1-9-1.test.ts +265 -0
- package/tests/core/replay-engine.test.ts +234 -0
- package/tests/core/rule-host-helpers.test.ts +120 -0
- package/tests/core/rule-host.test.ts +389 -0
- package/tests/core/rule-implementation-runtime.test.ts +64 -0
- package/tests/core/workspace-context.test.ts +53 -0
- package/tests/core/workspace-dir-validation.test.ts +272 -0
- package/tests/hooks/gate-rule-host-pipeline.test.ts +385 -0
- package/tests/hooks/pain.test.ts +74 -10
- package/tests/hooks/prompt.test.ts +63 -1
- package/tests/integration/principle-lifecycle.e2e.test.ts +197 -0
- package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +211 -0
- package/tests/service/data-endpoints-regression.test.ts +834 -0
- package/tests/service/evolution-worker.test.ts +0 -123
- package/tests/service/nocturnal-service-code-candidate.test.ts +330 -0
- package/tests/utils/nlp.test.ts +1 -19
- package/tests/utils/retry.test.ts +327 -0
- package/ui/src/App.tsx +1 -1
- package/ui/src/api.ts +4 -0
- package/ui/src/charts.tsx +366 -0
- package/ui/src/components/WorkspaceConfig.tsx +107 -75
- package/ui/src/i18n/ui.ts +89 -31
- package/ui/src/pages/EvolutionPage.tsx +1 -1
- package/ui/src/pages/OverviewPage.tsx +441 -81
- package/ui/src/pages/ThinkingModelsPage.tsx +287 -69
- package/ui/src/styles.css +43 -0
- package/ui/src/types.ts +17 -1
- package/src/agents/nocturnal-dreamer.md +0 -152
- package/src/agents/nocturnal-philosopher.md +0 -138
- package/src/agents/nocturnal-reflector.md +0 -126
- package/src/agents/nocturnal-scribe.md +0 -164
- package/templates/workspace/.principles/00-kernel.md +0 -51
- package/templates/workspace/.principles/DECISION_POLICY.json +0 -44
- package/templates/workspace/.principles/PRINCIPLES.md +0 -20
- package/templates/workspace/.principles/PROFILE.json +0 -54
- package/templates/workspace/.principles/PROFILE.schema.json +0 -56
- package/templates/workspace/.principles/THINKING_OS.md +0 -64
- package/templates/workspace/.principles/THINKING_OS_ARCHIVE.md +0 -7
- package/templates/workspace/.principles/THINKING_OS_CANDIDATES.md +0 -9
- package/templates/workspace/.principles/models/_INDEX.md +0 -27
- package/templates/workspace/.principles/models/first_principles.md +0 -62
- package/templates/workspace/.principles/models/marketing_4p.md +0 -52
- package/templates/workspace/.principles/models/porter_five.md +0 -63
- package/templates/workspace/.principles/models/swot.md +0 -60
- package/templates/workspace/.principles/models/user_story_map.md +0 -63
- package/templates/workspace/.state/WORKBOARD.json +0 -4
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule Host Tests
|
|
3
|
+
*
|
|
4
|
+
* PURPOSE: Verify that the RuleHost class:
|
|
5
|
+
* - Returns undefined when no active implementations exist
|
|
6
|
+
* - Returns block when an implementation returns block (with short-circuit)
|
|
7
|
+
* - Returns requireApproval when an implementation returns requireApproval
|
|
8
|
+
* - Returns undefined when all implementations return allow or matched=false
|
|
9
|
+
* - Degrades conservatively on vm load or execution errors
|
|
10
|
+
* - Merges multiple implementation decisions correctly
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
14
|
+
import { RuleHost } from '../../src/core/rule-host.js';
|
|
15
|
+
import type { RuleHostInput, RuleHostResult } from '../../src/core/rule-host-types.js';
|
|
16
|
+
|
|
17
|
+
// Mock the ledger module
|
|
18
|
+
vi.mock('../../src/core/principle-tree-ledger.js', () => ({
|
|
19
|
+
loadLedger: vi.fn(),
|
|
20
|
+
listImplementationsByLifecycleState: vi.fn(() => []),
|
|
21
|
+
findActiveImplementation: vi.fn(() => null),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
vi.mock('../../src/core/code-implementation-storage.js', () => ({
|
|
25
|
+
loadEntrySource: vi.fn(() => null),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
// Mock fs to avoid actual file reads
|
|
29
|
+
vi.mock('fs', () => ({
|
|
30
|
+
existsSync: vi.fn(() => false),
|
|
31
|
+
readFileSync: vi.fn(),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
vi.mock('../../src/core/rule-implementation-runtime.js', () => ({
|
|
35
|
+
loadRuleImplementationModule: vi.fn(),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
import { listImplementationsByLifecycleState } from '../../src/core/principle-tree-ledger.js';
|
|
39
|
+
import { loadEntrySource } from '../../src/core/code-implementation-storage.js';
|
|
40
|
+
import { loadRuleImplementationModule } from '../../src/core/rule-implementation-runtime.js';
|
|
41
|
+
import * as fs from 'fs';
|
|
42
|
+
|
|
43
|
+
const mockedListImplementations = vi.mocked(listImplementationsByLifecycleState);
|
|
44
|
+
const mockedLoadEntrySource = vi.mocked(loadEntrySource);
|
|
45
|
+
const mockedLoadRuleImplementationModule = vi.mocked(loadRuleImplementationModule);
|
|
46
|
+
const mockedExistsSync = vi.mocked(fs.existsSync);
|
|
47
|
+
const mockedReadFileSync = vi.mocked(fs.readFileSync);
|
|
48
|
+
|
|
49
|
+
function makeInput(overrides?: Partial<RuleHostInput>): RuleHostInput {
|
|
50
|
+
return {
|
|
51
|
+
action: {
|
|
52
|
+
toolName: 'write',
|
|
53
|
+
normalizedPath: 'src/test.ts',
|
|
54
|
+
paramsSummary: {},
|
|
55
|
+
},
|
|
56
|
+
workspace: {
|
|
57
|
+
isRiskPath: false,
|
|
58
|
+
planStatus: 'READY',
|
|
59
|
+
hasPlanFile: true,
|
|
60
|
+
},
|
|
61
|
+
session: {
|
|
62
|
+
sessionId: 'test-session',
|
|
63
|
+
currentGfi: 10,
|
|
64
|
+
recentThinking: false,
|
|
65
|
+
},
|
|
66
|
+
evolution: {
|
|
67
|
+
epTier: 3,
|
|
68
|
+
},
|
|
69
|
+
derived: {
|
|
70
|
+
estimatedLineChanges: 50,
|
|
71
|
+
bashRisk: 'normal',
|
|
72
|
+
},
|
|
73
|
+
...overrides,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function makeMockImpl(id: string, type: string = 'code', lifecycleState: string = 'active') {
|
|
78
|
+
return {
|
|
79
|
+
id,
|
|
80
|
+
ruleId: `RULE_${id}`,
|
|
81
|
+
type,
|
|
82
|
+
path: `/impls/${id}.js`,
|
|
83
|
+
version: '1.0.0',
|
|
84
|
+
coversCondition: 'test',
|
|
85
|
+
coveragePercentage: 100,
|
|
86
|
+
lifecycleState,
|
|
87
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
88
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
describe('RuleHost', () => {
|
|
93
|
+
beforeEach(() => {
|
|
94
|
+
vi.clearAllMocks();
|
|
95
|
+
mockedLoadEntrySource.mockReturnValue(null);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should return undefined when no active implementations exist (empty ledger)', () => {
|
|
99
|
+
mockedListImplementations.mockReturnValue([]);
|
|
100
|
+
const host = new RuleHost('/mock/state');
|
|
101
|
+
const result = host.evaluate(makeInput());
|
|
102
|
+
expect(result).toBeUndefined();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should return undefined when implementations exist but none are type=code', () => {
|
|
106
|
+
mockedListImplementations.mockReturnValue([
|
|
107
|
+
makeMockImpl('IMPL_01', 'skill', 'active'),
|
|
108
|
+
makeMockImpl('IMPL_02', 'lora', 'active'),
|
|
109
|
+
] as any);
|
|
110
|
+
const host = new RuleHost('/mock/state');
|
|
111
|
+
const result = host.evaluate(makeInput());
|
|
112
|
+
expect(result).toBeUndefined();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should return undefined when implementations exist but none are lifecycleState=active', () => {
|
|
116
|
+
// listImplementationsByLifecycleState('active') only returns active ones
|
|
117
|
+
mockedListImplementations.mockReturnValue([]);
|
|
118
|
+
const host = new RuleHost('/mock/state');
|
|
119
|
+
const result = host.evaluate(makeInput());
|
|
120
|
+
expect(result).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should return block when a loaded implementation returns decision=block', () => {
|
|
124
|
+
const blockResult: RuleHostResult = {
|
|
125
|
+
decision: 'block',
|
|
126
|
+
matched: true,
|
|
127
|
+
reason: 'Dangerous operation',
|
|
128
|
+
};
|
|
129
|
+
const mockImpl = makeMockImpl('IMPL_BLOCK');
|
|
130
|
+
|
|
131
|
+
mockedListImplementations.mockReturnValue([mockImpl] as any);
|
|
132
|
+
mockedExistsSync.mockReturnValue(true);
|
|
133
|
+
mockedReadFileSync.mockReturnValue('module.exports = {}');
|
|
134
|
+
mockedLoadRuleImplementationModule.mockReturnValue({
|
|
135
|
+
meta: { name: 'block-test', version: '1.0.0', ruleId: 'RULE_BLOCK', coversCondition: 'test' },
|
|
136
|
+
evaluate: (_input: any, _helpers: any) => blockResult,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const host = new RuleHost('/mock/state');
|
|
140
|
+
const result = host.evaluate(makeInput());
|
|
141
|
+
expect(result).toBeDefined();
|
|
142
|
+
expect(result?.decision).toBe('block');
|
|
143
|
+
expect(result?.matched).toBe(true);
|
|
144
|
+
expect(result?.reason).toBe('Dangerous operation');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should return block (short-circuit) when first of two implementations returns block', () => {
|
|
148
|
+
const blockResult: RuleHostResult = {
|
|
149
|
+
decision: 'block',
|
|
150
|
+
matched: true,
|
|
151
|
+
reason: 'First impl blocks',
|
|
152
|
+
};
|
|
153
|
+
const allowResult: RuleHostResult = {
|
|
154
|
+
decision: 'allow',
|
|
155
|
+
matched: true,
|
|
156
|
+
reason: 'Second impl allows',
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const impl1 = makeMockImpl('IMPL_01');
|
|
160
|
+
const impl2 = makeMockImpl('IMPL_02');
|
|
161
|
+
|
|
162
|
+
mockedListImplementations.mockReturnValue([impl1, impl2] as any);
|
|
163
|
+
mockedExistsSync.mockReturnValue(true);
|
|
164
|
+
mockedReadFileSync.mockReturnValue('module.exports = {}');
|
|
165
|
+
|
|
166
|
+
let callCount = 0;
|
|
167
|
+
mockedLoadRuleImplementationModule.mockImplementation(() => {
|
|
168
|
+
callCount++;
|
|
169
|
+
if (callCount === 1) {
|
|
170
|
+
return {
|
|
171
|
+
meta: { name: 'block-first', version: '1.0.0', ruleId: 'RULE_01', coversCondition: 'test' },
|
|
172
|
+
evaluate: () => blockResult,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
meta: { name: 'allow-second', version: '1.0.0', ruleId: 'RULE_02', coversCondition: 'test' },
|
|
177
|
+
evaluate: () => allowResult,
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const host = new RuleHost('/mock/state');
|
|
182
|
+
const result = host.evaluate(makeInput());
|
|
183
|
+
|
|
184
|
+
expect(result?.decision).toBe('block');
|
|
185
|
+
expect(result?.reason).toBe('First impl blocks');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should return requireApproval when a loaded implementation returns decision=requireApproval', () => {
|
|
189
|
+
const approvalResult: RuleHostResult = {
|
|
190
|
+
decision: 'requireApproval',
|
|
191
|
+
matched: true,
|
|
192
|
+
reason: 'High-risk path modification',
|
|
193
|
+
};
|
|
194
|
+
const mockImpl = makeMockImpl('IMPL_APPROVAL');
|
|
195
|
+
|
|
196
|
+
mockedListImplementations.mockReturnValue([mockImpl] as any);
|
|
197
|
+
mockedExistsSync.mockReturnValue(true);
|
|
198
|
+
mockedReadFileSync.mockReturnValue('module.exports = {}');
|
|
199
|
+
mockedLoadRuleImplementationModule.mockReturnValue({
|
|
200
|
+
meta: { name: 'approval-test', version: '1.0.0', ruleId: 'RULE_APPROVAL', coversCondition: 'test' },
|
|
201
|
+
evaluate: () => approvalResult,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const host = new RuleHost('/mock/state');
|
|
205
|
+
const result = host.evaluate(makeInput());
|
|
206
|
+
expect(result?.decision).toBe('requireApproval');
|
|
207
|
+
expect(result?.matched).toBe(true);
|
|
208
|
+
expect(result?.reason).toBe('High-risk path modification');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should return undefined when all implementations return allow or matched=false', () => {
|
|
212
|
+
const allowResult: RuleHostResult = {
|
|
213
|
+
decision: 'allow',
|
|
214
|
+
matched: true,
|
|
215
|
+
reason: 'OK',
|
|
216
|
+
};
|
|
217
|
+
const unmatchedResult: RuleHostResult = {
|
|
218
|
+
decision: 'allow',
|
|
219
|
+
matched: false,
|
|
220
|
+
reason: 'Not applicable',
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const impl1 = makeMockImpl('IMPL_01');
|
|
224
|
+
const impl2 = makeMockImpl('IMPL_02');
|
|
225
|
+
|
|
226
|
+
mockedListImplementations.mockReturnValue([impl1, impl2] as any);
|
|
227
|
+
mockedExistsSync.mockReturnValue(true);
|
|
228
|
+
mockedReadFileSync.mockReturnValue('module.exports = {}');
|
|
229
|
+
|
|
230
|
+
let callCount = 0;
|
|
231
|
+
mockedLoadRuleImplementationModule.mockImplementation(() => {
|
|
232
|
+
callCount++;
|
|
233
|
+
if (callCount === 1) {
|
|
234
|
+
return {
|
|
235
|
+
meta: { name: 'allow-1', version: '1.0.0', ruleId: 'RULE_01', coversCondition: 'test' },
|
|
236
|
+
evaluate: () => allowResult,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
meta: { name: 'unmatched-2', version: '1.0.0', ruleId: 'RULE_02', coversCondition: 'test' },
|
|
241
|
+
evaluate: () => unmatchedResult,
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const host = new RuleHost('/mock/state');
|
|
246
|
+
const result = host.evaluate(makeInput());
|
|
247
|
+
expect(result).toBeUndefined();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should return undefined on vm load error (conservative degradation)', () => {
|
|
251
|
+
const mockImpl = makeMockImpl('IMPL_BAD');
|
|
252
|
+
mockedListImplementations.mockReturnValue([mockImpl] as any);
|
|
253
|
+
mockedExistsSync.mockReturnValue(true);
|
|
254
|
+
mockedReadFileSync.mockReturnValue('bad syntax {{{');
|
|
255
|
+
mockedLoadRuleImplementationModule.mockImplementation(() => {
|
|
256
|
+
throw new Error('Compilation failed: unexpected token');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const host = new RuleHost('/mock/state');
|
|
260
|
+
const result = host.evaluate(makeInput());
|
|
261
|
+
expect(result).toBeUndefined();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should use injected logger for degradation warnings instead of direct console writes', () => {
|
|
265
|
+
const warn = vi.fn();
|
|
266
|
+
const mockImpl = makeMockImpl('IMPL_BAD');
|
|
267
|
+
mockedListImplementations.mockReturnValue([mockImpl] as any);
|
|
268
|
+
mockedExistsSync.mockReturnValue(true);
|
|
269
|
+
mockedReadFileSync.mockReturnValue('bad syntax {{{');
|
|
270
|
+
mockedLoadRuleImplementationModule.mockImplementation(() => {
|
|
271
|
+
throw new Error('Compilation failed: unexpected token');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const host = new RuleHost('/mock/state', { warn });
|
|
275
|
+
const result = host.evaluate(makeInput());
|
|
276
|
+
|
|
277
|
+
expect(result).toBeUndefined();
|
|
278
|
+
expect(warn).toHaveBeenCalledWith(
|
|
279
|
+
expect.stringContaining('Failed to compile implementation IMPL_BAD')
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('should return undefined on vm execution error (conservative degradation)', () => {
|
|
284
|
+
const mockImpl = makeMockImpl('IMPL_EXEC_ERR');
|
|
285
|
+
mockedListImplementations.mockReturnValue([mockImpl] as any);
|
|
286
|
+
mockedExistsSync.mockReturnValue(true);
|
|
287
|
+
mockedReadFileSync.mockReturnValue('module.exports = {}');
|
|
288
|
+
mockedLoadRuleImplementationModule.mockReturnValue({
|
|
289
|
+
meta: { name: 'exec-err', version: '1.0.0', ruleId: 'RULE_ERR', coversCondition: 'test' },
|
|
290
|
+
evaluate: () => {
|
|
291
|
+
throw new Error('Runtime error during evaluation');
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const host = new RuleHost('/mock/state');
|
|
296
|
+
const result = host.evaluate(makeInput());
|
|
297
|
+
expect(result).toBeUndefined();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('should return undefined when implementation asset path does not exist', () => {
|
|
301
|
+
const mockImpl = makeMockImpl('IMPL_NOFILE');
|
|
302
|
+
mockedListImplementations.mockReturnValue([mockImpl] as any);
|
|
303
|
+
mockedExistsSync.mockReturnValue(false);
|
|
304
|
+
|
|
305
|
+
const host = new RuleHost('/mock/state');
|
|
306
|
+
const result = host.evaluate(makeInput());
|
|
307
|
+
expect(result).toBeUndefined();
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('loads code implementations from storage assets before falling back to impl.path', () => {
|
|
311
|
+
const blockResult: RuleHostResult = {
|
|
312
|
+
decision: 'block',
|
|
313
|
+
matched: true,
|
|
314
|
+
reason: 'Loaded from storage asset',
|
|
315
|
+
};
|
|
316
|
+
const mockImpl = makeMockImpl('IMPL_STORAGE');
|
|
317
|
+
|
|
318
|
+
mockedListImplementations.mockReturnValue([mockImpl] as any);
|
|
319
|
+
mockedLoadEntrySource.mockReturnValue('export const meta = {}; export function evaluate() {}');
|
|
320
|
+
mockedExistsSync.mockReturnValue(false);
|
|
321
|
+
mockedLoadRuleImplementationModule.mockReturnValue({
|
|
322
|
+
meta: { name: 'storage-test', version: '1.0.0', ruleId: 'RULE_STORAGE', coversCondition: 'test' },
|
|
323
|
+
evaluate: () => blockResult,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const host = new RuleHost('/mock/state');
|
|
327
|
+
const result = host.evaluate(makeInput());
|
|
328
|
+
|
|
329
|
+
expect(mockedLoadEntrySource).toHaveBeenCalledWith('/mock/state', 'IMPL_STORAGE');
|
|
330
|
+
expect(mockedReadFileSync).not.toHaveBeenCalled();
|
|
331
|
+
expect(result?.decision).toBe('block');
|
|
332
|
+
expect(result?.reason).toBe('Loaded from storage asset');
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('should merge multiple requireApproval results', () => {
|
|
336
|
+
const approval1: RuleHostResult = {
|
|
337
|
+
decision: 'requireApproval',
|
|
338
|
+
matched: true,
|
|
339
|
+
reason: 'Risk path',
|
|
340
|
+
diagnostics: { riskLevel: 'high' },
|
|
341
|
+
};
|
|
342
|
+
const approval2: RuleHostResult = {
|
|
343
|
+
decision: 'requireApproval',
|
|
344
|
+
matched: true,
|
|
345
|
+
reason: 'Large change',
|
|
346
|
+
diagnostics: { lineCount: 500 },
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const impl1 = makeMockImpl('IMPL_01');
|
|
350
|
+
const impl2 = makeMockImpl('IMPL_02');
|
|
351
|
+
|
|
352
|
+
mockedListImplementations.mockReturnValue([impl1, impl2] as any);
|
|
353
|
+
mockedExistsSync.mockReturnValue(true);
|
|
354
|
+
mockedReadFileSync.mockReturnValue('module.exports = {}');
|
|
355
|
+
|
|
356
|
+
let callCount = 0;
|
|
357
|
+
mockedLoadRuleImplementationModule.mockImplementation(() => {
|
|
358
|
+
callCount++;
|
|
359
|
+
if (callCount === 1) {
|
|
360
|
+
return {
|
|
361
|
+
meta: { name: 'approval-1', version: '1.0.0', ruleId: 'RULE_01', coversCondition: 'test' },
|
|
362
|
+
evaluate: () => approval1,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
meta: { name: 'approval-2', version: '1.0.0', ruleId: 'RULE_02', coversCondition: 'test' },
|
|
367
|
+
evaluate: () => approval2,
|
|
368
|
+
};
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const host = new RuleHost('/mock/state');
|
|
372
|
+
const result = host.evaluate(makeInput());
|
|
373
|
+
|
|
374
|
+
expect(result?.decision).toBe('requireApproval');
|
|
375
|
+
expect(result?.reason).toContain('Risk path');
|
|
376
|
+
expect(result?.reason).toContain('Large change');
|
|
377
|
+
expect(result?.diagnostics).toEqual({ riskLevel: 'high', lineCount: 500 });
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('should return undefined when ledger access fails', () => {
|
|
381
|
+
mockedListImplementations.mockImplementation(() => {
|
|
382
|
+
throw new Error('Ledger file not found');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
const host = new RuleHost('/mock/state');
|
|
386
|
+
const result = host.evaluate(makeInput());
|
|
387
|
+
expect(result).toBeUndefined();
|
|
388
|
+
});
|
|
389
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { loadRuleImplementationModule } from '../../src/core/rule-implementation-runtime.js';
|
|
3
|
+
import { validateRuleImplementationCandidate } from '../../src/core/nocturnal-rule-implementation-validator.js';
|
|
4
|
+
|
|
5
|
+
describe('rule-implementation-runtime', () => {
|
|
6
|
+
it('does not leak global mutations from loaded rule implementations into the host realm', () => {
|
|
7
|
+
delete (globalThis as Record<string, unknown>).__pdRuleHostLeak;
|
|
8
|
+
|
|
9
|
+
const moduleExports = loadRuleImplementationModule(
|
|
10
|
+
`
|
|
11
|
+
globalThis.__pdRuleHostLeak = 'sandbox-only';
|
|
12
|
+
export const meta = {
|
|
13
|
+
name: 'leak-check',
|
|
14
|
+
version: '1.0.0',
|
|
15
|
+
ruleId: 'R-LEAK',
|
|
16
|
+
coversCondition: 'host isolation'
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function evaluate() {
|
|
20
|
+
globalThis.__pdRuleHostLeak = 'still-sandboxed';
|
|
21
|
+
return {
|
|
22
|
+
decision: 'allow',
|
|
23
|
+
matched: false,
|
|
24
|
+
reason: 'ok'
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
`,
|
|
28
|
+
'rule-leak-check.js',
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
expect(typeof moduleExports.evaluate).toBe('function');
|
|
32
|
+
expect((globalThis as Record<string, unknown>).__pdRuleHostLeak).toBeUndefined();
|
|
33
|
+
|
|
34
|
+
(moduleExports.evaluate as () => unknown)();
|
|
35
|
+
|
|
36
|
+
expect((globalThis as Record<string, unknown>).__pdRuleHostLeak).toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('keeps validator execution isolated from the host global object', () => {
|
|
40
|
+
delete (globalThis as Record<string, unknown>).__pdValidatorLeak;
|
|
41
|
+
|
|
42
|
+
const result = validateRuleImplementationCandidate(`
|
|
43
|
+
globalThis.__pdValidatorLeak = 'sandbox-only';
|
|
44
|
+
export const meta = {
|
|
45
|
+
name: 'validator-leak-check',
|
|
46
|
+
version: '1.0.0',
|
|
47
|
+
ruleId: 'R-VAL',
|
|
48
|
+
coversCondition: 'validator isolation'
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function evaluate() {
|
|
52
|
+
globalThis.__pdValidatorLeak = 'still-sandboxed';
|
|
53
|
+
return {
|
|
54
|
+
decision: 'allow',
|
|
55
|
+
matched: false,
|
|
56
|
+
reason: 'ok'
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
`);
|
|
60
|
+
|
|
61
|
+
expect(result.passed).toBe(true);
|
|
62
|
+
expect((globalThis as Record<string, unknown>).__pdValidatorLeak).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
|
2
2
|
import { WorkspaceContext } from '../../src/core/workspace-context.js';
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
import * as path from 'path';
|
|
5
|
+
import * as principleTreeLedger from '../../src/core/principle-tree-ledger.js';
|
|
5
6
|
|
|
6
7
|
vi.mock('fs');
|
|
7
8
|
vi.mock('../../src/core/trajectory.js', () => ({
|
|
@@ -14,6 +15,9 @@ vi.mock('../../src/core/trajectory.js', () => ({
|
|
|
14
15
|
clear: vi.fn(),
|
|
15
16
|
}
|
|
16
17
|
}));
|
|
18
|
+
vi.mock('../../src/core/principle-tree-ledger.js', () => ({
|
|
19
|
+
getPrincipleSubtree: vi.fn(),
|
|
20
|
+
}));
|
|
17
21
|
|
|
18
22
|
describe('WorkspaceContext', () => {
|
|
19
23
|
// Use path.resolve for cross-platform compatibility on Windows
|
|
@@ -131,4 +135,53 @@ describe('WorkspaceContext', () => {
|
|
|
131
135
|
expect(dictionary).toBeDefined();
|
|
132
136
|
expect(wctx.dictionary).toBe(dictionary);
|
|
133
137
|
});
|
|
138
|
+
|
|
139
|
+
it('should cache a workspace-scoped principle tree ledger accessor', () => {
|
|
140
|
+
const mockCtx = { workspaceDir };
|
|
141
|
+
const wctx = WorkspaceContext.fromHookContext(mockCtx);
|
|
142
|
+
|
|
143
|
+
const principleTreeLedgerAccessor = (wctx as any).principleTreeLedger;
|
|
144
|
+
|
|
145
|
+
expect(principleTreeLedgerAccessor).toBeDefined();
|
|
146
|
+
expect((wctx as any).principleTreeLedger).toBe(principleTreeLedgerAccessor);
|
|
147
|
+
expect(typeof principleTreeLedgerAccessor.getPrincipleSubtree).toBe('function');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should retrieve active principle subtrees through the workspace boundary', () => {
|
|
151
|
+
const mockCtx = { workspaceDir, stateDir };
|
|
152
|
+
const wctx = WorkspaceContext.fromHookContext(mockCtx);
|
|
153
|
+
const activePrinciples = [
|
|
154
|
+
{
|
|
155
|
+
id: 'P-001',
|
|
156
|
+
trigger: 'delete',
|
|
157
|
+
contextTags: ['write'],
|
|
158
|
+
valueMetrics: undefined,
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
const subtree = {
|
|
162
|
+
principle: { id: 'P-001', ruleIds: ['R-001'] },
|
|
163
|
+
rules: [
|
|
164
|
+
{
|
|
165
|
+
rule: { id: 'R-001', implementationIds: ['IMPL-001'] },
|
|
166
|
+
implementations: [{ id: 'IMPL-001', ruleId: 'R-001', type: 'prompt' }],
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
vi.mocked(principleTreeLedger.getPrincipleSubtree).mockReturnValue(subtree as any);
|
|
172
|
+
(wctx as any)._evolutionReducer = {
|
|
173
|
+
getActivePrinciples: vi.fn().mockReturnValue(activePrinciples),
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const activePrincipleSubtrees = (wctx as any).getActivePrincipleSubtrees();
|
|
177
|
+
|
|
178
|
+
expect((wctx as any)._evolutionReducer.getActivePrinciples).toHaveBeenCalled();
|
|
179
|
+
expect(principleTreeLedger.getPrincipleSubtree).toHaveBeenCalledWith(stateDir, 'P-001');
|
|
180
|
+
expect(activePrincipleSubtrees).toEqual([
|
|
181
|
+
{
|
|
182
|
+
principle: activePrinciples[0],
|
|
183
|
+
subtree,
|
|
184
|
+
},
|
|
185
|
+
]);
|
|
186
|
+
});
|
|
134
187
|
});
|