import type { FileSystem } from "../ports/filesystem.js"; /** * The bytes Graft analysis is permitted to see, and nothing else. * * Analysis previously received a general `FileSystem` and reached through it to * the live disk, so "read the workspace" and "read anything" were one * authority. A read view separates them, which is what will let analysis run * after an observation has settled rather than racing the disk it is * describing. * * The seam is landed; the settled side of it is not. Both production * composition roots pass `LiveWorkspaceReadSource`, so what this interface * buys today is that the authority is named and singular, not that it is * settled. See `AdmittedWorkspaceSnapshot` for what is still missing. * * The primitive is bytes, not text. A basis identifies bytes, so a seam that * decoded on the way through could not honour it: an undecodable file would * either throw at the boundary or arrive silently replaced, and neither is the * observed content. Decoding is an analysis projection, applied above. */ export interface WorkspaceReadView { /** Returns the bytes for a path. Rejects if it cannot produce them. */ readBytes(path: string): Promise; } /** * What an observation retained about itself. * * Analysis has to be attributable to the observation that produced it. A * result carrying no request or settlement identity cannot be replayed, and * cannot be told apart from bytes someone assembled. */ export interface WorkspaceReadEvidence { readonly requestId: string; readonly settlementId: string; readonly workspaceRoot: string; /** Identity of the exact bytes the observation settled. */ readonly basisDigest: string; } /** * A read view over bytes an observation settled. * * Separate from `WorkspaceReadView` because fetching bytes and possessing * settled bytes are not the same capability, and a single interface for both * forced the live filesystem to inhabit a contract it cannot satisfy. It has * no basis, so it supplied a sentinel in the field whose contract is "the * identity of the exact bytes" — a value meaning "this is not a basis" living * where the basis goes. One of these performs an effect and the other carries * evidence; they are not substitutable. */ export interface AdmittedWorkspaceReadView extends WorkspaceReadView { readonly evidence: WorkspaceReadEvidence; /** Every path the request admitted, in the order it declared them. */ admittedPaths(): readonly string[]; } /** * One file as a single observation saw it. * * Operations take this rather than a filesystem so that policy evaluation, * cache comparison, and projection all describe the same bytes. When each * step fetched its own copy, a file rewritten between two of them could be * authorised in one version and returned in another, and the cache would * record the hash of one beside the outline of the other. * * `utf8` is null when the bytes are not valid UTF-8. It is a separate field * rather than a decode at the point of use because a basis identifies bytes: * substituting replacement characters would return content the observation * never settled, under the identity of content it did. */ export interface ObservedFile { readonly path: string; readonly bytes: Uint8Array; readonly utf8: string | null; } /** * The size an observation has, whether or not it decodes as text. * * Policy must be evaluated for every observation, including bytes with no * faithful text projection: a binary or banned path is refused for being * binary or banned, and skipping the check when decoding fails would let it * through to a projection instead. Counting 0x0A is exact for any byte * sequence, because a newline byte cannot occur inside a multi-byte UTF-8 * sequence. */ export declare function observedActual(file: ObservedFile): { lines: number; bytes: number; }; /** Observes a path exactly once. Rejects if the view cannot produce it. */ export declare function observeFile(view: WorkspaceReadView, path: string): Promise; declare const admittedSnapshotBrand: unique symbol; /** * An immutable observation, shaped as Echo would settle one. * * NOT YET PRODUCED IN PRODUCTION. There is no decoder that turns an Echo * settlement into one of these, and no composition root constructs one. The * only constructor is the test constructor below, so today every value of this * type is admitted by assertion. Every production Graft read still goes through * `LiveWorkspaceReadSource` to the live disk. Building the decoder is the * remaining work in #228; until it exists, this type describes the destination, * not the current state. * * The brand is compile-time friction, not runtime evidence. It stops a caller * assembling one inline and having analysis treat it as settled, but a brand * cannot attest that Echo settled anything — only a decoder validating the * settlement envelope can, and that is what does not exist yet. * * The request-side fields are retained because analysis must be attributable * to the observation that produced it: a snapshot with no request or * settlement identity cannot be replayed, and cannot be told apart from bytes * someone assembled. */ interface WorkspaceSnapshotDescriptor { readonly requestId: string; readonly settlementId: string; readonly workspaceRoot: string; readonly basisDigest: string; /** The exact paths the request admitted. */ readonly aperture: readonly string[]; readonly byteBudget: number; readonly symlinkPolicy: "refuse"; } export interface AdmittedWorkspaceSnapshot extends WorkspaceSnapshotDescriptor { readonly [admittedSnapshotBrand]: true; } export interface WorkspaceSnapshotFields extends WorkspaceSnapshotDescriptor { /** The settled files, keyed by workspace-relative path. */ readonly files: ReadonlyMap; } /** * One file as the observation recorded it. * * The entry kind is carried rather than assumed because refusing symlinks is a * claim about what was observed, and a claim with nowhere to record its * subject cannot be checked. A settlement that reports a symlink while * declaring `symlinkPolicy: "refuse"` is self-contradictory, and the only * place that contradiction is visible is here. */ export interface SettledFile { readonly bytes: Uint8Array; readonly entryKind: "regular" | "symlink"; } export type SnapshotAdmissionErrorCode = "INVALID_BYTE_BUDGET" | "DUPLICATE_APERTURE_PATH" | "MISSING_APERTURE_BYTES" | "OUTSIDE_APERTURE" | "SYMLINK_REFUSED" | "BYTE_BUDGET_EXCEEDED" | "MISSING_RETAINED_FILES"; /** A snapshot that contradicts a field it declares. */ export declare class SnapshotAdmissionError extends Error { readonly code: SnapshotAdmissionErrorCode; readonly detail: string; constructor(code: SnapshotAdmissionErrorCode, detail: string); } /** * Builds a snapshot that is admitted by assertion rather than by settlement. * * Named for what it is so that a production composition root using it is * obvious in review. The assertion is only about provenance: no Echo * settlement stands behind this value. The fields it declares are still * checked, because a test fixture that could contradict itself would prove * analysis works against observations that cannot occur. * * Copies its input, so a caller mutating the maps and arrays it passed cannot * reach the snapshot afterwards. */ export declare function unsafeAdmittedWorkspaceSnapshotForTest(fields: WorkspaceSnapshotFields): AdmittedWorkspaceSnapshot; /** A path the snapshot does not admit. */ export declare class UnadmittedPathError extends Error { readonly path: string; readonly code: "UNADMITTED_PATH"; constructor(path: string); } /** A path the snapshot admits but for which it carries no bytes. */ export declare class MissingSnapshotBytesError extends Error { readonly path: string; readonly code: "MISSING_SETTLED_BYTES"; constructor(path: string); } /** * A read view over settled snapshot bytes. * * Refuses rather than falling back: a path outside the aperture is a request * for authority the observation did not grant, and answering it from disk * would silently reintroduce the coupling this type exists to remove. */ export declare class SnapshotWorkspaceReadView implements AdmittedWorkspaceReadView { readonly evidence: WorkspaceReadEvidence; constructor(snapshot: AdmittedWorkspaceSnapshot); readBytes(path: string): Promise; admittedPaths(): readonly string[]; } /** * Bytes read from the live filesystem at the moment of the call. * * This is the behaviour Graft had before observations were admitted, kept as * an explicitly named adapter rather than as a default. It is deliberately * **not** an `AdmittedWorkspaceReadView`: nothing about it is settled. The * bytes it returns describe the disk when it was asked, so two reads can * disagree, no basis over them means anything, and it has no admitted path set * to enumerate. It offers neither, so a caller needing either fails to compile * rather than receiving a sentinel or a rejected promise. * * Every production composition root still uses this. That is the remaining * work in #228, and it is visible by name at each one. */ export declare class LiveWorkspaceReadSource implements WorkspaceReadView { private readonly fs; readonly workspaceRoot: string; constructor(fs: Pick, workspaceRoot: string); readBytes(path: string): Promise; } export {}; //# sourceMappingURL=workspace-read-view.d.ts.map