@principles/pd-cli 1.135.1 → 1.136.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/commands/demo-story-a.d.ts +2 -0
  2. package/dist/commands/demo-story-a.d.ts.map +1 -1
  3. package/dist/commands/demo-story-a.js +41 -0
  4. package/dist/commands/demo-story-a.js.map +1 -1
  5. package/dist/commands/runtime-activation.d.ts.map +1 -1
  6. package/dist/commands/runtime-activation.js +36 -7
  7. package/dist/commands/runtime-activation.js.map +1 -1
  8. package/dist/commands/runtime-compatibility-scan.d.ts +32 -0
  9. package/dist/commands/runtime-compatibility-scan.d.ts.map +1 -0
  10. package/dist/commands/runtime-compatibility-scan.js +94 -0
  11. package/dist/commands/runtime-compatibility-scan.js.map +1 -0
  12. package/dist/commands/runtime-internalization-enqueue-successors.d.ts.map +1 -1
  13. package/dist/commands/runtime-internalization-enqueue-successors.js +48 -0
  14. package/dist/commands/runtime-internalization-enqueue-successors.js.map +1 -1
  15. package/dist/commands/runtime-internalization-retry.d.ts +38 -0
  16. package/dist/commands/runtime-internalization-retry.d.ts.map +1 -0
  17. package/dist/commands/runtime-internalization-retry.js +143 -0
  18. package/dist/commands/runtime-internalization-retry.js.map +1 -0
  19. package/dist/index.js +15 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/services/__tests__/evaluator-runner-deps.test.js +19 -7
  22. package/dist/services/__tests__/evaluator-runner-deps.test.js.map +1 -1
  23. package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
  24. package/dist/services/rulehost-pipeline-runner.js +6 -2
  25. package/dist/services/rulehost-pipeline-runner.js.map +1 -1
  26. package/package.json +1 -1
  27. package/src/commands/demo-story-a.ts +44 -0
  28. package/src/commands/runtime-activation.ts +38 -6
  29. package/src/commands/runtime-compatibility-scan.ts +105 -0
  30. package/src/commands/runtime-internalization-enqueue-successors.ts +48 -0
  31. package/src/commands/runtime-internalization-retry.ts +163 -0
  32. package/src/index.ts +17 -0
  33. package/src/services/__tests__/evaluator-runner-deps.test.ts +20 -8
  34. package/src/services/rulehost-pipeline-runner.ts +5 -2
  35. package/tests/commands/cli-command-tree.test.ts +15 -0
  36. package/tests/commands/demo-story-a.test.ts +63 -0
  37. package/tests/commands/runtime-compatibility-scan.test.ts +146 -0
  38. package/tests/commands/runtime-internalization-retry-owner-authority.test.ts +431 -0
  39. package/tests/e2e/cross-package-acceptance.test.ts +2 -2
  40. package/tests/services/demo-rule-compiler.test.ts +2 -2
