@principles/pd-cli 1.152.17 → 1.152.18
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/dist/commands/candidate.d.ts +27 -0
- package/dist/commands/candidate.d.ts.map +1 -1
- package/dist/commands/candidate.js +358 -14
- package/dist/commands/candidate.js.map +1 -1
- package/dist/commands/diagnose.d.ts.map +1 -1
- package/dist/commands/diagnose.js +17 -3
- package/dist/commands/diagnose.js.map +1 -1
- package/dist/commands/pain-retry.d.ts.map +1 -1
- package/dist/commands/pain-retry.js +17 -3
- package/dist/commands/pain-retry.js.map +1 -1
- package/dist/index.js +16 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -65,6 +65,33 @@ export declare function handleCandidateShow(opts: CandidateShowOptions): Promise
|
|
|
65
65
|
* If ledger write succeeds but DB update fails, exits non-zero with clear error.
|
|
66
66
|
*/
|
|
67
67
|
export declare function handleCandidateIntake(opts: CandidateIntakeOptions): Promise<void>;
|
|
68
|
+
interface CandidateReviewOptions {
|
|
69
|
+
candidateId: string;
|
|
70
|
+
workspace?: string;
|
|
71
|
+
json?: boolean;
|
|
72
|
+
/** Omit for the read-only proposal view; `reuse`/`create` enter decision mode. */
|
|
73
|
+
decide?: 'reuse' | 'create';
|
|
74
|
+
/** Required with `--decide reuse`. */
|
|
75
|
+
principleId?: string;
|
|
76
|
+
/** Required with `--decide` (rc-3): why the Owner resolved this candidate. */
|
|
77
|
+
reason?: string;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* pd candidate review — PRI-917 PR3A Owner decision surface (SPEC §18).
|
|
81
|
+
*
|
|
82
|
+
* Phase 1 (read-only): shows the candidate and the deterministic Top-K reuse
|
|
83
|
+
* proposal (score, reasons/shared terms, and the existing Principle's text /
|
|
84
|
+
* triggerPattern / action / status) so the Owner can decide with the same
|
|
85
|
+
* information the gate will consume.
|
|
86
|
+
*
|
|
87
|
+
* Phase 2 (`--decide`): constructs the SPEC §11 verdict (actor from
|
|
88
|
+
* resolveOwnerIdentity — fail-closed when unresolvable) and injects it into
|
|
89
|
+
* CandidateIntakeService, which runs the EXISTING gate → appendReuseEvidence
|
|
90
|
+
* chain. The CLI is an interaction layer only: it never writes reuseEvidence,
|
|
91
|
+
* decision files, or any other state (SPEC §18 — the only durable authority
|
|
92
|
+
* is `Principle.reuseEvidence[]`).
|
|
93
|
+
*/
|
|
94
|
+
export declare function handleCandidateReview(opts: CandidateReviewOptions): Promise<void>;
|
|
68
95
|
/**
|
|
69
96
|
* pd candidate audit --workspace <path> [--json]
|
|
70
97
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"candidate.d.ts","sourceRoot":"","sources":["../../src/commands/candidate.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"candidate.d.ts","sourceRoot":"","sources":["../../src/commands/candidate.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pd candidate commands — Principle candidate inspection, intake, audit, repair.
|
|
2
|
+
* pd candidate commands — Principle candidate inspection, intake, audit, repair, reuse review.
|
|
3
3
|
*
|
|
4
4
|
* Usage:
|
|
5
5
|
* pd candidate list --task-id <taskId> --workspace <path> [--json]
|
|
6
6
|
* pd candidate show <candidateId> --workspace <path> [--json]
|
|
7
7
|
* pd candidate intake --candidate-id <id> [--workspace <path>] [--json] [--dry-run]
|
|
8
|
+
* pd candidate review --candidate-id <id> [--workspace <path>] [--json]
|
|
9
|
+
* pd candidate review --candidate-id <id> --decide reuse --principle-id <id> --reason "..." [--json]
|
|
10
|
+
* pd candidate review --candidate-id <id> --decide create --reason "..." [--json]
|
|
8
11
|
* pd candidate audit --workspace <path> [--json]
|
|
9
12
|
* pd candidate repair --candidate-id <id> --workspace <path> [--json]
|
|
10
13
|
* pd candidate route --candidate-id <id> --workspace <path> [--json]
|
|
11
14
|
*/
|
|
12
15
|
import { randomUUID } from 'crypto';
|
|
13
16
|
import * as path from 'path';
|
|
14
|
-
import { RuntimeStateManager, SqliteConnection, candidateList, candidateShow, CandidateIntakeService, CandidateIntakeError, INTAKE_ERROR_CODES, decideInternalizationRoute, buildDreamerSeedFromCandidate, findExistingDreamerTask, parseSeedSourcePainId, PrincipleTreeLedgerAdapter,
|
|
17
|
+
import { RuntimeStateManager, SqliteConnection, candidateList, candidateShow, CandidateIntakeService, CandidateIntakeError, INTAKE_ERROR_CODES, decideInternalizationRoute, buildDreamerSeedFromCandidate, findExistingDreamerTask, parseSeedSourcePainId, PrincipleTreeLedgerAdapter,
|
|
18
|
+
// PRI-917 PR3A: the review surface consumes the SAME proposal/semantics
|
|
19
|
+
// machinery as the intake gate (audit C4 — one implementation, no drift).
|
|
20
|
+
buildReuseProposal, buildReuseShortlist, extractIntakeRecommendation, isPrincipleLedgerEligibleKind, resolveOwnerIdentity, defaultOwnerIdentityHomeDir, } from '@principles/core/runtime-v2';
|
|
15
21
|
import { loadLedger, getLedgerFilePathPublic } from '@principles/core/principle-tree-ledger';
|
|
16
22
|
import { resolveWorkspaceDir } from '../resolve-workspace.js';
|
|
17
23
|
import { resolvePromptFullPipelineSeedMode } from '../services/pd-config-loader.js';
|
|
@@ -380,6 +386,180 @@ export async function handleCandidateShow(opts) {
|
|
|
380
386
|
}
|
|
381
387
|
}
|
|
382
388
|
// ── Intake ───────────────────────────────────────────────────────────────────
|
|
389
|
+
// ── Shared intake/decision finalizers (PRI-917 PR3A) ─────────────────────────
|
|
390
|
+
/**
|
|
391
|
+
* PRI-917 PR3A (surface audit C2): unify the candidate terminal state after a
|
|
392
|
+
* successful ledger write OR a recorded reuse resolution — always through the
|
|
393
|
+
* guarded {@link updateCandidateStatus} (optimistic pending→consumed +
|
|
394
|
+
* consumed_at), never bypassing the existing mechanism (SPEC §13: the
|
|
395
|
+
* candidate still reaches `consumed`).
|
|
396
|
+
*
|
|
397
|
+
* Returns 'already_consumed' when the candidate was consumed before this call
|
|
398
|
+
* (e.g. an INV-R07 replay). Exits non-zero on a guarded-transition failure —
|
|
399
|
+
* the ledger/LEDGER fact is already durable, so the inconsistency must be
|
|
400
|
+
* loud (same posture as the pre-PR3A intake path).
|
|
401
|
+
*/
|
|
402
|
+
async function markCandidateConsumed(stateManager, candidateId, ledgerFact) {
|
|
403
|
+
const candidate = await stateManager.getCandidate(candidateId);
|
|
404
|
+
if (candidate?.status === 'consumed') {
|
|
405
|
+
return 'already_consumed';
|
|
406
|
+
}
|
|
407
|
+
try {
|
|
408
|
+
await updateCandidateStatus({ stateManager, candidateId, targetStatus: 'consumed', expectedCurrentStatus: 'pending' });
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
const msg = `The ledger fact is durable (${ledgerFact}) but the candidate status update failed: ${err instanceof Error ? err.message : String(err)}. ` +
|
|
412
|
+
`Candidate ${candidateId} may be in inconsistent state.`;
|
|
413
|
+
console.error(`ERROR: ${msg}`);
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
return 'consumed';
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* PRI-917 PR3A (surface audit C1/C2): report a `reuse_selected` intake result
|
|
420
|
+
* as the SUCCESS it is — resolution + selected Principle + evidence, exit 0 —
|
|
421
|
+
* and unify the candidate terminal state (C2). Used by BOTH `pd candidate
|
|
422
|
+
* intake` (replay path) and `pd candidate review --decide reuse` (fresh
|
|
423
|
+
* decision). The CLI prints the durable fact; it does not create one.
|
|
424
|
+
*/
|
|
425
|
+
async function reportReuseResolution(opts) {
|
|
426
|
+
const { stateManager, candidateId, result, json } = opts;
|
|
427
|
+
if (!result.selectedPrincipleId || !result.reuseEvidence) {
|
|
428
|
+
// Adapter/service contract violation — never print a hollow resolution.
|
|
429
|
+
throw new CandidateIntakeError(INTAKE_ERROR_CODES.INPUT_INVALID, `reuse_selected result for candidate ${candidateId} is missing selectedPrincipleId/reuseEvidence (contract violation)`, { candidateId, reason: 'reuse_resolution_contract_violation' });
|
|
430
|
+
}
|
|
431
|
+
const consumedState = await markCandidateConsumed(stateManager, candidateId, `reuse evidence on ${result.selectedPrincipleId}`);
|
|
432
|
+
const replayed = result.reuseProposal === undefined;
|
|
433
|
+
const resolution = {
|
|
434
|
+
candidateId,
|
|
435
|
+
status: 'reused',
|
|
436
|
+
selectedPrincipleId: result.selectedPrincipleId,
|
|
437
|
+
reuseEvidence: result.reuseEvidence,
|
|
438
|
+
replayed,
|
|
439
|
+
candidateStatus: consumedState,
|
|
440
|
+
message: result.message,
|
|
441
|
+
};
|
|
442
|
+
if (json) {
|
|
443
|
+
console.log(JSON.stringify(resolution, null, 2));
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
console.log(`\nPrinciple Candidate Reuse: ${candidateId}\n`);
|
|
447
|
+
console.log(` Candidate: ${candidateId}`);
|
|
448
|
+
console.log(` Reused Principle: ${result.selectedPrincipleId}`);
|
|
449
|
+
console.log(` Pain: ${result.reuseEvidence.painId}`);
|
|
450
|
+
console.log(` Decided By: ${result.reuseEvidence.actor.kind} ${result.reuseEvidence.actor.id}`);
|
|
451
|
+
console.log(` Decided At: ${result.reuseEvidence.decidedAt}`);
|
|
452
|
+
console.log(` Reason: ${result.reuseEvidence.reason}`);
|
|
453
|
+
console.log(` Candidate Status: ${consumedState === 'already_consumed' ? 'already consumed (INV-R07 replay)' : 'consumed'}`);
|
|
454
|
+
console.log(' Evidence: recorded on the Principle\'s reuseEvidence (append-only)\n');
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Decision mode of {@link handleCandidateReview}: validate the CLI contract
|
|
458
|
+
* (rc-3), resolve the Owner identity (fail-closed), then run the EXISTING
|
|
459
|
+
* intake chain with the §11 verdict injected. No CLI-side persistence.
|
|
460
|
+
*/
|
|
461
|
+
async function executeOwnerDecision(deps) {
|
|
462
|
+
const { opts, stateManager, ledgerAdapter, stateDir } = deps;
|
|
463
|
+
const failCli = (message, nextAction) => {
|
|
464
|
+
if (opts.json) {
|
|
465
|
+
console.log(JSON.stringify({ candidateId: opts.candidateId, status: 'refused', decision: opts.decide, error: message, nextAction }, null, 2));
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
console.error(`ERROR: ${message}`);
|
|
469
|
+
console.error(` Next Action: ${nextAction}`);
|
|
470
|
+
}
|
|
471
|
+
process.exit(1);
|
|
472
|
+
};
|
|
473
|
+
if (typeof opts.reason !== 'string' || opts.reason.trim() === '') {
|
|
474
|
+
failCli('--reason is required with --decide (the evidence entry must record why the Owner resolved this candidate)', 'Re-run with --reason "..." (non-empty).');
|
|
475
|
+
}
|
|
476
|
+
if (opts.decide === 'reuse' && (typeof opts.principleId !== 'string' || opts.principleId.trim() === '')) {
|
|
477
|
+
failCli('--principle-id is required with --decide reuse', 'Re-run with --principle-id <id from the review proposal>.');
|
|
478
|
+
}
|
|
479
|
+
// Owner identity: env > ~/.pd/owner.json > none (ADR-0022 resolver).
|
|
480
|
+
const identity = resolveOwnerIdentity(process.env, defaultOwnerIdentityHomeDir());
|
|
481
|
+
if ((identity.source !== 'env' && identity.source !== 'file') || !identity.ownerId) {
|
|
482
|
+
failCli(`Owner identity is required to record a ${opts.decide} decision (identity source: ${identity.source})${identity.error ? `: ${identity.error}` : ''}`, 'Register the Owner identity file (~/.pd/owner.json) or set PD_OWNER_ID and PD_OWNER_CREDENTIAL_ID together, then retry.');
|
|
483
|
+
}
|
|
484
|
+
const decision = opts.decide === 'reuse'
|
|
485
|
+
? {
|
|
486
|
+
decision: 'reuse',
|
|
487
|
+
selectedPrincipleId: opts.principleId,
|
|
488
|
+
actor: { kind: 'owner', id: identity.ownerId },
|
|
489
|
+
reason: opts.reason,
|
|
490
|
+
decidedAt: new Date().toISOString(),
|
|
491
|
+
}
|
|
492
|
+
: { decision: 'create' };
|
|
493
|
+
// INV-R08: verify the selected principle is in the current proposal.
|
|
494
|
+
// This prevents a stale or empty proposal from silently degrading reuse
|
|
495
|
+
// into create.
|
|
496
|
+
if (opts.decide === 'reuse') {
|
|
497
|
+
const candidate = await stateManager.getCandidate(opts.candidateId);
|
|
498
|
+
const artifact = candidate ? await stateManager.getArtifact(candidate.artifactId) : null;
|
|
499
|
+
const extracted = candidate ? extractIntakeRecommendation(candidate, artifact) : null;
|
|
500
|
+
if (!candidate || !extracted || !extracted.ok) {
|
|
501
|
+
failCli('Cannot build the reuse proposal for this candidate', 'Run `pd candidate review --candidate-id <id>` first.');
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
const proposal = buildReuseProposal(opts.candidateId, buildReuseShortlist({
|
|
505
|
+
text: extracted.recommendation.text || candidate.description || '',
|
|
506
|
+
triggerPattern: extracted.recommendation.triggerPattern ?? '',
|
|
507
|
+
action: extracted.recommendation.action ?? '',
|
|
508
|
+
}, stateDir, { recommendationKind: candidate.rawRecommendationKind }));
|
|
509
|
+
if (!proposal.candidates.some((c) => c.principleId === opts.principleId)) {
|
|
510
|
+
failCli(`Principle ${opts.principleId} is not in the current reuse proposal`, 'Re-run `pd candidate review` to see the current proposal.');
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const service = new CandidateIntakeService({
|
|
514
|
+
stateManager,
|
|
515
|
+
ledgerAdapter,
|
|
516
|
+
// The verdict is fixed by the CLI invocation; the intake gate still
|
|
517
|
+
// validates it (rc-1/rc-2/rc-3) and checks the selection against the
|
|
518
|
+
// proposal (INV-R08) — the CLI cannot smuggle a decision past the gate.
|
|
519
|
+
reuseDecision: () => decision,
|
|
520
|
+
reuseStateDir: stateDir,
|
|
521
|
+
});
|
|
522
|
+
const intakeResult = await service.intake(opts.candidateId);
|
|
523
|
+
if (intakeResult.outcome === 'refused' && intakeResult.reason === 'reuse_selected') {
|
|
524
|
+
await reportReuseResolution({ stateManager, candidateId: opts.candidateId, result: intakeResult, json: opts.json === true });
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (intakeResult.outcome === 'refused') {
|
|
528
|
+
// A non-principle kind or unknown kind cannot carry a reuse decision.
|
|
529
|
+
throw new CandidateIntakeError(INTAKE_ERROR_CODES.INPUT_INVALID, `Principle Ledger write refused: ${intakeResult.message}`, { candidateId: opts.candidateId, reason: intakeResult.reason });
|
|
530
|
+
}
|
|
531
|
+
// create verdict (or gate absent shapes) — same durable outcome as intake.
|
|
532
|
+
const { entry } = intakeResult;
|
|
533
|
+
const consumedState = await markCandidateConsumed(stateManager, opts.candidateId, entry.id);
|
|
534
|
+
if (consumedState === 'already_consumed') {
|
|
535
|
+
// Consumed before this call — the ledger entry is identical by intake
|
|
536
|
+
// idempotency; report it like `pd candidate intake` does.
|
|
537
|
+
const infoMessage = `Candidate ${opts.candidateId} was already consumed. Ledger entry: ${entry.id}`;
|
|
538
|
+
if (opts.json) {
|
|
539
|
+
console.log(JSON.stringify({ candidateId: opts.candidateId, ledgerEntryId: entry.id, status: 'already_consumed', message: infoMessage }, null, 2));
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
console.log(infoMessage);
|
|
543
|
+
}
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const result = {
|
|
547
|
+
candidateId: opts.candidateId,
|
|
548
|
+
decision: opts.decide,
|
|
549
|
+
ledgerEntryId: entry.id,
|
|
550
|
+
status: 'consumed',
|
|
551
|
+
};
|
|
552
|
+
if (opts.json) {
|
|
553
|
+
console.log(JSON.stringify(result, null, 2));
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
console.log(`\nPrinciple Candidate Decision: ${opts.candidateId}\n`);
|
|
557
|
+
console.log(` Decision: ${opts.decide}`);
|
|
558
|
+
console.log(` Candidate: ${opts.candidateId}`);
|
|
559
|
+
console.log(` Ledger Entry: ${entry.id}`);
|
|
560
|
+
console.log(` Status: consumed\n`);
|
|
561
|
+
console.log('Decision complete.\n');
|
|
562
|
+
}
|
|
383
563
|
/**
|
|
384
564
|
* pd candidate intake --candidate-id <id> [--workspace <path>] [--json] [--dry-run]
|
|
385
565
|
*
|
|
@@ -465,12 +645,22 @@ export async function handleCandidateIntake(opts) {
|
|
|
465
645
|
// candidate id, so a Principle Ledger write boundary refusal is surfaced
|
|
466
646
|
// loudly (never silently swallowed) instead of being treated as success.
|
|
467
647
|
const intakeResult = await service.intake(opts.candidateId);
|
|
648
|
+
if (intakeResult.outcome === 'refused' && intakeResult.reason === 'reuse_selected') {
|
|
649
|
+
// PRI-917 PR3A (surface audit C1/C2): a recorded reuse resolution is a
|
|
650
|
+
// SUCCESS, not a failure — the evidence is already durable on the
|
|
651
|
+
// selected Principle (fresh decision) or was recorded earlier (INV-R07
|
|
652
|
+
// replay via intake step 2b, which is how this branch is reachable from
|
|
653
|
+
// the gate-less intake command). Report the resolution, unify the
|
|
654
|
+
// candidate terminal state with the automatic paths (SPEC §13), exit 0.
|
|
655
|
+
await reportReuseResolution({ stateManager, candidateId: opts.candidateId, result: intakeResult, json: opts.json === true });
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
468
658
|
if (intakeResult.outcome === 'refused') {
|
|
469
659
|
throw new CandidateIntakeError(INTAKE_ERROR_CODES.INPUT_INVALID, `Principle Ledger write refused: ${intakeResult.message}`, { candidateId: opts.candidateId, reason: intakeResult.reason });
|
|
470
660
|
}
|
|
471
661
|
const { entry } = intakeResult;
|
|
472
|
-
|
|
473
|
-
if (
|
|
662
|
+
const consumedState = await markCandidateConsumed(stateManager, opts.candidateId, entry.id);
|
|
663
|
+
if (consumedState === 'already_consumed') {
|
|
474
664
|
const infoMessage = `Candidate ${opts.candidateId} was already consumed. Ledger entry: ${entry.id}`;
|
|
475
665
|
if (opts.json) {
|
|
476
666
|
console.log(JSON.stringify({
|
|
@@ -485,16 +675,6 @@ export async function handleCandidateIntake(opts) {
|
|
|
485
675
|
}
|
|
486
676
|
return;
|
|
487
677
|
}
|
|
488
|
-
// Update DB status — this must succeed; if it fails, exit non-zero
|
|
489
|
-
try {
|
|
490
|
-
await updateCandidateStatus({ stateManager, candidateId: opts.candidateId, targetStatus: 'consumed', expectedCurrentStatus: 'pending' });
|
|
491
|
-
}
|
|
492
|
-
catch (err) {
|
|
493
|
-
const msg = `Ledger write succeeded (entry ${entry.id}) but DB status update failed: ${err instanceof Error ? err.message : String(err)}. ` +
|
|
494
|
-
`Candidate ${opts.candidateId} may be in inconsistent state.`;
|
|
495
|
-
console.error(`ERROR: ${msg}`);
|
|
496
|
-
process.exit(1);
|
|
497
|
-
}
|
|
498
678
|
const result = {
|
|
499
679
|
candidateId: opts.candidateId,
|
|
500
680
|
ledgerEntryId: entry.id,
|
|
@@ -526,6 +706,170 @@ export async function handleCandidateIntake(opts) {
|
|
|
526
706
|
await stateManager.close();
|
|
527
707
|
}
|
|
528
708
|
}
|
|
709
|
+
/**
|
|
710
|
+
* pd candidate review — PRI-917 PR3A Owner decision surface (SPEC §18).
|
|
711
|
+
*
|
|
712
|
+
* Phase 1 (read-only): shows the candidate and the deterministic Top-K reuse
|
|
713
|
+
* proposal (score, reasons/shared terms, and the existing Principle's text /
|
|
714
|
+
* triggerPattern / action / status) so the Owner can decide with the same
|
|
715
|
+
* information the gate will consume.
|
|
716
|
+
*
|
|
717
|
+
* Phase 2 (`--decide`): constructs the SPEC §11 verdict (actor from
|
|
718
|
+
* resolveOwnerIdentity — fail-closed when unresolvable) and injects it into
|
|
719
|
+
* CandidateIntakeService, which runs the EXISTING gate → appendReuseEvidence
|
|
720
|
+
* chain. The CLI is an interaction layer only: it never writes reuseEvidence,
|
|
721
|
+
* decision files, or any other state (SPEC §18 — the only durable authority
|
|
722
|
+
* is `Principle.reuseEvidence[]`).
|
|
723
|
+
*/
|
|
724
|
+
export async function handleCandidateReview(opts) {
|
|
725
|
+
const workspaceDir = resolveWorkspaceDir(opts.workspace);
|
|
726
|
+
const stateManager = new RuntimeStateManager({ workspaceDir });
|
|
727
|
+
try {
|
|
728
|
+
await stateManager.initialize();
|
|
729
|
+
const stateDir = path.join(workspaceDir, '.state');
|
|
730
|
+
const ledgerAdapter = new PrincipleTreeLedgerAdapter({ stateDir });
|
|
731
|
+
const candidate = await stateManager.getCandidate(opts.candidateId);
|
|
732
|
+
if (!candidate) {
|
|
733
|
+
console.error(`Candidate not found: ${opts.candidateId}`);
|
|
734
|
+
process.exit(1);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
if (opts.decide === 'reuse' || opts.decide === 'create') {
|
|
738
|
+
// PRI-442 Stage 4: admission gate check — same as handleCandidateIntake.
|
|
739
|
+
// Prevents CLI from bypassing the production admission gate.
|
|
740
|
+
const admissionBlock = checkAdmissionGate(candidate);
|
|
741
|
+
if (admissionBlock) {
|
|
742
|
+
const payload = {
|
|
743
|
+
candidateId: opts.candidateId,
|
|
744
|
+
status: 'refused',
|
|
745
|
+
admissionDecision: admissionBlock.decision,
|
|
746
|
+
reason: admissionBlock.reason,
|
|
747
|
+
nextAction: admissionBlock.nextAction,
|
|
748
|
+
};
|
|
749
|
+
if (opts.json) {
|
|
750
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
751
|
+
}
|
|
752
|
+
else {
|
|
753
|
+
console.error(`Admission gate refused candidate ${opts.candidateId}: ${admissionBlock.decision}`);
|
|
754
|
+
console.error(` Reason: ${admissionBlock.reason}`);
|
|
755
|
+
console.error(` Next Action: ${admissionBlock.nextAction}`);
|
|
756
|
+
}
|
|
757
|
+
process.exit(1);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
// opts.decide is narrowed here — no cast needed to build the decision-mode shape.
|
|
761
|
+
await executeOwnerDecision({ opts: { ...opts, decide: opts.decide }, stateManager, ledgerAdapter, stateDir });
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
// ── Read-only proposal view ──
|
|
765
|
+
const rawKindEligible = isPrincipleLedgerEligibleKind(candidate.rawRecommendationKind);
|
|
766
|
+
if (!rawKindEligible) {
|
|
767
|
+
// Fail closed like the intake boundary: a non-principle candidate never
|
|
768
|
+
// reaches the reuse gate, so reviewing a proposal for it would lie.
|
|
769
|
+
const rawKind = typeof candidate.rawRecommendationKind === 'string' ? candidate.rawRecommendationKind : null;
|
|
770
|
+
const payload = {
|
|
771
|
+
candidateId: opts.candidateId,
|
|
772
|
+
recommendationKindEligible: false,
|
|
773
|
+
rawRecommendationKind: rawKind,
|
|
774
|
+
message: `Candidate ${opts.candidateId} has recommendation_kind '${rawKind ?? 'absent'}' which does not target the Principle Ledger; there is no reuse proposal to review.`,
|
|
775
|
+
nextAction: 'Run `pd candidate route --candidate-id <id>` for this candidate\'s internalization route instead.',
|
|
776
|
+
};
|
|
777
|
+
if (opts.json) {
|
|
778
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
console.log(`\nPrinciple Candidate Review: ${opts.candidateId}\n`);
|
|
782
|
+
console.log(` Kind: ${rawKind ?? 'absent'} (not Principle-Ledger eligible)`);
|
|
783
|
+
console.log(` ${payload.message}`);
|
|
784
|
+
console.log(` Next Action: ${payload.nextAction}\n`);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
const artifact = await stateManager.getArtifact(candidate.artifactId);
|
|
788
|
+
if (!artifact) {
|
|
789
|
+
console.error(`Artifact not found for candidate: ${opts.candidateId} (artifact ${candidate.artifactId})`);
|
|
790
|
+
process.exit(1);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
// C4: the SAME extraction intake uses (4b/4c) — never a second parser.
|
|
794
|
+
const extracted = extractIntakeRecommendation(candidate, artifact);
|
|
795
|
+
if (!extracted.ok) {
|
|
796
|
+
throw extracted.error;
|
|
797
|
+
}
|
|
798
|
+
const { recommendation } = extracted;
|
|
799
|
+
const shortlist = buildReuseShortlist({
|
|
800
|
+
text: recommendation.text || candidate.description || '',
|
|
801
|
+
triggerPattern: recommendation.triggerPattern ?? '',
|
|
802
|
+
action: recommendation.action ?? '',
|
|
803
|
+
}, stateDir, { recommendationKind: candidate.rawRecommendationKind });
|
|
804
|
+
const proposal = buildReuseProposal(opts.candidateId, shortlist);
|
|
805
|
+
// Enrich each entry with the existing Principle's durable content.
|
|
806
|
+
const ledger = loadLedger(stateDir);
|
|
807
|
+
const entries = proposal.candidates.map((entry) => {
|
|
808
|
+
const principle = ledger.tree.principles[entry.principleId];
|
|
809
|
+
return {
|
|
810
|
+
principleId: entry.principleId,
|
|
811
|
+
score: entry.score,
|
|
812
|
+
reasons: entry.reasons,
|
|
813
|
+
existingPrinciple: principle
|
|
814
|
+
? { text: principle.text, triggerPattern: principle.triggerPattern, action: principle.action, status: principle.status }
|
|
815
|
+
: { text: '', triggerPattern: '', action: '', status: 'missing' },
|
|
816
|
+
};
|
|
817
|
+
});
|
|
818
|
+
const nextAction = entries.length > 0
|
|
819
|
+
? `pd candidate review --candidate-id ${opts.candidateId} --decide reuse --principle-id <id> --reason "..." (or --decide create --reason "...")`
|
|
820
|
+
: `No credible reuse proposal — intake will create a new Principle: pd candidate intake --candidate-id ${opts.candidateId} --workspace "${workspaceDir}"`;
|
|
821
|
+
if (opts.json) {
|
|
822
|
+
console.log(JSON.stringify({
|
|
823
|
+
candidateId: opts.candidateId,
|
|
824
|
+
title: candidate.title,
|
|
825
|
+
description: candidate.description,
|
|
826
|
+
status: candidate.status,
|
|
827
|
+
taskId: candidate.taskId,
|
|
828
|
+
recommendationKindEligible: true,
|
|
829
|
+
claim: { text: recommendation.text || candidate.description || '', triggerPattern: recommendation.triggerPattern ?? '', action: recommendation.action ?? '' },
|
|
830
|
+
proposal: { status: proposal.status, eligibleCount: proposal.eligibleCount, candidates: entries },
|
|
831
|
+
nextAction,
|
|
832
|
+
}, null, 2));
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
console.log(`\nPrinciple Candidate Review: ${opts.candidateId}\n`);
|
|
836
|
+
console.log(` Title: ${candidate.title}`);
|
|
837
|
+
console.log(` Description: ${candidate.description}`);
|
|
838
|
+
console.log(` Status: ${candidate.status}`);
|
|
839
|
+
console.log(` Task: ${candidate.taskId}`);
|
|
840
|
+
console.log(` Claim: ${recommendation.text || candidate.description || ''}`);
|
|
841
|
+
console.log(` Trigger: ${recommendation.triggerPattern ?? ''}`);
|
|
842
|
+
console.log(` Action: ${recommendation.action ?? ''}`);
|
|
843
|
+
console.log(`\n Reuse Proposal (${entries.length} of ${proposal.eligibleCount} eligible, Top-3):\n`);
|
|
844
|
+
if (entries.length === 0) {
|
|
845
|
+
console.log(' No credible existing Principle found — the intake gate will create a new one.\n');
|
|
846
|
+
}
|
|
847
|
+
entries.forEach((entry, index) => {
|
|
848
|
+
console.log(` ${index + 1}. ${entry.principleId} (score ${entry.score.toFixed(3)})`);
|
|
849
|
+
for (const reason of entry.reasons) {
|
|
850
|
+
console.log(` reason: ${reason}`);
|
|
851
|
+
}
|
|
852
|
+
console.log(` text: ${entry.existingPrinciple.text}`);
|
|
853
|
+
console.log(` trigger: ${entry.existingPrinciple.triggerPattern}`);
|
|
854
|
+
console.log(` action: ${entry.existingPrinciple.action}`);
|
|
855
|
+
console.log(` status: ${entry.existingPrinciple.status}`);
|
|
856
|
+
});
|
|
857
|
+
console.log(`\n Next Action: ${nextAction}\n`);
|
|
858
|
+
}
|
|
859
|
+
catch (err) {
|
|
860
|
+
if (err instanceof CandidateIntakeError || err.name === 'CandidateIntakeError') {
|
|
861
|
+
const e = err;
|
|
862
|
+
console.error(`Review failed [${e.code ?? 'unknown'}]: ${e.message}`);
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
console.error(`Review failed: ${String(err)}`);
|
|
866
|
+
}
|
|
867
|
+
process.exit(1);
|
|
868
|
+
}
|
|
869
|
+
finally {
|
|
870
|
+
await stateManager.close();
|
|
871
|
+
}
|
|
872
|
+
}
|
|
529
873
|
// ── Audit ─────────────────────────────────────────────────────────────────────
|
|
530
874
|
/**
|
|
531
875
|
* pd candidate audit --workspace <path> [--json]
|