@principles/pd-cli 1.152.7 → 1.152.8

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 (67) hide show
  1. package/dist/commands/__tests__/runtime-probe-config.test.js +2 -1
  2. package/dist/commands/__tests__/runtime-probe-config.test.js.map +1 -1
  3. package/dist/commands/__tests__/version.test.js +104 -0
  4. package/dist/commands/__tests__/version.test.js.map +1 -1
  5. package/dist/commands/diagnose.d.ts.map +1 -1
  6. package/dist/commands/diagnose.js +3 -6
  7. package/dist/commands/diagnose.js.map +1 -1
  8. package/dist/commands/pain-retry.d.ts.map +1 -1
  9. package/dist/commands/pain-retry.js +3 -6
  10. package/dist/commands/pain-retry.js.map +1 -1
  11. package/dist/commands/runtime-activation.d.ts.map +1 -1
  12. package/dist/commands/runtime-activation.js +9 -3
  13. package/dist/commands/runtime-activation.js.map +1 -1
  14. package/dist/commands/runtime-internalization-run-once.d.ts.map +1 -1
  15. package/dist/commands/runtime-internalization-run-once.js +5 -11
  16. package/dist/commands/runtime-internalization-run-once.js.map +1 -1
  17. package/dist/commands/runtime-internalization-run-rulehost.d.ts +14 -0
  18. package/dist/commands/runtime-internalization-run-rulehost.d.ts.map +1 -1
  19. package/dist/commands/runtime-internalization-run-rulehost.js +67 -15
  20. package/dist/commands/runtime-internalization-run-rulehost.js.map +1 -1
  21. package/dist/index.js +0 -12
  22. package/dist/index.js.map +1 -1
  23. package/dist/services/__tests__/rulehost-readiness.test.js +0 -3
  24. package/dist/services/__tests__/rulehost-readiness.test.js.map +1 -1
  25. package/dist/services/pain-flood-simulation-runner.d.ts.map +1 -1
  26. package/dist/services/pain-flood-simulation-runner.js +0 -4
  27. package/dist/services/pain-flood-simulation-runner.js.map +1 -1
  28. package/dist/services/rulehost-pipeline-runner.d.ts +90 -21
  29. package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
  30. package/dist/services/rulehost-pipeline-runner.js +328 -24
  31. package/dist/services/rulehost-pipeline-runner.js.map +1 -1
  32. package/dist/services/synthetic-baseline-runner.d.ts.map +1 -1
  33. package/dist/services/synthetic-baseline-runner.js +0 -4
  34. package/dist/services/synthetic-baseline-runner.js.map +1 -1
  35. package/dist/services/version-report.d.ts +16 -0
  36. package/dist/services/version-report.d.ts.map +1 -1
  37. package/dist/services/version-report.js +37 -2
  38. package/dist/services/version-report.js.map +1 -1
  39. package/package.json +1 -1
  40. package/src/commands/__tests__/runtime-probe-config.test.ts +2 -1
  41. package/src/commands/__tests__/version.test.ts +110 -0
  42. package/src/commands/diagnose.ts +3 -6
  43. package/src/commands/pain-retry.ts +3 -6
  44. package/src/commands/runtime-activation.ts +9 -3
  45. package/src/commands/runtime-internalization-run-once.ts +5 -11
  46. package/src/commands/runtime-internalization-run-rulehost.ts +69 -15
  47. package/src/index.ts +0 -13
  48. package/src/services/__tests__/rulehost-readiness.test.ts +0 -3
  49. package/src/services/pain-flood-simulation-runner.ts +0 -4
  50. package/src/services/rulehost-pipeline-runner.ts +368 -35
  51. package/src/services/synthetic-baseline-runner.ts +0 -4
  52. package/src/services/version-report.ts +52 -2
  53. package/tests/commands/config-doctor.test.ts +0 -2
  54. package/tests/commands/pain-evidence.test.ts +1 -1
  55. package/tests/commands/run-rulehost-handler.test.ts +43 -0
  56. package/tests/commands/runtime-features.test.ts +0 -1
  57. package/tests/commands/runtime-internalization-run-once-language.test.ts +0 -1
  58. package/tests/e2e/cross-package-acceptance.test.ts +13 -3
  59. package/tests/services/pd-config-loader.test.ts +0 -1
  60. package/tests/services/rulehost-pipeline-runner.test.ts +378 -12
  61. package/dist/commands/runtime-internalization-context-trace.d.ts +0 -25
  62. package/dist/commands/runtime-internalization-context-trace.d.ts.map +0 -1
  63. package/dist/commands/runtime-internalization-context-trace.js +0 -374
  64. package/dist/commands/runtime-internalization-context-trace.js.map +0 -1
  65. package/src/commands/runtime-internalization-context-trace.ts +0 -445
  66. package/tests/bdd/context-trace.steps.ts +0 -229
  67. package/tests/commands/runtime-internalization-context-trace.test.ts +0 -110
@@ -711,3 +711,46 @@ describe('handleRunRuleHost — PR #1122 behavior-examples fail-fast (CodeRabbit
711
711
  expect(stdout).toMatch(/Next: fix the code-rule capability issue above/);
712
712
  });
713
713
  });