@@ -100,6 +100,8 @@ function createMockStateManager(): {
100
100
  } {
101
101
  const createdTasks: TaskRecord[] = [];
102
102
  const stateManager = {
103
+ getTask: vi.fn(async (taskId: string) =>
104
+ createdTasks.find((t) => t.taskId === taskId) ?? null),
103
105
  createTask: vi.fn(async (record: Omit<TaskRecord, 'createdAt' | 'updatedAt'>) => {
104
106
  const now = new Date().toISOString();
105
107
  const task: TaskRecord = { ...record, createdAt: now, updatedAt: now };
@@ -185,7 +187,7 @@ describe('PRI-510 (DEFECT-004): createEvaluatorRunnerDeps wires repair loop into
185
187
  expect(deps.isRepairLoopEnabled()).toBe(false);
186
188
  });
187
189
 
188
- it('flag absent in config → isRepairLoopEnabled() returns false (defaults apply)', () => {
190
+ it('flag absent in config → isRepairLoopEnabled() returns true (P0-D: default-on since core-loop closure)', () => {
189
191
  const workspaceDir = createTempWorkspace(null);
190
192
  tmpWorkspaces.push(workspaceDir);
191
193
  const { stateManager } = createMockStateManager();
@@ -201,10 +203,11 @@ describe('PRI-510 (DEFECT-004): createEvaluatorRunnerDeps wires repair loop into
201
203
 
202
204
  expect(typeof deps.isRepairLoopEnabled).toBe('function');
203
205
  if (typeof deps.isRepairLoopEnabled !== 'function') throw new Error('isRepairLoopEnabled missing');
204
- expect(deps.isRepairLoopEnabled()).toBe(false);
206
+ // 契约变更 (2026-08-18, INV-02): registry 默认 ON;flag 缺省 = 默认生效
207
+ expect(deps.isRepairLoopEnabled()).toBe(true);
205
208
  });
206
209
 
207
- it('malformed config → isRepairLoopEnabled() returns false (rc-9: fail safe, not throw)', () => {
210
+ it('malformed config → isRepairLoopEnabled() falls back to registry defaults (default-on), never throws', () => {
208
211
  // CodeQL: use mkdtempSync for atomic, unpredictable temp dir creation.
209
212
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-pri-510-malformed-'));
210
213
  tmpWorkspaces.push(tmpDir);
@@ -223,10 +226,11 @@ describe('PRI-510 (DEFECT-004): createEvaluatorRunnerDeps wires repair loop into
223
226
  workspaceDir: tmpDir,
224
227
  });
225
228
 
226
- // Malformed config must NOT throw — return false so the legacy path runs.
229
+ // Malformed config must NOT throw — falls back to registry defaults
230
+ // (P0-D: evaluator_artificer_repair_loop default-on since core-loop closure).
227
231
  expect(typeof deps.isRepairLoopEnabled).toBe('function');
228
232
  if (typeof deps.isRepairLoopEnabled !== 'function') throw new Error('isRepairLoopEnabled missing');
229
- expect(deps.isRepairLoopEnabled()).toBe(false);
233
+ expect(deps.isRepairLoopEnabled()).toBe(true);
230
234
  });
231
235
 
232
236
  it('seedArtificerRepairTask → creates artificer task with repairPayload in diagnosticJson (rc-1, rc-6)', async () => {
@@ -286,7 +290,7 @@ describe('PRI-510 (DEFECT-004): createEvaluatorRunnerDeps wires repair loop into
286
290
  expect(meta.inputArtifactRefs).toEqual(params.inheritedInputArtifactRefs);
287
291
  });
288
292
 
289
- it('seedArtificerRepairTask → each call returns a UNIQUE task ID (rc-7: no stale state)', async () => {
293
+ it('seedArtificerRepairTask → deterministic id + replay reuse (P0-4); 不同 iteration 不同 id (rc-7)', async () => {
290
294
  const workspaceDir = createTempWorkspace(true);
291
295
  tmpWorkspaces.push(workspaceDir);
292
296
  const { stateManager } = createMockStateManager();
@@ -303,9 +307,17 @@ describe('PRI-510 (DEFECT-004): createEvaluatorRunnerDeps wires repair loop into
303
307
 
304
308
  if (typeof deps.seedArtificerRepairTask !== 'function') throw new Error('seedArtificerRepairTask missing');
305
309
  const id1 = await deps.seedArtificerRepairTask(params);
310
+ // P0-4: 同一 evaluator+iteration 的重放 (consumer 重复周期 / crash 恢复)
311
+ // reuse 同一确定性 id,不重复创建
312
+ expect(id1).toBe(`artificer-repair-${params.repairPayload.sourceEvaluatorTaskId}-r${params.repairPayload.repairIteration}`);
306
313
  const id2 = await deps.seedArtificerRepairTask(params);
307
-
308
- expect(id1).not.toBe(id2);
314
+ expect(id2).toBe(id1);
315
+ // 不同 iteration (下一逻辑修复轮) → 不同 id
316
+ const id3 = await deps.seedArtificerRepairTask({
317
+ ...params,
318
+ repairPayload: { ...params.repairPayload, repairIteration: params.repairPayload.repairIteration + 1 },
319
+ });
320
+ expect(id3).not.toBe(id1);
309
321
  });
310
322
 
311
323
  it('deps spread contains all required base PeerRunnerDeps fields (EP-02: real path gets full deps)', () => {
@@ -66,7 +66,7 @@ import type {
66
66
  SeedArtificerRepairParams,
67
67
  EvaluatorValidator,
68
68
  } from '@principles/core/runtime-v2';
69
- import { randomUUID, createHash } from 'node:crypto';
69
+ import { createHash } from 'node:crypto';
70
70
  import { loadPdConfig } from './pd-config-loader.js';
71
71
  /* eslint-disable @typescript-eslint/no-use-before-define -- helpers declared after main, matching codebase convention */
72
72
  import { compileDemoRule } from './demo-rule-compiler.js';
@@ -646,7 +646,10 @@ export function createEvaluatorRunnerDeps(inputs: CreateEvaluatorRunnerDepsInput
646
646
  },
647
647
  seedArtificerRepairTask: async (params: SeedArtificerRepairParams): Promise<string> => {
648
648
  // rc-7: each call gets a fresh task ID — never reuse a cached ID.
649
- const repairTaskId = `artificer-repair-${randomUUID()}`;
649
+ // P0-4: deterministic revision identity + reuse on replay
650
+ const repairTaskId = `artificer-repair-${params.repairPayload.sourceEvaluatorTaskId}-r${params.repairPayload.repairIteration}`;
651
+ const existing = await stateManager.getTask(repairTaskId);
652
+ if (existing) return repairTaskId;
650
653
  await stateManager.createTask({
651
654
  taskId: repairTaskId,
652
655
  // D1 (PRI-509): task kind is 'artificer' — reuses the artificer
@@ -126,4 +126,19 @@ describe('CLI command tree structure', () => {
126
126
  const output = runPdHelp(['legacy', 'cleanup', '--help']);
127
127
  expect(output).toContain('V1 Artificer');
128
128
  });
129
+
130
+ // cli-7-test-wiring (PR #1358 known gap 补齐): retry 是 INV-03 的核心 Owner
131
+ // 出边,command-tree 注册必须有 wiring 证明,不能只靠 handler 测试。
132
+ it('internalization retry command exists under runtime internalization (pd runtime internalization retry --help)', () => {
133
+ const output = runPdHelp(['runtime', 'internalization', 'retry', '--help']);
134
+ expect(output).toContain('--task');
135
+ expect(output).toContain('--confirm');
136
+ expect(output).toContain('--workspace');
137
+ expect(output).toContain('--json');
138
+ });
139
+
140
+ it('internalization subcommand list includes retry (pd runtime internalization --help)', () => {
141
+ const output = runPdHelp(['runtime', 'internalization', '--help']);
142
+ expect(output).toMatch(/retry\s/);
143
+ });
129
144
  });
@@ -238,6 +238,69 @@ describe('pd demo story-a CLI', () => {
238
238
  expect(parsed.narrative).toContain('[SIMULATED]');
239
239
  expect(parsed.narrative).toContain('[REAL]');
240
240
  });
241
+ // ── Demo isolation (2026-08-19): demo must not pollute real PD workspaces ──
242
+
243
+ it('refuses to write into a workspace that already contains PD state (default)', async () => {
244
+ const existing = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-demo-isolation-'));
245
+ try {
246
+ fs.mkdirSync(path.join(existing, '.pd'), { recursive: true });
247
+ fs.writeFileSync(path.join(existing, '.pd', 'state.db'), '');
248
+
249
+ await handleDemoStoryA({ workspace: existing, json: true });
250
+
251
+ expect(process.exitCode).toBe(1);
252
+ const output = stdoutSpy.mock.calls.map(c => c[0]).join('');
253
+ const parsed = JSON.parse(output);
254
+ expect(parsed.status).toBe('refused');
255
+ expect(parsed.refusal.reason).toBe('demo_write_to_existing_workspace');
256
+ expect(parsed.refusal.nextAction).toContain('--allow-demo-write-to-existing-workspace');
257
+ // cli-5: no mutation on the refused path — the marker file is untouched.
258
+ const stat = fs.statSync(path.join(existing, '.pd', 'state.db'));
259
+ expect(stat.size).toBe(0);
260
+ } finally {
261
+ fs.rmSync(existing, { recursive: true, force: true });
262
+ }
263
+ });
264
+
265
+ it('text mode refusal points at the override flag', async () => {
266
+ const existing = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-demo-isolation-'));
267
+ try {
268
+ fs.mkdirSync(path.join(existing, '.pd'), { recursive: true });
269
+ fs.writeFileSync(path.join(existing, '.pd', 'state.db'), '');
270
+
271
+ await handleDemoStoryA({ workspace: existing });
272
+
273
+ expect(process.exitCode).toBe(1);
274
+ const output = stderrSpy.mock.calls.map(c => c[0]).join('');
275
+ expect(output).toContain('existing PD workspace');
276
+ expect(output).toContain('--allow-demo-write-to-existing-workspace');
277
+ } finally {
278
+ fs.rmSync(existing, { recursive: true, force: true });
279
+ }
280
+ });
281
+
282
+ it('developer override allows writing into the existing workspace with origin:demo provenance', async () => {
283
+ const existing = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-demo-override-'));
284
+ try {
285
+ fs.mkdirSync(path.join(existing, '.pd'), { recursive: true });
286
+ fs.writeFileSync(path.join(existing, '.pd', 'config.yaml'), 'features: {}');
287
+
288
+ await handleDemoStoryA({ workspace: existing, json: true, allowDemoWriteToExistingWorkspace: true });
289
+
290
+ const output = stdoutSpy.mock.calls.map(c => c[0]).join('');
291
+ const parsed = JSON.parse(output);
292
+ expect(parsed.status).not.toBe('refused');
293
+ const db = new Database(path.join(existing, '.pd', 'state.db'), { readonly: true });
294
+ const rows = db.prepare('SELECT content_json FROM pi_artifacts').all() as { content_json: string }[];
295
+ db.close();
296
+ expect(rows.length).toBeGreaterThan(0);
297
+ for (const row of rows) {
298
+ expect(JSON.parse(row.content_json).origin).toBe('demo');
299
+ }
300
+ } finally {
301
+ fs.rmSync(existing, { recursive: true, force: true });
302
+ }
303
+ });
241
304
  });
242
305
 
243
306
  describe('cleanupTempWorkspace', () => {
@@ -0,0 +1,146 @@
1
+ /**
2
+ * runtime compatibility-scan tests — pd runtime compatibility-scan (P1-3).
3
+ *
4
+ * Real persisted-workspace fixtures (SqliteConnection + activation store) —
5
+ * no DB mocks, the production read path is exercised end to end (EP-09).
6
+ *
7
+ * Covers:
8
+ * - SCAN-01: clean RuleContextV2-only active rule → exit 0, status clean (cli-1/cli-6)
9
+ * - SCAN-02: active rule reading session.recentThinking → exit 1,
10
+ * reason legacy_rule_contract_dependency, remediation names the rule (cli-6)
11
+ * - SCAN-03: workspace without state.db → exit 0, status no_state_db
12
+ * - SCAN-04: --json emits exactly one parseable JSON object (cli-1)
13
+ * - SCAN-05: command wiring — real Commander registration (cli-7)
14
+ */
15
+
16
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
17
+ import * as path from 'node:path';
18
+ import * as os from 'node:os';
19
+ import * as fs from 'node:fs';
20
+ import { SqliteConnection, SqliteActivationStateStore } from '@principles/core/runtime-v2';
21
+ import { handleRuntimeCompatibilityScan } from '../../src/commands/runtime-compatibility-scan.js';
22
+
23
+ const LEGACY_CODE = `
24
+ function evaluate(input, helpers) {
25
+ if (input.session && input.session.recentThinking === true) {
26
+ return { decision: 'block', matched: true };
27
+ }
28
+ return { decision: 'allow', matched: false };
29
+ }
30
+ `;
31
+
32
+ const CLEAN_CODE = `
33
+ function evaluate(input, helpers) {
34
+ var h = input.context && input.context.history;
35
+ return { decision: 'allow', matched: false };
36
+ }
37
+ `;
38
+
39
+ let tempWorkspaceDir: string;
40
+ let conn: SqliteConnection;
41
+
42
+ beforeEach(() => {
43
+ vi.spyOn(console, 'log').mockImplementation(() => undefined);
44
+ vi.spyOn(console, 'error').mockImplementation(() => undefined);
45
+ process.exitCode = undefined;
46
+ tempWorkspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-compat-cmd-'));
47
+ conn = new SqliteConnection(tempWorkspaceDir);
48
+ conn.getDb();
49
+ });
50
+
51
+ afterEach(() => {
52
+ vi.restoreAllMocks();
53
+ try { conn?.close(); } catch { /* best-effort */ }
54
+ try { fs.rmSync(tempWorkspaceDir, { recursive: true, force: true }); } catch { /* Windows */ }
55
+ });
56
+
57
+ async function seedActiveRule(artifactId: string, ruleId: string, implementationCode: string): Promise<void> {
58
+ const now = new Date().toISOString();
59
+ conn.getDb().prepare(`
60
+ 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)
61
+ VALUES (?, 'rule', ?, ?, ?, '[]', 'validated', ?, ?, ?)
62
+ `).run(artifactId, `task-${artifactId}`, `principle-${ruleId}`, ruleId, JSON.stringify({ ruleId, implementationCode }), now, now);
63
+ await new SqliteActivationStateStore(conn).recordActivation({
64
+ activationId: `act-${artifactId}`,
65
+ idempotencyKey: `${artifactId}::code_tool_hook`,
66
+ artifactId,
67
+ channel: 'code_tool_hook',
68
+ action: 'code_tool_hook_live_activate',
69
+ targetRef: `impl://${ruleId}`,
70
+ activatedAt: now,
71
+ deactivatedAt: null,
72
+ });
73
+ }
74
+
75
+ function capturedStdout(): string {
76
+ const calls = (console.log as unknown as { mock: { calls: unknown[][] } }).mock.calls;
77
+ return calls.map(c => String(c[0] ?? '')).join('\n');
78
+ }
79
+
80
+ describe('pd runtime compatibility-scan', () => {
81
+ it('SCAN-01: clean current-contract rule exits 0 with status clean', async () => {
82
+ await seedActiveRule('art-clean', 'rule-clean', CLEAN_CODE);
83
+ await handleRuntimeCompatibilityScan({ workspace: tempWorkspaceDir, json: true });
84
+ expect(process.exitCode).toBeUndefined();
85
+ const parsed = JSON.parse(capturedStdout()) as Record<string, unknown>;
86
+ expect(parsed['status']).toBe('clean');
87
+ expect(parsed['ok']).toBe(true);
88
+ expect(parsed['findings']).toEqual([]);
89
+ });
90
+
91
+ it('SCAN-02: legacy recentThinking rule exits 1 with structured reason + remediation', async () => {
92
+ await seedActiveRule('art-legacy', 'rule-real-diagnosis-first', LEGACY_CODE);
93
+ await handleRuntimeCompatibilityScan({ workspace: tempWorkspaceDir, json: true });
94
+ expect(process.exitCode).toBe(1);
95
+ const parsed = JSON.parse(capturedStdout()) as Record<string, unknown>;
96
+ expect(parsed['ok']).toBe(false);
97
+ expect(parsed['status']).toBe('legacy_dependency');
98
+ expect(parsed['reason']).toBe('legacy_rule_contract_dependency');
99
+ const findings = parsed['findings'] as Array<Record<string, unknown>>;
100
+ expect(findings).toHaveLength(1);
101
+ expect(findings[0]).toMatchObject({ symbol: 'recentThinking', ruleId: 'rule-real-diagnosis-first' });
102
+ const remediation = parsed['remediation'] as string;
103
+ expect(remediation).toContain('rule-real-diagnosis-first');
104
+ expect(remediation).toContain('igrate or deactivate');
105
+ });
106
+
107
+ it('SCAN-03: workspace without state.db exits 0 with status no_state_db', async () => {
108
+ const emptyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-compat-empty-cmd-'));
109
+ try {
110
+ await handleRuntimeCompatibilityScan({ workspace: emptyDir, json: true });
111
+ expect(process.exitCode).toBeUndefined();
112
+ const parsed = JSON.parse(capturedStdout()) as Record<string, unknown>;
113
+ expect(parsed['status']).toBe('no_state_db');
114
+ expect(parsed['ok']).toBe(true);
115
+ // Side-effect-free: the scan must not create a state.db (cli-5).
116
+ expect(fs.existsSync(path.join(emptyDir, '.pd', 'state.db'))).toBe(false);
117
+ } finally {
118
+ fs.rmSync(emptyDir, { recursive: true, force: true });
119
+ }
120
+ });
121
+
122
+ it('SCAN-04: --json stdout is exactly one parseable JSON object (cli-1)', async () => {
123
+ await seedActiveRule('art-clean2', 'rule-clean2', CLEAN_CODE);
124
+ await handleRuntimeCompatibilityScan({ workspace: tempWorkspaceDir, json: true });
125
+ const out = capturedStdout().trim();
126
+ expect(out.startsWith('{')).toBe(true);
127
+ expect(out.endsWith('}')).toBe(true);
128
+ expect(() => JSON.parse(out)).not.toThrow();
129
+ expect((out.match(/\{/g) ?? []).length).toBeGreaterThanOrEqual(1);
130
+ });
131
+
132
+ it('SCAN-05: command is registered on a real Commander program (cli-7)', async () => {
133
+ const { Command } = await import('commander');
134
+ const { registerRuntimeCompatibilityScanCommand } = await import('../../src/commands/runtime-compatibility-scan.js');
135
+ const program = new Command();
136
+ program.name('pd').exitOverride();
137
+ const runtimeCmd = program.command('runtime').description('Runtime inspection and health checks');
138
+ registerRuntimeCompatibilityScanCommand(runtimeCmd);
139
+ const scanCmd = runtimeCmd.commands.find(c => c.name() === 'compatibility-scan');
140
+ expect(scanCmd).toBeDefined();
141
+ expect(scanCmd?.description()).toContain('retired RuleHost contract');
142
+ // Flag wiring: -w/--workspace and --json registered; no mutating flags exist.
143
+ expect(scanCmd?.options.map(o => o.long)).toContain('--workspace');
144
+ expect(scanCmd?.options.map(o => o.long)).toContain('--json');
145
+ });
146
+ });