/** * Doc → SUBJECT-REPO grounding for the interactive seams (acceptance findings F-046 + follow-up). * * WHAT WAS WRONG: a document created in a multi-repo project was grounded on the project's FIRST * `crew.repo` member — a brochure about wicked-studio was drafted against a wicked-core snapshot, * the brief's "use the wicked-studio repo in this project" had no way to reach the seam, and the * thread never said which repository the worker actually saw. The bridge's create wire carries * `name/kind/brief/style/project` only and builds `doc.created` explicitly (server.js `POST * /api/docs`), so a repo named on the create request never rides the bus: crew has to remember it * itself, keyed by the document id the bridge answers with. * * Three things live here, shared by the proxy's create interception and the draft/demo seams: * * 1. THE REQUEST GRAMMAR — `repo_ref` (one) / `repo_refs` (several) on the create body, each a * registered repo id, its registry name, or its root directory's basename; `style` from the * bridge's own set, or inferred from the brief's format words when the client sent none * (`inferDocStyle`) so a print/A4 brief reaches the bridge's print instructions instead of * defaulting to `web`. * 2. THE RESOLUTION RULE (`resolveGroundingRepos`) — in preference order: the repos NAMED on the * request; else the member repos the BRIEF names by name; else the project's SOLE repo; else * NONE — never a silent first-member substitution. A named repo that is not (or no longer) a * member is reported as `missing`, so the seam can say so on the thread and the proxy can * refuse the create up front. * 3. THE DURABLE BINDING (`DocGroundingStore`) — a `crew-grounding.json` sidecar BESIDE THE DOC, * `//crew-grounding.json`, written atomically (tmp + rename, the handoff-ledger * discipline): the proxy records `{project, repo_refs, style}` under the doc name the bridge's * create answered with, the seams read it when `doc.created` arrives. The bridge emits * `doc.created` BEFORE it answers the create, and the seams poll the bus, so a seam can see the * event a few ms before the proxy has recorded the binding: `beginCreate`/`settleCreate`/ * `waitFor` close that window in-process (proxy and seams run in the same daemon) — a seam that * finds no binding but sees an unsettled create for the same project waits, bounded, for it. */ import { type Stats } from 'node:fs'; import type { CoreAdapter } from '../core/adapter.js'; /** The bridge's own style set (server.js `POST /api/docs`: anything else is dropped to null → web). */ export declare const DOC_STYLES: readonly ["web", "ppt", "brochure", "doc"]; export type DocStyle = (typeof DOC_STYLES)[number]; export declare function isDocStyle(value: unknown): value is DocStyle; /** Infer the bridge style a brief asks for by its format words, or `undefined` when it names none. */ export declare function inferDocStyle(brief: string): DocStyle | undefined; /** The one-line format contract a style implies — folded into the worker's problem statement so * the DRAFT phase (not only the outline's legend) knows what "brochure" must mean on disk. The * brochure line is the F-050/F-053 lesson: a print doc rendered as fixed slide pages with * `overflow:hidden` clipped its content. */ export declare function styleContract(style: string): string; /** How many repositories one document may name. A brochure is about a product, not a monorepo * census; the cap also bounds how many snapshots one launch can clone. */ export declare const REPO_REFS_MAX = 8; /** A failed parse still carries `requested` — the refs AS THE CLIENT SPELLED them (best-effort: * non-strings stringified, capped), so the refusal can render exactly what was sent (Copilot, #506). */ export type RepoRefsParse = { ok: true; refs: string[]; } | { ok: false; error: string; requested: string[]; }; /** The raw spellings a body carried, for a refusal's `requested` — never used for matching. Every * refusal carries them, project mismatches included (Copilot on crew#506). */ export declare function spelledRefs(body: Record): string[]; /** * Read `repo_ref` / `repo_refs` off a create body. Both may be present; the union is de-duplicated * in order. `{ok: true, refs: []}` when neither is present — the common case, nothing named. */ export declare function parseRepoRefs(body: Record): RepoRefsParse; /** A project repo a document can be grounded on: the registry identity, its human name, and the * root the snapshot is cloned from (never handed to the worker directly — wicked-core#294). */ export interface GroundingRepo { repoRef: string; name: string; rootPath: string; } /** Why a launch grounds where it grounds — narrated on the thread verbatim (F-046 follow-up). */ export type GroundingSource = 'named' | 'brief' | 'sole-member' | 'none'; export interface GroundingDecision { repos: GroundingRepo[]; source: GroundingSource; /** Named refs that resolve to no member repo (detached since the create, or never attached). */ missing: string[]; /** Named refs that resolve to SEVERAL member repos (two repos sharing a name/basename): never * guessed — reported, so the user names the id (codex on crew#506). */ ambiguous: string[]; /** How many member repos the project has — the honest "none of N was named" note needs it. */ memberCount: number; } /** * The project's `crew.repo` members verified against the repo registry (a stale membership whose * repo left the registry is skipped — it has no root to snapshot). `[]` when the project has no * repo members or the adapter cannot answer (old addon, engine hiccup): every caller degrades to an * ungrounded launch, narrated. */ export declare function projectRepoCandidates(adapter: CoreAdapter, projectId: string, log?: (message: string) => void): Promise; /** Does a request ref name this repo? Its registry id, its name, or its root basename — case- * insensitive on the human spellings, exact on the id. */ export declare function matchRepoRef(ref: string, repo: GroundingRepo): boolean; /** * EVERY candidate a ref names. An exact registry id is unique by construction and wins alone; a * human spelling (name / root basename) can name several repos — two checkouts of `wicked-studio` * under different parents share a basename — and the caller must treat >1 as AMBIGUOUS, never * pick the first (codex on crew#506). */ export declare function matchingRepos(ref: string, candidates: readonly GroundingRepo[]): GroundingRepo[]; /** * The member repos a brief names OUTRIGHT — "use the wicked-studio repo in this project" names * wicked-studio. Whole-token matches only (a name followed by `-` or an alphanumeric is a * different repo: `wicked-studio` must not match `wicked-studio-archived`); case-insensitive. * Order follows the candidates, not the brief. */ export declare function reposNamedInBrief(brief: string, candidates: GroundingRepo[]): GroundingRepo[]; /** * Decide which repositories a document is grounded on. The order IS the contract: * * - `namedRefs` given (the create request said so) → THOSE, with unresolvable ones in `missing`; * - else the member repos the brief names by name; * - else the project's only repo; * - else none — a multi-repo project whose document named nothing is NOT grounded on an arbitrary * member (the F-046 defect); the caller narrates how to name one. */ export declare function resolveGroundingRepos(adapter: CoreAdapter, projectId: string, brief: string, namedRefs: readonly string[] | undefined, log?: (message: string) => void): Promise; /** * A safe directory name for a repo's snapshot under `/repos/` — derived from the CANONICAL * registry id (unique by construction; names and basenames collide — codex on crew#506), reduced * to a path-safe charset with its case kept, and made unique among `taken` CASE-INSENSITIVELY so * ids `Foo` and `foo` never overwrite each other on a case-insensitive filesystem. The problem * statement still calls the repo by its NAME beside the path. Adds the result to `taken`. */ export declare function snapshotDirName(repo: GroundingRepo, taken?: Set): string; /** The thread line that says WHERE a launch is grounded and WHY (F-046 follow-up: the worker's * own words were the only place "grounded on wicked-core only" appeared). `null` when there is * nothing to say about repositories at all (a repo-less project, an unfiled doc). */ export declare function groundingNarration(decision: GroundingDecision, snapshotted: readonly GroundingRepo[], kind: 'draft' | 'demo'): string | null; /** Why a requested repository grounded nothing — the record's spelling of the narration's "skipped" clauses. */ export type GroundingSkipReason = 'not-a-member' | 'ambiguous' | 'unsnapshotable'; /** * The structured twin of {@link groundingNarration} (crew#512) — the published * `InteractiveDocGrounding`: what the thread line SAYS, as data. Built from the same inputs, once per * launch; rides the narration's status frame and is recorded in the sidecar so the docs list can * serve it to a reopened document without the thread. */ export interface GroundingRecord { /** Registry ids of the repos the launch grounded on (snapshotted), in decision order. */ repo_refs: string[]; source: GroundingSource; skipped: Array<{ ref: string; reason: GroundingSkipReason; }>; member_count: number; } /** * The record for a decision and the repos whose snapshot actually landed: `repo_refs` are the * snapshotted ids (the "Grounded on …" clause); `skipped` is every repository the launch did NOT * ground on — the named refs that are not members, the ambiguous ones, and the resolved members * whose snapshot failed (the narration claims nothing for those either). `source` is the decision's, * even when nothing landed — WHY the launch looked where it looked is still true. */ export declare function groundingRecord(decision: GroundingDecision, snapshotted: readonly GroundingRepo[]): GroundingRecord; /** The sidecar's filename, beside `versions.json`. */ export declare const CREW_GROUNDING_FILE = "crew-grounding.json"; /** What the proxy recorded for one document at create time. */ export interface DocGroundingBinding { project_id: string; /** Registry ids, canonicalized from whatever spelling the request used. */ repo_refs: string[]; /** The style the create was forwarded with (client-given or brief-inferred), when known. */ style?: string | undefined; /** Per-doc council roster override from the create request (#631). JSON-serialized `AgenticCli[]`. */ clis_json?: string | undefined; /** Launch channel from the create request (#632): 'studio' | 'cli' | 'api'. */ channel?: 'studio' | 'cli' | 'api' | undefined; /** Opaque launch actor string from the create request (#632). */ actor?: string | undefined; /** crew#512: what the draft launch RESOLVED — written by the seam at launch (`recordGrounding`), * after the create-time fields above; absent until then. The docs list serves it per row. */ grounding?: GroundingRecord | undefined; recorded_at: string; } /** Why a sidecar path was refused — each is a containment failure, never a "not found". */ export declare class GroundingPathRefusedError extends Error { constructor(message: string); } /** Test seams into the containment sequence (never wired in production): `afterLstat` runs after * the path checks and before the descriptor is opened — the swap window the fd discipline closes. */ export interface DocGroundingStoreHooks { afterLstat?: (path: string) => void; } export declare class DocGroundingStore { private readonly pending; private nextToken; private readonly hooks; constructor(hooks?: DocGroundingStoreHooks); /** `//crew-grounding.json`, or null for an id that is not a safe segment. * LEXICAL only — see {@link DocGroundingStore.verifiedSidecar} for the containment-checked path. */ static sidecarPath(docsRoot: string, documentId: string): string | null; /** * The sidecar path anchored to the REAL docs root, every component below it checked (codex on * crew#506): the docs root must resolve (`realpath`); the doc directory, when present, must be a * real directory — a symlink there would let a planted `/` → elsewhere read or * write a sidecar outside the root — and the sidecar (and its rename temp) must not be a link. * Returns `{ path, docDir, docDirExists }`; throws {@link GroundingPathRefusedError} on a link or * a non-directory; throws the fs error when the root itself does not resolve. */ static verifiedSidecar(docsRoot: string, documentId: string): { path: string; docDir: string; docDirExists: boolean; sidecar: Stats | null; }; /** The binding beside the doc, or `undefined` when absent, malformed, or REFUSED (a symlinked * doc dir / sidecar is read as "nothing named" — the thread narrates the fallback, the daemon * never dies over it, and nothing outside the docs root is ever read). */ get(docsRoot: string, documentId: string): DocGroundingBinding | undefined; /** Write the sidecar atomically under the VERIFIED real docs root. The doc directory normally * exists by now (the bridge created it before answering the create); it is created — as a plain * directory directly under the real root, never through a link — when it does not, so a record * never fails on ordering alone. Throws ({@link GroundingPathRefusedError} on containment, the * fs error otherwise) — the caller logs and the doc stays unbound. */ record(docsRoot: string, documentId: string, binding: Omit & { recorded_at?: string; }): void; /** * crew#512: persist what a draft launch RESOLVED for `documentId`, keeping every create-time field * the proxy recorded (refs as spelled, style, seat override, provenance). A document the proxy * never bound (nothing named on its create, or created past the proxy) gets a binding with * `repo_refs: []` — "nothing was named" — carrying the record. Same containment and atomicity as * {@link DocGroundingStore.record}; throws the same way, the caller logs and the frame still rides. */ recordGrounding(docsRoot: string, documentId: string, projectId: string, grounding: GroundingRecord): void; /** Drop a document's sidecar. `true` when one was removed; a refused path removes nothing. */ remove(docsRoot: string, documentId: string): boolean; /** The proxy is about to forward a create for `projectId` that WILL record a binding. */ beginCreate(projectId: string): number; /** The create answered (recorded, refused, or failed) — wake every waiter. */ settleCreate(token: number): void; /** How many creates for `projectId` are still unanswered (diagnostics / tests). */ pendingCount(projectId: string): number; /** * The binding for `documentId` under `docsRoot`, waiting (bounded) for an in-flight create of the * same project to settle first — the seam's answer to the bus racing the create response. * Resolves immediately when the binding is already there or nothing is pending for the project. */ waitFor(docsRoot: string, documentId: string, projectId: string, timeoutMs: number): Promise; } //# sourceMappingURL=doc-grounding.d.ts.map