@principles/core 1.288.1 → 1.289.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.
@@ -40,6 +40,14 @@ interface ShapeBridgeResultBase {
40
40
  * reported as routed / not_internalizable rather than as a failed run.
41
41
  */
42
42
  ledgerEligibleCandidateCount: number;
43
+ /**
44
+ * PRI-917 v0.3.3 — candidateIds parked by the Reuse Review Gate (suspected
45
+ * duplicates awaiting the Owner). A parked ledger-eligible candidate has NO
46
+ * ledger entry BY DESIGN, so a batch whose ledger-eligible candidates are
47
+ * ALL parked is a degraded "waiting for the Owner" outcome, not the intake
48
+ * failure this branch used to report.
49
+ */
50
+ reuseReviewRequiredCandidateIds?: string[];
43
51
  /** PRI-539: candidates admitted+ledgered but not internalizable (MVP-disabled channel). */
44
52
  notInternalizable?: NotInternalizableCandidate[];
45
53
  }
@@ -1 +1 @@
1
- {"version":3,"file":"bridge-result-shaper.d.ts","sourceRoot":"","sources":["../../src/runtime-v2/bridge-result-shaper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,sBAAsB,EAAE,0BAA0B,EAAE,MAAM,yBAAyB,CAAC;AAClG,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAEpE,2DAA2D;AAC3D,UAAU,qBAAqB;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,OAAO,CAAC;IAC3B;;;;;;;;;;OAUG;IACH,4BAA4B,EAAE,MAAM,CAAC;IACrC,2FAA2F;IAC3F,iBAAiB,CAAC,EAAE,0BAA0B,EAAE,CAAC;CAClD;AAED,2EAA2E;AAC3E,MAAM,WAAW,2BAA4B,SAAQ,qBAAqB;IACxE,IAAI,EAAE,OAAO,CAAC;IACd,gBAAgB,EAAE,wBAAwB,EAAE,CAAC;IAC7C,yEAAyE;IACzE,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,+EAA+E;AAC/E,MAAM,WAAW,8BAA+B,SAAQ,qBAAqB;IAC3E,IAAI,EAAE,UAAU,CAAC;CAClB;AAED,MAAM,MAAM,sBAAsB,GAC9B,2BAA2B,GAC3B,8BAA8B,CAAC;AAEnC;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,GAAG,sBAAsB,CAiIvF"}
1
+ {"version":3,"file":"bridge-result-shaper.d.ts","sourceRoot":"","sources":["../../src/runtime-v2/bridge-result-shaper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,sBAAsB,EAAE,0BAA0B,EAAE,MAAM,yBAAyB,CAAC;AAClG,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAEpE,2DAA2D;AAC3D,UAAU,qBAAqB;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,OAAO,CAAC;IAC3B;;;;;;;;;;OAUG;IACH,4BAA4B,EAAE,MAAM,CAAC;IACrC;;;;;;OAMG;IACH,+BAA+B,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3C,2FAA2F;IAC3F,iBAAiB,CAAC,EAAE,0BAA0B,EAAE,CAAC;CAClD;AAED,2EAA2E;AAC3E,MAAM,WAAW,2BAA4B,SAAQ,qBAAqB;IACxE,IAAI,EAAE,OAAO,CAAC;IACd,gBAAgB,EAAE,wBAAwB,EAAE,CAAC;IAC7C,yEAAyE;IACzE,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,+EAA+E;AAC/E,MAAM,WAAW,8BAA+B,SAAQ,qBAAqB;IAC3E,IAAI,EAAE,UAAU,CAAC;CAClB;AAED,MAAM,MAAM,sBAAsB,GAC9B,2BAA2B,GAC3B,8BAA8B,CAAC;AAEnC;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,GAAG,sBAAsB,CA4LvF"}
@@ -37,12 +37,37 @@ export function shapeBridgeResult(input) {
37
37
  const notInternalizableNote = notInternalizable.length > 0
38
38
  ? `not_internalizable:${notInternalizable.map((n) => `${n.candidateId}=${n.reason}`).join(',')}`
39
39
  : '';
40
+ // PRI-917 v0.3.3 review fix (P2-2): parked candidates must stay visible in
41
+ // MIXED batches too. Previously only a fully-parked batch surfaced the
42
+ // review requirement; a batch with one ledgered and one parked candidate
43
+ // reported plain succeeded and hid the pending item from the Owner.
44
+ const parked = input.reuseReviewRequiredCandidateIds ?? [];
45
+ const parkedNote = parked.length > 0
46
+ ? `reuse_review_required:${parked.join(',')} — suspected duplicates parked for Owner review; nextAction: pd candidate review --candidate-id <id> --decide reuse|create`
47
+ : '';
40
48
  const admittedCount = admissionResults.filter((a) => a.admission.decision === 'admitted').length;
41
49
  const nonAdmittedCount = admissionResults.length - admittedCount;
42
50
  // Admitted LEDGER-ELIGIBLE candidates exist but intake produced no ledger
43
51
  // entries. Non-principle kinds are excluded here: the Principle Ledger write
44
52
  // boundary refuses them by design (Phase 1 / PR1), which is not a failure.
53
+ // PRI-917 v0.3.3: candidates parked by the Reuse Review Gate also have no
54
+ // ledger entry BY DESIGN — when EVERY ledger-eligible candidate is parked,
55
+ // the run is a degraded "awaiting the Owner" outcome (rc-9), not a failure.
45
56
  if (autoIntakeEnabled && input.ledgerEligibleCandidateCount > 0 && ledgerEntryIds.length === 0) {
57
+ if (parked.length > 0 && parked.length >= input.ledgerEligibleCandidateCount) {
58
+ return {
59
+ status: 'degraded',
60
+ painId,
61
+ taskId,
62
+ runId,
63
+ artifactId,
64
+ candidateIds,
65
+ ledgerEntryIds,
66
+ admissionResults,
67
+ notInternalizable: input.notInternalizable,
68
+ message: parkedNote,
69
+ };
70
+ }
46
71
  return {
47
72
  status: 'failed',
48
73
  painId,
@@ -83,11 +108,12 @@ export function shapeBridgeResult(input) {
83
108
  ledgerEntryIds,
84
109
  admissionResults,
85
110
  notInternalizable: input.notInternalizable,
86
- message: `partial_admission:${admittedCount}_admitted_${nonAdmittedCount}_gated${seedFailureNote ? `; ${seedFailureNote}` : ''}${notInternalizableNote ? `; ${notInternalizableNote}` : ''}`,
111
+ message: `partial_admission:${admittedCount}_admitted_${nonAdmittedCount}_gated${seedFailureNote ? `; ${seedFailureNote}` : ''}${notInternalizableNote ? `; ${notInternalizableNote}` : ''}${parkedNote ? `; ${parkedNote}` : ''}`,
87
112
  };
88
113
  }
89
- // Success (or degraded when seed failed or a candidate was not internalizable)
90
- const combinedNote = [notInternalizableNote, seedFailureNote].filter(Boolean).join('; ');
114
+ // Success (or degraded when seed failed, a candidate was not internalizable,
115
+ // or a candidate is parked awaiting the Owner — P2-2)
116
+ const combinedNote = [notInternalizableNote, seedFailureNote, parkedNote].filter(Boolean).join('; ');
91
117
  return {
92
118
  status: combinedNote ? 'degraded' : 'succeeded',
93
119
  painId,
@@ -104,7 +130,25 @@ export function shapeBridgeResult(input) {
104
130
  // Existing path: no admission results, simpler decision tree.
105
131
  // Phase 1 / PR1: same reasoning as the fresh path — only ledger-eligible
106
132
  // candidates can be legitimately "missing" a ledger entry.
133
+ // The caller passes candidates whose replay admission and read-only reuse
134
+ // review confirmed a current reuse recommendation. Pending alone proves
135
+ // neither admission nor reuse.
136
+ // A replay of a parked task used to flip from the fresh path's
137
+ // review_required outcome into a bare intake failure here.
138
+ const parked = input.reuseReviewRequiredCandidateIds ?? [];
107
139
  if (autoIntakeEnabled && input.ledgerEligibleCandidateCount > 0 && ledgerEntryIds.length === 0) {
140
+ if (parked.length > 0 && parked.length >= input.ledgerEligibleCandidateCount) {
141
+ return {
142
+ status: 'degraded',
143
+ painId,
144
+ taskId,
145
+ runId,
146
+ artifactId,
147
+ candidateIds,
148
+ ledgerEntryIds,
149
+ message: `reuse_review_required:${parked.join(',')} — suspected duplicates confirmed by read-only reuse review; nextAction: pd candidate review --candidate-id <id> --decide reuse|create`,
150
+ };
151
+ }
108
152
  return {
109
153
  status: 'failed',
110
154
  painId,
@@ -116,6 +160,20 @@ export function shapeBridgeResult(input) {
116
160
  message: 'Candidate intake did not produce a ledger entry — treating as failed',
117
161
  };
118
162
  }
163
+ // P2-2/F3: a replayed batch that ledgered SOME candidates while others are
164
+ // still pending keeps the same review_required visibility as the fresh path.
165
+ if (autoIntakeEnabled && parked.length > 0) {
166
+ return {
167
+ status: 'degraded',
168
+ painId,
169
+ taskId,
170
+ runId,
171
+ artifactId,
172
+ candidateIds,
173
+ ledgerEntryIds,
174
+ message: `Task already succeeded; reuse_review_required:${parked.join(',')} — pending candidates await Owner review; nextAction: pd candidate review --candidate-id <id> --decide reuse|create`,
175
+ };
176
+ }
119
177
  return {
120
178
  status: 'succeeded',
121
179
  painId,
@@ -1 +1 @@
1
- {"version":3,"file":"bridge-result-shaper.js","sourceRoot":"","sources":["../../src/runtime-v2/bridge-result-shaper.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"bridge-result-shaper.js","sourceRoot":"","sources":["../../src/runtime-v2/bridge-result-shaper.ts"],"names":[],"mappings":"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"}
@@ -45,6 +45,8 @@ import type { LedgerAdapter, LedgerPrincipleEntry, Recommendation } from './cand
45
45
  import { CandidateIntakeError } from './candidate-intake.js';
46
46
  import type { RuntimeStateManager } from './store/runtime-state-manager.js';
47
47
  import type { ReuseDecision, ReuseProposal } from './principle-reuse/reuse-proposal.js';
48
+ import type { ReuseEvaluationOutputV1 } from './principle-reuse/reuse-evaluation-output.js';
49
+ import type { ReuseCandidateInput } from './principle-reuse/reuse-domain.js';
48
50
  import type { ReuseEvidenceEntry } from './types/principle-schema.js';
49
51
  /**
50
52
  * Why the Principle Ledger write boundary refused a candidate (Phase 1 / PR1).
@@ -72,8 +74,15 @@ import type { ReuseEvidenceEntry } from './types/principle-schema.js';
72
74
  * yet, so this cannot surface today; but any consumer that renders
73
75
  * refusal-specific operator messages MUST branch on
74
76
  * `reason === 'reuse_selected'` BEFORE production reuse is enabled.
77
+ * - `reuse_pending_owner` (PRI-917 v0.3.3, OD-PRI917-05) — the Reuse Review
78
+ * Gate ran the semantic evaluation on the AUTO path (no Owner decision
79
+ * function present) and the recommendation was `reuse` with a selection
80
+ * inside the proposal. No Principle is created, NO evidence is written, and
81
+ * the candidate stays PENDING for the Owner. Consumers MUST branch on this
82
+ * reason: unlike every other refusal, the candidate is intentionally kept
83
+ * pending — do not mark it consumed and do not seed internalization for it.
75
84
  */
76
- export type LedgerRefusalReason = 'non_principle_kind' | 'unknown_kind' | 'reuse_selected';
85
+ export type LedgerRefusalReason = 'non_principle_kind' | 'unknown_kind' | 'reuse_selected' | 'reuse_pending_owner';
77
86
  /**
78
87
  * Explicit disposition returned by {@link CandidateIntakeService.intake}.
79
88
  *
@@ -109,6 +118,26 @@ export type CandidateIntakeResult = {
109
118
  * resolved into `selectedPrincipleId` instead of creating a Principle.
110
119
  */
111
120
  readonly reuseEvidence?: ReuseEvidenceEntry;
121
+ /**
122
+ * PRI-917 v0.3.3 — present only when `reason === 'reuse_pending_owner'`:
123
+ * the semantic recommendation that triggered the park. NEVER persisted —
124
+ * the Owner re-evaluates via `pd candidate review` (T7: proposal output
125
+ * cannot mutate ledger/evidence; the park itself writes nothing).
126
+ */
127
+ readonly reuseRecommendation?: ReuseRecommendationOutcome;
128
+ };
129
+ /**
130
+ * PRI-917 v0.3.3 — outcome of the auto-path Reuse Review Gate's evaluation
131
+ * hook. `recommended` wraps the validated `reuse-evaluation-output-v1` payload
132
+ * (a RECOMMENDATION — never a decision); `unavailable` is the observable
133
+ * degradation that lets CREATE proceed (SPEC v0.3.3 Rule 4: evaluation failure
134
+ * must never block learning).
135
+ */
136
+ export type ReuseRecommendationOutcome = ({
137
+ status: 'recommended';
138
+ } & ReuseEvaluationOutputV1) | {
139
+ status: 'unavailable';
140
+ reason: string;
112
141
  };
113
142
  /**
114
143
  * PRI-917 Slice 2 — what the reuse gate did before a ledger write.
@@ -119,8 +148,12 @@ export type CandidateIntakeResult = {
119
148
  * - `no_candidates` — the gate ran and found nothing credible.
120
149
  * - `create_decided` — the gate found candidates and the decision function
121
150
  * answered `create`; the write proceeded exactly as before.
151
+ * - `recommended_create` (PRI-917 v0.3.3) — the auto-path recommendation hook
152
+ * ran and did NOT answer reuse (create / uncertain / unavailable / selection
153
+ * outside the proposal); the write proceeded per Rules 2-4. Never emitted
154
+ * when only an Owner decision function is configured.
122
155
  */
123
- export type ReuseCheckOutcome = 'not_configured' | 'no_candidates' | 'create_decided';
156
+ export type ReuseCheckOutcome = 'not_configured' | 'no_candidates' | 'create_decided' | 'recommended_create';
124
157
  export interface CandidateIntakeServiceOptions {
125
158
  stateManager: RuntimeStateManager;
126
159
  ledgerAdapter: LedgerAdapter;
@@ -132,12 +165,25 @@ export interface CandidateIntakeServiceOptions {
132
165
  * `reuseCheck: 'not_configured'` so "no gate" is never mistaken for "no
133
166
  * duplicate found".
134
167
  *
168
+ * Mutually exclusive with reuseRecommendation; supplying both fails at construction.
135
169
  * When provided, it receives the bounded proposal and MUST answer `reuse`
136
170
  * or `create` — there is no automatic option and no default.
137
171
  */
138
172
  reuseDecision?: (proposal: ReuseProposal) => ReuseDecision;
139
173
  /** Workspace state dir used for read-only reuse retrieval. */
140
174
  reuseStateDir?: string;
175
+ /**
176
+ * PRI-917 v0.3.3 (OD-PRI917-05) — optional auto-path Reuse Review Gate
177
+ * recommendation hook. When injected (WITHOUT `reuseDecision`), the gate
178
+ * runs the semantic evaluation for every candidate with a credible proposal
179
+ * and parks the candidate (`refused/reuse_pending_owner`) when the
180
+ * recommendation is `reuse` with a selection inside the proposal. Any other
181
+ * outcome (create / uncertain / unavailable / throw) degrades to CREATE —
182
+ * evaluation failure must never block learning (Rule 4). The hook is a
183
+ * RECOMMENDATION channel only: it can never write, and it is never injected
184
+ * together with an Owner decision function by production callers.
185
+ */
186
+ reuseRecommendation?: (claim: ReuseCandidateInput, proposal: ReuseProposal) => Promise<ReuseRecommendationOutcome>;
141
187
  }
142
188
  /**
143
189
  * PRI-917 PR3A (surface audit C4): the candidate-semantics extraction shared
@@ -189,5 +235,9 @@ export declare class CandidateIntakeService {
189
235
  * - LEDGER_WRITE_FAILED when ledger write fails
190
236
  */
191
237
  intake(candidateId: string): Promise<CandidateIntakeResult>;
238
+ /** Whether the automatic recommendation channel is configured for read-only review. */
239
+ get reuseReviewEnabled(): boolean;
240
+ /** Uses the same input validation and gate as intake, but never writes. */
241
+ reviewReuse(candidateId: string): Promise<CandidateIntakeResult | null>;
192
242
  }
193
243
  //# sourceMappingURL=candidate-intake-service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"candidate-intake-service.d.ts","sourceRoot":"","sources":["../../src/runtime-v2/candidate-intake-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACjG,OAAO,EAAE,oBAAoB,EAAwD,MAAM,uBAAuB,CAAC;AACnH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAQ5E,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAC;AACxF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAEtE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,MAAM,mBAAmB,GAAG,oBAAoB,GAAG,cAAc,GAAG,gBAAgB,CAAC;AAE3F;;;;;;GAMG;AACH,MAAM,MAAM,qBAAqB,GAC7B;IACE,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,iFAAiF;IACjF,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,oBAAoB,CAAC;IACrC;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,iBAAiB,CAAC;CACzC,GACD;IACE,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,mBAAmB,CAAC;IACrC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,wFAAwF;IACxF,QAAQ,CAAC,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,yEAAyE;IACzE,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;IACvC,yEAAyE;IACzE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IACtC;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,kBAAkB,CAAC;CAC7C,CAAC;AAEN;;;;;;;;;GASG;AACH,MAAM,MAAM,iBAAiB,GAAG,gBAAgB,GAAG,eAAe,GAAG,gBAAgB,CAAC;AAGtF,MAAM,WAAW,6BAA6B;IAC5C,YAAY,EAAE,mBAAmB,CAAC;IAClC,aAAa,EAAE,aAAa,CAAC;IAC7B;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,aAAa,KAAK,aAAa,CAAC;IAC3D,8DAA8D;IAC9D,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAyDD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,iCAAiC,GACzC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,cAAc,EAAE,cAAc,CAAA;CAAE,GAC5C;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,oBAAoB,CAAA;CAAE,CAAC;AAE/C,wBAAgB,2BAA2B,CACzC,SAAS,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,wBAAwB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EAC5E,QAAQ,EAAE;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,GACvC,iCAAiC,CAgEnC;AAED,qBAAa,sBAAsB;;IAMjC,YAAY,IAAI,EAAE,6BAA6B,EAK9C;IAED;;;;;;;;;;;;;;;;OAgBG;IACG,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CA0ShE;CAuDF"}
1
+ {"version":3,"file":"candidate-intake-service.d.ts","sourceRoot":"","sources":["../../src/runtime-v2/candidate-intake-service.ts"],"names":[],"mappings":"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"}
@@ -169,13 +169,38 @@ export class CandidateIntakeService {
169
169
  #stateManager;
170
170
  #ledgerAdapter;
171
171
  #reuseDecision;
172
+ #reuseRecommendation;
172
173
  #reuseStateDir;
173
174
  constructor(opts) {
175
+ if (opts.reuseDecision && opts.reuseRecommendation) {
176
+ throw new CandidateIntakeError(INTAKE_ERROR_CODES.REUSE_CHECK_FAILED, 'reuseDecision and reuseRecommendation are mutually exclusive. nextAction: supply reuseRecommendation for automatic intake or reuseDecision for Owner decisions, never both.', { reason: 'reuse_gate_misconfigured' });
177
+ }
174
178
  this.#stateManager = opts.stateManager;
175
179
  this.#ledgerAdapter = opts.ledgerAdapter;
176
180
  this.#reuseDecision = opts.reuseDecision;
181
+ this.#reuseRecommendation = opts.reuseRecommendation;
177
182
  this.#reuseStateDir = opts.reuseStateDir;
178
183
  }
184
+ /**
185
+ * PRI-917 v0.3.3 — run the auto-path recommendation hook. A throwing hook
186
+ * (buggy builder, adapter crash) is an `unavailable` recommendation, NOT an
187
+ * intake failure: Rule 4 forbids evaluation failures from blocking learning.
188
+ */
189
+ async #runReuseRecommendation(claim, proposal) {
190
+ const hook = this.#reuseRecommendation;
191
+ if (!hook) {
192
+ return { status: 'unavailable', reason: 'reuse recommendation hook is not configured' };
193
+ }
194
+ try {
195
+ return await hook(claim, proposal);
196
+ }
197
+ catch (err) {
198
+ return {
199
+ status: 'unavailable',
200
+ reason: err instanceof Error ? err.message : String(err),
201
+ };
202
+ }
203
+ }
179
204
  /**
180
205
  * Consume a pending candidate: load it and its artifact, build a
181
206
  * LedgerPrincipleEntry, and write it to the ledger via the adapter — but ONLY
@@ -194,6 +219,19 @@ export class CandidateIntakeService {
194
219
  * - LEDGER_WRITE_FAILED when ledger write fails
195
220
  */
196
221
  async intake(candidateId) {
222
+ return this.#intake(candidateId, false);
223
+ }
224
+ /** Whether the automatic recommendation channel is configured for read-only review. */
225
+ get reuseReviewEnabled() {
226
+ return this.#reuseRecommendation !== undefined && !this.#reuseDecision;
227
+ }
228
+ /** Uses the same input validation and gate as intake, but never writes. */
229
+ async reviewReuse(candidateId) {
230
+ if (!this.reuseReviewEnabled)
231
+ return null;
232
+ return this.#intake(candidateId, true);
233
+ }
234
+ async #intake(candidateId, reviewOnly) {
197
235
  // 1. Input validation (E-01)
198
236
  if (!candidateId || typeof candidateId !== 'string' || candidateId.trim() === '') {
199
237
  throw new CandidateIntakeError(INTAKE_ERROR_CODES.INPUT_INVALID, 'candidateId must be a non-empty string', { candidateId });
@@ -310,15 +348,19 @@ export class CandidateIntakeService {
310
348
  if (this.#reuseDecision && !this.#reuseStateDir) {
311
349
  throw new CandidateIntakeError(INTAKE_ERROR_CODES.REUSE_CHECK_FAILED, `Reuse gate is misconfigured: reuseDecision was provided without reuseStateDir; no Principle was created for candidate ${candidateId}. nextAction: pass reuseStateDir, or remove reuseDecision to disable the gate.`, { candidateId, reason: 'reuse_gate_misconfigured' });
312
350
  }
351
+ if (this.#reuseRecommendation && !this.#reuseStateDir) {
352
+ throw new CandidateIntakeError(INTAKE_ERROR_CODES.REUSE_CHECK_FAILED, `Reuse gate is misconfigured: reuseRecommendation was provided without reuseStateDir; no Principle was created for candidate ${candidateId}. nextAction: pass reuseStateDir, or remove reuseRecommendation to disable the gate.`, { candidateId, reason: 'reuse_gate_misconfigured' });
353
+ }
313
354
  let reuseCheck = 'not_configured';
314
- if (this.#reuseDecision && this.#reuseStateDir) {
355
+ if ((this.#reuseDecision || this.#reuseRecommendation) && this.#reuseStateDir) {
356
+ const reuseClaim = {
357
+ text: recommendation.text || candidate.description || '',
358
+ triggerPattern: recommendation.triggerPattern ?? '',
359
+ action: recommendation.action ?? '',
360
+ };
315
361
  let proposal;
316
362
  try {
317
- proposal = buildReuseProposal(candidateId, buildReuseShortlist({
318
- text: recommendation.text || candidate.description || '',
319
- triggerPattern: recommendation.triggerPattern ?? '',
320
- action: recommendation.action ?? '',
321
- }, this.#reuseStateDir,
363
+ proposal = buildReuseProposal(candidateId, buildReuseShortlist(reuseClaim, this.#reuseStateDir,
322
364
  // Single truth: the persisted candidate's own kind. Step 3b has
323
365
  // already proved eligibility fail-closed, so this is a restatement,
324
366
  // never a second source a caller can override.
@@ -328,88 +370,131 @@ export class CandidateIntakeService {
328
370
  throw new CandidateIntakeError(INTAKE_ERROR_CODES.REUSE_CHECK_FAILED, `Reuse check could not be completed for candidate ${candidateId}; no Principle was created. Cause: ${reuseErr instanceof Error ? reuseErr.message : String(reuseErr)}`, { candidateId, cause: reuseErr });
329
371
  }
330
372
  if (proposalNeedsDecision(proposal)) {
331
- // PRI-917 hardening: the decision function is an UNTRUSTED boundary
332
- // (an AI Owner parses model output), so its return value is validated
333
- // at runtime. Previously anything that was not exactly 'reuse' fell
334
- // through to 'create' — a malformed answer silently manufactured the
335
- // very duplicate this gate exists to prevent (rc-1/rc-2/rc-3).
336
- let decision;
337
- try {
338
- const raw = this.#reuseDecision(proposal);
339
- const validated = validateReuseDecision(raw);
340
- if (!validated.ok) {
341
- throw new CandidateIntakeError(INTAKE_ERROR_CODES.REUSE_CHECK_FAILED, `Reuse decision for candidate ${candidateId} is not a valid reuse|create decision (${validated.reason}); no Principle was created.`, { candidateId, reason: validated.reason });
373
+ // PRI-917 v0.3.3 (OD-PRI917-05) — AUTO-path Reuse Review Gate.
374
+ //
375
+ // Runs only when a recommendation hook is injected WITHOUT an Owner
376
+ // decision function (the production auto shape: bridge / diagnose /
377
+ // pain-retry). The hook is a RECOMMENDATION channel: `reuse` parks the
378
+ // candidate for the Owner (refused/reuse_pending_owner — no Principle,
379
+ // no evidence, candidate stays pending); every other outcome
380
+ // (create / uncertain / unavailable / selection outside the proposal)
381
+ // proceeds to CREATE exactly as the unconfigured gate would (Rules
382
+ // 2-4: uncertain keeps creating in phase 1; evaluation failure must
383
+ // never block learning; a hallucinated id is treated as unavailable).
384
+ if (this.#reuseRecommendation && !this.#reuseDecision) {
385
+ const recommendationOutcome = await this.#runReuseRecommendation(reuseClaim, proposal);
386
+ if (recommendationOutcome.status === 'recommended') {
387
+ const { recommendation: verdict, selectedPrincipleId, confidence } = recommendationOutcome;
388
+ if (verdict === 'reuse' &&
389
+ selectedPrincipleId !== undefined &&
390
+ proposal.candidates.some((entry) => entry.principleId === selectedPrincipleId)) {
391
+ return {
392
+ outcome: 'refused',
393
+ reason: 'reuse_pending_owner',
394
+ candidateId,
395
+ rawRecommendationKind: typeof candidate.rawRecommendationKind === 'string' ? candidate.rawRecommendationKind : null,
396
+ message: `Candidate ${candidateId} is a suspected duplicate of existing Principle ${selectedPrincipleId} ` +
397
+ `(semantic reuse recommendation, confidence ${confidence}). No Principle was created and no evidence ` +
398
+ 'was written; the candidate stays PENDING for the Owner. ' +
399
+ `nextAction: pd candidate review --candidate-id ${candidateId} --decide reuse|create --reason "..."`,
400
+ reuseProposal: proposal,
401
+ reuseRecommendation: recommendationOutcome,
402
+ };
403
+ }
342
404
  }
343
- ({ decision } = validated);
405
+ // create / uncertain / unavailable / hallucinated id → Rules 2/3/4:
406
+ // CREATE proceeds (uncertain keeps creating in phase 1; evaluation
407
+ // failure must never block learning).
408
+ reuseCheck = 'recommended_create';
344
409
  }
345
- catch (decisionErr) {
346
- if (decisionErr instanceof CandidateIntakeError)
347
- throw decisionErr;
348
- // A throwing decision function (LLM timeout, parse crash) is a reuse
349
- // check failure, not an internal error: it must fail closed with a
350
- // candidate-scoped code rather than escaping as an arbitrary throw.
351
- throw new CandidateIntakeError(INTAKE_ERROR_CODES.REUSE_CHECK_FAILED, `Reuse decision for candidate ${candidateId} failed (${decisionErr instanceof Error ? decisionErr.message : String(decisionErr)}); no Principle was created.`, { candidateId, cause: decisionErr });
352
- }
353
- if (decision.decision === 'reuse') {
354
- const known = new Set(proposal.candidates.map((c) => c.principleId));
355
- if (!known.has(decision.selectedPrincipleId)) {
356
- // INV-R08: a selection outside the proposal fails closed. It must
357
- // NOT silently degrade into "create". This is a defect in the
358
- // reuse decision contract, not a malformed intake input, so it
359
- // carries REUSE_CHECK_FAILED rather than INPUT_INVALID — the
360
- // latter would file a governance error under "bad input".
361
- throw new CandidateIntakeError(INTAKE_ERROR_CODES.REUSE_CHECK_FAILED, `Reuse decision selected principle '${decision.selectedPrincipleId}' which is not in the proposal for candidate ${candidateId}; no Principle was created.`, { candidateId, selectedPrincipleId: decision.selectedPrincipleId, reason: 'selected_principle_not_in_proposal' });
362
- }
363
- // PRI-917 PR3B Phase 2 — materialise the verdict as durable evidence
364
- // BEFORE reporting it. A reuse verdict that is not persisted would
365
- // leave the resolution invisible: replay would re-ask the Owner, and
366
- // the SPEC §11/§12 asymmetry (REUSE must be auditable, CREATE's
367
- // artifact speaks for itself) would silently break. The write runs
368
- // INSIDE the ledger's single-writer lock (appendReuseEvidence) and is
369
- // fail-closed: an integrity/shape failure propagates and the
370
- // candidate stays pending — it NEVER degrades into creating a
371
- // Principle (INV-R03/INV-R08).
372
- const painId = await this.#resolveReusePainId(candidateId, candidate);
373
- let evidence;
410
+ else if (this.#reuseDecision) {
411
+ // PRI-917 hardening: the decision function is an UNTRUSTED boundary
412
+ // (an AI Owner parses model output), so its return value is validated
413
+ // at runtime. Previously anything that was not exactly 'reuse' fell
414
+ // through to 'create' — a malformed answer silently manufactured the
415
+ // very duplicate this gate exists to prevent (rc-1/rc-2/rc-3).
416
+ let decision;
374
417
  try {
375
- const appended = this.#ledgerAdapter.appendReuseEvidence(decision.selectedPrincipleId, {
376
- painId,
377
- candidateId,
378
- decision: 'reuse',
379
- actor: decision.actor,
380
- reason: decision.reason,
381
- decidedAt: decision.decidedAt,
382
- });
383
- // The writer either appended this candidate's entry or returned the
384
- // already-recorded one (concurrent-intake dedupe) — the entry for
385
- // THIS candidateId must be present; anything else is a broken
386
- // adapter and fails loud instead of reporting a fabricated record.
387
- const recorded = appended.reuseEvidence.find((e) => e.candidateId === candidateId);
388
- if (!recorded) {
389
- throw new Error(`appendReuseEvidence returned no evidence entry for candidate ${candidateId} (adapter contract violation)`);
418
+ const raw = this.#reuseDecision(proposal);
419
+ const validated = validateReuseDecision(raw);
420
+ if (!validated.ok) {
421
+ throw new CandidateIntakeError(INTAKE_ERROR_CODES.REUSE_CHECK_FAILED, `Reuse decision for candidate ${candidateId} is not a valid reuse|create decision (${validated.reason}); no Principle was created.`, { candidateId, reason: validated.reason });
390
422
  }
391
- evidence = recorded;
423
+ ({ decision } = validated);
392
424
  }
393
- catch (evidenceErr) {
394
- throw new CandidateIntakeError(INTAKE_ERROR_CODES.REUSE_CHECK_FAILED, `Reuse decision for candidate ${candidateId} could not be recorded on principle '${decision.selectedPrincipleId}'; no Principle was created and the decision is not durable. Cause: ${evidenceErr instanceof Error ? evidenceErr.message : String(evidenceErr)}`, { candidateId, selectedPrincipleId: decision.selectedPrincipleId, cause: evidenceErr });
425
+ catch (decisionErr) {
426
+ if (decisionErr instanceof CandidateIntakeError)
427
+ throw decisionErr;
428
+ // A throwing decision function (LLM timeout, parse crash) is a reuse
429
+ // check failure, not an internal error: it must fail closed with a
430
+ // candidate-scoped code rather than escaping as an arbitrary throw.
431
+ throw new CandidateIntakeError(INTAKE_ERROR_CODES.REUSE_CHECK_FAILED, `Reuse decision for candidate ${candidateId} failed (${decisionErr instanceof Error ? decisionErr.message : String(decisionErr)}); no Principle was created.`, { candidateId, cause: decisionErr });
395
432
  }
396
- return {
397
- outcome: 'refused',
398
- reason: 'reuse_selected',
399
- candidateId,
400
- rawRecommendationKind: typeof candidate.rawRecommendationKind === 'string' ? candidate.rawRecommendationKind : null,
401
- message: `Candidate ${candidateId} was resolved to existing Principle ${decision.selectedPrincipleId}; the reuse decision was recorded on that Principle's reuseEvidence and no new Principle was created. This candidate DOES target the Principle Ledger — review the reuse proposal, and do not re-run intake to force a new Principle.`,
402
- reuseProposal: proposal,
403
- selectedPrincipleId: decision.selectedPrincipleId,
404
- reuseEvidence: evidence,
405
- };
433
+ if (decision.decision === 'reuse') {
434
+ const known = new Set(proposal.candidates.map((c) => c.principleId));
435
+ if (!known.has(decision.selectedPrincipleId)) {
436
+ // INV-R08: a selection outside the proposal fails closed. It must
437
+ // NOT silently degrade into "create". This is a defect in the
438
+ // reuse decision contract, not a malformed intake input, so it
439
+ // carries REUSE_CHECK_FAILED rather than INPUT_INVALID — the
440
+ // latter would file a governance error under "bad input".
441
+ throw new CandidateIntakeError(INTAKE_ERROR_CODES.REUSE_CHECK_FAILED, `Reuse decision selected principle '${decision.selectedPrincipleId}' which is not in the proposal for candidate ${candidateId}; no Principle was created.`, { candidateId, selectedPrincipleId: decision.selectedPrincipleId, reason: 'selected_principle_not_in_proposal' });
442
+ }
443
+ // PRI-917 PR3B Phase 2 — materialise the verdict as durable evidence
444
+ // BEFORE reporting it. A reuse verdict that is not persisted would
445
+ // leave the resolution invisible: replay would re-ask the Owner, and
446
+ // the SPEC §11/§12 asymmetry (REUSE must be auditable, CREATE's
447
+ // artifact speaks for itself) would silently break. The write runs
448
+ // INSIDE the ledger's single-writer lock (appendReuseEvidence) and is
449
+ // fail-closed: an integrity/shape failure propagates and the
450
+ // candidate stays pending — it NEVER degrades into creating a
451
+ // Principle (INV-R03/INV-R08).
452
+ const painId = await this.#resolveReusePainId(candidateId, candidate);
453
+ let evidence;
454
+ try {
455
+ const appended = this.#ledgerAdapter.appendReuseEvidence(decision.selectedPrincipleId, {
456
+ painId,
457
+ candidateId,
458
+ decision: 'reuse',
459
+ actor: decision.actor,
460
+ reason: decision.reason,
461
+ decidedAt: decision.decidedAt,
462
+ });
463
+ // The writer either appended this candidate's entry or returned the
464
+ // already-recorded one (concurrent-intake dedupe) — the entry for
465
+ // THIS candidateId must be present; anything else is a broken
466
+ // adapter and fails loud instead of reporting a fabricated record.
467
+ const recorded = appended.reuseEvidence.find((e) => e.candidateId === candidateId);
468
+ if (!recorded) {
469
+ throw new Error(`appendReuseEvidence returned no evidence entry for candidate ${candidateId} (adapter contract violation)`);
470
+ }
471
+ evidence = recorded;
472
+ }
473
+ catch (evidenceErr) {
474
+ throw new CandidateIntakeError(INTAKE_ERROR_CODES.REUSE_CHECK_FAILED, `Reuse decision for candidate ${candidateId} could not be recorded on principle '${decision.selectedPrincipleId}'; no Principle was created and the decision is not durable. Cause: ${evidenceErr instanceof Error ? evidenceErr.message : String(evidenceErr)}`, { candidateId, selectedPrincipleId: decision.selectedPrincipleId, cause: evidenceErr });
475
+ }
476
+ return {
477
+ outcome: 'refused',
478
+ reason: 'reuse_selected',
479
+ candidateId,
480
+ rawRecommendationKind: typeof candidate.rawRecommendationKind === 'string' ? candidate.rawRecommendationKind : null,
481
+ message: `Candidate ${candidateId} was resolved to existing Principle ${decision.selectedPrincipleId}; the reuse decision was recorded on that Principle's reuseEvidence and no new Principle was created. This candidate DOES target the Principle Ledger — review the reuse proposal, and do not re-run intake to force a new Principle.`,
482
+ reuseProposal: proposal,
483
+ selectedPrincipleId: decision.selectedPrincipleId,
484
+ reuseEvidence: evidence,
485
+ };
486
+ }
487
+ reuseCheck = 'create_decided';
406
488
  }
407
- reuseCheck = 'create_decided';
408
489
  }
409
490
  else {
410
491
  reuseCheck = 'no_candidates';
411
492
  }
412
493
  }
494
+ // Replay review is read-only: create/uncertain/unavailable never resumes a
495
+ // ledger write, and the Owner verdict path is excluded by reviewReuse.
496
+ if (reviewOnly)
497
+ return null;
413
498
  // 5. Build 11-field LedgerPrincipleEntry (E-06)
414
499
  const entry = {
415
500
  id: randomUUID(),
@@ -466,6 +551,26 @@ export class CandidateIntakeService {
466
551
  catch (err) {
467
552
  throw new CandidateIntakeError(INTAKE_ERROR_CODES.REUSE_CHECK_FAILED, `Reuse decision for candidate ${candidateId} cannot be recorded: the pain diagnosis lookup failed. Cause: ${err instanceof Error ? err.message : String(err)}`, { candidateId, reason: 'reuse_pain_unresolvable', cause: err });
468
553
  }
554
+ if (diagnoses.length === 0) {
555
+ // PRI-917 v0.3.3 replay fix (R6 reality replay finding): production
556
+ // candidates are minted on the diag_router SUB-task, while pain_diagnoses
557
+ // rows are persisted under the TOP diagnostician task — a lookup by the
558
+ // candidate's own taskId can therefore NEVER resolve, making every fresh
559
+ // reuse decision fail closed. Walk the canonical router → diagnostician
560
+ // chain (same discipline as the seeding-side resolver: verify the task
561
+ // kind, read diagnosisId from the router run's validated outputPayload)
562
+ // and retry the lookup against the top task. rc-6: the walk only follows
563
+ // real task/run records, never parses ids out of task-id strings.
564
+ const topTaskId = await this.#resolveTopDiagnosticianTaskId(candidate);
565
+ if (topTaskId !== null) {
566
+ try {
567
+ diagnoses = await this.#stateManager.getDiagnosesByTaskId(topTaskId);
568
+ }
569
+ catch (err) {
570
+ throw new CandidateIntakeError(INTAKE_ERROR_CODES.REUSE_CHECK_FAILED, `Reuse decision for candidate ${candidateId} cannot be recorded: the pain diagnosis lookup for the top diagnostician task failed. Cause: ${err instanceof Error ? err.message : String(err)}`, { candidateId, reason: 'reuse_pain_unresolvable', cause: err });
571
+ }
572
+ }
573
+ }
469
574
  if (diagnoses.length === 0) {
470
575
  refuse('no persisted pain diagnosis exists for its task');
471
576
  }
@@ -483,5 +588,25 @@ export class CandidateIntakeService {
483
588
  }
484
589
  return latestPainId;
485
590
  }
591
+ /**
592
+ * PRI-917 v0.3.3 replay fix — resolve the TOP diagnostician task behind a
593
+ * diag_router sub-task. The sub-task's canonical parent link is its
594
+ * `inputRef` (verified against a real diagnostician task record — rc-6: the
595
+ * walk only follows persisted task records, never parses ids out of
596
+ * task-id strings). Returns `null` for any non-router task or broken link
597
+ * (the caller keeps the direct-lookup behavior).
598
+ */
599
+ async #resolveTopDiagnosticianTaskId(candidate) {
600
+ const task = await this.#stateManager.getTask(candidate.taskId);
601
+ if (!task || task.taskKind !== 'diag_router')
602
+ return null;
603
+ const parentTaskId = typeof task.inputRef === 'string' ? task.inputRef.trim() : '';
604
+ if (parentTaskId === '')
605
+ return null;
606
+ const topTask = await this.#stateManager.getTask(parentTaskId);
607
+ if (!topTask || topTask.taskKind !== 'diagnostician')
608
+ return null;
609
+ return topTask.taskId;
610
+ }
486
611
  }
487
612
  //# sourceMappingURL=candidate-intake-service.js.map