Skip to content

Rulvar API reference


Rulvar API reference / @rulvar/core / FinishValidationSpec

Interface: FinishValidationSpec

Defined in: packages/core/src/orchestrator/orchestrate.ts:642

The opt in deterministic validation of the orchestrator finish result (the v1.40.0 improvement plan's RV-204 slice). Every SCHEMA valid finish({ result }) call first passes the configured host validators; a rejection returns the failure reasons to the model as the call's error tool result and the turn continues (a repair turn: the model fixes the result and calls finish again), bounded by maxRepairs within the composition invocation (RV3602). A rejection past the bound fails the run with the typed FailRunError (code 'fail_run', data.source 'orchestrator_finish_validation'), BEFORE the acceptance settle, so acceptance never judges a finish the validators rejected. Every verdict journals as ONE decision entry keyed by the finish call id (decisionType 'orchestrator_finish_validation'), so a resume rolls the SAME verdicts forward without re-running validator code, and the whole exchange replays without new paid calls. The toolset never changes (the contract rides the orchestrator prompt), zero configuration adds zero journal entries, and the budget cap paths keep their posture: the reserved finalize dispatch is never validated, exactly as acceptance never judges it. Repair turns spend from the orchestrator's ordinary limits and ceilings (maxTurns, budget caps, the root budgetUsd); maxRepairs is the explicit bound, and a dedicated repair budget reserve is deliberately out of scope here.

Properties

PropertyTypeDescriptionDefined in
candidatePersistence?"transcript" | "hash-only"The candidate persistence policy (RV4207, the sixth comparison experiment): ONE declaration that closes the candidate lineage surface, superseding the boolean above (declaring both is a ConfigError; the boolean stays for existing configs). Declared (either mode), EVERY finish-validation decision carries the candidate identity, the ACCEPTED verdict included: the sha256 over the canonical resolved document (the deterministic patch or the sectional splice applied first) and its char count, so the whole chain proposed/repaired/rejected/accepted reads off synthesisCandidatesFromJournal (and rulvar inspect --candidates) by hash, and the accepted hash is the same recipe the claim judge's judgedHash and the audit's auditedHash bind (candidateHashOf: sha256 over the JCS serialization; see verifyCandidateBytes for the audit recipe). Undeclared, the decisions keep their historical bytes exactly (identity on non-accepted verdicts only). 'transcript' additionally retains each REJECTED candidate's bytes as its own addressable blob, byte for byte the retainRejectedCandidates: true behavior. 'hash-only' retains no bytes ON PURPOSE and says so: every non-accepted decision carries bytesUnavailableReason: 'hash-only-persistence', so an auditor finding no blob reads a policy, not an accident; a declared 'transcript' whose store write failed stamps 'store-write-failed' the same way. The experiment's auditor recovered the rejected composition only by digging a binary transcript with no documented recipe; the reason field is the difference between "not retained by declared policy" and "lost".packages/core/src/orchestrator/orchestrate.ts:719
contract?FinishContractThe unified output contract this validator set enforces (the v1.71 experiment review, P0.1/P0.2). Construction then runs the golden self test with the contract's fixtures as defaults, the contract's promptLines join the validator statement in BOTH the coordination and synthesis prompts, every contract validator must appear in validators by name (a promised contract nobody enforces is drift by omission, a ConfigError), and the run journals ONE frozen bundle descriptor (decisionType 'orchestrator_finish_validation_bundle') recording the contract hash and the validator names. A resumed segment whose live contract hash differs appends a SUPERSEDING descriptor instead of failing, because fixing a stale validator and resuming is the intended remedy, never a fault. The remedy is generation-scoped (cycle 73): every decision entry written under a contract carries contractHash, and only the CURRENT generation is judged, so repairsUsed restarts under a fixed contract and a final rejection a superseded generation left in the crash window neither rolls forward at boot nor re-arms on replay (its exchange replays byte identical and the loop continues to a live repair turn). Decisions recorded before 1.77 carry no hash and bind to the current contract only while the journal holds a single bundle descriptor; once a supersession is recorded they are stale. The bundle is deeply frozen and the construction self test also runs the contract's per validator reject goldens against the CONFIGURED set (cycle 74), so a post construction mutation throws and a same-name replacement weaker than the contract's own validator is a ConfigError before any provider call. Absent = byte identical pre 1.72 behavior.packages/core/src/orchestrator/orchestrate.ts:888
draftPolicy?| "contract" | { minWords?: number; requireSections?: string[]; } | "digest"The coordination draft gate (the v1.74 experiment review, P0.3), meaningful ONLY with synthesis configured: with validators bound to the synthesis finish, the coordination finish is an unvalidated draft, and the experiment's model escaped six failed finish exchanges with the schema-valid draft 'test', which then starved synthesis of every citation the validators demanded. The policy runs deterministic library checks on each coordination finish (whitespace-token minWords, literal requireSections markers, the wordCountValidator and requiredSectionsValidator semantics); a failing draft returns to the model as the finish call's error result and the turn continues, exactly like a host validation rejection, and repairTurnReserve grants coordination the same per-rejected-exchange headroom it grants the synthesis finish. Pure text checks over the durable exchange: nothing journals, a resumed segment recounts identically, and maxRepairs is not consumed (it belongs to the synthesis-bound validators). Absent = byte identical pre 1.76 behavior; configured without synthesis = ConfigError. The sentinel 'contract' (RV808a) gates the draft by the FULL declared validator set instead of a hand-written subset, with the same children snapshot the synthesis-bound validation reads. The twelfth comparison run showed why the subset starves the skipWhenDraftValid gate: the coordination repair loop drove the draft only to the weak policy, the pre-pass then judged it by the full contract and failed, and the run paid the whole synthesis plus its own repair for defects a coordination exchange could have fixed. Under 'contract' the rejection feedback names the failing validators, so coordination repairs drive the draft toward exactly what the pre-pass will judge, making the skip reachable. Same posture otherwise: nothing journals, the durable exchange recounts identically, maxRepairs untouched. Honest bound: validators that fold the children snapshot (the evidence share) can still fail the pre-pass when a child settles between the draft finish and synthesis; the pre-pass stays the authority. The sentinel 'digest' (RV4210, the sixth comparison experiment) inverts the draft's economics for configurations that do NOT use skipWhenDraftValid: the harness under audit forced a full contract-valid prose draft (344.8 s of model output) that the composition then rewrote whole, because draftPolicy: 'contract' is priced for the skip gate it was built to feed. Under 'digest' the coordination prompt asks for a compact STRUCTURAL EVIDENCE MAP (one list row per planned section naming its claims and the evidence behind them) and the gate enforces the inversion deterministically: at least one list row, at most DIGEST_DRAFT_MAX_WORDS words, so the draft cannot decay back into the prose it replaces. The synthesis invocation embeds the digest exactly as it embeds any draft; wire counts are unchanged. Because a digest is NOT a candidate deliverable, the intake refuses the combinations that would ship or judge it as one: synthesis.skipWhenDraftValid and synthesis.fallbackToValidDraft are both ConfigError beside it.packages/core/src/orchestrator/orchestrate.ts:807
estRepairCostUsd?numberThe declared price of ONE mechanical repair turn in USD (RV3802), the money twin of repairTurnReserve's turn grant: the bounded claim repair round (claimConsistency.onFound: 'repair') holds this beside the verdict money (RV3701) from the moment the round is admitted, so the one repair turn the round's own finish contract can grant is funded when the candidate materializes; the leg releases to the round's finish loop at its first journaled verdict. Undeclared, the hold falls back to the run's own observed last mechanical repair price (lastMechanicalRepairCostUsd over the journal, absent when no priced repair window exists), else zero, which keeps every pre-RV3802 admission byte identical. A nonnegative finite number; refused typed otherwise.packages/core/src/orchestrator/orchestrate.ts:751
maxRepairs?numberHow many rejected finishes are returned to the model for repair before the run fails; a nonnegative integer, default DEFAULT_FINISH_MAX_REPAIRS. Zero means the first rejected finish fails the run. The bound belongs to one composition invocation (RV3602): with the bounded claim repair round armed (claimConsistency.onFound: 'repair'), the initial composition and the round each enter with the full bound, because the third comparison run's round inherited a spent run wide pool and its first regression was final by construction. At most two invocations exist, so the worst case is maxRepairs + 1 judged finishes per invocation, twice.packages/core/src/orchestrator/orchestrate.ts:663
repairTurnReserve?numberThe repair turn reserve (the v1.71 experiment review, P0.4; the reserve RV-204 deliberately deferred). A nonnegative integer, default 0: max EXTRA turns the invocation the validators bind (the synthesis invocation when synthesis is configured, the coordination loop otherwise) may consume past its maxTurns, one granted per rejected finish exchange, schema-invalid finish arguments and host validation rejections alike. Without it, repair exchanges and generation compete for the same turn budget: the v1.71 experiment lost its whole run to one malformed finish plus one validator rejection inside maxTurns 3. The reserve is bounded, spends from the ordinary budget ceilings (a granted turn is a paid provider turn), and folds into the preflight turn projection (projectedProviderTurns and the run ceiling) when declared there. Zero keeps the pre 1.73 ceiling byte identical.packages/core/src/orchestrator/orchestrate.ts:736
retainRejectedCandidates?booleanRetain the BYTES of every rejected finish candidate as its own addressable transcript blob (RV2507, the 1.226.0 comparison run), default off. The identity of a rejected candidate always rides the terminal (rejectedFinishCandidates: the call id, the sha256 that names WHICH document drew the verdict, its size, and the validator diffs); that costs nothing, because it is derived from decisions the journal already holds. A COPY of the document costs storage, so it is a decision the host makes: with this on, each rejected candidate is written to <runId>/finish-rejected/<callId> and the terminal row carries its ref, one transcripts.get away from the bytes. Turn it on for evaluation and comparison runs. The comparison run's three rejected syntheses were reachable only by an external script that re-parsed the whole agent transcript; nothing on the terminal or in the journal said where they were, or even that they differed from each other. Bounded by construction: at most maxRepairs + 1 candidates per finish-validated invocation, under the run's own prefix, so Engine.deleteRun cascades over them like every other run blob. A store that refuses the write costs the run nothing: the row keeps its identity and drops its ref, and absence means NOT RECORDED.packages/core/src/orchestrator/orchestrate.ts:687
sectionalRepair?{ sections: string[]; }Sectional bounded repair (RV808b). A rejected finish used to resend the WHOLE document to fix one violated section: on the twelfth comparison run the coordination draft plus its repairs alone cost 406 s of model output. With this declared, every rejection feedback of a gated finish teaches the sectional vocabulary, and the model may repair by calling finish({ sections: { '<declared marker>': '<new body>' } }) instead of resending the document: the host splices the patch into the RETAINED rejected attempt (line-anchored, the exported spliceSections semantics: a marker absent from the attempt is appended in declared order) and validates the reconstructed document whole. The vocabulary rides every finish the host actually gates: the validator-bound finish (the synthesis invocation when synthesis is configured, the coordination loop otherwise) and, when a draftPolicy is declared, the coordination draft gate; the synthesis invocation is additionally SEEDED with the coordination draft as its retained base, so a synthesis that agrees with the draft repairs only the named gaps without ever resending it (the carryDraftGaps pairing). Mechanics refusals (sections beside result, an undeclared marker, no retained attempt to splice into) are typed error results, the moral twin of a schema rejection: they journal nothing, spend no maxRepairs, and stay bounded by the turn budget; only the verdict over the SPLICED document spends the repair bound. Nothing new journals anywhere: the exchange is durable in the transcript, the splice is a pure function of it, and the accepted invocation output IS the reconstructed document. Honest bound: the retained attempt lives in the invocation; a segment resumed from a mid-invocation checkpoint retains nothing yet and refuses the first sectional call with the full-resubmission remedy (the synthesis seed re-derives from the journaled draft and never has this window). Declaring the option swaps the finish tool schema and description for the gated invocations, so their toolset hash moves BY DESIGN (the exposeChildResultTools precedent); absent = every byte identical.packages/core/src/orchestrator/orchestrate.ts:854
sectionalRepair.sectionsstring[]The marker lines that partition the document, unique, in document order.packages/core/src/orchestrator/orchestrate.ts:856
selfTest?FinishSelfTestFixturesGolden fixtures of the construction self test (the v1.71 experiment review, P0.3), overriding the contract's generated fixtures: a host with custom validators supplies an accept fixture those validators actually accept. Fixtures without a contract run the self test on their own. Absent with no contract = no self test, the pre 1.72 behavior.packages/core/src/orchestrator/orchestrate.ts:897
validatorsFinishValidator[]Run in configuration order on every schema valid finish call; names must be unique (pass name to a factory to run several instances). A validator that THROWS is a host defect: the run fails as ConfigError, nothing journals, and no repair turn is granted.packages/core/src/orchestrator/orchestrate.ts:649