@principles/pd-cli 1.109.3 → 1.110.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 (37) hide show
  1. package/dist/commands/candidate.js +1 -2
  2. package/dist/commands/candidate.js.map +1 -1
  3. package/dist/commands/diagnose.js +1 -2
  4. package/dist/commands/diagnose.js.map +1 -1
  5. package/dist/commands/pain-retry.js +1 -2
  6. package/dist/commands/pain-retry.js.map +1 -1
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.js +20 -31
  9. package/dist/index.js.map +1 -1
  10. package/dist/legacy/session-history-import.js +0 -19
  11. package/dist/legacy/session-history-import.js.map +1 -1
  12. package/dist/services/mainline-snapshot-assembler.d.ts.map +1 -1
  13. package/dist/services/mainline-snapshot-assembler.js +13 -2
  14. package/dist/services/mainline-snapshot-assembler.js.map +1 -1
  15. package/dist/utils/production-workspace-guard.d.ts +0 -7
  16. package/dist/utils/production-workspace-guard.d.ts.map +1 -1
  17. package/dist/utils/production-workspace-guard.js +0 -11
  18. package/dist/utils/production-workspace-guard.js.map +1 -1
  19. package/package.json +1 -1
  20. package/src/commands/candidate.ts +1 -1
  21. package/src/commands/diagnose.ts +1 -1
  22. package/src/commands/pain-retry.ts +1 -1
  23. package/src/index.ts +20 -32
  24. package/src/legacy/session-history-import.ts +0 -28
  25. package/src/services/mainline-snapshot-assembler.ts +17 -2
  26. package/src/utils/production-workspace-guard.ts +0 -12
  27. package/tests/commands/candidate-audit-repair.test.ts +0 -3
  28. package/tests/commands/candidate-intake.test.ts +0 -3
  29. package/tests/commands/candidate-internalization-backfill.test.ts +6 -9
  30. package/tests/commands/diagnose.test.ts +1 -4
  31. package/tests/commands/pain-retry.test.ts +1 -4
  32. package/tests/services/mainline-snapshot-assembler.test.ts +147 -3
  33. package/dist/principle-tree-ledger-adapter.d.ts +0 -12
  34. package/dist/principle-tree-ledger-adapter.d.ts.map +0 -1
  35. package/dist/principle-tree-ledger-adapter.js +0 -12
  36. package/dist/principle-tree-ledger-adapter.js.map +0 -1
  37. package/src/principle-tree-ledger-adapter.ts +0 -13
