/** * Chat citation verification (crew#561, F-RC1-117 — Wave 1 criterion 2). * * # The defect * * A chat seat answers with citations — repo-relative paths, `path:line` refs, `path:symbol` refs * and commit SHAs — and the daemon relays that text UNTOUCHED. On the RC1 Phase 6 re-run the * opencode seat answered "summarize the last 10 commits across all 9 repos" with 26 SHAs, of which * **2 did not exist in any repo**, and three line refs were off by a few lines. Every citation * rendered as plain text, so a reader could not tell a real SHA from an invented one — and these * are exactly the tokens a reader pastes into `git show`, a PR or a commit message. * * The daemon is the ONE place that can tell: it already knows the chat's read roots (the scope it * handed the seats), and `stat` + `git cat-file -e` are cheap. So it verifies, and says what it * found — it never edits the seat's text. Mark, don't rewrite. * * # What is verified * * | kind | citation | check | * |----------|-----------------------------------|-------| * | `path` | `crew/src/api/routes.ts` | the file exists under a read root | * | `line` | `routes.ts:2264`, `x.ts:80-90` | the file exists AND has that many lines; when a symbol is named right before the ref, that the symbol is ON that line — otherwise the line it IS on, as a CORRECTION | * | `symbol` | `execute_wrapped.rs:build_worker_command` | the file exists AND contains the symbol | * | `sha` | `6d77153`, 7–40 hex | `git rev-parse --verify --quiet ^{commit}` in the repo the text names, else in any read root | * * # Why a false positive is worse than a miss * * Prose looks like a path (`e.g`, `something.this`) and words look like SHAs. Flagging prose * UNVERIFIED would train the reader to ignore the marks, which is worse than not marking. So: * * - a bare `name.ext` token with no directory and no `:line`/`:symbol` suffix that resolves * nowhere is DROPPED, not marked — it was probably never a citation; * - a hex run with no digit or no `a-f` letter is not a SHA candidate (`effaced` is a word; the * price is that an all-digit short SHA — ~4% of 7-char prefixes — is not checked); * - a token WITH a separator or a `:line`/`:symbol` suffix IS a citation: unresolvable means * `unverified`, because that shape is not prose. * * # Bounded, and never in the reply's way * * The pass runs AFTER the `chatReply` frame is broadcast (`server.ts`), so a reply is never delayed * by verification, and it is bounded twice: at most {@link DEFAULT_MAX_ITEMS} citations and * {@link DEFAULT_BUDGET_MS} of wall clock. Whatever the budget did not reach is reported * `unchecked` — a state of its own, never silently "verified". * * The verdicts ride `/ws` as ONE `chatCitations` frame per reply, stamped with the same `turn_id` * the reply carries, and the studio marks the reply from it (badge + counter). The text on the wire * and in the transcript is the seat's own. */ /** One read root of a chat's scope: the registered repo root and the name the seats were given. */ export interface CitationRoot { /** Resolved absolute path of the repo root. */ absRoot: string; /** The repo's display name — the prefix the reply's rewritten paths carry (crew#618). */ name: string; } /** What class of thing a citation is. */ export type ChatCitationKind = 'path' | 'line' | 'symbol' | 'sha' | 'rule'; /** * The verdict on one citation. * - `verified` — it exists, exactly as cited; * - `corrected` — the thing exists, but not where the reply said (the real place is `resolved`); * - `unverified` — it does not exist in the chat's scope (the fabricated-SHA case); * - `unchecked` — the bounded pass did not reach it. Not a claim either way. */ export type ChatCitationStatus = 'verified' | 'corrected' | 'unverified' | 'unchecked'; /** One citation, as verified. `raw` is the token EXACTLY as it appears in the reply. */ export interface ChatCitationItem { raw: string; kind: ChatCitationKind; status: ChatCitationStatus; /** Where the thing actually is, when the status is `corrected` (`routes.ts:2069`, `crew`). */ resolved?: string; /** Why, in one short phrase — the hover text on the studio's badge. */ note?: string; } /** The `/ws` frame: one per verified `chatReply`, stamped with the reply's own `turn_id`. */ export interface ChatCitationsFrame { type: 'chatCitations'; chat: string; cliKey: string; turn_id?: string; /** Counts, so a skin renders the header without walking `items`. */ verified: number; unverifiable: number; corrected: number; unchecked: number; items: ChatCitationItem[]; project_id?: string; } /** At most this many citations are checked per reply; the rest are `unchecked`. */ export declare const DEFAULT_MAX_ITEMS = 80; /** * Wall-clock budget for one reply's pass, ms. Overrun ⇒ the remainder is `unchecked`. * * 15 s, not 3: the pass runs entirely off the reply's path (nothing waits on it but the marks * appearing a moment later), and the number worth protecting is COVERAGE, not latency. The RC1 * Phase 6 answer cited 26 SHAs over 9 repos — a fabricated one sweeps every root — and on a loaded * desktop a `git cat-file` spawn is not always instant, so a 3 s budget would have reported most of * that answer `unchecked`, which reads like the feature is not working. The budget exists so the * work is BOUNDED, not so it is fast; it is re-read between citations AND between roots. */ export declare const DEFAULT_BUDGET_MS = 15000; /** * The answer to "is this commit in this repo?" — THREE states, not two. * * `git cat-file -e` exits 1 for an object that is not there and something else (or nothing, on a * timeout) when the CHECK failed. Collapsing those into `false` is how a real SHA gets branded * UNVERIFIED on a loaded desktop, which is the one failure this feature must not have: a false mark * teaches the reader to ignore marks. */ export type CommitCheck = 'present' | 'absent' | 'error'; /** The reads the pass needs — injected so the whole thing is testable without a repo or git. */ export interface ChatCitationDeps { /** Does this absolute path exist as a FILE? */ fileExists(abs: string): boolean; /** The file's lines, or `null` when it cannot be read (missing, too big, binary). */ readLines(abs: string): string[] | null; /** Repo-relative paths tracked in the root, or `null` when the listing itself failed. */ listFiles(absRoot: string): Promise; /** `git cat-file -e ^{commit}` in the root — see {@link CommitCheck}. */ commitExists(absRoot: string, sha: string): Promise; now(): number; } /** A citation as EXTRACTED — before anything was checked. */ export interface RawCitation { raw: string; kind: ChatCitationKind; /** The path part of a path/line/symbol citation (`crew/src/x.ts`). */ path?: string; /** The first line of a `line` citation. */ line?: number; /** The symbol of a `symbol` citation, or the one the sentence named before a `line` citation. */ symbol?: string; /** The repo a `sha` citation's sentence named, when it named one. */ repo?: string; /** The id of a `rule` citation (`[rule:]`, DC-S7). */ ruleId?: string; } /** DC-S7 (§4.7): the label on a cited in-force rule. It is a citation, never a "followed" verdict. */ export declare const RULE_CITED_LABEL = "cited by the step \u2014 unchecked"; export declare const RULE_UNKNOWN_LABEL = "not an in-force rule here"; /** Every `[rule:]` in a text, once each, in order. */ export declare function extractRuleCitations(text: string): string[]; /** `wicked-crew` is cited as `crew` as often as by its name: every alias a reply may use. */ export declare function repoAliases(root: CitationRoot): string[]; /** * Every citation in a reply, in text order, deduplicated by (kind, raw). * * Pure: no filesystem, no git. `roots` is read only for the repo aliases a SHA's sentence may name. */ export declare function extractCitations(text: string, roots: readonly CitationRoot[]): RawCitation[]; /** The default deps: the real filesystem and the real git. */ export declare function chatCitationDeps(): ChatCitationDeps; export interface VerifyOptions { /** DC-S7: the in-force rule ids of the chat's project; `null`/absent = not readable (rule citations stay `unchecked`). */ ruleIds?: ReadonlySet | null; maxItems?: number; budgetMs?: number; } /** The result of one reply's pass. `items` is in text order; the counts match it. */ export interface ChatCitationsResult { verified: number; unverifiable: number; corrected: number; unchecked: number; items: ChatCitationItem[]; } /** * Verify every citation in a reply, bounded. Never throws: a broken root, an unreadable file or a * git that is not there is a verdict (`unverified` / `unchecked`), not an error. */ export declare function verifyCitations(text: string, roots: readonly CitationRoot[], deps?: ChatCitationDeps, opts?: VerifyOptions): Promise; /** * The `/ws` frame for one reply's verdicts, or `null` when there is nothing to say (no citation * survived extraction — a chatty reply with no references marks nothing). */ export declare function citationsFrame(reply: { chat: string; cliKey: string; turn_id?: string; project_id?: string; }, result: ChatCitationsResult): ChatCitationsFrame | null; //# sourceMappingURL=chat-citations.d.ts.map