/** * Leg-deposit contract — the machine-readable record a falsification leg * leaves behind after it READ a diff (mmnto-ai/totem#2698, ruled 2026-09-03). * * The gate's question is deliberately narrow: *was THIS head read by a leg?* * So the store is keyed by the READ sha — `/artifacts/legs/.json` * — and NOT content-addressed like the verdict/admission/run families. Two * legs that read the same head are the same answer to that question; a rerun * on the same head supersedes by INTENT, which is why the writer is * create-exclusive and a `replace` discloses the `readAt` it overwrote rather * than silently deduping (the content-addressed stores' EEXIST-is-dedup rule * would be a lie here — the second read is a different observation). * * Store mechanics still mirror the sibling families where the semantics agree: * validate-on-write so a writer bug never poisons the store, a * tolerant-within-major reader whose newer-major refusal is NAMED (an older * CLI meeting a newer deposit reports "upgrade", never "corrupt"), unknown * keys tolerated (forward-minor additive fields), and `0o600` bytes. * * Two properties are specific to this family: * * 1. **The loader never throws.** A deposit is a hand-editable JSON file that * a pre-push hook reads on every push; one bad file must never take the * gate down or hide a valid sibling. Every per-file failure — unreadable, * non-JSON, schema-invalid, wrong major, or a filename that disagrees with * the stored `diffSha` — becomes a one-line `corrupt` row the caller * prints (Tenet 4: loud, never silent). * 2. **Every stored string that reaches stdout is control-byte free at the * SCHEMA boundary**, at write and at read alike. `verdict`, `claim`, * `counterexample` and `file` land inside a hook's `[Totem] …` line; a * newline in any of them would forge a second line, so the schema refuses * C0 (0-31) and DEL/C1 (127-159) outright. "Single-line" needs no separate * rule: LF and CR are inside that band. * * Core never shells out. Ancestry is supplied by the CLI through the injected * {@link LegGitAdapter}, so every resolution rule here is unit-testable * without a git fixture. */ import { z } from 'zod'; import { TotemError } from '../errors.js'; /** The leg-deposit schemaVersion WRITTEN by this code. Readers accept any 1.x. */ export declare const LEG_DEPOSIT_SCHEMA_VERSION = "1.0.0"; /** The major this reader understands; another major needs a migration entry. */ export declare const LEG_DEPOSIT_KNOWN_MAJOR = 1; /** * The leg's severity vocabulary. Deliberately NOT the verdict family's * CRITICAL/WARN/INFO: a leg deposit answers "what did the leg find", and the * doctrine spelling for that is BLOCKING (the fold must land) / MATERIAL (the * seat rules) / MINOR (disclosed). Order here is documentation order; the * value is data. One spelling — {@link LegFindingSeveritySchema} is built * from this array, never a second literal list. */ export declare const LEG_FINDING_SEVERITIES: readonly ["BLOCKING", "MATERIAL", "MINOR"]; export type LegFindingSeverity = (typeof LEG_FINDING_SEVERITIES)[number]; export declare const LegFindingSeveritySchema: z.ZodEnum<["BLOCKING", "MATERIAL", "MINOR"]>; /** The finding id — required, so `folded` can NAME a finding rather than index it. */ export declare const LegFindingIdSchema: z.ZodString; /** * One typed finding. `id` is required (mmnto-ai/totem#2698 OQ1, ruled): the * contract JSON had none, and without one `folded` cannot reference a finding * — positional indexes are fragile under reordering, and a bare count cannot * be checked against anything. */ export declare const LegFindingSchema: z.ZodObject<{ /** Unique within the deposit; the referent of every `folded` entry. */ id: z.ZodString; severity: z.ZodEnum<["BLOCKING", "MATERIAL", "MINOR"]>; /** Repo-relative path the finding is about (free-form: core never resolves it). */ file: z.ZodEffects; /** 0 means "the file, no particular line" — never a sentinel like -1. */ line: z.ZodNumber; /** The falsifiable claim, one line. */ claim: z.ZodEffects; /** The evidence. MAY be empty — a claim with no counterexample is still disclosed. */ counterexample: z.ZodEffects; }, "strip", z.ZodTypeAny, { severity: "BLOCKING" | "MATERIAL" | "MINOR"; line: number; file: string; id: string; claim: string; counterexample: string; }, { severity: "BLOCKING" | "MATERIAL" | "MINOR"; line: number; file: string; id: string; claim: string; counterexample: string; }>; export type LegFinding = z.infer; /** * The deposit itself. Every field is required; the arrays may be empty (a leg * that found nothing still deposits — that IS the evidence the gate wants). * * Unknown keys are TOLERATED, not refused (the run-artifact precedent): a * forward-minor writer may add a field this reader strips, and a deposit is * evidence a newer leg wrote, not a contract this reader gets to narrow. */ export declare const LegDepositSchema: z.ZodEffects; /** Repo-relative path the finding is about (free-form: core never resolves it). */ file: z.ZodEffects; /** 0 means "the file, no particular line" — never a sentinel like -1. */ line: z.ZodNumber; /** The falsifiable claim, one line. */ claim: z.ZodEffects; /** The evidence. MAY be empty — a claim with no counterexample is still disclosed. */ counterexample: z.ZodEffects; }, "strip", z.ZodTypeAny, { severity: "BLOCKING" | "MATERIAL" | "MINOR"; line: number; file: string; id: string; claim: string; counterexample: string; }, { severity: "BLOCKING" | "MATERIAL" | "MINOR"; line: number; file: string; id: string; claim: string; counterexample: string; }>, "many">; /** Ids of findings the seat FOLDED — a subset of `findings[].id` (superRefine). */ folded: z.ZodArray; /** The leg's one-line disposition; echoed verbatim on the gate's evidence line. */ verdict: z.ZodEffects; }, "strip", z.ZodTypeAny, { schemaVersion: string; findings: { severity: "BLOCKING" | "MATERIAL" | "MINOR"; line: number; file: string; id: string; claim: string; counterexample: string; }[]; verdict: string; diffSha: string; readAt: string; folded: string[]; }, { schemaVersion: string; findings: { severity: "BLOCKING" | "MATERIAL" | "MINOR"; line: number; file: string; id: string; claim: string; counterexample: string; }[]; verdict: string; diffSha: string; readAt: string; folded: string[]; }>, { schemaVersion: string; findings: { severity: "BLOCKING" | "MATERIAL" | "MINOR"; line: number; file: string; id: string; claim: string; counterexample: string; }[]; verdict: string; diffSha: string; readAt: string; folded: string[]; }, { schemaVersion: string; findings: { severity: "BLOCKING" | "MATERIAL" | "MINOR"; line: number; file: string; id: string; claim: string; counterexample: string; }[]; verdict: string; diffSha: string; readAt: string; folded: string[]; }>; export type LegDeposit = z.infer; /** A loaded deposit paired with the address it was found at (the filename stem). */ export interface LegDepositWithAddress { deposit: LegDeposit; /** Absolute path of the stored deposit. */ path: string; /** The verified address = the filename stem = `deposit.diffSha`. */ diffSha: string; } /** Absolute legs directory for a given absolute totem dir. */ export declare function legsDir(totemDirAbs: string): string; /** Absolute path of the deposit for `diffSha` — the sha IS the filename stem. */ export declare function legDepositPath(totemDirAbs: string, diffSha: string): string; /** * Refusal to overwrite an existing deposit without `replace`. Typed so the * CLI can print the incumbent's `readAt` and the one-flag cure rather than * re-deriving either. `existingReadAt` is `undefined` when the incumbent * itself is unreadable/corrupt — the refusal still stands (the seat decides * whether to replace it), and the honest report is that its instant could not * be read. */ export declare class LegDepositExistsError extends TotemError { readonly diffSha: string; readonly existingReadAt: string | undefined; readonly depositPath: string; constructor(diffSha: string, existingReadAt: string | undefined, depositPath: string); } export interface SaveLegDepositOptions { /** Overwrite an existing deposit for this sha, reporting the `readAt` replaced. */ replace?: boolean; } export interface SaveLegDepositResult { /** Absolute path of the stored deposit. */ path: string; /** * Present IFF an existing deposit was overwritten. `readAt` is the * incumbent's instant, or `undefined` when the incumbent was corrupt enough * that its instant could not be read. */ replaced?: { readAt: string | undefined; }; } /** * Persist a deposit at `/.json`, create-exclusive by default. * * Ordering is contract: the deposit is VALIDATED before the filesystem is * touched at all, and the occupancy check precedes the write — so a refused * write (schema-invalid, or occupied without `replace`) leaves no file and no * temp behind. The bytes go through the shared atomic helper (temp in the same * directory, fsync), so a reader never observes a torn deposit. * * The PUBLISH is exclusive, not merely checked (Greptile P1 on PR * mmnto-ai/totem#2745). `existsSync` then rename is check-then-act: two * no-replace writers for one sha both see "absent" and the later rename wins * silently, overwriting one leg's read with another's. Without `replace` the * final name is therefore created by `fs.linkSync`, which is atomic and fails * `EEXIST` if the name is taken — the loser refuses exactly as the pre-check * does, and the incumbent's bytes are never touched. `replace: true` keeps the * rename path, because overwriting is what it asks for. * * The file's name is DERIVED from `deposit.diffSha`, which is how the store's * "the name is the read sha" invariant holds by construction; the loader * re-checks it on the way back in, since the name comes off a filesystem a * human can rename. */ export declare function saveLegDeposit(totemDirAbs: string, deposit: LegDeposit, options?: SaveLegDepositOptions): SaveLegDepositResult; /** One disclosed unusable file: the name as it sits on disk, and why it is not a deposit. */ export interface LegDepositCorruptEntry { /** The filename as read from the directory (not a path — the caller joins). */ file: string; /** One line, control-byte free, safe to echo. */ reason: string; } export interface LoadLegDepositsResult { deposits: LegDepositWithAddress[]; corrupt: LegDepositCorruptEntry[]; } /** * Read every deposit in the store, TOLERANTLY. * * Never throws: a missing directory is an empty store, a non-`.json` entry is * not a deposit at all (ignored silently — the directory is not the store's * inventory), and every other per-file failure is a `corrupt` row carrying a * one-line reason. The reading is JSON-AWARE through the schema, so a file * that merely *mentions* `diffSha` or `findings` in some other shape — a * review artifact copied in, say — is corrupt, never a deposit. * * The filename is re-checked against the stored `diffSha`: the name is the * store's address, a human can rename a file, and a deposit resolved under * someone else's sha would answer the gate's question about the wrong head. */ export declare function loadLegDeposits(totemDirAbs: string): LoadLegDepositsResult; /** * The git seam the resolver needs, INJECTED. Core never shells out — the CLI * supplies an adapter over `git cat-file`/`merge-base --is-ancestor`/ * `rev-list --count`, and every rule below stays unit-testable with a fake. */ export interface LegGitAdapter { /** Does this sha name a commit object in THIS repo? */ isCommit(sha: string): boolean; /** Is `base` an ancestor of `head`? (Called only for shas `isCommit` accepted.) */ isAncestor(base: string, head: string): boolean; /** Commits from `base` to `head` (`rev-list --count base..head`). */ distance(base: string, head: string): number; /** * Paths changed between `base` and `head` (`git diff --name-only base...head`, * three-dot). This is what a leg reading at `head` COULD have seen, and it is * the only input the coverage predicate needs. Called only for ancestor * candidates, and only when a coverage query is supplied. */ changedFiles(base: string, head: string): readonly string[]; } /** How a deposit reaches the head: it IS the head, or it is behind it. */ export type LegDepositRank = 'exact' | 'ancestor'; /** Why a deposit cannot answer for this head. */ export type LegDepositStaleReason = 'unknown-commit' | 'not-ancestor' | 'no-coverage'; /** * How much of what this push OWES a leg the candidate could actually have read * (mmnto-ai/totem#2698 fold 3, operator-ruled). * * Ancestry alone is not freshness. A deposit written against the branch's merge * base satisfies ancestor-or-equal and reports a small `distance`, yet the leg * that wrote it saw NONE of the diff the push proposes — the exhibit that * produced this rule. Coverage is the second half of the question: of the owed * paths, how many were inside the diff the candidate's own head contained? */ export interface LegDepositCoverage { /** Owed paths the candidate's own branch diff contained. */ covered: number; /** Owed paths in total — the deduplicated files in the basis. */ owed: number; /** The owed paths the candidate could NOT have read, in basis order. */ missing: string[]; } /** * The inputs the coverage predicate needs, supplied by a caller that resolved a * BRANCH scope. Omitted by callers that cannot (a staged or explicit-range * scope has no branch base to measure a candidate's reach against), and the * resolution then reports `coverage: undefined` so those callers disclose the * limit rather than imply full coverage. */ export interface LegCoverageQuery { /** The base ref the caller resolved for HEAD — the same one, or the measure lies. */ base: string; /** The owed paths (the deduplicated files in the basis). */ owedFiles: readonly string[]; } export interface LegDepositWinner extends LegDepositWithAddress { rank: LegDepositRank; /** Commits landed since the leg read: 0 for `exact`. */ distance: number; /** Absent iff no coverage query was supplied. */ coverage?: LegDepositCoverage; } export interface LegDepositSuperseded { diffSha: string; readAt: string; rank: LegDepositRank; distance: number; /** Absent iff no coverage query was supplied. */ coverage?: LegDepositCoverage; } export interface LegDepositStale { diffSha: string; readAt: string; reason: LegDepositStaleReason; } export interface LegDepositResolution { /** Absent when no deposit is ancestor-or-equal of the head. */ winner?: LegDepositWinner; /** Valid candidates the winner outranked — disclosed, never silently dropped. */ superseded: LegDepositSuperseded[]; /** Deposits that name no commit here, that are not ancestors, or that cover nothing. */ stale: LegDepositStale[]; /** Files that are not deposits at all, with their reasons. */ corrupt: LegDepositCorruptEntry[]; } /** * Resolve which deposit (if any) answers for `headSha`. * * A deposit read at X answers for every DESCENDANT of X — the contract's * ancestor-or-equal rule — so the ranking is: `exact` first, then the NEAREST * ancestor (fewest commits since the read), ties to the LATEST `readAt`. The * winner carries its `distance` so the caller can print "+N commits since the * leg read" rather than pass a stale read off as a fresh one. * * Since mmnto-ai/totem#2698 fold 3 ancestry is only HALF the question. When a * `coverage` query is supplied, each ancestor candidate is also measured on * what it could have READ — the branch diff up to its own head, intersected * with the owed paths — and a candidate covering NONE of them is stale, not a * winner: it satisfies ancestor-or-equal while its leg saw nothing this push * proposes (the merge-base deposit that produced the ruling). An EXACT match * covers everything by construction and costs no git call. * * Ranking is unchanged: along one lineage a nearer ancestor's diff is a * superset of a farther one's, so nearest-first already orders by coverage. * * Nothing here decides POLICY beyond that floor: partial coverage still * resolves, and a distance of 200 still resolves. Whether either is evidence * enough is the gate's disclosure to make and doctrine's re-arm to rule. */ export declare function findLegDepositForHead(totemDirAbs: string, headSha: string, git: LegGitAdapter, coverage?: LegCoverageQuery): LegDepositResolution; export interface LegFindingCounts { blocking: number; material: number; minor: number; /** `folded.length` — the ids the seat folded, which are STILL counted above. */ folded: number; } /** * Count a deposit's findings by severity, plus how many were folded. * * A folded finding is still a finding: it is counted in its severity bucket * AND in `folded`. The covariate prints both, so a reader can see "3 blocking, * 3 folded" (all addressed) apart from "3 blocking, 0 folded" (none were). */ export declare function countLegFindings(deposit: LegDeposit): LegFindingCounts; /** * Render the covariate line's format-v1.2 `leg:` field — the ONE spelling of * this text, exactly: * * `leg: blocking=N material=N folded=N` (a deposit resolved) * `leg: none` (none did) * * The field is COMPOSED BESIDE {@link renderCovariateLine} / `renderAdmissionLine`, * never inside them: the v1 shapes stay byte-identical, and a consumer keeps * discriminating on the second token. The text is contract — the pilot ledger * greps this line — so it is spelled here and nowhere else. * * `minor` is deliberately absent from the field: the round rules on blocking * and material, and `folded` is what says whether they were addressed. The * full counts stay available via {@link countLegFindings}. */ export declare function renderLegField(deposit: LegDeposit | undefined): string; //# sourceMappingURL=legs.d.ts.map