import { type ConfigRootEnvironment, type ConfigRootPlatform } from "./config-store.js"; /** Report format of a saved artifact (spec: `--save-format json|markdown`). */ export type ArtifactFormat = "json" | "markdown"; /** Environment keys {@link resolveArtifactsDir} reads. */ export interface ArtifactsDirEnvironment extends ConfigRootEnvironment { readonly SCOUTLINE_ARTIFACTS_DIR?: string; readonly SCOUTLINE_ISOLATED?: string; } /** Byte source for the request-id hex tail; defaults to crypto.randomBytes. */ export type RandomBytesSource = (size: number) => Uint8Array; /** * Build a request id from the injected instant: `-<4 hex>`, * e.g. `20260829T142233Z-7f3a`. The hex tail comes from two random bytes * per call, so ids generated within the same second still differ. * Sorting ids lexicographically sorts them chronologically (same-second * ids tie — order between them is not defined). */ export declare function newRequestId(now: Date | number, randomBytes?: RandomBytesSource): string; export interface ArtifactsPlatform extends ConfigRootPlatform { readonly pid?: number; } /** * Artifacts root: `SCOUTLINE_ARTIFACTS_DIR` (canonical SCOUTLINE_* name, no * legacy alias) wins; otherwise the config root's `artifacts/` sibling. * Pure — the caller supplies env and platform; the convenience wrapper is * left to the command layer (T2/T3) so tests never touch process.env. * * Test-isolation guard (issue #137): `node --test` sets NODE_TEST_CONTEXT * in every spawned test child, so a bare default-dir resolve there means * the caller FORGOT dependency injection and is about to touch the real * `~/.scoutline/artifacts` — fail loud instead. Decided ONLY from the injected env * (the resolver never reads process.env.SCOUTLINE_ARTIFACTS_DIR — an ambient value must * NOT silence the guard). Lives only on this ambient-env * seam; pure resolver stays total/pure. `SCOUTLINE_NO_TEST_GUARD=1` is the * documented escape hatch for suites deliberately exercising the default path. * Note the shell convention: ANY non-empty value bypasses (JS truthiness) — * `=0` does NOT re-arm the guard; unset it (or set it empty) to re-arm. */ export declare function resolveArtifactsDir(env: ArtifactsDirEnvironment, platform?: ArtifactsPlatform): string; export interface WriteArtifactOptions { /** Report extension; `"json"` (default) or `"markdown"` (`.md`). */ readonly format?: ArtifactFormat; /** true → replace an existing target via the atomic path; false → refuse. */ readonly force?: boolean; /** Lock-timing overrides for the master-write critical section (tests use small values). */ readonly lock?: { readonly timeoutMs?: number; readonly staleMs?: number; readonly setTimeout?: typeof setTimeout; }; } /** * Write one master artifact `/.json|.md` through * {@link atomicReplaceFile} so the 0700-dir / 0600-temp / fsync / rename * discipline is inherited, never reimplemented. The one addition that * primitive lacks: without `force`, an existing target throws * {@link FileError} (`FILE_ERROR`, exit 1 — owner ruling, no new code) * BEFORE any write, leaving the file byte-identical. Resolves with the * target path (the later log's `masterPath`). * * Review fixup (atomic no-overwrite): the existence check and the write * are serialized through the shared `artifacts-write` lock (the same * identity {@link appendLogEntry} uses), and the check is re-run INSIDE * the critical section. Two concurrent saves racing on the same * requestId (or a same-path export) can no longer both pass the * pre-check and have the second silently overwrite the first — the * loser gets the FileError. `force` writes ride the same lock so a * forced replace cannot interleave with a concurrent no-force refusal * window; atomicReplaceFile keeps the replacement itself atomic. */ export declare function writeArtifact(dir: string, requestId: string, content: string, options?: WriteArtifactOptions): Promise; /** * Atomic check-and-place for the export copy: creates the target's * directory as needed (0700 when newly created; pre-existing directories * keep their permissions), writes the content to a unique 0600 temp file * in that directory (fsync'd), * then makes the target via {@link fs.link} — an atomic exclusive create * that fails with EEXIST when the target appeared meanwhile. Resolves * `true` when placed, `false` when the target already existed (which is * left byte-identical — the link never touched it). Review fixup: closes * the export TOCTOU the exists-recheck could only narrow (check and * place are one atomic step now). */ export declare function atomicPlaceNoClobber(filePath: string, contents: string): Promise; /** Log filename under the artifacts dir (DESIGN.md D5). */ export declare const ARTIFACTS_LOG_FILENAME = "index.json"; /** The log's own version namespace — independent of the report schemaVersion. */ export declare const ARTIFACTS_LOG_VERSION = 1; /** Fixed lock identity serializing every index.json append (cache-write precedent). */ export declare const ARTIFACTS_LOG_LOCK_IDENTITY = "artifacts-write"; /** CLI version stamped into each entry (the src/index.ts pkg-import idiom). */ export declare const CLI_VERSION: string; /** * Entry kind discriminator — "save" masters plus "journal" (history-journal * merge D1): journal entries are LOG-ONLY (no master file); their body * fields arrive with the T2a/T3 writers. A kind outside this union still * fails the whole-log open — the fail-loud path is load-bearing. */ export type LogEntryKind = "save" | "journal"; /** Single-provider routing: what was requested and what actually served. */ export interface SingleProviderRouting { readonly mode: "single"; readonly requested?: string; readonly effective: string; /** * Where the serving bytes came from (issue #108): "live" = the effective * provider was actually contacted; "cache" = served from that provider's * on-disk response cache (v2 partitioned or v0.2 legacy read-through), * possibly while the provider was unreachable. Optional so pre-#108 * entries stay valid; save entries always set it. */ readonly servedFrom?: "live" | "cache"; } /** Fan-out routing (ADR-0004): ordered arms; no single effective exists. */ export interface FanoutProviderRouting { readonly mode: "fanout"; readonly requested?: string; readonly arms: readonly string[]; } /** `provider` field of a log entry (the search.ts FanoutPlan vocabulary). */ export type ProviderRouting = SingleProviderRouting | FanoutProviderRouting; /** * One save record in `index.json` — the field set is pinned exactly by * tests/artifacts-log.test.js so unknown additions fail loudly. `args` is * the redacted allow-list of provider-influencing options (exact list * locked at ticket T4; no positionals, no presentation flags, no --save*). */ export interface SaveLogEntry { readonly kind: LogEntryKind; readonly requestId: string; /** ms epoch — the CALLER's injected instant; never Date.now() in here. */ readonly timestamp: number; readonly command: string; readonly args: Readonly>; readonly provider: ProviderRouting; readonly outputFormat: string; readonly artifactFormat: ArtifactFormat; readonly cliVersion: string; /** Master filename relative to the artifacts dir (basename of writeArtifact's return). */ readonly masterPath: string; /** Absolute export-copy path when `--save ` was given. */ readonly exportPath?: string; } /** `index.json` shape: own version field, entries in append order. */ export interface ArtifactsLog { readonly version: typeof ARTIFACTS_LOG_VERSION; readonly entries: readonly SaveLogEntry[]; } /** readLog result: the (possibly empty) log plus an optional stderr notice. */ export interface ReadLogResult { readonly log: ArtifactsLog; readonly notice?: string; } /** Lock-timing overrides for {@link appendLogEntry} (tests use small values). */ export interface AppendLogEntryOptions { readonly timeoutMs?: number; readonly staleMs?: number; /** Injectable timer so lock retries resolve faster than the 500ms sleep. */ readonly setTimeout?: typeof setTimeout; } /** * Lock-free, fail-open read of `/index.json`. A missing store is the * normal empty case (no notice); a corrupt or unrecognized file degrades to * an empty log plus a notice the caller flushes on stderr. NEVER throws — * `history` is a read-only inventory (D7), not a failure surface. */ export declare function readLog(dir: string): Promise; /** * Append one entry to `/index.json`, serialized through the * `artifacts-write` file lock — the cache-write precedent (src/lib/cache.ts): * concurrent CLI invocations read-modify-write under one lockfile, so a * Promise.all of appends persists every entry intact (no lost update, no * torn entry). The write itself rides atomicReplaceFile (0700 dir / 0600 * temp / fsync / rename). Resolves with a stderr notice when a corrupt * pre-existing log was reset by this append; write and lock-acquire * failures propagate — the save hook (T3) wraps them into FileError. */ export declare function appendLogEntry(dir: string, entry: SaveLogEntry, options?: AppendLogEntryOptions): Promise; /** * The save hook's ONE critical section (PR #111 review batch 1, cubic P2): * master write + log append under a single `artifacts-write` hold. The old * writeArtifact → appendLogEntry sequence took the lock twice, leaving a * crash/kill window between the holds — a written master with no log * entry, invisible to `history` and swept as an orphan by * `history clear --all`. The entry is CONSTRUCTED BY THE CALLER (it needs * the requestId, routing, args — hook-owned facts) with `masterPath` * already the bare filename; the target `.` is computed * exactly as {@link writeArtifact} does, and the caller precomputes the * same path for `entry.masterPath` (keep the two in lockstep — the * duplication is pinned by tests/save-artifact.test.js). The no-force * existence refusal keeps its {@link FileError} contract; the append * mirrors {@link appendLogEntry} exactly (same fail-open read, same * notice, same 2-space JSON shape). An I/O failure INSIDE the section can * still leave the master written and unlogged — it surfaces as the save * hook's FILE_ERROR and a retry rewrites both; the closed window is the * crash between the two old lock holds. */ export declare function writeArtifactWithLogEntry(dir: string, requestId: string, content: string, entry: SaveLogEntry, options?: WriteArtifactOptions): Promise; /** Lock options for {@link clearArtifactsLog} (tests shrink timings). */ export interface ClearArtifactsLogOptions { readonly timeoutMs?: number; readonly staleMs?: number; /** Injectable timer so lock retries resolve faster than the 500ms sleep. */ readonly setTimeout?: typeof setTimeout; /** `--all`: also remove save entries AND delete their master files. */ readonly all?: boolean; } /** * `history clear` (T6a): rewrite `/index.json` in place under the * SAME `artifacts-write` lock every append uses, so a clear never * interleaves with a concurrent append (the rewrite is read-filter-write * INSIDE the critical section — the whole-log consistency append relies * on). Read side rides the fail-open {@link readLog} contract: a corrupt * or unrecognized pre-state reads as EMPTY, so clear "removes nothing" * and writes back a valid empty log — the wipe still succeeds. * * Bare clear keeps every non-journal entry (saves + their masters are * byte-untouched); `--all` removes save entries too and unlinks their * master files (a master that vanished is fine; an `--all` sweep also * leaves no logged master behind — no orphans). */ export declare function clearArtifactsLog(dir: string, options?: ClearArtifactsLogOptions): Promise; /** `clearArtifactsLog` outcome: what the valve removed and what stayed. */ export interface ClearArtifactsLogResult { /** Total entries removed (all kinds). */ readonly removed: number; /** Removed counts by entry kind. */ readonly removedByKind: Readonly>; /** Entries that survived the clear. */ readonly kept: number; /** * Master files actually unlinked by the `--all` sweep (review batch * 3, issue 7) — orphans add, vanished/failed unlinks subtract. * Present only under `--all`; a bare clear never sets it. */ readonly mastersDeleted?: number; /** The fail-open read notice (corrupt pre-state), for stderr. */ readonly notice?: string; } //# sourceMappingURL=artifacts.d.ts.map