/** * CandidateIntakeService — consumes pending candidates and writes ledger entries. * * Workflow: * 1. Validate input * 2. Check idempotency (existsForCandidate) — O(1) lookup * 2b. Check durable reuse resolution replay (PRI-917 PR3B Phase 2) — * a candidate already resolved into an existing Principle returns that * resolution; no decision is re-asked, no Principle is created * 3. Load candidate from DB (RuntimeStateManager) * 3b. ★ Principle Ledger write boundary (Phase 1 / PR1) — see below * 4. Load artifact from DB and parse recommendation * 5. Build 11-field LedgerPrincipleEntry * 6. Write via adapter.writeProbationEntry() * * 4d (PRI-917 Slice 2 + PR3B Phase 2) — the reuse gate sits between 4c and 5: * a credible proposal + a `reuse` verdict is materialised as durable evidence * on the selected Principle (appendReuseEvidence) and reported as a * `reuse_selected` refusal; a `create` verdict proceeds exactly as before. * * ── Principle Ledger write boundary (Phase 1 / PR1) ───────────────────────── * This service is the single shared chokepoint through which EVERY * candidate-origin write to the Principle Ledger flows (pd-cli `candidate * intake` / `repair` / `backfill`, `diagnose --intake`, `pain-retry`, and the * automatic PainSignalBridge path). The boundary is therefore enforced HERE, * once, rather than at each call site. * * Only a candidate whose persisted `recommendation_kind` is EXACTLY * `'principle'` may write the ledger. Every other kind (`rule` / `prompt` / * `implementation` / `defer`) and every unknown / missing / malformed value is * refused FAIL CLOSED and reported as an explicit `refused` disposition — the * refusal never collapses into a principle, and it never throws, so sibling * candidates in a batch keep their existing persistence / routing / defer * behaviour. * * On error: candidate stays `pending`, throws CandidateIntakeError. * Idempotent: if adapter already has entry for candidate, returns it (no-op). * * Non-goals (M7): * - No DB status update to 'consumed' (m7-04 CLI handler does that) * - No promotion to active principle (M8+) * - No pain signal bridge */ import type { LedgerAdapter, LedgerPrincipleEntry, Recommendation } from './candidate-intake.js'; import { CandidateIntakeError } from './candidate-intake.js'; import type { RuntimeStateManager } from './store/runtime-state-manager.js'; import type { ReuseDecision, ReuseProposal } from './principle-reuse/reuse-proposal.js'; import type { ReuseEvaluationOutputV1 } from './principle-reuse/reuse-evaluation-output.js'; import type { ReuseCandidateInput } from './principle-reuse/reuse-domain.js'; import type { ReuseEvidenceEntry } from './types/principle-schema.js'; /** * Why the Principle Ledger write boundary refused a candidate (Phase 1 / PR1). * * - `non_principle_kind` — a *valid* kind that simply does not target the * Principle Ledger (`rule` / `prompt` / `implementation` / `defer`). The * candidate keeps flowing through its existing route / defer handling. * - `unknown_kind` — no usable kind at all (missing / non-string / not in the * known set). This is the case that previously FAILED OPEN into a principle. * - `reuse_selected` (PRI-917 Slice 2) — the reuse gate produced a credible * proposal and the injected decision function answered `reuse`, so no new * Principle was written. Encoded as a refusal rather than a new result * variant, which keeps the result-union shape stable for every consumer. * PRI-917 PR3B Phase 2: the verdict is now ALSO materialised as durable * evidence on the selected Principle (`reuseEvidence[]`) before this * disposition is returned, and this reason doubles as the replay * disposition when a candidate's resolution already exists (no new decision * is asked, no Principle is created — INV-R07). * * ⚠ That is shape compatibility ONLY, not message compatibility. Known * counter-example (verified, not fixed here): `pd candidate` presents ANY * refusal as "this candidate kind does not target the Principle Ledger" and * re-throws it as INPUT_INVALID — both wrong for a reuse decision, which * DOES target the Ledger. No production caller injects `reuseDecision` * yet, so this cannot surface today; but any consumer that renders * refusal-specific operator messages MUST branch on * `reason === 'reuse_selected'` BEFORE production reuse is enabled. * - `reuse_pending_owner` (PRI-917 v0.3.3, OD-PRI917-05) — the Reuse Review * Gate ran the semantic evaluation on the AUTO path (no Owner decision * function present) and the recommendation was `reuse` with a selection * inside the proposal. No Principle is created, NO evidence is written, and * the candidate stays PENDING for the Owner. Consumers MUST branch on this * reason: unlike every other refusal, the candidate is intentionally kept * pending — do not mark it consumed and do not seed internalization for it. */ export type LedgerRefusalReason = 'non_principle_kind' | 'unknown_kind' | 'reuse_selected' | 'reuse_pending_owner'; /** * Explicit disposition returned by {@link CandidateIntakeService.intake}. * * `intake()` intentionally does NOT return a bare `LedgerPrincipleEntry`: a * refused ledger write must never be indistinguishable from a successful one * (rc-9-no-silent-fallback). */ export type CandidateIntakeResult = { readonly outcome: 'ledger_entry'; /** `true` when THIS call wrote a new entry; `false` for the idempotent no-op. */ readonly written: boolean; readonly entry: LedgerPrincipleEntry; /** * PRI-917 Slice 2 — what the reuse gate did, for observability. * Optional so every existing producer of this variant stays valid. */ readonly reuseCheck?: ReuseCheckOutcome; /** * PRI-939 Option A — present ONLY when the auto-path recommendation hook * ran and the write proceeded under Rules 2-4 (reuseCheck * `recommended_create`): `unavailable` is the observable degradation * (evaluation failed — Owner can see why no duplicate protection ran), * `recommended` means the hook answered create/uncertain. The park shape * carries its own outcome on the refused variant; a recorded reuse * resolution (replay) never runs the hook and stays undefined. */ readonly reuseRecommendation?: ReuseRecommendationOutcome; } | { readonly outcome: 'refused'; readonly reason: LedgerRefusalReason; readonly candidateId: string; /** The raw persisted kind, or `null` when it was absent / not a string / not loaded. */ readonly rawRecommendationKind: string | null; readonly message: string; /** PRI-917 Slice 2 — present only when `reason === 'reuse_selected'`. */ readonly reuseProposal?: ReuseProposal; /** The Principle the decision function selected, when it chose reuse. */ readonly selectedPrincipleId?: string; /** * PRI-917 PR3B Phase 2 — present only when `reason === 'reuse_selected'`: * the evidence entry just written (fresh decision), or the pre-existing * resolution (replay). This is the durable record that the candidate was * resolved into `selectedPrincipleId` instead of creating a Principle. */ readonly reuseEvidence?: ReuseEvidenceEntry; /** * PRI-917 v0.3.3 — present only when `reason === 'reuse_pending_owner'`: * the semantic recommendation that triggered the park. NEVER persisted — * the Owner re-evaluates via `pd candidate review` (T7: proposal output * cannot mutate ledger/evidence; the park itself writes nothing). */ readonly reuseRecommendation?: ReuseRecommendationOutcome; }; /** * PRI-917 v0.3.3 — outcome of the auto-path Reuse Review Gate's evaluation * hook. `recommended` wraps the validated `reuse-evaluation-output-v1` payload * (a RECOMMENDATION — never a decision); `unavailable` is the observable * degradation that lets CREATE proceed (SPEC v0.3.3 Rule 4: evaluation failure * must never block learning). */ export type ReuseRecommendationOutcome = ({ status: 'recommended'; } & ReuseEvaluationOutputV1) | { status: 'unavailable'; reason: string; }; /** * PRI-917 Slice 2 — what the reuse gate did before a ledger write. * * - `not_configured` — no decision function was injected, so no gate ran. This * is the current production shape; it is reported rather than assumed, so a * caller can never mistake "gate absent" for "gate found nothing". * - `no_candidates` — the gate ran and found nothing credible. * - `create_decided` — the gate found candidates and the decision function * answered `create`; the write proceeded exactly as before. * - `recommended_create` (PRI-917 v0.3.3) — the auto-path recommendation hook * ran and did NOT answer reuse (create / uncertain / unavailable / selection * outside the proposal); the write proceeded per Rules 2-4. Never emitted * when only an Owner decision function is configured. */ export type ReuseCheckOutcome = 'not_configured' | 'no_candidates' | 'create_decided' | 'recommended_create'; export interface CandidateIntakeServiceOptions { stateManager: RuntimeStateManager; ledgerAdapter: LedgerAdapter; /** * PRI-917 Slice 2 — optional reuse decision function. * * When omitted (the current production shape) the reuse gate does not run * and intake behaves exactly as before; the result still reports * `reuseCheck: 'not_configured'` so "no gate" is never mistaken for "no * duplicate found". * * Mutually exclusive with reuseRecommendation; supplying both fails at construction. * When provided, it receives the bounded proposal and MUST answer `reuse` * or `create` — there is no automatic option and no default. */ reuseDecision?: (proposal: ReuseProposal) => ReuseDecision; /** Workspace state dir used for read-only reuse retrieval. */ reuseStateDir?: string; /** * PRI-917 v0.3.3 (OD-PRI917-05) — optional auto-path Reuse Review Gate * recommendation hook. When injected (WITHOUT `reuseDecision`), the gate * runs the semantic evaluation for every candidate with a credible proposal * and parks the candidate (`refused/reuse_pending_owner`) when the * recommendation is `reuse` with a selection inside the proposal. Any other * outcome (create / uncertain / unavailable / throw) degrades to CREATE — * evaluation failure must never block learning (Rule 4). The hook is a * RECOMMENDATION channel only: it can never write, and it is never injected * together with an Owner decision function by production callers. */ reuseRecommendation?: (claim: ReuseCandidateInput, proposal: ReuseProposal) => Promise; } /** * PRI-917 PR3A (surface audit C4): the candidate-semantics extraction shared * by BOTH the intake flow (steps 4b/4c) and the `pd candidate review` * surface — one implementation, so the retrieval input shown to the Owner can * never drift from what intake actually consumes. * * Resolution order (identical to intake steps 4b→4c): * 1. candidate.sourceRecommendationJson — validated directly, then via the * diagnostician description→text normalization; malformed JSON warns and * falls through (rc-9 observable degradation); * 2. artifact.contentJson — the three historical shapes. * * Pure apart from the rc-9 warn; never throws — failures come back as * `{ ok: false, error }` carrying the exact CandidateIntakeError intake * itself would throw. */ export type CandidateRecommendationExtraction = { ok: true; recommendation: Recommendation; } | { ok: false; error: CandidateIntakeError; }; export declare function extractIntakeRecommendation(candidate: { candidateId: string; sourceRecommendationJson?: string | null; }, artifact: { contentJson: string; } | null): CandidateRecommendationExtraction; export declare class CandidateIntakeService { #private; constructor(opts: CandidateIntakeServiceOptions); /** * Consume a pending candidate: load it and its artifact, build a * LedgerPrincipleEntry, and write it to the ledger via the adapter — but ONLY * when the candidate's persisted `recommendation_kind` is exactly * `'principle'` (Principle Ledger write boundary, Phase 1 / PR1). * * @param candidateId - The candidate ID to intake. * @returns `{ outcome: 'ledger_entry' }` with the written (or already * existing) entry, or `{ outcome: 'refused' }` when the write * boundary rejected the candidate's kind. Refusal is a normal * disposition, NOT an error. * @throws CandidateIntakeError with code: * - INPUT_INVALID when candidateId is empty/invalid * - CANDIDATE_NOT_FOUND when candidate does not exist * - ARTIFACT_NOT_FOUND when artifact is missing or unreadable * - LEDGER_WRITE_FAILED when ledger write fails */ intake(candidateId: string): Promise; /** Whether the automatic recommendation channel is configured for read-only review. */ get reuseReviewEnabled(): boolean; /** Uses the same input validation and gate as intake, but never writes. */ reviewReuse(candidateId: string): Promise; } //# sourceMappingURL=candidate-intake-service.d.ts.map