714
+
715
+ // ── Owner Decision v1 review fix (Codex P1): identity-gated publication ─────
716
+
717
+ describe('describePublicationOutcome — identity-gated refusal reporting (Codex P1)', () => {
718
+ const baseResult = {
719
+ decision: 'candidate_ready_for_owner_review' as const,
720
+ painId: 'pain-1',
721
+ stages: [],
722
+ scribeTaskId: 'scribe-1',
723
+ ruleArtifactId: 'pi-rule-1' as string | null,
724
+ principleArtifactId: null as string | null,
725
+ approvalId: null as string | null,
726
+ degradationReason: null as string | null,
727
+ };
728
+
729
+ it('flags the identity-blocked state and provides a repair nextAction', async () => {
730
+ const { describePublicationOutcome } = await import('../../src/commands/runtime-internalization-run-rulehost.js');
731
+ const outcome = describePublicationOutcome({
732
+ ...baseResult,
733
+ degradationReason: 'identity_binding_unverified: ledger_missing. Rule artifact NOT enqueued for Owner review (governance publication boundary).',
734
+ });
735
+ expect(outcome.blocked).toBe(true);
736
+ expect(outcome.nextAction).toContain('NOT enqueued');
737
+ expect(outcome.nextAction).toContain('re-run');
738
+ });
739
+
740
+ it('does not flag a healthy ready result with a real approval id', async () => {
741
+ const { describePublicationOutcome } = await import('../../src/commands/runtime-internalization-run-rulehost.js');
742
+ const outcome = describePublicationOutcome({ ...baseResult, approvalId: 'apr-1' });
743
+ expect(outcome.blocked).toBe(false);
744
+ });
745
+
746
+ it('text output for the identity-blocked result never claims WAITING for owner review', async () => {
747
+ const { formatTextOutput } = await import('../../src/commands/runtime-internalization-run-rulehost.js');
748
+ const text = formatTextOutput({
749
+ ...baseResult,
750
+ degradationReason: 'identity_binding_unverified: ledger_missing. Rule artifact NOT enqueued for Owner review (governance publication boundary).',
751
+ });
752
+ expect(text).toContain('PUBLICATION REFUSED');
753
+ expect(text).not.toContain('WAITING for owner review');
754
+ expect(text).toContain('NOT enqueued for Owner review');
755
+ });
756
+ });
@@ -59,7 +59,6 @@ function makeValidConfigYaml(): string {
59
59
  evaluator: { enabled: false },
60
60
  rolloutReviewer: { enabled: false },
61
61
  correctionObserver: { enabled: false },
62
- empathyObserver: { enabled: false },
63
62
  },
64
63
  },
65
64
  ui: { diagnostics: { mode: 'simple' } },
@@ -72,7 +72,6 @@ function makeWorkspace(outputLanguage?: 'en'): string {
72
72
  evaluator: { enabled: false },
73
73
  rolloutReviewer: { enabled: false },
74
74
  correctionObserver: { enabled: false },
75
- empathyObserver: { enabled: false },
76
75
  },
77
76
  },
78
77
  ui: { diagnostics: { mode: 'simple' } },
@@ -56,6 +56,7 @@ import {
56
56
  createPITaskDiagnosticJson,
57
57
  } from '@principles/core/runtime-v2';
58
58
  import type { PIArtifactSnapshot, RuleHostInput } from '@principles/core/runtime-v2';
59
+ import { addPrincipleToLedger } from '@principles/core/principle-tree-ledger';
59
60
 
60
61
  // ── Scripted adapter (same pattern as rulehost-pipeline-runner.test.ts) ──────
61
62
 
@@ -236,11 +237,11 @@ function makeTmpDir(): string {
236
237
  return dir;
237
238
  }
238
239
 
