principles-disciple 1.122.0 → 1.124.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.
@@ -364,7 +364,7 @@ describe('PRI-288: EvolutionWorkerService quarantine', () => {
364
364
  expect(flags.flags['evolution_worker']?.enabled).toBe(true);
365
365
  });
366
366
 
367
- it('core flags cannot be disabled by user override', () => {
367
+ it('PRI-435: core flags can be explicitly emergency-disabled by user override with warning', () => {
368
368
  writeConfigYaml(workspaceDir, {
369
369
  prompt: { enabled: false },
370
370
  code_tool_hook: { enabled: false },
@@ -377,9 +377,11 @@ describe('PRI-288: EvolutionWorkerService quarantine', () => {
377
377
  expect(isRecord(parsed)).toBe(true);
378
378
  const features = (parsed as Record<string, unknown>).features;
379
379
  const flags = computeEffectiveFlags(features as Record<string, unknown>, DEFAULT_FEATURE_FLAGS, configPath);
380
- expect(flags.flags['prompt']?.enabled).toBe(true); // core cannot be disabled
381
- expect(flags.flags['code_tool_hook']?.enabled).toBe(true); // core cannot be disabled
382
- expect(flags.warnings.length).toBeGreaterThan(0); // warnings about core override attempt
380
+ // PRI-435: core flags honor explicit emergency disable when deliberately configured
381
+ expect(flags.flags['prompt']?.enabled).toBe(false);
382
+ expect(flags.flags['code_tool_hook']?.enabled).toBe(false);
383
+ expect(flags.warnings.length).toBeGreaterThan(0); // warnings about emergency disable
384
+ expect(flags.warnings.some(w => w.includes('core flag explicitly disabled'))).toBe(true);
383
385
  });
384
386
  });
385
387
 
@@ -52,7 +52,6 @@ vi.mock('../../src/core/rule-host.js', () => ({
52
52
 
53
53
  vi.mock('../../src/core/principle-tree-ledger.js', () => ({
54
54
  loadLedger: vi.fn(),
55
- listImplementationsByLifecycleState: vi.fn(() => []),
56
55
  }));
57
56
 
58
57
  vi.mock('../../src/hooks/gate-block-helper.js', () => ({
@@ -55,7 +55,6 @@ vi.mock('../../src/core/rule-host.js', () => ({
55
55
 
56
56
  vi.mock('../../src/core/principle-tree-ledger.js', () => ({
57
57
  loadLedger: vi.fn(),
58
- listImplementationsByLifecycleState: vi.fn(() => []),
59
58
  }));
60
59
 
61
60
  vi.mock('../../src/hooks/gate-block-helper.js', () => ({
@@ -54,7 +54,6 @@ vi.mock('../../src/core/rule-host.js', () => ({
54
54
 
55
55
  vi.mock('../../src/core/principle-tree-ledger.js', () => ({
56
56
  loadLedger: vi.fn(),
57
- listImplementationsByLifecycleState: vi.fn(() => []),
58
57
  }));
59
58
 
60
59
  // Mock WorkspaceContext to return a controlled instance with our mockEventLog.
@@ -53,7 +53,6 @@ vi.mock('../../src/core/rule-host.js', () => ({
53
53
 
54
54
  vi.mock('../../src/core/principle-tree-ledger.js', () => ({
55
55
  loadLedger: vi.fn(),
56
- listImplementationsByLifecycleState: vi.fn(() => []),
57
56
  }));
58
57
 
59
58
  describe('Gate Rule Host Only Pipeline', () => {
@@ -26,8 +26,8 @@ import { RuleHost } from '../../src/core/rule-host.js';
26
26
  import { EvolutionReducerImpl } from '../../src/core/evolution-reducer.js';
27
27
  import {
28
28
  loadLedger,
29
- transitionImplementationState,
30
29
  } from '../../src/core/principle-tree-ledger.js';
30
+ import { SqliteConnection, SqliteActivationStateStore } from '@principles/core/runtime-v2';
31
31
  import { safeRmDir } from '../test-utils.js';
32
32
  import type { RuleHostInput } from '../../src/core/rule-host-types.js';
33
33
 
@@ -58,6 +58,60 @@ function disposeTestWorkspace(ws: TestWorkspace): void {
58
58
  safeRmDir(ws.workspaceDir);
59
59
  }
60
60
 
61
+ /**
62
+ * PRI-436: Insert compiled rule code as a pi_artifacts row + activations row
63
+ * so RuleHost can load it from SQLite (the sole production source).
64
+ */
65
+ function activateCompiledRule(
66
+ workspaceDir: string,
67
+ ruleId: string,
68
+ implementationCode: string,
69
+ principleId: string,
70
+ ): void {
71
+ const sqliteConn = new SqliteConnection(workspaceDir);
72
+ try {
73
+ const db = sqliteConn.getDb();
74
+ const now = new Date().toISOString();
75
+ const artifactId = `art-${ruleId}`;
76
+
77
+ const contentJson = JSON.stringify({
78
+ principleId,
79
+ ruleId,
80
+ implementationCode,
81
+ });
82
+
83
+ db.prepare(`
84
+ INSERT INTO pi_artifacts (artifact_id, artifact_kind, source_task_id, source_principle_id, source_rule_id, lineage_artifact_ids, validation_status, content_json, created_at, updated_at)
85
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
86
+ `).run(
87
+ artifactId,
88
+ 'rule',
89
+ `task-${ruleId}`,
90
+ principleId,
91
+ ruleId,
92
+ '[]',
93
+ 'validated',
94
+ contentJson,
95
+ now,
96
+ now,
97
+ );
98
+
99
+ const store = new SqliteActivationStateStore(sqliteConn);
100
+ store.recordActivation({
101
+ activationId: `act_code_${ruleId}`,
102
+ idempotencyKey: `${artifactId}::code_tool_hook`,
103
+ artifactId,
104
+ channel: 'code_tool_hook',
105
+ action: 'code_tool_hook_shadow_activate',
106
+ targetRef: `impl://${ruleId}`,
107
+ activatedAt: now,
108
+ deactivatedAt: null,
109
+ });
110
+ } finally {
111
+ try { sqliteConn.close(); } catch { /* best-effort */ }
112
+ }
113
+ }
114
+
61
115
  // ---------------------------------------------------------------------------
62
116
  // Tests
63
117
  // ---------------------------------------------------------------------------
@@ -134,16 +188,16 @@ describe('Pain ID Chain E2E: pain event → principle → compile → RuleHost',
134
188
  expect(compileResult.ruleId).toBeDefined();
135
189
  expect(compileResult.implementationId).toBeDefined();
136
190
 
137
- // Verify implementation is candidate (not active — must be promoted before enforcing)
191
+ // Verify implementation is candidate (not active — must be activated before enforcing)
138
192
  const updatedLedger = loadLedger(ws.stateDir);
139
193
  const impl = updatedLedger.tree.implementations[compileResult.implementationId!];
140
194
  expect(impl.lifecycleState).toBe('candidate');
141
195
 
142
- // ── Step 5: Promote to active so RuleHost will enforce ──
143
- transitionImplementationState(ws.stateDir, compileResult.implementationId!, 'active');
196
+ // ── Step 5: Activate via SQLite (PRI-436: sole production source) ──
197
+ activateCompiledRule(ws.workspaceDir, compileResult.ruleId!, compileResult.code!, principleId!);
144
198
 
145
199
  // ── Step 6: RuleHost.evaluate(matching input) → block ──
146
- const host = new RuleHost(ws.stateDir, { warn: () => {} });
200
+ const host = new RuleHost(ws.stateDir, { warn: () => {} }, { workspaceDir: ws.workspaceDir });
147
201
 
148
202
  const matchingInput: RuleHostInput = {
149
203
  action: {
@@ -21,8 +21,8 @@ import { RuleHost } from '../../src/core/rule-host.js';
21
21
  import {
22
22
  loadLedger,
23
23
  saveLedger,
24
- transitionImplementationState,
25
24
  } from '../../src/core/principle-tree-ledger.js';
25
+ import { SqliteConnection, SqliteActivationStateStore } from '@principles/core/runtime-v2';
26
26
  import type { RuleHostInput } from '../../src/core/rule-host-types.js';
27
27
 
28
28
  // ---------------------------------------------------------------------------
@@ -50,6 +50,60 @@ function disposeTestWorkspace(ws: TestWorkspace): void {
50
50
  fs.rmSync(ws.workspaceDir, { recursive: true, force: true });
51
51
  }
52
52
 
53
+ /**
54
+ * PRI-436: Insert compiled rule code as a pi_artifacts row + activations row
55
+ * so RuleHost can load it from SQLite (the sole production source).
56
+ */
57
+ function activateCompiledRule(
58
+ workspaceDir: string,
59
+ ruleId: string,
60
+ implementationCode: string,
61
+ principleId: string,
62
+ ): void {
63
+ const sqliteConn = new SqliteConnection(workspaceDir);
64
+ try {
65
+ const db = sqliteConn.getDb();
66
+ const now = new Date().toISOString();
67
+ const artifactId = `art-${ruleId}`;
68
+
69
+ const contentJson = JSON.stringify({
70
+ principleId,
71
+ ruleId,
72
+ implementationCode,
73
+ });
74
+
75
+ db.prepare(`
76
+ INSERT INTO pi_artifacts (artifact_id, artifact_kind, source_task_id, source_principle_id, source_rule_id, lineage_artifact_ids, validation_status, content_json, created_at, updated_at)
77
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
78
+ `).run(
79
+ artifactId,
80
+ 'rule',
81
+ `task-${ruleId}`,
82
+ principleId,
83
+ ruleId,
84
+ '[]',
85
+ 'validated',
86
+ contentJson,
87
+ now,
88
+ now,
89
+ );
90
+
91
+ const store = new SqliteActivationStateStore(sqliteConn);
92
+ store.recordActivation({
93
+ activationId: `act_code_${ruleId}`,
94
+ idempotencyKey: `${artifactId}::code_tool_hook`,
95
+ artifactId,
96
+ channel: 'code_tool_hook',
97
+ action: 'code_tool_hook_shadow_activate',
98
+ targetRef: `impl://${ruleId}`,
99
+ activatedAt: now,
100
+ deactivatedAt: null,
101
+ });
102
+ } finally {
103
+ try { sqliteConn.close(); } catch { /* best-effort */ }
104
+ }
105
+ }
106
+
53
107
  // ---------------------------------------------------------------------------
54
108
  // Tests
55
109
  // ---------------------------------------------------------------------------
@@ -168,21 +222,16 @@ describe('Principle Compiler E2E: compile → promote → RuleHost blocks', () =
168
222
  },
169
223
  };
170
224
 
171
- // ── Step 4: RuleHost should NOT block yet (candidate not loaded) ──
172
- const hostBeforePromote = new RuleHost(ws.stateDir, { warn: () => {} });
225
+ // ── Step 4: RuleHost should NOT block yet (no SQLite activation yet) ──
226
+ const hostBeforePromote = new RuleHost(ws.stateDir, { warn: () => {} }, { workspaceDir: ws.workspaceDir });
173
227
  const noBlockResult = hostBeforePromote.evaluate(matchingInput);
174
- expect(noBlockResult).toBeUndefined(); // candidate not loaded → no block
175
-
176
- // ── Step 5: Promote to 'active' so RuleHost will enforce ──
177
- transitionImplementationState(ws.stateDir, implId, 'active');
228
+ expect(noBlockResult).toBeUndefined(); // no activation → no block
178
229
 
179
- // Verify promotion
180
- const ledgerAfterPromote = loadLedger(ws.stateDir);
181
- const implAfterPromote = ledgerAfterPromote.tree.implementations[implId];
182
- expect(implAfterPromote.lifecycleState).toBe('active');
230
+ // ── Step 5: Activate via SQLite (PRI-436: sole production source) ──
231
+ activateCompiledRule(ws.workspaceDir, result.ruleId!, result.code!, principleId);
183
232
 
184
233
  // ── Step 6: Create RuleHost and evaluate with matching input ──
185
- const host = new RuleHost(ws.stateDir, { warn: () => {} });
234
+ const host = new RuleHost(ws.stateDir, { warn: () => {} }, { workspaceDir: ws.workspaceDir });
186
235
 
187
236
  // Matching input: bash tool with a heartbeat command (defined in Step 4)
188
237
  const blockResult = host.evaluate(matchingInput);
@@ -224,7 +273,7 @@ describe('Principle Compiler E2E: compile → promote → RuleHost blocks', () =
224
273
  });
225
274
 
226
275
  it('should return undefined when no active implementations exist', () => {
227
- const host = new RuleHost(ws.stateDir, { warn: () => {} });
276
+ const host = new RuleHost(ws.stateDir, { warn: () => {} }, { workspaceDir: ws.workspaceDir });
228
277
 
229
278
  const input: RuleHostInput = {
230
279
  action: {