@@ -235,15 +235,20 @@ async function findDiagnosticianArtifact(
235
235
  ): Promise<DiagnosticianArtifactSnapshot | null> {
236
236
  const { stateManager, diagnosisTaskId, painId, warnings } = input;
237
237
  const db = stateManager.connection.getDb();
238
+
239
+ // PRI-411: Split pipeline stores the diagnostician artifact under the
240
+ // diag_router child task (e.g. "diag_router-diagnosis_xxx"), not the
241
+ // parent "diagnosis_xxx". Fallback to diag_router-{parentTaskId} when
242
+ // the parent lookup returns no rows.
238
243
  const rows = db
239
244
  .prepare(
240
245
  `SELECT artifact_id, run_id, task_id, artifact_kind, content_json
241
246
  FROM artifacts
242
- WHERE task_id = ? AND artifact_kind = 'diagnostician_output'
247
+ WHERE (task_id = ? OR task_id = 'diag_router-' || ?) AND artifact_kind = 'diagnostician_output'
243
248
  ORDER BY created_at DESC
244
249
  LIMIT 1`,
245
250
  )
246
- .all(diagnosisTaskId);
251
+ .all(diagnosisTaskId, diagnosisTaskId);
247
252
 
248
253
  const [raw] = rows;
249
254
  if (!raw) return null;
@@ -254,6 +259,8 @@ async function findDiagnosticianArtifact(
254
259
  return null;
255
260
  }
256
261
 
262
+ // EP-07: If artifact was found via diag_router fallback, verify lineage
263
+ // consistency — the artifact's sourcePainId must match the parent pain.
257
264
  let sourcePainId: string | null = painId;
258
265
  const parsed = safeJsonParse(row.contentJson);
259
266
  if (parsed.ok && isObject(parsed.value)) {
@@ -265,6 +272,14 @@ async function findDiagnosticianArtifact(
265
272
  warnings.push(`Artifact ${row.artifactId} content_json parse failed: ${parsed.reason}`);
266
273
  }
267
274
 
275
+ // EP-07 mismatch guard: warn when artifact lineage doesn't match parent pain
276
+ if (painId && sourcePainId && sourcePainId !== painId) {
277
+ warnings.push(
278
+ `Artifact ${row.artifactId} (task ${row.taskId}) sourcePainId "${sourcePainId}" ` +
279
+ `mismatches parent painId "${painId}" — lineage may be broken`,
280
+ );
281
+ }
282
+
268
283
  return {
269
284
  artifactId: row.artifactId,
270
285
  sourcePainId,
@@ -173,18 +173,6 @@ export function getSafeUatWorkspacePath(): string {
173
173
  return uatWorkspace;
174
174
  }
175
175
 
176
- /**
177
- * Check if --allow-production-workspace-for-uat flag is set.
178
- *
179
- * This is the escape hatch for cases where the operator explicitly wants to run UAT on production.
180
- * The flag must be very explicit in both name and output (as required by PRI-334).
181
- */
182
- export function isProductionWorkspaceAllowed(): boolean {
183
- // Check if the flag was parsed and passed through opts
184
- // This will be called from command handlers after Commander parses flags
185
- return false; // Placeholder; actual check depends on Commander opts
186
- }
187
-
188
176
  /**
189
177
  * Format guard refusal for console output.
190
178
  *
@@ -97,9 +97,6 @@ vi.mock('@principles/core/runtime-v2', () => ({
97
97
  loadLedger: mockLoadLedger,
98
98
  getLedgerFilePathPublic: mockGetLedgerFilePath,
99
99
  resolveOutputLanguage: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
100
- }));
101
-
102
- vi.mock('../../src/principle-tree-ledger-adapter.js', () => ({
103
100
  PrincipleTreeLedgerAdapter: MockPrincipleTreeLedgerAdapter,
104
101
  }));
105
102
 
@@ -71,9 +71,6 @@ vi.mock('@principles/core/runtime-v2', () => ({
71
71
  CandidateIntakeError: MockCandidateIntakeError,
72
72
  RuntimeStateManager: MockRuntimeStateManager,
73
73
  resolveOutputLanguage: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
74
- }));
75
-
76
- vi.mock('../../src/principle-tree-ledger-adapter.js', () => ({
77
74
  PrincipleTreeLedgerAdapter: MockPrincipleTreeLedgerAdapter,
78
75
  }));
79
76
 
@@ -17,19 +17,16 @@ vi.mock('../../src/resolve-workspace.js', () => ({
17
17
  resolveWorkspaceDir: vi.fn((workspace?: string) => workspace ?? '/fake/workspace'),
18
18
  }));
19
19
 
20
- vi.mock('../../src/principle-tree-ledger-adapter.js', () => ({
21
- PrincipleTreeLedgerAdapter: vi.fn().mockImplementation(function () {
22
- return {
23
- intake: mockIntake,
24
- existsForCandidate: mockExistsForCandidate,
25
- };
26
- }),
27
- }));
28
-
29
20
  vi.mock('@principles/core/runtime-v2', async (importOriginal) => {
30
21
  const original = await importOriginal() as Record<string, unknown>;
31
22
  return {
32
23
  ...original,
24
+ PrincipleTreeLedgerAdapter: vi.fn().mockImplementation(function () {
25
+ return {
26
+ intake: mockIntake,
27
+ existsForCandidate: mockExistsForCandidate,
28
+ };
29
+ }),
33
30
  RuntimeStateManager: vi.fn().mockImplementation(function (opts: Record<string, unknown>) {
34
31
  mockRuntimeStateManagerOpts(opts);
35
32
  return {
@@ -123,13 +123,10 @@ vi.mock('@principles/core/runtime-v2', () => {
123
123
  },
124
124
  }),
125
125
  status: vi.fn(),
126
+ PrincipleTreeLedgerAdapter: MockPrincipleTreeLedgerAdapter,
126
127
  };
127
128
  });
128
129
 
129
- vi.mock('../../src/principle-tree-ledger-adapter.js', () => ({
130
- PrincipleTreeLedgerAdapter: MockPrincipleTreeLedgerAdapter,
131
- }));
132
-
133
130
  vi.mock('../../src/config-reader.js', () => ({
134
131
  readOutputLanguageFromWorkspace: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
135
132
  }));
@@ -130,13 +130,10 @@ vi.mock('@principles/core/runtime-v2', () => {
130
130
  redactPdConfig: vi.fn().mockImplementation((c) => c),
131
131
  run: mockRun,
132
132
  status: vi.fn(),
133
+ PrincipleTreeLedgerAdapter: MockPrincipleTreeLedgerAdapter,
133
134
  };
134
135
  });
135
136
 
136
- vi.mock('../../src/principle-tree-ledger-adapter.js', () => ({
137
- PrincipleTreeLedgerAdapter: MockPrincipleTreeLedgerAdapter,
138
- }));
139
-
140
137
  vi.mock('../../src/config-reader.js', () => ({
141
138
  readOutputLanguageFromWorkspace: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
142
139
  }));
@@ -216,17 +216,20 @@ describe('assembleMainlineSnapshot', () => {
216
216
  rmTmpDir(workspaceDir);
217
217
  });
218
218
 
219
- it('returns a snapshot with degraded readiness when no readiness snapshot is provided', async () => {
219
+ it('returns a snapshot with default readiness when no readiness snapshot is provided', async () => {
220
220
  await sm.initialize();
221
221
  const painId = 'pain-empty';
222
222
  await seedDiagnosisTask(sm, painId);
223
223
 
224
224
  const { snapshot, warnings } = await assembleMainlineSnapshot({ workspaceDir, painId });
225
225
 
226
- expect(snapshot.readiness.diagnosticianReady).toBe(false);
226
+ // Default config resolves a diagnostician profile, so readiness is "configured"
227
+ // (actual connectivity is unknown without probe, but config alignment is satisfied)
228
+ expect(snapshot.readiness.diagnosticianReady).toBe(true);
227
229
  expect(warnings.length).toBe(0);
228
230
  const verdict = assertMainlineContract(snapshot);
229
- expect(verdict.stages.some((s) => s.stage === 'diagnostician_readiness' && s.status === 'violation')).toBe(true);
231
+ // diagnosis_task should be a violation since the task has no succeeded run
232
+ expect(verdict.stages.some((s) => s.stage === 'diagnosis_task' && s.status === 'violation')).toBe(true);
230
233
  });
231
234
 
232
235
  it('malformed artifact content_json does not crash; contract reports violation with reason + nextAction', async () => {
@@ -422,4 +425,145 @@ describe('assembleMainlineSnapshot', () => {
422
425
 
423
426
  expect(resolvedPainId).toBe(painId);
424
427
  });
428
+
429
+ // ── PRI-411: Split pipeline artifact fallback ─────────────────────────────
430
+
431
+ it('finds diagnostician artifact stored under diag_router child task (split pipeline layout)', async () => {
432
+ await sm.initialize();
433
+ const painId = 'pain-split-pipeline';
434
+ const parentTaskId = `diagnosis_${painId}`;
435
+ const routerTaskId = `diag_router-${parentTaskId}`;
436
+
437
+ // Create the parent diagnosis task (split pipeline parent)
438
+ await sm.createTask({
439
+ taskId: parentTaskId,
440
+ taskKind: 'diagnostician',
441
+ inputRef: painId,
442
+ status: 'pending',
443
+ attemptCount: 0,
444
+ maxAttempts: 3,
445
+ diagnosticJson: diagnosticianDiagnosticJson(painId),
446
+ });
447
+
448
+ // Create the diag_router child task
449
+ await sm.createTask({
450
+ taskId: routerTaskId,
451
+ taskKind: 'diag_router',
452
+ inputRef: painId,
453
+ status: 'pending',
454
+ attemptCount: 0,
455
+ maxAttempts: 3,
456
+ diagnosticJson: diagnosticianDiagnosticJson(painId),
457
+ });
458
+
459
+ // Commit the artifact under the diag_router child task (not the parent)
460
+ const output = validDiagnosticianOutput(painId);
461
+ const committer = new SqliteDiagnosticianCommitter(sm.connection);
462
+ await sm.acquireLease({ taskId: routerTaskId, owner: 'test-owner', durationMs: 60_000, runtimeKind: 'openclaw' });
463
+ const runs = await sm.getRunsByTask(routerTaskId);
464
+ const runId = runs[0]?.runId;
465
+ if (!runId) throw new Error(`No run created for task ${routerTaskId}`);
466
+ const commitResult = await committer.commit({
467
+ runId,
468
+ taskId: routerTaskId,
469
+ output,
470
+ idempotencyKey: `idem-${routerTaskId}`,
471
+ });
472
+ await sm.markTaskSucceeded(routerTaskId, `artifact://${commitResult.artifactId}`);
473
+ // Also mark parent as succeeded
474
+ await sm.markTaskSucceeded(parentTaskId, `artifact://${commitResult.artifactId}`);
475
+
476
+ const { snapshot, warnings } = await assembleMainlineSnapshot({ workspaceDir, painId, readiness: healthyReadiness() });
477
+
478
+ // PRI-411: fallback should find the artifact under diag_router-*
479
+ expect(snapshot.chain.diagnosticianArtifact).not.toBeNull();
480
+ expect(snapshot.chain.diagnosticianArtifact?.artifactId).toBe(commitResult.artifactId);
481
+ expect(snapshot.chain.diagnosticianArtifact?.sourcePainId).toBe(painId);
482
+ // No mismatch warning since lineage is consistent
483
+ expect(warnings.some((w) => w.includes('mismatches'))).toBe(false);
484
+ });
485
+
486
+ it('split pipeline artifact with mismatched sourcePainId emits lineage warning (EP-07)', async () => {
487
+ await sm.initialize();
488
+ const painId = 'pain-split-mismatch';
489
+ const wrongPainId = 'pain-wrong-lineage';
490
+ const parentTaskId = `diagnosis_${painId}`;
491
+ const routerTaskId = `diag_router-${parentTaskId}`;
492
+
493
+ await sm.createTask({
494
+ taskId: parentTaskId,
495
+ taskKind: 'diagnostician',
496
+ inputRef: painId,
497
+ status: 'pending',
498
+ attemptCount: 0,
499
+ maxAttempts: 3,
500
+ diagnosticJson: diagnosticianDiagnosticJson(painId),
501
+ });
502
+
503
+ await sm.createTask({
504
+ taskId: routerTaskId,
505
+ taskKind: 'diag_router',
506
+ inputRef: painId,
507
+ status: 'pending',
508
+ attemptCount: 0,
509
+ maxAttempts: 3,
510
+ diagnosticJson: diagnosticianDiagnosticJson(painId),
511
+ });
512
+
513
+ // Acquire lease + run for the router task (needed for artifact insertion)
514
+ await sm.acquireLease({ taskId: routerTaskId, owner: 'test-owner', durationMs: 60_000, runtimeKind: 'openclaw' });
515
+ const runs = await sm.getRunsByTask(routerTaskId);
516
+ const runId = runs[0]?.runId;
517
+ if (!runId) throw new Error(`No run created for task ${routerTaskId}`);
518
+
519
+ // Directly insert artifact with a DIFFERENT painId in content_json
520
+ // (simulates lineage mismatch — committer stores DiagnosticianOutputV1
521
+ // which doesn't have painId, but the assembler reads it for lineage checks)
522
+ const artifactId = 'mismatch-artifact-001';
523
+ const contentJson = JSON.stringify({
524
+ ...validDiagnosticianOutput(painId),
525
+ painId: wrongPainId, // mismatched lineage
526
+ });
527
+ sm.connection.getDb().prepare(
528
+ `INSERT INTO artifacts (artifact_id, run_id, task_id, artifact_kind, content_json, created_at)
529
+ VALUES (?, ?, ?, ?, ?, ?)`,
530
+ ).run(artifactId, runId, routerTaskId, 'diagnostician_output', contentJson, new Date().toISOString());
531
+
532
+ await sm.markTaskSucceeded(routerTaskId, `artifact://${artifactId}`);
533
+ await sm.markTaskSucceeded(parentTaskId, `artifact://${artifactId}`);
534
+
535
+ const { snapshot, warnings } = await assembleMainlineSnapshot({ workspaceDir, painId, readiness: healthyReadiness() });
536
+
537
+ // Artifact should still be found via fallback
538
+ expect(snapshot.chain.diagnosticianArtifact).not.toBeNull();
539
+ // EP-07: lineage mismatch warning must be emitted
540
+ expect(warnings.some((w) => w.includes('mismatches') && w.includes(wrongPainId))).toBe(true);
541
+ });
542
+
543
+ it('monolithic diagnostician artifact lookup still works (no regression)', async () => {
544
+ await sm.initialize();
545
+ const painId = 'pain-monolithic-regression';
546
+ const taskId = `diagnostician-${painId}`;
547
+
548
+ // Monolithic layout: artifact stored directly under the parent task
549
+ await sm.createTask({
550
+ taskId,
551
+ taskKind: 'diagnostician',
552
+ inputRef: painId,
553
+ status: 'pending',
554
+ attemptCount: 0,
555
+ maxAttempts: 3,
556
+ diagnosticJson: diagnosticianDiagnosticJson(painId),
557
+ });
558
+
559
+ const output = validDiagnosticianOutput(painId);
560
+ const { artifactId } = await runDiagnosisToSucceeded(sm, taskId, output);
561
+
562
+ const { snapshot, warnings } = await assembleMainlineSnapshot({ workspaceDir, painId, readiness: healthyReadiness() });
563
+
564
+ expect(snapshot.chain.diagnosticianArtifact).not.toBeNull();
565
+ expect(snapshot.chain.diagnosticianArtifact?.artifactId).toBe(artifactId);
566
+ expect(snapshot.chain.diagnosticianArtifact?.sourcePainId).toBe(painId);
567
+ expect(warnings.some((w) => w.includes('mismatches'))).toBe(false);
568
+ });
425
569
  });
@@ -1,12 +0,0 @@
1
- /**
2
- * Re-exports the canonical PrincipleTreeLedgerAdapter from @principles/core/runtime-v2.
3
- *
4
- * This adapter uses @principles/core's addPrincipleToLedger/loadLedger, which write to
5
- * <stateDir>/principle_training_state.json (HybridLedgerStore format), matching the ledger
6
- * format used by the OpenClaw plugin's pain-signal-bridge.
7
- *
8
- * audit/repair commands use this adapter — it ensures consistency between
9
- * what intake writes and what audit/repair read back.
10
- */
11
- export { PrincipleTreeLedgerAdapter, } from '@principles/core/runtime-v2';
12
- //# sourceMappingURL=principle-tree-ledger-adapter.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"principle-tree-ledger-adapter.d.ts","sourceRoot":"","sources":["../src/principle-tree-ledger-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EACL,0BAA0B,GAC3B,MAAM,6BAA6B,CAAC"}
@@ -1,12 +0,0 @@
1
- /**
2
- * Re-exports the canonical PrincipleTreeLedgerAdapter from @principles/core/runtime-v2.
3
- *
4
- * This adapter uses @principles/core's addPrincipleToLedger/loadLedger, which write to
5
- * <stateDir>/principle_training_state.json (HybridLedgerStore format), matching the ledger
6
- * format used by the OpenClaw plugin's pain-signal-bridge.
7
- *
8
- * audit/repair commands use this adapter — it ensures consistency between
9
- * what intake writes and what audit/repair read back.
10
- */
11
- export { PrincipleTreeLedgerAdapter, } from '@principles/core/runtime-v2';
12
- //# sourceMappingURL=principle-tree-ledger-adapter.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"principle-tree-ledger-adapter.js","sourceRoot":"","sources":["../src/principle-tree-ledger-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EACL,0BAA0B,GAC3B,MAAM,6BAA6B,CAAC"}
@@ -1,13 +0,0 @@
1
- /**
2
- * Re-exports the canonical PrincipleTreeLedgerAdapter from @principles/core/runtime-v2.
3
- *
4
- * This adapter uses @principles/core's addPrincipleToLedger/loadLedger, which write to
5
- * <stateDir>/principle_training_state.json (HybridLedgerStore format), matching the ledger
6
- * format used by the OpenClaw plugin's pain-signal-bridge.
7
- *
8
- * audit/repair commands use this adapter — it ensures consistency between
9
- * what intake writes and what audit/repair read back.
10
- */
11
- export {
12
- PrincipleTreeLedgerAdapter,
13
- } from '@principles/core/runtime-v2';