239
- async function seedDreamerWithId(sm: RuntimeStateManager, taskId: string, painId: string): Promise<void> {
240
+ async function seedDreamerWithId(sm: RuntimeStateManager, taskId: string, painId: string, candidateId?: string): Promise<void> {
240
241
  const baseMetadata = JSON.parse(createPITaskDiagnosticJson({
241
242
  dependencyTaskIds: [], channel: 'code_tool_hook', timeoutMs: 1000, inputArtifactRefs: [], outputArtifactRefs: [],
242
243
  })) as Record<string, unknown>;
243
- const diagnosticJson = JSON.stringify({ ...baseMetadata, sourcePainId: painId });
244
+ const diagnosticJson = JSON.stringify({ ...baseMetadata, sourcePainId: painId, ...(candidateId ? { candidateId } : {}) });
244
245
  await sm.createTask({ taskId, taskKind: 'dreamer', status: 'pending', attemptCount: 0, maxAttempts: 3, diagnosticJson });
245
246
  }
246
247
 
@@ -270,8 +271,17 @@ describe('Cross-Package Acceptance Test (PRI-408 P1/P2 fixes) — unsplippable c
270
271
  tmpDir = makeTmpDir();
271
272
  const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
272
273
  await sm.initialize();
273
- await seedDreamerWithId(sm, 'dreamer-xpkg-001', 'pain-xpkg-001');
274
+ // Owner Decision Experience v1 Phase A: the governance publication boundary
275
+ // requires a verified chain identity (candidateId → unique ledger entry),
276
+ // mirroring what the intake bridge creates in production workspaces.
277
+ await seedDreamerWithId(sm, 'dreamer-xpkg-001', 'pain-xpkg-001', 'cand-xpkg-001');
274
278
  await sm.close();
279
+ addPrincipleToLedger(path.join(tmpDir, '.state'), {
280
+ id: 'ledger-xpkg-001', version: 1, text: 'cross-package ledger principle', triggerPattern: '', action: '',
281
+ status: 'candidate', evaluability: 'weak_heuristic', priority: 'P1', scope: 'general',
282
+ valueScore: 0, adherenceRate: 0, painPreventedCount: 0, derivedFromPainIds: ['cand-xpkg-001'],
283
+ ruleIds: [], conflictsWithPrincipleIds: [], createdAt: '2026-09-15T00:00:00.000Z', updatedAt: '2026-09-15T00:00:00.000Z',
284
+ });
275
285
 
276
286
  // ── Step 2: Run RuleHost pipeline → candidate + auto-enqueue (P1 #1) ───
277
287
  const adapter = makeAdapter();
@@ -76,7 +76,6 @@ function makeValidConfigYaml(): string {
76
76
  evaluator: { enabled: false },
77
77
  rolloutReviewer: { enabled: false },
78
78
  correctionObserver: { enabled: false },
79
- empathyObserver: { enabled: false },
80
79
  },
81
80
  },
82
81
  ui: { diagnostics: { mode: 'simple' } },
@@ -16,10 +16,11 @@ import { describe, it, expect, afterEach, vi } from 'vitest';
16
16
  import * as os from 'node:os';
17
17
  import * as path from 'node:path';
18
18
  import * as fs from 'node:fs';
19
- import { runRuleHostPipeline } from '../../src/services/rulehost-pipeline-runner.js';
19
+ import { runRuleHostPipeline, backfillScribeIdentity } from '../../src/services/rulehost-pipeline-runner.js';
20
20
  import type { CodeRuleCapability } from '../../src/services/rulehost-pipeline-runner.js';
21
21
  import type { PDRuntimeAdapter, RunHandle, RunStatus, PIArtifactStore, RuntimeCapabilities, RuntimeHealth, RuntimeArtifactRef, ContextItem, StructuredRunOutput, StartRunInput } from '@principles/core/runtime-v2';
22
- import { RuntimeStateManager, createPITaskDiagnosticJson } from '@principles/core/runtime-v2';
22
+ import { RuntimeStateManager, createPITaskDiagnosticJson, SqliteApprovalQueueStore } from '@principles/core/runtime-v2';
23
+ import { addPrincipleToLedger, updatePrinciple } from '@principles/core/principle-tree-ledger';
23
24
  import { saveHostToolDeclaration } from '@principles/host-runtime';
24
25
 
25
26
  type StageFactory = (taskId: string, priorArtifactId?: string) => unknown;
@@ -248,11 +249,12 @@ async function seedDreamerWithId(
248
249
  taskId: string,
249
250
  painId: string,
250
251
  channel: 'prompt' | 'code_tool_hook' | 'defer_archive' = 'code_tool_hook',
252
+ candidateId?: string,
251
253
  ): Promise<void> {
252
254
  const baseMetadata = JSON.parse(createPITaskDiagnosticJson({
253
255
  dependencyTaskIds: [], channel, timeoutMs: 1000, inputArtifactRefs: [], outputArtifactRefs: [],
254
256
  })) as Record<string, unknown>;
255
- const diagnosticJson = JSON.stringify({ ...baseMetadata, sourcePainId: painId });
257
+ const diagnosticJson = JSON.stringify({ ...baseMetadata, sourcePainId: painId, ...(candidateId ? { candidateId } : {}) });
256
258
  await sm.createTask({ taskId, taskKind: 'dreamer', status: 'pending', attemptCount: 0, maxAttempts: 3, diagnosticJson });
257
259
  }
258
260
 
@@ -270,12 +272,18 @@ describe('runRuleHostPipeline (PRI-429) — atomic capability + exact pain match
270
272
  });
271
273
 
272
274
  // ── Test 1: Capability ON + approved → candidate_ready_for_owner_review ──
273
- it('capability ON + evaluator approved → candidate_ready_for_owner_review', async () => {
275
+ it('capability ON + evaluator approved → candidate_ready_for_owner_review; scribe identity backfilled on the rule path too (EP002-R4 follow-up #2)', async () => {
274
276
  tmpDir = makeTmpDir();
275
277
  const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
276
278
  await sm.initialize();
277
- await seedDreamerWithId(sm, 'dreamer-seeded-001', 'pain-test-001');
279
+ await seedDreamerWithId(sm, 'dreamer-seeded-001', 'pain-test-001', 'code_tool_hook', 'cand-001');
278
280
  await sm.close();
281
+ addPrincipleToLedger(path.join(tmpDir, '.state'), {
282
+ id: 'ledger-principle-001', version: 1, text: 'test ledger principle', triggerPattern: '', action: '',
283
+ status: 'candidate', evaluability: 'weak_heuristic', priority: 'P1', scope: 'general',
284
+ valueScore: 0, adherenceRate: 0, painPreventedCount: 0, derivedFromPainIds: ['cand-001'],
285
+ ruleIds: [], conflictsWithPrincipleIds: [], createdAt: '2026-09-15T00:00:00.000Z', updatedAt: '2026-09-15T00:00:00.000Z',
286
+ });
279
287
 
280
288
  const adapter = makeAdapter();
281
289
  const capability: CodeRuleCapability = { enabled: true, artificerAdapter: adapter };
@@ -292,6 +300,23 @@ describe('runRuleHostPipeline (PRI-429) — atomic capability + exact pain match
292
300
  expect(result.ruleArtifactId).not.toBeNull();
293
301
  // P1 #1 fix: candidate should be auto-enqueued into the ApprovalQueue
294
302
  expect(result.approvalId).not.toBeNull();
303
+
304
+ // EP002-R4 follow-up #2: the scribe artifact carries the LEDGER UUID (not
305
+ // a title) so the evaluator propagates it into the rule artifact and the
306
+ // Console groups the approval by an id the detail page can resolve.
307
+ const verify = new RuntimeStateManager({ workspaceDir: tmpDir });
308
+ await verify.initialize();
309
+ try {
310
+ const scribeArt = await verify.piArtifactStore.getArtifactById(
311
+ (await verify.piArtifactStore.listBySourceTaskId(result.stages.find((s) => s.name === 'scribe')!.taskId!))
312
+ .find((a) => a.artifactKind === 'principle')!.artifactId,
313
+ );
314
+ expect(scribeArt?.sourcePrincipleId).toBe('ledger-principle-001');
315
+ const ruleArt = await verify.piArtifactStore.getArtifactById(result.ruleArtifactId!);
316
+ expect(ruleArt?.sourcePrincipleId).toBe('ledger-principle-001');
317
+ } finally {
318
+ await verify.close();
319
+ }
295
320
  }, 60_000);
296
321
 
297
322
  it('adversarial feedback loop drives a second artificer round before creating a candidate', async () => {
@@ -350,17 +375,29 @@ describe('runRuleHostPipeline (PRI-429) — atomic capability + exact pain match
350
375
  expect(artificerPrompts[1]).toContain('Prior adversarial replay failures');
351
376
  expect(result.decision, JSON.stringify(result)).toBe('candidate_ready_for_owner_review');
352
377
  expect(result.ruleArtifactId).toMatch(/^pi-rule-/);
353
- // P1 #1 fix: candidate should be auto-enqueued into the ApprovalQueue
354
- expect(result.approvalId).not.toBeNull();
378
+ // Owner Decision Experience v1 Phase A: this seed carries no candidateId and
379
+ // no ledger principle, so identity binding is unverified (candidate_unresolved)
380
+ // and the governance publication boundary refuses the approval enqueue — the
381
+ // two-round adversarial loop itself is unaffected.
382
+ expect(result.approvalId).toBeNull();
383
+ expect(result.degradationReason ?? '').toContain('identity_binding_unverified');
355
384
  }, 60_000);
356
385
 
357
386
  // ── Test 2: Capability OFF (explicitly disabled) → text_principle_only ──
358
- it('capability OFF (explicitly disabled) → text_principle_only', async () => {
387
+ it('capability OFF (explicitly disabled) → text_principle_only + prompt approval enqueue + identity backfill (PRI-804)', async () => {
359
388
  tmpDir = makeTmpDir();
360
389
  const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
361
390
  await sm.initialize();
362
- await seedDreamerWithId(sm, 'dreamer-seeded-002', 'pain-test-001');
391
+ await seedDreamerWithId(sm, 'dreamer-seeded-002', 'pain-test-001', 'code_tool_hook', 'cand-002');
363
392
  await sm.close();
393
+ // PRI-804(a): seed the ledger with the principle this chain's candidate maps
394
+ // to, mirroring what the intake bridge creates in production workspaces.
395
+ addPrincipleToLedger(path.join(tmpDir, '.state'), {
396
+ id: 'ledger-principle-002', version: 1, text: 'test ledger principle', triggerPattern: '', action: '',
397
+ status: 'candidate', evaluability: 'weak_heuristic', priority: 'P1', scope: 'general',
398
+ valueScore: 0, adherenceRate: 0, painPreventedCount: 0, derivedFromPainIds: ['cand-002'],
399
+ ruleIds: [], conflictsWithPrincipleIds: [], createdAt: '2026-09-15T00:00:00.000Z', updatedAt: '2026-09-15T00:00:00.000Z',
400
+ });
364
401
 
365
402
  const adapter = makeAdapter();
366
403
  const capability: CodeRuleCapability = { enabled: false, disabledReason: 'artificer agent disabled in config' };
@@ -380,6 +417,29 @@ describe('runRuleHostPipeline (PRI-429) — atomic capability + exact pain match
380
417
  // Adversarial loop stage should be skipped (not present or skipped status).
381
418
  const advStage = result.stages.find((s) => s.name === 'adversarial_loop');
382
419
  expect(advStage?.status).toBe('skipped');
420
+
421
+ // PRI-804: the text principle must NOT dead-end — it is enqueued into the
422
+ // EXISTING approval queue on the prompt channel for Owner Console review.
423
+ expect(result.approvalId).not.toBeNull();
424
+ expect(result.approvalId).toBe(`apr_prompt_${result.principleArtifactId}`);
425
+
426
+ // Verify the persisted approval row, the PRI-804(a) identity backfill, and
427
+ // the terminal validation marking (real store, real SQLite, real ledger).
428
+ const verify = new RuntimeStateManager({ workspaceDir: tmpDir });
429
+ await verify.initialize();
430
+ try {
431
+ const approvalStore = new SqliteApprovalQueueStore(verify.connection);
432
+ const record = await approvalStore.getById(result.approvalId!);
433
+ expect(record).not.toBeNull();
434
+ expect(record!.status).toBe('pending');
435
+ expect(record!.channel).toBe('prompt');
436
+ expect(record!.artifactId).toBe(result.principleArtifactId);
437
+ const artifact = await verify.piArtifactStore.getArtifactById(result.principleArtifactId!);
438
+ expect(artifact?.sourcePrincipleId).toBe('ledger-principle-002');
439
+ expect(artifact?.validationStatus).toBe('validated');
440
+ } finally {
441
+ await verify.close();
442
+ }
383
443
  }, 60_000);
384
444
 
385
445
  // ── Test 3: Capability ON + evaluator rejected → generation_rejected ──
@@ -626,8 +686,14 @@ describe('runRuleHostPipeline (PRI-429) — atomic capability + exact pain match
626
686
  expect(dreamerCallCount).toBe(2);
627
687
  expect(dreamerStage?.status).toBe('succeeded');
628
688
  expect(result.decision).toBe('candidate_ready_for_owner_review');
629
- // P1 #1 fix: candidate should be auto-enqueued into the ApprovalQueue
630
- expect(result.approvalId).not.toBeNull();
689
+ // Owner Decision Experience v1 Phase A: this seed carries NO candidateId,
690
+ // so the chain cannot bind a ledger principle (candidate_unresolved). The
691
+ // rule artifact is produced and preserved, but the governance publication
692
+ // boundary REFUSES to create an Owner approval subject for it.
693
+ expect(result.approvalId).toBeNull();
694
+ expect(result.degradationReason ?? '').toContain('identity_binding_unverified');
695
+ expect(result.degradationReason ?? '').toContain('candidate_unresolved');
696
+ expect(result.ruleArtifactId).not.toBeNull();
631
697
  }, 60_000);
632
698
 
633
699
  // ── Test 9 (E fix): retried status exhausted → stage marked 'degraded' ──
@@ -705,7 +771,6 @@ function writeLanguageConfig(dir: string, outputLanguage?: 'en'): void {
705
771
  evaluator: { enabled: false },
706
772
  rolloutReviewer: { enabled: false },
707
773
  correctionObserver: { enabled: false },
708
- empathyObserver: { enabled: false },
709
774
  },
710
775
  },
711
776
  ui: { diagnostics: { mode: 'simple' } },
@@ -807,3 +872,304 @@ describe('runRuleHostPipeline — outputLanguage reaches adapter.startRun messag
807
872
  expect(dreamer?.payload).toContain('dreamer-lang-zh-001');
808
873
  }, 60_000);
809
874
  });
875
+
876
+ // ── Owner Decision Experience v1 Phase A — identity fix-forward (SPEC §9) ────
877
+ //
878
+ // §9.4 deterministic acceptance slice, exercised against the REAL production
879
+ // path (real RuntimeStateManager, real SQLite, real ledger adapter):
880
+ // success / ledger missing / ambiguous / conflict / write failure / replay.
881
+ // Failure slices must prove: NO approval enqueue, NO activation, NO duplicate
882
+ // ledger — while the generated artifacts stay preserved for later replay.
883
+
884
+ describe('runRuleHostPipeline — Phase A governance publication boundary (identity fix-forward)', () => {
885
+ afterEach(() => {
886
+ vi.restoreAllMocks();
887
+ if (tmpDir) { try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ } tmpDir = ''; }
888
+ });
889
+
890
+ function seedLedger(dir: string, id: string, candidateId: string): void {
891
+ addPrincipleToLedger(path.join(dir, '.state'), {
892
+ id, version: 1, text: `principle ${id}`, triggerPattern: '', action: '',
893
+ status: 'candidate', evaluability: 'weak_heuristic', priority: 'P1', scope: 'general',
894
+ valueScore: 0, adherenceRate: 0, painPreventedCount: 0, derivedFromPainIds: [candidateId],
895
+ ruleIds: [], conflictsWithPrincipleIds: [], createdAt: '2026-09-15T00:00:00.000Z', updatedAt: '2026-09-15T00:00:00.000Z',
896
+ });
897
+ }
898
+
899
+ async function countApprovalsForArtifact(dir: string, artifactId: string): Promise<number> {
900
+ const verify = new RuntimeStateManager({ workspaceDir: dir });
901
+ await verify.initialize();
902
+ try {
903
+ const rows = verify.connection.getDb()
904
+ .prepare('SELECT COUNT(*) AS n FROM approvals WHERE artifact_id = ?').all(artifactId);
905
+ const first = rows[0] as { n: number } | undefined;
906
+ return first?.n ?? 0;
907
+ } finally {
908
+ await verify.close();
909
+ }
910
+ }
911
+
912
+ async function countActivations(dir: string): Promise<number> {
913
+ const verify = new RuntimeStateManager({ workspaceDir: dir });
914
+ await verify.initialize();
915
+ try {
916
+ const rows = verify.connection.getDb().prepare('SELECT COUNT(*) AS n FROM activations').all();
917
+ const first = rows[0] as { n: number } | undefined;
918
+ return first?.n ?? 0;
919
+ } finally {
920
+ await verify.close();
921
+ }
922
+ }
923
+
924
+ async function ledgerPrincipleCount(dir: string): Promise<number> {
925
+ const ledgerPath = path.join(dir, '.state', 'principle_training_state.json');
926
+ if (!fs.existsSync(ledgerPath)) return 0; // no ledger file yet = zero principles
927
+ const parsed = JSON.parse(fs.readFileSync(ledgerPath, 'utf8')) as { _tree?: { principles?: Record<string, unknown> } };
928
+ return Object.keys(parsed._tree?.principles ?? {}).length;
929
+ }
930
+
931
+ it('ledger_missing: rule artifact preserved, but NO approval subject, NO activation, ledger untouched', async () => {
932
+ tmpDir = makeTmpDir();
933
+ const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
934
+ await sm.initialize();
935
+ await seedDreamerWithId(sm, 'dreamer-id-missing', 'pain-id-missing', 'code_tool_hook', 'cand-no-ledger');
936
+ await sm.close();
937
+ // Deliberately NO ledger principle for cand-no-ledger.
938
+
939
+ const adapter = makeAdapter();
940
+ const result = await runRuleHostPipeline({
941
+ workspaceDir: tmpDir, painId: 'pain-id-missing', runtimeAdapter: adapter,
942
+ channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
943
+ codeRuleCapability: { enabled: true, artificerAdapter: adapter },
944
+ behaviorExamplePack: PIPE_PACK,
945
+ onStoreReady: (store) => { adapter.artifactStore = store; },
946
+ });
947
+
948
+ expect(result.decision, JSON.stringify(result)).toBe('candidate_ready_for_owner_review');
949
+ // The generation chain produced and PRESERVED its artifacts…
950
+ expect(result.ruleArtifactId).not.toBeNull();
951
+ // …but the publication boundary refused to create an Owner approval subject.
952
+ expect(result.approvalId).toBeNull();
953
+ expect(result.degradationReason ?? '').toContain('identity_binding_unverified');
954
+ expect(result.degradationReason ?? '').toContain('ledger_missing');
955
+ expect(await countApprovalsForArtifact(tmpDir, result.ruleArtifactId!)).toBe(0);
956
+ expect(await countActivations(tmpDir)).toBe(0);
957
+ expect(await ledgerPrincipleCount(tmpDir)).toBe(0);
958
+ }, 60_000);
959
+
960
+ it('ledger_ambiguous: two ledger targets for one candidate refuse publication and refuse a silent first-match binding', async () => {
961
+ tmpDir = makeTmpDir();
962
+ const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
963
+ await sm.initialize();
964
+ await seedDreamerWithId(sm, 'dreamer-id-ambiguous', 'pain-id-ambiguous', 'code_tool_hook', 'cand-ambiguous');
965
+ await sm.close();
966
+ seedLedger(tmpDir, 'ledger-A', 'cand-ambiguous');
967
+ seedLedger(tmpDir, 'ledger-B', 'cand-ambiguous');
968
+
969
+ const adapter = makeAdapter();
970
+ const result = await runRuleHostPipeline({
971
+ workspaceDir: tmpDir, painId: 'pain-id-ambiguous', runtimeAdapter: adapter,
972
+ channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
973
+ codeRuleCapability: { enabled: true, artificerAdapter: adapter },
974
+ behaviorExamplePack: PIPE_PACK,
975
+ onStoreReady: (store) => { adapter.artifactStore = store; },
976
+ });
977
+
978
+ expect(result.decision).toBe('candidate_ready_for_owner_review');
979
+ expect(result.approvalId).toBeNull();
980
+ expect(result.degradationReason ?? '').toContain('ledger_ambiguous');
981
+ expect(await countApprovalsForArtifact(tmpDir, result.ruleArtifactId!)).toBe(0);
982
+ // The scribe artifact must NOT be silently bound to the first match.
983
+ const verify = new RuntimeStateManager({ workspaceDir: tmpDir });
984
+ await verify.initialize();
985
+ try {
986
+ const scribeTaskId = result.stages.find((s) => s.name === 'scribe')!.taskId!;
987
+ const scribeArt = (await verify.piArtifactStore.listBySourceTaskId(scribeTaskId)).find((a) => a.artifactKind === 'principle');
988
+ expect(scribeArt?.sourcePrincipleId).toBeUndefined();
989
+ } finally {
990
+ await verify.close();
991
+ }
992
+ }, 60_000);
993
+
994
+ it('identity_conflict: an existing binding to a different ledger target is never overwritten (§9.1.7)', async () => {
995
+ tmpDir = makeTmpDir();
996
+ const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
997
+ await sm.initialize();
998
+ await seedDreamerWithId(sm, 'dreamer-id-conflict', 'pain-id-conflict', 'code_tool_hook', 'cand-conflict');
999
+ await sm.close();
1000
+ seedLedger(tmpDir, 'ledger-original', 'cand-conflict');
1001
+
1002
+ const adapter = makeAdapter();
1003
+ const first = await runRuleHostPipeline({
1004
+ workspaceDir: tmpDir, painId: 'pain-id-conflict', runtimeAdapter: adapter,
1005
+ channel: 'code_tool_hook', pollIntervalMs: 5, timeoutMs: 1000,
1006
+ codeRuleCapability: { enabled: true, artificerAdapter: adapter },
1007
+ behaviorExamplePack: PIPE_PACK,
1008
+ onStoreReady: (store) => { adapter.artifactStore = store; },
1009
+ });
1010
+ expect(first.approvalId).not.toBeNull();
1011
+
1012
+ // Repoint the candidate: detach it from the original principle so the
1013
+ // chain now resolves to a DIFFERENT (still unique) target. Otherwise the
1014
+ // plural lookup would correctly report ledger_ambiguous, not a conflict.
1015
+ updatePrinciple(path.join(tmpDir, '.state'), 'ledger-original', { derivedFromPainIds: [] });
1016
+ seedLedger(tmpDir, 'ledger-replacement', 'cand-conflict');
1017
+ const verify = new RuntimeStateManager({ workspaceDir: tmpDir });
1018
+ await verify.initialize();
1019
+ try {
1020
+ const scribeTaskId = first.stages.find((s) => s.name === 'scribe')!.taskId!;
1021
+ const binding = await backfillScribeIdentity({
1022
+ stateManager: verify,
1023
+ artifactStore: verify.piArtifactStore,
1024
+ scribeTaskId,
1025
+ dreamerTaskId: 'dreamer-id-conflict',
1026
+ workspaceDir: tmpDir,
1027
+ now: new Date().toISOString(),
1028
+ });
1029
+ expect(binding).toEqual({ status: 'identity_conflict', existingPrincipleId: 'ledger-original', resolvedPrincipleId: 'ledger-replacement' });
1030
+ const scribeArt = (await verify.piArtifactStore.listBySourceTaskId(scribeTaskId)).find((a) => a.artifactKind === 'principle');
1031
+ expect(scribeArt?.sourcePrincipleId).toBe('ledger-original');
1032
+ } finally {
1033
+ await verify.close();
1034
+ }
1035
+ }, 60_000);
1036
+
1037
+ it('binding_write_failed / binding_readback_failed: store failures are structured, never silently accepted', async () => {
1038
+ tmpDir = makeTmpDir();
1039
+ const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
1040
+ await sm.initialize();
1041
+ await seedDreamerWithId(sm, 'dreamer-id-wf', 'pain-id-wf', 'code_tool_hook', 'cand-wf');
1042
+ await sm.close();
1043
+ seedLedger(tmpDir, 'ledger-wf', 'cand-wf');
1044
+
1045
+ // Run once through the real path so a REAL scribe artifact exists.
1046
+ const adapter = makeAdapter();
1047
+ const run = await runRuleHostPipeline({
1048
+ workspaceDir: tmpDir, painId: 'pain-id-wf', runtimeAdapter: adapter,
1049
+ channel: 'prompt', pollIntervalMs: 5, timeoutMs: 1000,
1050
+ onStoreReady: (store) => { adapter.artifactStore = store; },
1051
+ });
1052
+ expect(run.decision).toBe('text_principle_only');
1053
+
1054
+ const verify = new RuntimeStateManager({ workspaceDir: tmpDir });
1055
+ await verify.initialize();
1056
+ const scribeTaskId = run.stages.find((s) => s.name === 'scribe')!.taskId!;
1057
+ try {
1058
+ // The real run already bound + verified — replay must be idempotent.
1059
+ const idempotent = await backfillScribeIdentity({
1060
+ stateManager: verify, artifactStore: verify.piArtifactStore, scribeTaskId,
1061
+ dreamerTaskId: 'dreamer-id-wf', workspaceDir: tmpDir, now: new Date().toISOString(),
1062
+ });
1063
+ expect(idempotent).toEqual({ status: 'already_bound', principleId: 'ledger-wf' });
1064
+
1065
+ // Write failure: upsert throws.
1066
+ const throwingStore = new Proxy(verify.piArtifactStore, {
1067
+ get(target, prop, receiver) {
1068
+ if (prop === 'upsertArtifact') {
1069
+ return async () => { throw new Error('sqlite locked (simulated)'); };
1070
+ }
1071
+ return Reflect.get(target, prop, receiver);
1072
+ },
1073
+ });
1074
+ // For the write-failure slice we need an UNBOUND artifact: use a second,
1075
+ // unbound scribe-shaped artifact created directly in the store.
1076
+ const taskId2 = 'scribe-unbound-task';
1077
+ await verify.createTask({ taskId: taskId2, taskKind: 'scribe', status: 'succeeded', attemptCount: 0, maxAttempts: 3, diagnosticJson: JSON.stringify({ pi_metadata: { channel: 'prompt', dependencyTaskIds: [], timeoutMs: 1000 } }) });
1078
+ await verify.piArtifactStore.createArtifact({
1079
+ artifactId: 'pi-art-unbound-1', artifactKind: 'principle', sourceTaskId: taskId2,
1080
+ lineageArtifactIds: [], validationStatus: 'pending', contentJson: '{}',
1081
+ createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
1082
+ });
1083
+ const failed = await backfillScribeIdentity({
1084
+ stateManager: verify, artifactStore: throwingStore, scribeTaskId: taskId2,
1085
+ dreamerTaskId: 'dreamer-id-wf', workspaceDir: tmpDir, now: new Date().toISOString(),
1086
+ });
1087
+ expect(failed.status).toBe('binding_write_failed');
1088
+
1089
+ // Read-back failure: upsert "succeeds" but the store returns a row without
1090
+ // the binding (write lost / wrong row).
1091
+ const staleStore = new Proxy(verify.piArtifactStore, {
1092
+ get(target, prop, receiver) {
1093
+ if (prop === 'getArtifactById') {
1094
+ return async () => null;
1095
+ }
1096
+ return Reflect.get(target, prop, receiver);
1097
+ },
1098
+ });
1099
+ const stale = await backfillScribeIdentity({
1100
+ stateManager: verify, artifactStore: staleStore, scribeTaskId: taskId2,
1101
+ dreamerTaskId: 'dreamer-id-wf', workspaceDir: tmpDir, now: new Date().toISOString(),
1102
+ });
1103
+ expect(stale.status).toBe('binding_readback_failed');
1104
+ } finally {
1105
+ await verify.close();
1106
+ }
1107
+ }, 60_000);
1108
+
1109
+ it('text path: unverified binding preserves the principle artifact but creates NO approval (candidate_unresolved)', async () => {
1110
+ tmpDir = makeTmpDir();
1111
+ const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
1112
+ await sm.initialize();
1113
+ // No candidateId on the seed — candidate_unresolved.
1114
+ await seedDreamerWithId(sm, 'dreamer-text-noid', 'pain-text-noid', 'prompt');
1115
+ await sm.close();
1116
+
1117
+ const adapter = makeAdapter();
1118
+ const result = await runRuleHostPipeline({
1119
+ workspaceDir: tmpDir, painId: 'pain-text-noid', runtimeAdapter: adapter,
1120
+ channel: 'prompt', pollIntervalMs: 5, timeoutMs: 1000,
1121
+ onStoreReady: (store) => { adapter.artifactStore = store; },
1122
+ });
1123
+
1124
+ expect(result.decision).toBe('text_principle_only');
1125
+ expect(result.principleArtifactId).not.toBeNull();
1126
+ expect(result.approvalId).toBeNull();
1127
+ expect(result.degradationReason ?? '').toContain('identity_binding_unverified');
1128
+ expect(result.degradationReason ?? '').toContain('candidate_unresolved');
1129
+ expect(await countApprovalsForArtifact(tmpDir, result.principleArtifactId!)).toBe(0);
1130
+ // The artifact is still marked validated — preserved, publishable later.
1131
+ const verify = new RuntimeStateManager({ workspaceDir: tmpDir });
1132
+ await verify.initialize();
1133
+ try {
1134
+ const artifact = await verify.piArtifactStore.getArtifactById(result.principleArtifactId!);
1135
+ expect(artifact?.validationStatus).toBe('validated');
1136
+ expect(artifact?.sourcePrincipleId).toBeUndefined();
1137
+ } finally {
1138
+ await verify.close();
1139
+ }
1140
+ }, 60_000);
1141
+
1142
+ it('replay after identity repair publishes: seed ledger mid-flight is unnecessary — repair-then-rerun enqueues (§9.1.8)', async () => {
1143
+ tmpDir = makeTmpDir();
1144
+ const sm = new RuntimeStateManager({ workspaceDir: tmpDir });
1145
+ await sm.initialize();
1146
+ await seedDreamerWithId(sm, 'dreamer-repair', 'pain-repair', 'prompt', 'cand-repair');
1147
+ await sm.close();
1148
+
1149
+ const adapter1 = makeAdapter();
1150
+ const refused = await runRuleHostPipeline({
1151
+ workspaceDir: tmpDir, painId: 'pain-repair', runtimeAdapter: adapter1,
1152
+ channel: 'prompt', pollIntervalMs: 5, timeoutMs: 1000,
1153
+ onStoreReady: (store) => { adapter1.artifactStore = store; },
1154
+ });
1155
+ expect(refused.approvalId).toBeNull();
1156
+
1157
+ // Identity repair: the ledger entry for the candidate appears (e.g. intake
1158
+ // completed or the ledger was restored). Replay publishes.
1159
+ seedLedger(tmpDir, 'ledger-repaired', 'cand-repair');
1160
+ const adapter2 = makeAdapter();
1161
+ // Run 2 RESUMES run 1's succeeded dreamer seed (no new dreamer startRun) —
1162
+ // pre-register that call so the scripted philosopher can resolve its
1163
+ // sourceDreamerArtifactId lineage from the shared store.
1164
+ adapter2.startRunCalls.push({ taskId: 'dreamer-repair' });
1165
+ const published = await runRuleHostPipeline({
1166
+ workspaceDir: tmpDir, painId: 'pain-repair', runtimeAdapter: adapter2,
1167
+ channel: 'prompt', pollIntervalMs: 5, timeoutMs: 1000,
1168
+ onStoreReady: (store) => { adapter2.artifactStore = store; },
1169
+ });
1170
+ expect(published.approvalId, JSON.stringify(published, null, 1)).not.toBeNull();
1171
+ expect(published.principleArtifactId).not.toBe(refused.principleArtifactId);
1172
+ // Exactly one ledger principle exists — the pipeline never creates one.
1173
+ expect(await ledgerPrincipleCount(tmpDir)).toBe(1);
1174
+ }, 60_000);
1175
+ });
@@ -1,25 +0,0 @@
1
- /**
2
- * Layer 3 — read-only context-trace CLI command (design §6.7, PR 5 task 11.1).
3
- *
4
- * `pd runtime internalization context-trace --task <taskId> [--artifact <artifactId>] [--json]`
5
- *
6
- * Produces three-segment diagnosis (pain→dreamer / dreamer→scribe / scribe→artificer),
7
- * per-hop summary presence/freshness, budget truncation records, and structured
8
- * degradation reasons — the single Owner-visible exit point for all internal
9
- * signals from Layers 0/1/2.
10
- *
11
- * CLI gates (cli-1..cli-7):
12
- * - --json emits exactly one parseable JSON object on stdout (cli-1)
13
- * - uses process.exitCode (not process.exit) so stdout isn't truncated (cli-2)
14
- * - no --dry-run / --confirm (command is inherently read-only, cli-4 N/A)
15
- * - zero state writes: no DB/ledger/artifact/enqueue/successor mutations (cli-5)
16
- * - every degraded/refused output includes structured reason + nextAction (cli-6)
17
- */
18
- export interface ContextTraceOptions {
19
- workspace?: string;
20
- task?: string;
21
- artifact?: string;
22
- json?: boolean;
23
- }
24
- export declare function handleRuntimeInternalizationContextTrace(opts: ContextTraceOptions): Promise<void>;
25
- //# sourceMappingURL=runtime-internalization-context-trace.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"runtime-internalization-context-trace.d.ts","sourceRoot":"","sources":["../../src/commands/runtime-internalization-context-trace.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAeH,MAAM,WAAW,mBAAmB;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAuLD,wBAAsB,wCAAwC,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiOvG"}