@descent-vtt/spec-brief 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The engine: one repository's briefs, with the operations the CLI offers and\n * the same ones for a harness or an orchestrator that would rather call a\n * function than parse a terminal.\n *\n * This module and the CLI are where the pieces meet their implementations -\n * the real filesystem, the real git, the plugins a configuration names.\n * Everything they compose is a pure function that takes those as arguments,\n * so an engine can equally run over a `MemoryFileSystem` and no git at all.\n */\n\nimport { access } from 'node:fs/promises';\nimport { basename, dirname, relative, resolve, sep } from 'node:path';\n\nimport { applyPlan } from './apply.js';\nimport {\n applyWaivers,\n planArchive,\n type Plan,\n planUnarchive,\n type PullRequest,\n type UnarchiveRequest,\n type Unmeasured,\n WAIVABLE,\n type Waiver,\n} from './archive.js';\nimport type { Brief } from './brief.js';\nimport { collisionFindings, collisions, type CollisionOptions, type CollisionReport } from './collisions.js';\nimport { type Config, ConfigError, DEFAULT_CONFIG, locateConfig, parseConfig, typeSections } from './config.js';\nimport { buildCorpus, type Corpus, findBriefs, isReady, pendingDependencies, type SourceFile } from './corpus.js';\nimport { canonicalPath, type FileSystem, NodeFileSystem } from './fs.js';\nimport { type CommitInfo, type FileChange, type Git, NodeGit, pullRequestUrl } from './git.js';\nimport { matchGlob, parseGlob } from './glob.js';\nimport { normalisePath } from './links.js';\nimport { lint, type Plugin } from './lint.js';\nimport { asWaivers, loadPlugins } from './plugins.js';\nimport { fileNameFor, nextId, renderNewBrief } from './scaffold.js';\nimport { planWaves, type Schedule, schedule, scheduleFindings, type ScheduleOptions } from './schedule.js';\nimport type { Finding, Phase } from './types.js';\n\nexport class EngineError extends Error {\n readonly code: 'not-found' | 'ambiguous' | 'invalid' | 'refused' | 'exists';\n\n constructor(code: EngineError['code'], message: string) {\n super(message);\n this.name = 'EngineError';\n this.code = code;\n }\n}\n\nexport interface EngineSetup {\n /** Absolute root: the directory holding the configuration. */\n readonly root: string;\n readonly config: Config;\n /** Repository-relative configuration path, or `null` when the defaults apply. */\n readonly configFile: string | null;\n readonly fs: FileSystem;\n readonly git: Git | null;\n readonly plugins?: readonly Plugin[];\n}\n\nexport interface OpenOptions {\n /** Where discovery starts. Defaults to the process's directory. */\n readonly cwd?: string;\n /** An explicit configuration file; discovery is skipped. */\n readonly config?: string | undefined;\n /** Use the defaults even where a configuration file exists. */\n readonly noConfig?: boolean;\n /** `null` runs without git. Defaults to git when the root is inside a working tree. */\n readonly git?: Git | null;\n readonly fs?: FileSystem;\n}\n\nexport interface ArchiveOptions {\n readonly pr?: number | undefined;\n /** The commit the round landed as. */\n readonly commit?: string | undefined;\n /** The branch the round started from; the diff runs from its merge base. */\n readonly base?: string | undefined;\n readonly summary?: string | undefined;\n readonly date?: string | undefined;\n readonly allowDirty?: boolean | undefined;\n readonly strict?: boolean | undefined;\n /** Leave git out, even where there is a repository. */\n readonly noGit?: boolean | undefined;\n}\n\nexport interface NewOptions {\n readonly title: string;\n readonly id?: string | undefined;\n readonly type?: string | undefined;\n readonly wave?: number | undefined;\n readonly dependsOn?: readonly string[] | undefined;\n readonly date?: string | undefined;\n}\n\nasync function exists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n // Absent, or not to be reached: the search goes on either way. An empty\n // catch would answer `undefined`, which the search reads the same.\n return false;\n }\n}\n\n/** The last second of 9999-12-31, the last date written YYYY-MM-DD. */\nconst LAST_SECOND = 253402300799;\n\n/**\n * Today, in UTC. `SOURCE_DATE_EPOCH` wins where it is set, which is what makes\n * a banner written in a reproducible pipeline reproducible.\n *\n * Set, it is what its specification says it is: whole seconds since\n * 1970-01-01 in digits alone, as `date +%s` prints them, and here no later\n * than the last date written YYYY-MM-DD. Anything else is refused by name,\n * an empty value too, which is what a command substitution that failed\n * leaves. Read as unset, it had the run write the clock's date where a\n * pipeline asked for its own, and say nothing.\n */\nexport function today(env: Readonly<Record<string, string | undefined>> = process.env): string {\n const epoch = env['SOURCE_DATE_EPOCH'];\n let when = new Date();\n if (epoch !== undefined) {\n // Quoted as JSON, so a newline or a tab in it is seen, and the refusal stays one line.\n const held = `SOURCE_DATE_EPOCH is ${JSON.stringify(epoch)}`;\n if (!/^\\d+$/.test(epoch)) {\n throw new EngineError('invalid', `${held}, which is not a whole number of seconds since 1970-01-01; set it to one in digits alone, as \"date +%s\" prints it, or unset it`);\n }\n if (Number(epoch) > LAST_SECOND) {\n throw new EngineError('invalid', `${held}, which is after 9999-12-31, the last date written YYYY-MM-DD; set it to ${LAST_SECOND} or less, or unset it`);\n }\n when = new Date(Number(epoch) * 1000);\n }\n return when.toISOString().slice(0, 10);\n}\n\n/**\n * A deep copy of a value in which every object and array is frozen. Plain\n * data only, as `structuredClone` takes it; the recursion ends at the data's\n * own depth.\n */\nfunction frozenCopy<T>(value: T): T {\n const freeze = (item: unknown): void => {\n if (typeof item !== 'object' || item === null) return;\n for (const child of Object.values(item)) freeze(child);\n Object.freeze(item);\n };\n const copy = structuredClone(value);\n freeze(copy);\n return copy;\n}\n\nexport function isDate(text: string): boolean {\n // The shape keeps out the one other text that survives the round trip\n // below: a year past 9999 with a month, \"+010000-01\". Its anchors are for\n // the reader, since nothing longer than a date comes back as it went in.\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(text)) return false;\n const parsed = new Date(`${text}T00:00:00Z`);\n return !Number.isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === text;\n}\n\nexport class BriefEngine {\n readonly root: string;\n readonly config: Config;\n readonly configFile: string | null;\n readonly fs: FileSystem;\n readonly git: Git | null;\n private plugins: readonly Plugin[] | undefined;\n private loaded: Corpus | undefined;\n /** Known once the briefs are read, which every command does before it asks. */\n private missingBriefs: boolean | undefined;\n\n constructor(setup: EngineSetup) {\n this.root = setup.root;\n this.config = setup.config;\n this.configFile = setup.configFile;\n this.fs = setup.fs;\n this.git = setup.git;\n this.plugins = setup.plugins;\n }\n\n /**\n * Finds the configuration, the root it defines and the repository around it,\n * and reads every brief. The root is the directory holding the\n * configuration; without one it is the working tree's top, or `cwd`.\n */\n static async open(options: OpenOptions = {}): Promise<BriefEngine> {\n // Named as git names them, so the root and the work tree compare.\n const cwd = await canonicalPath(resolve(options.cwd ?? process.cwd()));\n const explicit = options.config === undefined ? undefined : await canonicalPath(resolve(cwd, options.config));\n const configPath = options.noConfig === true ? undefined : (explicit ?? (await locateConfig(cwd, exists)));\n // Asking git costs a process; without git there is nothing to ask.\n const toplevel = options.git === null ? null : await NodeGit.toplevel(cwd);\n const root = configPath === undefined ? (toplevel ?? cwd) : dirname(configPath);\n let config = DEFAULT_CONFIG;\n if (configPath !== undefined) {\n const text = await new NodeFileSystem(root).read(basename(configPath)).catch((error: NodeJS.ErrnoException) => {\n // A directory of that name, found or named with --config, is a\n // configuration that does not load, and no defect to report a stack for.\n if (error.code !== 'EISDIR') throw error;\n throw new ConfigError(relative(cwd, configPath), ['is a directory, not a configuration file']);\n });\n if (text === null) throw new EngineError('not-found', `${configPath} does not exist`);\n config = parseConfig(text, relative(cwd, configPath));\n }\n // Outside the work tree is its parent or a path through it, \"..\", \"../x\":\n // a directory inside it may itself be named \"..drafts\".\n const fromTop = toplevel === null ? null : relative(toplevel, root);\n const inTree = fromTop !== null && fromTop !== '..' && !fromTop.startsWith(`..${sep}`);\n const engine = new BriefEngine({\n root,\n config,\n // The root is the configuration's directory, so its path from there is its name.\n configFile: configPath === undefined ? null : basename(configPath),\n fs: options.fs ?? new NodeFileSystem(root),\n git: options.git === undefined ? (inTree ? new NodeGit(root) : null) : options.git,\n });\n await engine.load();\n return engine;\n }\n\n get corpus(): Corpus {\n if (this.loaded === undefined) throw new Error('the engine has not loaded its briefs; call load() first');\n return this.loaded;\n }\n\n /** Reads every brief again, as after an archival. */\n async load(): Promise<Corpus> {\n const files: SourceFile[] = [];\n const accept = parseGlob(this.config.files);\n const reject = this.config.exclude.map((pattern) => parseGlob(pattern));\n const isBrief = (name: string): boolean =>\n accept.ok && matchGlob(accept.glob, name) && !reject.some((r) => r.ok && matchGlob(r.glob, name));\n const read = async (directory: string, phase: Phase): Promise<boolean> => {\n const dir = normalisePath(directory);\n const names = await this.fs.list(dir);\n if (names === null) return false;\n for (const name of names) {\n if (!isBrief(name)) continue;\n const path = dir === '' ? name : `${dir}/${name}`;\n const text = await this.fs.read(path);\n // A file listed a moment ago and gone now was removed under the run; there is nothing to read.\n if (text !== null) files.push({ path, text, phase });\n }\n return true;\n };\n this.missingBriefs = !(await read(this.config.briefs, 'live'));\n await read(this.config.archive, 'archived');\n this.loaded = buildCorpus(files, this.config);\n return this.loaded;\n }\n\n /**\n * Refuses to report on a briefs directory that does not exist. A check over\n * nothing looks exactly like a clean one, and the likelier cause is a\n * configuration pointing at the wrong place.\n */\n requireBriefs(): void {\n if (this.missingBriefs) {\n throw new EngineError('not-found', `${normalisePath(this.config.briefs)}/ does not exist; run \"spec-brief init\", or set \"briefs\" in the configuration`);\n }\n }\n\n /** The plugins the configuration names, loaded once. */\n async loadedPlugins(): Promise<readonly Plugin[]> {\n this.plugins ??= await loadPlugins(this.config.plugins, this.root);\n return this.plugins;\n }\n\n /** The one brief a reference names: an id, a file name or a path. */\n find(reference: string): Brief {\n const found = findBriefs(this.corpus, reference);\n if (found.length === 0) throw new EngineError('not-found', `no brief is named \"${reference}\"`);\n if (found.length > 1) {\n throw new EngineError('ambiguous', `\"${reference}\" names ${found.length} briefs: ${found.map((b) => b.file).join(', ')}`);\n }\n return found[0] as Brief;\n }\n\n /** The files git sees, or every file when there is no git; `null` when neither can be read. */\n async repoFiles(): Promise<string[] | null> {\n try {\n return this.git === null ? await this.fs.walk() : await this.git.files();\n } catch {\n return null;\n }\n }\n\n async lint(references: readonly string[] = []): Promise<Finding[]> {\n const only = references.length === 0 ? undefined : references.map((r) => this.find(r));\n return lint(this.corpus, { plugins: await this.loadedPlugins(), repoFiles: await this.repoFiles(), ...(only ? { only } : {}) });\n }\n\n async collisions(options: CollisionOptions = {}): Promise<{ report: CollisionReport; findings: Finding[] }> {\n const report = collisions(this.corpus, { repoFiles: await this.repoFiles(), ...options });\n return { report, findings: collisionFindings(this.corpus, report) };\n }\n\n async schedule(options: ScheduleOptions = {}): Promise<{ schedule: Schedule; findings: Finding[] }> {\n const computed = schedule(this.corpus, { repoFiles: await this.repoFiles(), ...options });\n return { schedule: computed, findings: scheduleFindings(this.corpus, computed) };\n }\n\n /**\n * Writes each brief's proposed wave into its front matter, every other line\n * as it was, as one transaction. Refused, with nothing written, when the\n * dependencies form a cycle or a front matter cannot be edited.\n */\n async writeWaves(computed: Schedule): Promise<{ written: string[]; refused: Finding[] }> {\n const plan = planWaves(computed);\n if (computed.cycles.length > 0 || plan.refused.length > 0) return { written: [], refused: [...plan.refused] };\n await applyPlan(this.fs, plan.ops);\n await this.load();\n return { written: plan.ops.map((op) => op.path), refused: [] };\n }\n\n /** Live briefs that can run now: every dependency archived, and neither a draft nor deferred. */\n ready(): Brief[] {\n return this.corpus.live.filter((b) => isReady(this.corpus, b));\n }\n\n /** The live dependencies holding a brief back. */\n waitingOn(brief: Brief): string[] {\n return pendingDependencies(this.corpus, brief);\n }\n\n async planArchive(reference: string, options: ArchiveOptions = {}): Promise<Plan> {\n const brief = this.find(reference);\n const date = options.date ?? today();\n if (!isDate(date)) throw new EngineError('invalid', `\"${date}\" is not a date written YYYY-MM-DD`);\n if (options.noGit === true && (options.commit !== undefined || options.base !== undefined)) {\n throw new EngineError('invalid', 'a commit or a base needs git; drop --no-git, or drop --commit and --base');\n }\n const git = options.noGit === true ? null : this.git;\n const base = options.base ?? this.config.archiving.base ?? undefined;\n const findings = await this.lint();\n\n let commit: CommitInfo | undefined;\n let changes: FileChange[] | undefined;\n let unmeasured: Unmeasured | undefined;\n let dirty: string[] | undefined;\n let pr: PullRequest | undefined;\n if (git !== null) {\n const revision = options.commit ?? (base === undefined ? undefined : 'HEAD');\n if (revision !== undefined) {\n commit = (await git.commit(revision)) ?? undefined;\n if (commit === undefined) throw new EngineError('not-found', `\"${revision}\" names no commit`);\n let from: string | null = null;\n if (base !== undefined) {\n from = await git.mergeBase(base, commit.sha);\n // No merge base: a base with a history of its own is diffed from\n // directly, and one that names nothing is a typo to report, not a\n // diff for git to fail.\n if (from === null) {\n if ((await git.commit(base)) === null) {\n throw new EngineError('not-found', `the base \"${base}\" names no commit; check --base and \"archiving.base\"`);\n }\n from = base;\n }\n }\n // A commit that is its own merge base with the base branch is already\n // in it: the diff is empty whatever the round changed, and an empty\n // diff would pass every scope check without measuring one.\n if (from === commit.sha) unmeasured = { reason: 'merged', base: base as string, commit: commit.sha };\n else changes = await git.changes(from, commit.sha);\n }\n // With neither a commit nor a base, nothing was read, and the planner\n // says so itself of a request that brings no changes and no reason.\n dirty = await git.dirty();\n } else {\n unmeasured = { reason: 'git' };\n }\n if (options.pr !== undefined) {\n pr = { number: options.pr, url: git === null ? null : pullRequestUrl(await git.remoteUrl('origin'), options.pr) };\n }\n const plan = planArchive(this.corpus, brief, {\n date,\n summary: options.summary,\n pr,\n commit,\n changes,\n unmeasured,\n dirty,\n allowDirty: options.allowDirty,\n strict: options.strict,\n findings,\n });\n return this.waive(plan, base ?? null, commit?.sha ?? null, options.strict === true);\n }\n\n /**\n * Asks each plugin with a `waive` hook which of a plan's refusals its own\n * check lifts, and turns those into notes. Asked only when the plan has a\n * refusal a plugin may lift: a hook may read git and verify signatures, and a\n * plan with nothing to waive has no question for it. A hook that throws, or\n * answers in the wrong shape, stops the run as a plugin that fails to load\n * does: an archival decided without the check it asked for is not one to\n * trust.\n *\n * A hook is handed a frozen copy of the refusals, and its waivers are\n * matched against the plan's own. Handed the plan's list, a hook could drop\n * a refusal from it, or relabel an open task as a protected file and waive\n * that, and so lift what only spec-brief may; writing to the copy throws,\n * which stops the run like any failing hook.\n */\n private async waive(plan: Plan, base: string | null, commit: string | null, strict: boolean): Promise<Plan> {\n if (!plan.blocking.some((f) => WAIVABLE.includes(f.rule))) return plan;\n const context = frozenCopy({\n root: this.root,\n brief: { id: plan.brief.id, file: plan.brief.file, text: plan.brief.text },\n findings: plan.blocking,\n base,\n commit,\n });\n const waivers: { plugin: string; waiver: Waiver }[] = [];\n for (const plugin of await this.loadedPlugins()) {\n if (plugin.waive === undefined) continue;\n let answer: unknown;\n try {\n answer = await plugin.waive(context);\n } catch (error) {\n throw new ConfigError(`plugin \"${plugin.name}\"`, [`\"waive\" failed: ${error instanceof Error ? error.message : String(error)}`]);\n }\n for (const waiver of asWaivers(answer, plugin.name)) waivers.push({ plugin: plugin.name, waiver });\n }\n return applyWaivers(plan, waivers, strict);\n }\n\n planUnarchive(reference: string, options: UnarchiveRequest = {}): Plan {\n return planUnarchive(this.corpus, this.find(reference), options);\n }\n\n /** Executes a plan and reads the briefs again. A refused plan is never applied. */\n async apply(plan: Plan): Promise<void> {\n if (plan.blocking.length > 0) {\n throw new EngineError('refused', `${plan.action} of ${plan.brief.file} is refused: ${(plan.blocking[0] as Finding).message}`);\n }\n if (plan.done) return;\n await applyPlan(this.fs, plan.ops);\n await this.load();\n }\n\n /** Scaffolds a brief and writes it; the file is new, or nothing is written. */\n async create(options: NewOptions): Promise<{ file: string; content: string }> {\n const title = options.title.trim();\n if (title === '') throw new EngineError('invalid', 'a brief needs a title');\n const id = options.id ?? nextId(this.corpus);\n if (id === null) throw new EngineError('invalid', 'the ids here are not numbers, so there is no next one; pass --id');\n if (/[\\s/\\\\]/.test(id) || id.includes(this.config.id.separator)) {\n throw new EngineError('invalid', `\"${id}\" cannot be an id: no whitespace, slashes or \"${this.config.id.separator}\"`);\n }\n if (findBriefs(this.corpus, id).some((b) => b.id !== null)) throw new EngineError('exists', `the id \"${id}\" is taken`);\n if (options.type !== undefined && typeSections(this.config, options.type) === undefined) {\n const names = Object.keys(this.config.types);\n throw new EngineError('invalid', `\"${options.type}\" is not a brief type here; use one of ${names.join(', ') || '(none)'}`);\n }\n const date = options.date ?? today();\n if (!isDate(date)) throw new EngineError('invalid', `\"${date}\" is not a date written YYYY-MM-DD`);\n const template = this.config.template === null ? null : await this.fs.read(this.config.template);\n if (this.config.template !== null && template === null) {\n throw new EngineError('not-found', `the template ${this.config.template} does not exist`);\n }\n const content = renderNewBrief(\n this.config,\n { id, title, date, type: options.type, wave: options.wave, dependsOn: options.dependsOn },\n template,\n );\n const dir = normalisePath(this.config.briefs);\n const file = `${dir}/${fileNameFor(this.config, id, title)}`;\n if (await this.fs.exists(file)) throw new EngineError('exists', `${file} already exists`);\n await this.fs.write(file, content);\n await this.load();\n return { file, content };\n }\n}\n"]}
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The engine: one repository's briefs, with the operations the CLI offers and\n * the same ones for a harness or an orchestrator that would rather call a\n * function than parse a terminal.\n *\n * This module and the CLI are where the pieces meet their implementations -\n * the real filesystem, the real git, the plugins a configuration names.\n * Everything they compose is a pure function that takes those as arguments,\n * so an engine can equally run over a `MemoryFileSystem` and no git at all.\n */\n\nimport { access } from 'node:fs/promises';\nimport { basename, dirname, relative, resolve, sep } from 'node:path';\n\nimport { applyPlan } from './apply.js';\nimport {\n applyWaivers,\n planArchive,\n type Plan,\n planUnarchive,\n type PullRequest,\n type UnarchiveRequest,\n type Unmeasured,\n WAIVABLE,\n type Waiver,\n} from './archive.js';\nimport type { Brief } from './brief.js';\nimport { collisionFindings, collisions, type CollisionOptions, type CollisionReport } from './collisions.js';\nimport { type Config, ConfigError, DEFAULT_CONFIG, locateConfig, parseConfig, typeSections } from './config.js';\nimport { buildCorpus, type Corpus, findBriefs, isReady, pendingDependencies, type SourceFile } from './corpus.js';\nimport { canonicalPath, type FileSystem, NodeFileSystem, NotAFileError, type Reads } from './fs.js';\nimport { type CommitInfo, type FileChange, type Git, NodeGit, pullRequestUrl } from './git.js';\nimport { matchGlob, parseGlob } from './glob.js';\nimport { normalisePath } from './links.js';\nimport { lint, type Plugin } from './lint.js';\nimport { asWaivers, loadPlugins } from './plugins.js';\nimport { fileNameFor, nextId, renderNewBrief } from './scaffold.js';\nimport { planWaves, type Schedule, schedule, scheduleFindings, type ScheduleOptions } from './schedule.js';\nimport type { Finding, Phase } from './types.js';\n\nexport class EngineError extends Error {\n readonly code: 'not-found' | 'ambiguous' | 'invalid' | 'refused' | 'exists';\n\n constructor(code: EngineError['code'], message: string) {\n super(message);\n this.name = 'EngineError';\n this.code = code;\n }\n}\n\nexport interface EngineSetup {\n /** Absolute root: the directory holding the configuration. */\n readonly root: string;\n readonly config: Config;\n /** Repository-relative configuration path, or `null` when the defaults apply. */\n readonly configFile: string | null;\n readonly fs: FileSystem;\n readonly git: Git | null;\n readonly plugins?: readonly Plugin[];\n}\n\nexport interface OpenOptions {\n /** Where discovery starts. Defaults to the process's directory. */\n readonly cwd?: string;\n /** An explicit configuration file; discovery is skipped. */\n readonly config?: string | undefined;\n /** Use the defaults even where a configuration file exists. */\n readonly noConfig?: boolean;\n /** `null` runs without git. Defaults to git when the root is inside a working tree. */\n readonly git?: Git | null;\n readonly fs?: FileSystem;\n /**\n * `false` where nothing the caller asks of the engine reads git, as `list`\n * asks it nothing: where a configuration file names the root, git is then\n * not asked where the work tree's top is, and the engine has no git.\n * Without one, git is still asked, since its answer is the root.\n */\n readonly needsGit?: boolean;\n}\n\n/**\n * Everything an answer was read from, for a tool that would give it again\n * without starting spec-brief (ADR-0002, amended 2026-10-19). Each file is\n * named with the SHA-256 of its bytes, or `null` where it was looked for and\n * was not there; each directory with the names of the files in it, sorted,\n * or `null` where there was no directory to list. Each `null` is a path\n * that must still not be there. Paths are from the root, which wherever `keep` is true is the directory\n * the run started in. `git` is `null`, since no answer named here asks git\n * anything. `keep` is `false`, and the lists are empty, wherever that cannot\n * be named whole.\n */\nexport interface Inputs {\n readonly keep: boolean;\n readonly files: readonly { readonly path: string; readonly sha256: string | null }[];\n readonly directories: readonly { readonly path: string; readonly entries: readonly string[] | null }[];\n readonly git: null;\n}\n\nexport interface ArchiveOptions {\n readonly pr?: number | undefined;\n /** The commit the round landed as. */\n readonly commit?: string | undefined;\n /** The branch the round started from; the diff runs from its merge base. */\n readonly base?: string | undefined;\n readonly summary?: string | undefined;\n readonly date?: string | undefined;\n readonly allowDirty?: boolean | undefined;\n readonly strict?: boolean | undefined;\n /** Leave git out, even where there is a repository. */\n readonly noGit?: boolean | undefined;\n}\n\nexport interface NewOptions {\n readonly title: string;\n readonly id?: string | undefined;\n readonly type?: string | undefined;\n readonly wave?: number | undefined;\n readonly dependsOn?: readonly string[] | undefined;\n readonly date?: string | undefined;\n}\n\nasync function exists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n // Absent, or not to be reached: the search goes on either way. An empty\n // catch would answer `undefined`, which the search reads the same.\n return false;\n }\n}\n\n/** The last second of 9999-12-31, the last date written YYYY-MM-DD. */\nconst LAST_SECOND = 253402300799;\n\n/**\n * Today, in UTC. `SOURCE_DATE_EPOCH` wins where it is set, which is what makes\n * a banner written in a reproducible pipeline reproducible.\n *\n * Set, it is what its specification says it is: whole seconds since\n * 1970-01-01 in digits alone, as `date +%s` prints them, and here no later\n * than the last date written YYYY-MM-DD. Anything else is refused by name,\n * an empty value too, which is what a command substitution that failed\n * leaves. Read as unset, it had the run write the clock's date where a\n * pipeline asked for its own, and say nothing.\n */\nexport function today(env: Readonly<Record<string, string | undefined>> = process.env): string {\n const epoch = env['SOURCE_DATE_EPOCH'];\n let when = new Date();\n if (epoch !== undefined) {\n // Quoted as JSON, so a newline or a tab in it is seen, and the refusal stays one line.\n const held = `SOURCE_DATE_EPOCH is ${JSON.stringify(epoch)}`;\n if (!/^\\d+$/.test(epoch)) {\n throw new EngineError('invalid', `${held}, which is not a whole number of seconds since 1970-01-01; set it to one in digits alone, as \"date +%s\" prints it, or unset it`);\n }\n if (Number(epoch) > LAST_SECOND) {\n throw new EngineError('invalid', `${held}, which is after 9999-12-31, the last date written YYYY-MM-DD; set it to ${LAST_SECOND} or less, or unset it`);\n }\n when = new Date(Number(epoch) * 1000);\n }\n return when.toISOString().slice(0, 10);\n}\n\n/**\n * A deep copy of a value in which every object and array is frozen. Plain\n * data only, as `structuredClone` takes it; the recursion ends at the data's\n * own depth.\n */\nfunction frozenCopy<T>(value: T): T {\n const freeze = (item: unknown): void => {\n if (typeof item !== 'object' || item === null) return;\n for (const child of Object.values(item)) freeze(child);\n Object.freeze(item);\n };\n const copy = structuredClone(value);\n freeze(copy);\n return copy;\n}\n\nexport function isDate(text: string): boolean {\n // The shape keeps out the one other text that survives the round trip\n // below: a year past 9999 with a month, \"+010000-01\". Its anchors are for\n // the reader, since nothing longer than a date comes back as it went in.\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(text)) return false;\n const parsed = new Date(`${text}T00:00:00Z`);\n return !Number.isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === text;\n}\n\nexport class BriefEngine {\n readonly root: string;\n readonly config: Config;\n readonly configFile: string | null;\n readonly fs: FileSystem;\n readonly git: Git | null;\n private plugins: readonly Plugin[] | undefined;\n private loaded: Corpus | undefined;\n /**\n * What the engine has read, where `open` made it, and whether that is\n * every input of an answer made of the briefs and the configuration alone.\n * Set false by a read that is not on it: the files git or a walk lists,\n * and a plugin's module.\n */\n private reading: { readonly reads: Reads; whole: boolean } | undefined;\n /** Known once the briefs are read, which every command does before it asks. */\n private missingBriefs: boolean | undefined;\n\n constructor(setup: EngineSetup) {\n this.root = setup.root;\n this.config = setup.config;\n this.configFile = setup.configFile;\n this.fs = setup.fs;\n this.git = setup.git;\n this.plugins = setup.plugins;\n }\n\n /**\n * Finds the configuration, the root it defines and the repository around it,\n * and reads every brief. The root is the directory holding the\n * configuration; without one it is the working tree's top, or `cwd`.\n */\n static async open(options: OpenOptions = {}): Promise<BriefEngine> {\n // Named as git names them, so the root and the work tree compare.\n const cwd = await canonicalPath(resolve(options.cwd ?? process.cwd()));\n const explicit = options.config === undefined ? undefined : await canonicalPath(resolve(cwd, options.config));\n // Each name looked for, so that an answer can say which were not there.\n const looked: string[] = [];\n const configPath =\n options.noConfig === true\n ? undefined\n : (explicit ??\n (await locateConfig(cwd, (path) => {\n looked.push(path);\n return exists(path);\n })));\n // Asking git costs a process; without git there is nothing to ask, and\n // with a root a configuration file names nothing to ask it where the\n // caller reads no git.\n const asksGit = options.git !== null && (configPath === undefined || options.needsGit !== false);\n const toplevel = asksGit ? await NodeGit.toplevel(cwd) : null;\n const root = configPath === undefined ? (toplevel ?? cwd) : dirname(configPath);\n const reads: Reads = { files: new Map(), directories: new Map() };\n // Each is named as not there, and the one found again by its read below.\n // Wherever an answer is kept, each was looked for in the root itself, so\n // its path from there is its name.\n for (const path of looked) reads.files.set(relative(root, path), null);\n let config = DEFAULT_CONFIG;\n if (configPath !== undefined) {\n // Only a file is read. A named pipe under the configuration's name was\n // read without end, by every command; and a directory, found or named\n // with --config, ended the run on Node's EISDIR. Each is a\n // configuration that does not load, and no defect to report a stack for.\n const text = await new NodeFileSystem(root, reads).read(basename(configPath)).catch((error: unknown) => {\n throw error instanceof NotAFileError ? new ConfigError(relative(cwd, configPath), [`is ${error.what}, not a configuration file`]) : error;\n });\n if (text === null) throw new EngineError('not-found', `${configPath} does not exist`);\n config = parseConfig(text, relative(cwd, configPath));\n }\n // Outside the work tree is its parent or a path through it, \"..\", \"../x\":\n // a directory inside it may itself be named \"..drafts\".\n const fromTop = toplevel === null ? null : relative(toplevel, root);\n const inTree = fromTop !== null && fromTop !== '..' && !fromTop.startsWith(`..${sep}`);\n const engine = new BriefEngine({\n root,\n config,\n // The root is the configuration's directory, so its path from there is its name.\n configFile: configPath === undefined ? null : basename(configPath),\n fs: options.fs ?? new NodeFileSystem(root, reads),\n git: options.git === undefined ? (inTree ? new NodeGit(root) : null) : options.git,\n });\n // Named whole only where each input is read from the directory the run\n // started in, so that a reader finds it by the path named: the engine\n // reads through its own disk, the root is that directory, and a\n // configuration file there names it, or neither a configuration nor git\n // was asked. A configuration not found was looked for above the root as\n // well, and a root git names is decided by what git reads to find the\n // top of a work tree, which no list here names.\n const whole = options.fs === undefined && !asksGit && root === cwd && (configPath !== undefined || options.noConfig === true);\n engine.reading = { reads, whole };\n await engine.load();\n return engine;\n }\n\n get corpus(): Corpus {\n if (this.loaded === undefined) throw new Error('the engine has not loaded its briefs; call load() first');\n return this.loaded;\n }\n\n /** Reads every brief again, as after an archival. */\n async load(): Promise<Corpus> {\n const files: SourceFile[] = [];\n const accept = parseGlob(this.config.files);\n const reject = this.config.exclude.map((pattern) => parseGlob(pattern));\n const isBrief = (name: string): boolean =>\n accept.ok && matchGlob(accept.glob, name) && !reject.some((r) => r.ok && matchGlob(r.glob, name));\n const read = async (directory: string, phase: Phase): Promise<boolean> => {\n const dir = normalisePath(directory);\n const names = await this.fs.list(dir);\n if (names === null) return false;\n for (const name of names) {\n if (!isBrief(name)) continue;\n const path = dir === '' ? name : `${dir}/${name}`;\n // The listing named it a file, so the read does not ask again.\n const text = await this.fs.read(path, true);\n // A file listed a moment ago and gone now was removed under the run; there is nothing to read.\n if (text !== null) files.push({ path, text, phase });\n }\n return true;\n };\n this.missingBriefs = !(await read(this.config.briefs, 'live'));\n await read(this.config.archive, 'archived');\n this.loaded = buildCorpus(files, this.config);\n return this.loaded;\n }\n\n /**\n * Refuses to report on a briefs directory that does not exist. A check over\n * nothing looks exactly like a clean one, and the likelier cause is a\n * configuration pointing at the wrong place.\n */\n requireBriefs(): void {\n if (this.missingBriefs) {\n throw new EngineError('not-found', `${normalisePath(this.config.briefs)}/ does not exist; run \"spec-brief init\", or set \"briefs\" in the configuration`);\n }\n }\n\n /** The plugins the configuration names, loaded once. */\n async loadedPlugins(): Promise<readonly Plugin[]> {\n if (this.reading !== undefined) this.reading.whole = false;\n this.plugins ??= await loadPlugins(this.config.plugins, this.root);\n return this.plugins;\n }\n\n /** The one brief a reference names: an id, a file name or a path. */\n find(reference: string): Brief {\n const found = findBriefs(this.corpus, reference);\n if (found.length === 0) throw new EngineError('not-found', `no brief is named \"${reference}\"`);\n if (found.length > 1) {\n throw new EngineError('ambiguous', `\"${reference}\" names ${found.length} briefs: ${found.map((b) => b.file).join(', ')}`);\n }\n return found[0] as Brief;\n }\n\n /** The files git sees, or every file when there is no git; `null` when neither can be read. */\n async repoFiles(): Promise<string[] | null> {\n if (this.reading !== undefined) this.reading.whole = false;\n try {\n return this.git === null ? await this.fs.walk() : await this.git.files();\n } catch {\n return null;\n }\n }\n\n async lint(references: readonly string[] = []): Promise<Finding[]> {\n const only = references.length === 0 ? undefined : references.map((r) => this.find(r));\n return lint(this.corpus, { plugins: await this.loadedPlugins(), repoFiles: await this.repoFiles(), ...(only ? { only } : {}) });\n }\n\n async collisions(options: CollisionOptions = {}): Promise<{ report: CollisionReport; findings: Finding[] }> {\n const report = collisions(this.corpus, { repoFiles: await this.repoFiles(), ...options });\n return { report, findings: collisionFindings(this.corpus, report) };\n }\n\n async schedule(options: ScheduleOptions = {}): Promise<{ schedule: Schedule; findings: Finding[] }> {\n const computed = schedule(this.corpus, { repoFiles: await this.repoFiles(), ...options });\n return { schedule: computed, findings: scheduleFindings(this.corpus, computed) };\n }\n\n /**\n * Writes each brief's proposed wave into its front matter, every other line\n * as it was, as one transaction. Refused, with nothing written, when the\n * dependencies form a cycle or a front matter cannot be edited.\n */\n async writeWaves(computed: Schedule): Promise<{ written: string[]; refused: Finding[] }> {\n const plan = planWaves(computed);\n if (computed.cycles.length > 0 || plan.refused.length > 0) return { written: [], refused: [...plan.refused] };\n await applyPlan(this.fs, plan.ops);\n await this.load();\n return { written: plan.ops.map((op) => op.path), refused: [] };\n }\n\n /**\n * Every input of an answer made of the briefs and the configuration alone,\n * as `list` gives one, read since the engine was opened (ADR-0002, amended\n * 2026-10-19).\n */\n inputs(): Inputs {\n const reading = this.reading;\n if (reading?.whole !== true) return { keep: false, files: [], directories: [], git: null };\n // Paths are unique, so no two compare equal.\n const byPath = (a: { readonly path: string }, b: { readonly path: string }): number => (a.path < b.path ? -1 : 1);\n return {\n keep: true,\n files: [...reading.reads.files].map(([path, sha256]) => ({ path, sha256 })).sort(byPath),\n // The root is \".\" where a briefs directory is the root itself. One that\n // was not there, an archive not yet made, is named with no entries: one\n // made with a brief in it changes what `list --archived` answers.\n directories: [...reading.reads.directories]\n .map(([path, entries]) => ({ path: path === '' ? '.' : path, entries: entries === null ? null : [...entries].sort() }))\n .sort(byPath),\n git: null,\n };\n }\n\n /** Live briefs that can run now: every dependency archived, and neither a draft nor deferred. */\n ready(): Brief[] {\n return this.corpus.live.filter((b) => isReady(this.corpus, b));\n }\n\n /** The live dependencies holding a brief back. */\n waitingOn(brief: Brief): string[] {\n return pendingDependencies(this.corpus, brief);\n }\n\n async planArchive(reference: string, options: ArchiveOptions = {}): Promise<Plan> {\n const brief = this.find(reference);\n const date = options.date ?? today();\n if (!isDate(date)) throw new EngineError('invalid', `\"${date}\" is not a date written YYYY-MM-DD`);\n if (options.noGit === true && (options.commit !== undefined || options.base !== undefined)) {\n throw new EngineError('invalid', 'a commit or a base needs git; drop --no-git, or drop --commit and --base');\n }\n const git = options.noGit === true ? null : this.git;\n const base = options.base ?? this.config.archiving.base ?? undefined;\n const findings = await this.lint();\n\n let commit: CommitInfo | undefined;\n let changes: FileChange[] | undefined;\n let unmeasured: Unmeasured | undefined;\n let dirty: string[] | undefined;\n let pr: PullRequest | undefined;\n if (git !== null) {\n const revision = options.commit ?? (base === undefined ? undefined : 'HEAD');\n if (revision !== undefined) {\n commit = (await git.commit(revision)) ?? undefined;\n if (commit === undefined) throw new EngineError('not-found', `\"${revision}\" names no commit`);\n let from: string | null = null;\n if (base !== undefined) {\n from = await git.mergeBase(base, commit.sha);\n // No merge base: a base with a history of its own is diffed from\n // directly, and one that names nothing is a typo to report, not a\n // diff for git to fail.\n if (from === null) {\n if ((await git.commit(base)) === null) {\n throw new EngineError('not-found', `the base \"${base}\" names no commit; check --base and \"archiving.base\"`);\n }\n from = base;\n }\n }\n // A commit that is its own merge base with the base branch is already\n // in it: the diff is empty whatever the round changed, and an empty\n // diff would pass every scope check without measuring one.\n if (from === commit.sha) unmeasured = { reason: 'merged', base: base as string, commit: commit.sha };\n else changes = await git.changes(from, commit.sha);\n }\n // With neither a commit nor a base, nothing was read, and the planner\n // says so itself of a request that brings no changes and no reason.\n dirty = await git.dirty();\n } else {\n unmeasured = { reason: 'git' };\n }\n if (options.pr !== undefined) {\n pr = { number: options.pr, url: git === null ? null : pullRequestUrl(await git.remoteUrl('origin'), options.pr) };\n }\n const plan = planArchive(this.corpus, brief, {\n date,\n summary: options.summary,\n pr,\n commit,\n changes,\n unmeasured,\n dirty,\n allowDirty: options.allowDirty,\n strict: options.strict,\n findings,\n });\n return this.waive(plan, base ?? null, commit?.sha ?? null, options.strict === true);\n }\n\n /**\n * Asks each plugin with a `waive` hook which of a plan's refusals its own\n * check lifts, and turns those into notes. Asked only when the plan has a\n * refusal a plugin may lift: a hook may read git and verify signatures, and a\n * plan with nothing to waive has no question for it. A hook that throws, or\n * answers in the wrong shape, stops the run as a plugin that fails to load\n * does: an archival decided without the check it asked for is not one to\n * trust.\n *\n * A hook is handed a frozen copy of the refusals, and its waivers are\n * matched against the plan's own. Handed the plan's list, a hook could drop\n * a refusal from it, or relabel an open task as a protected file and waive\n * that, and so lift what only spec-brief may; writing to the copy throws,\n * which stops the run like any failing hook.\n */\n private async waive(plan: Plan, base: string | null, commit: string | null, strict: boolean): Promise<Plan> {\n if (!plan.blocking.some((f) => WAIVABLE.includes(f.rule))) return plan;\n const context = frozenCopy({\n root: this.root,\n brief: { id: plan.brief.id, file: plan.brief.file, text: plan.brief.text },\n findings: plan.blocking,\n base,\n commit,\n });\n const waivers: { plugin: string; waiver: Waiver }[] = [];\n for (const plugin of await this.loadedPlugins()) {\n if (plugin.waive === undefined) continue;\n let answer: unknown;\n try {\n answer = await plugin.waive(context);\n } catch (error) {\n throw new ConfigError(`plugin \"${plugin.name}\"`, [`\"waive\" failed: ${error instanceof Error ? error.message : String(error)}`]);\n }\n for (const waiver of asWaivers(answer, plugin.name)) waivers.push({ plugin: plugin.name, waiver });\n }\n return applyWaivers(plan, waivers, strict);\n }\n\n planUnarchive(reference: string, options: UnarchiveRequest = {}): Plan {\n return planUnarchive(this.corpus, this.find(reference), options);\n }\n\n /** Executes a plan and reads the briefs again. A refused plan is never applied. */\n async apply(plan: Plan): Promise<void> {\n if (plan.blocking.length > 0) {\n throw new EngineError('refused', `${plan.action} of ${plan.brief.file} is refused: ${(plan.blocking[0] as Finding).message}`);\n }\n if (plan.done) return;\n await applyPlan(this.fs, plan.ops);\n await this.load();\n }\n\n /** Scaffolds a brief and writes it; the file is new, or nothing is written. */\n async create(options: NewOptions): Promise<{ file: string; content: string }> {\n const title = options.title.trim();\n if (title === '') throw new EngineError('invalid', 'a brief needs a title');\n const id = options.id ?? nextId(this.corpus);\n if (id === null) throw new EngineError('invalid', 'the ids here are not numbers, so there is no next one; pass --id');\n if (/[\\s/\\\\]/.test(id) || id.includes(this.config.id.separator)) {\n throw new EngineError('invalid', `\"${id}\" cannot be an id: no whitespace, slashes or \"${this.config.id.separator}\"`);\n }\n if (findBriefs(this.corpus, id).some((b) => b.id !== null)) throw new EngineError('exists', `the id \"${id}\" is taken`);\n if (options.type !== undefined && typeSections(this.config, options.type) === undefined) {\n const names = Object.keys(this.config.types);\n throw new EngineError('invalid', `\"${options.type}\" is not a brief type here; use one of ${names.join(', ') || '(none)'}`);\n }\n const date = options.date ?? today();\n if (!isDate(date)) throw new EngineError('invalid', `\"${date}\" is not a date written YYYY-MM-DD`);\n const named = this.config.template;\n const template =\n named === null\n ? null\n : await this.fs.read(named).catch((error: unknown) => {\n // A named pipe was read without end, and a directory or a socket\n // ended the run on Node's error and a stack: each is a template\n // the configuration names wrongly, and said so in a line.\n throw error instanceof NotAFileError ? new EngineError('invalid', `the template ${named} is ${error.what}; point \"template\" at a file, or set it to null`) : error;\n });\n if (this.config.template !== null && template === null) {\n throw new EngineError('not-found', `the template ${this.config.template} does not exist`);\n }\n const content = renderNewBrief(\n this.config,\n { id, title, date, type: options.type, wave: options.wave, dependsOn: options.dependsOn },\n template,\n );\n const dir = normalisePath(this.config.briefs);\n const file = `${dir}/${fileNameFor(this.config, id, title)}`;\n if (await this.fs.exists(file)) throw new EngineError('exists', `${file} already exists`);\n await this.fs.write(file, content);\n await this.load();\n return { file, content };\n }\n}\n"]}
package/dist/fs.d.ts CHANGED
@@ -9,8 +9,12 @@
9
9
  * exactly the operation that matters.
10
10
  */
11
11
  export interface FileSystem {
12
- /** Contents, or `null` when there is no such file. */
13
- read(path: string): Promise<string | null>;
12
+ /**
13
+ * Contents, or `null` when there is no such file. What is there and is not
14
+ * a file is refused, and not read, unless `listed` says that `list` has
15
+ * just named it a file.
16
+ */
17
+ read(path: string, listed?: boolean): Promise<string | null>;
14
18
  /** Writes a file, creating its directory. Replaces an existing file atomically where the platform allows. */
15
19
  write(path: string, content: string): Promise<void>;
16
20
  /** Removes a file. Removing a file that is absent is an error. */
@@ -21,6 +25,35 @@ export interface FileSystem {
21
25
  walk(): Promise<string[]>;
22
26
  exists(path: string): Promise<boolean>;
23
27
  }
28
+ /**
29
+ * What a run read from the disk, by repository-relative path: each file with
30
+ * the SHA-256 of its bytes, or `null` where it was looked for and was not
31
+ * there, and each directory with the names of the files it listed, or
32
+ * `null` where there was none. An answer that names every input it was read
33
+ * from is one another tool may give again without starting this one
34
+ * (ADR-0002, amended 2026-10-19).
35
+ */
36
+ export interface Reads {
37
+ readonly files: Map<string, string | null>;
38
+ readonly directories: Map<string, readonly string[] | null>;
39
+ }
40
+ /** What stands at a path that is not a regular file, in the words a refusal says it in. */
41
+ export type NotAFile = 'a directory' | 'neither a file nor a directory';
42
+ /**
43
+ * What stands at a path where it is not a regular file, a link followed:
44
+ * `null` for a file, and for a path where nothing is or that cannot be
45
+ * asked about, which the read that follows reports as it always has. A
46
+ * named pipe was read without end, a device as nothing or without end, and
47
+ * a directory or a socket ended the run on Node's error and a stack
48
+ * (ADR-0003, amended 2026-10-24).
49
+ */
50
+ export declare function notAFile(path: string): Promise<NotAFile | null>;
51
+ /** A path that is there and is not a regular file, refused before it is opened. */
52
+ export declare class NotAFileError extends Error {
53
+ readonly path: string;
54
+ readonly what: NotAFile;
55
+ constructor(path: string, what: NotAFile);
56
+ }
24
57
  /**
25
58
  * The path as the filesystem names it: links and junctions followed, and on
26
59
  * Windows short names such as `RUNNER~1` expanded. Git reports the top of a
@@ -32,9 +65,12 @@ export declare function canonicalPath(path: string): Promise<string>;
32
65
  export declare const IGNORED_DIRECTORIES: ReadonlySet<string>;
33
66
  export declare class NodeFileSystem implements FileSystem {
34
67
  readonly root: string;
35
- constructor(root: string);
68
+ private readonly reads;
69
+ /** `reads`, where it is given, is told every file read and every directory listed. */
70
+ constructor(root: string, reads?: Reads);
36
71
  private host;
37
- read(path: string): Promise<string | null>;
72
+ /** Contents, or `null` when there is no such file; a {@link NotAFileError} where what is there is not one. */
73
+ read(path: string, listed?: boolean): Promise<string | null>;
38
74
  write(path: string, content: string): Promise<void>;
39
75
  remove(path: string): Promise<void>;
40
76
  list(directory: string): Promise<string[] | null>;
package/dist/fs.js CHANGED
@@ -8,10 +8,35 @@
8
8
  * written, and how the transaction's rollback is exercised with a failure on
9
9
  * exactly the operation that matters.
10
10
  */
11
- import { randomBytes } from 'node:crypto';
11
+ import { createHash, randomBytes } from 'node:crypto';
12
12
  import { mkdir, readdir, readFile, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';
13
13
  import { dirname, join, relative, resolve, sep } from 'node:path';
14
14
  import { normalisePath as repoPath } from './links.js';
15
+ /**
16
+ * What stands at a path where it is not a regular file, a link followed:
17
+ * `null` for a file, and for a path where nothing is or that cannot be
18
+ * asked about, which the read that follows reports as it always has. A
19
+ * named pipe was read without end, a device as nothing or without end, and
20
+ * a directory or a socket ended the run on Node's error and a stack
21
+ * (ADR-0003, amended 2026-10-24).
22
+ */
23
+ export async function notAFile(path) {
24
+ const found = await stat(path).catch(() => null);
25
+ if (found === null || found.isFile())
26
+ return null;
27
+ return found.isDirectory() ? 'a directory' : 'neither a file nor a directory';
28
+ }
29
+ /** A path that is there and is not a regular file, refused before it is opened. */
30
+ export class NotAFileError extends Error {
31
+ path;
32
+ what;
33
+ constructor(path, what) {
34
+ super(`${path} is ${what}; only a file is read`);
35
+ this.name = 'NotAFileError';
36
+ this.path = path;
37
+ this.what = what;
38
+ }
39
+ }
15
40
  /**
16
41
  * The path as the filesystem names it: links and junctions followed, and on
17
42
  * Windows short names such as `RUNNER~1` expanded. Git reports the top of a
@@ -48,21 +73,42 @@ async function pause(ms) {
48
73
  }
49
74
  export class NodeFileSystem {
50
75
  root;
51
- constructor(root) {
76
+ reads;
77
+ /** `reads`, where it is given, is told every file read and every directory listed. */
78
+ constructor(root, reads) {
52
79
  this.root = resolve(root);
80
+ this.reads = reads;
53
81
  }
54
82
  host(path) {
55
83
  return join(this.root, ...repoPath(path).split('/'));
56
84
  }
57
- async read(path) {
85
+ /** Contents, or `null` when there is no such file; a {@link NotAFileError} where what is there is not one. */
86
+ async read(path, listed = false) {
87
+ const host = this.host(path);
88
+ // Asked before anything is opened: a path may come from the
89
+ // configuration, as a template's does, or be one a plan writes over,
90
+ // which no listing has said is a file. One a listing has just named a
91
+ // file is not asked again, which cost `list` over 200 briefs 12 to 38 ms
92
+ // on Windows.
93
+ if (!listed) {
94
+ const what = await notAFile(host);
95
+ if (what !== null)
96
+ throw new NotAFileError(repoPath(path), what);
97
+ }
98
+ let bytes;
58
99
  try {
59
- return await readFile(this.host(path), 'utf8');
100
+ bytes = await readFile(host);
60
101
  }
61
102
  catch (error) {
62
- if (error.code === 'ENOENT')
63
- return null;
64
- throw error;
103
+ if (error.code !== 'ENOENT')
104
+ throw error;
105
+ this.reads?.files.set(repoPath(path), null);
106
+ return null;
65
107
  }
108
+ // The digest is of the bytes the text is decoded from, so that nothing
109
+ // written between a read and a digest of it can go unseen.
110
+ this.reads?.files.set(repoPath(path), createHash('sha256').update(bytes).digest('hex'));
111
+ return bytes.toString('utf8');
66
112
  }
67
113
  async write(path, content) {
68
114
  const target = this.host(path);
@@ -100,12 +146,16 @@ export class NodeFileSystem {
100
146
  async list(directory) {
101
147
  try {
102
148
  const entries = await readdir(this.host(directory), { withFileTypes: true });
103
- return entries.filter((e) => e.isFile()).map((e) => e.name);
149
+ const names = entries.filter((e) => e.isFile()).map((e) => e.name);
150
+ this.reads?.directories.set(repoPath(directory), names);
151
+ return names;
104
152
  }
105
153
  catch (error) {
106
154
  const code = error.code;
107
- if (code === 'ENOENT' || code === 'ENOTDIR')
155
+ if (code === 'ENOENT' || code === 'ENOTDIR') {
156
+ this.reads?.directories.set(repoPath(directory), null);
108
157
  return null;
158
+ }
109
159
  // A directory that may not be read must not read as "no briefs".
110
160
  throw error;
111
161
  }
package/dist/fs.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"fs.js","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACnG,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAElE,OAAO,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,YAAY,CAAC;AAgBvD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAAY;IAC9C,IAAI,CAAC;QACH,OAAO,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,4EAA4E;AAC5E,MAAM,CAAC,MAAM,mBAAmB,GAAwB,IAAI,GAAG,CAAC;IAC9D,MAAM;IACN,KAAK;IACL,MAAM;IACN,cAAc;IACd,MAAM;IACN,OAAO;IACP,UAAU;IACV,QAAQ;IACR,OAAO;IACP,aAAa;IACb,cAAc;CACf,CAAC,CAAC;AAEH,sFAAsF;AACtF,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;AACxD,MAAM,eAAe,GAAG,CAAC,CAAC;AAE1B,KAAK,UAAU,KAAK,CAAC,EAAU;IAC7B,MAAM,IAAI,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;AACpD,CAAC;AAED,MAAM,OAAO,cAAc;IAChB,IAAI,CAAS;IAEtB,YAAY,IAAY;QACtB,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAEO,IAAI,CAAC,IAAY;QACvB,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IACvD,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,IAAY;QACrB,IAAI,CAAC;YACH,OAAO,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;QACjD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;gBAAE,OAAO,IAAI,CAAC;YACpE,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,IAAY,EAAE,OAAe;QACvC,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/B,MAAM,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAClD,qFAAqF;QACrF,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;QAC7F,0EAA0E;QAC1E,uEAAuE;QACvE,MAAM,SAAS,CAAC,SAAS,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACpD,IAAI,CAAC;YACH,KAAK,IAAI,OAAO,GAAG,CAAC,GAAI,OAAO,IAAI,CAAC,EAAE,CAAC;gBACrC,IAAI,CAAC;oBACH,MAAM,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;oBAChC,OAAO;gBACT,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,kDAAkD;oBAClD,MAAM,IAAI,GAAI,KAA+B,CAAC,IAAc,CAAC;oBAC7D,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,OAAO,IAAI,eAAe;wBAAE,MAAM,KAAK,CAAC;oBACpE,mEAAmE;oBACnE,MAAM,KAAK,CAAC,EAAE,GAAG,OAAO,CAAC,CAAC;gBAC5B,CAAC;YACH,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,4FAA4F;YAC5F,MAAM,EAAE,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YACrC,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,IAAY;QACvB,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAC5B,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,SAAiB;QAC1B,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;YAC7E,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAC9D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,GAAI,KAA+B,CAAC,IAAI,CAAC;YACnD,IAAI,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,SAAS;gBAAE,OAAO,IAAI,CAAC;YACzD,iEAAiE;YACjE,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,KAAK,CAAC,IAAI;QACR,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,KAAK,EAAE,SAAiB,EAAiB,EAAE;YACvD,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,SAAS,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;YAClE,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;gBAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;gBACzC,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;oBACxB,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;wBAAE,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC;gBAC9D,CAAC;qBAAM,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;oBAC1B,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;gBAC7D,CAAC;YACH,CAAC;QACH,CAAC,CAAC;QACF,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvB,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC;IACtB,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,IAAY;QACvB,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;YAC5B,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;CACF;AAID;;;GAGG;AACH,MAAM,OAAO,gBAAgB;IAClB,KAAK,CAAsB;IACpC,IAAI,CAAoE;IAExE,YAAY,KAAK,GAAqC,EAAE;QACtD,IAAI,CAAC,KAAK,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;QAChG,IAAI,CAAC,IAAI,GAAG,SAAS,CAAC;IACxB,CAAC;IAED,IAAI,CAAC,IAAY;QACf,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC;IACjE,CAAC;IAED,KAAK,CAAC,IAAY,EAAE,OAAe;QACjC,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,IAAI;YAAE,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,4BAA4B,MAAM,EAAE,CAAC,CAAC,CAAC;QAClH,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAChC,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC3B,CAAC;IAED,MAAM,CAAC,IAAY;QACjB,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC,KAAK,IAAI;YAAE,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,6BAA6B,MAAM,EAAE,CAAC,CAAC,CAAC;QACpH,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC;YAAE,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,GAAG,MAAM,iBAAiB,CAAC,CAAC,CAAC;QAC7F,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC3B,CAAC;IAED,IAAI,CAAC,SAAiB;QACpB,MAAM,MAAM,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC;QACnC,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,IAAI,WAAW,GAAG,MAAM,KAAK,EAAE,CAAC;QAChC,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC;YACrC,IAAI,MAAM,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,MAAM,GAAG,CAAC;gBAAE,SAAS;YAC9D,WAAW,GAAG,IAAI,CAAC;YACnB,MAAM,IAAI,GAAG,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YAClE,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;gBAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5C,CAAC;QACD,OAAO,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAC5D,CAAC;IAED,IAAI;QACF,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACtH,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;IACvC,CAAC;IAED,MAAM,CAAC,IAAY;QACjB,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC9B,OAAO,OAAO,CAAC,OAAO,CACpB,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,KAAK,EAAE,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,MAAM,GAAG,CAAC,CAAC,CAC1G,CAAC;IACJ,CAAC;CACF","sourcesContent":["/**\n * The filesystem, behind an interface.\n *\n * Every path crossing this boundary is repository-relative and POSIX; the\n * Node implementation is the only place a host path exists. The in-memory\n * implementation is not a test double bolted on afterwards: it is how a\n * harness asks \"what would archiving this do\" against files that were never\n * written, and how the transaction's rollback is exercised with a failure on\n * exactly the operation that matters.\n */\n\nimport { randomBytes } from 'node:crypto';\nimport { mkdir, readdir, readFile, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';\nimport { dirname, join, relative, resolve, sep } from 'node:path';\n\nimport { normalisePath as repoPath } from './links.js';\n\nexport interface FileSystem {\n /** Contents, or `null` when there is no such file. */\n read(path: string): Promise<string | null>;\n /** Writes a file, creating its directory. Replaces an existing file atomically where the platform allows. */\n write(path: string, content: string): Promise<void>;\n /** Removes a file. Removing a file that is absent is an error. */\n remove(path: string): Promise<void>;\n /** Names of the files directly inside a directory, or `null` when it does not exist. */\n list(directory: string): Promise<string[] | null>;\n /** Every file beneath the root, as sorted repository-relative paths, skipping {@link IGNORED_DIRECTORIES}. */\n walk(): Promise<string[]>;\n exists(path: string): Promise<boolean>;\n}\n\n/**\n * The path as the filesystem names it: links and junctions followed, and on\n * Windows short names such as `RUNNER~1` expanded. Git reports the top of a\n * work tree this way, so a root compared with it has to be named the same.\n * A path that does not exist is returned as given, for the caller to report.\n */\nexport async function canonicalPath(path: string): Promise<string> {\n try {\n return await realpath(path);\n } catch {\n return path;\n }\n}\n\n/** Directories a walk never enters: build output and other tools' state. */\nexport const IGNORED_DIRECTORIES: ReadonlySet<string> = new Set([\n '.git',\n '.hg',\n '.svn',\n 'node_modules',\n 'dist',\n 'build',\n 'coverage',\n 'target',\n '.venv',\n '__pycache__',\n '.stryker-tmp',\n]);\n\n/** Errors a rename on Windows raises while another process briefly holds the file. */\nconst TRANSIENT = new Set(['EPERM', 'EACCES', 'EBUSY']);\nconst RENAME_ATTEMPTS = 6;\n\nasync function pause(ms: number): Promise<void> {\n await new Promise((done) => setTimeout(done, ms));\n}\n\nexport class NodeFileSystem implements FileSystem {\n readonly root: string;\n\n constructor(root: string) {\n this.root = resolve(root);\n }\n\n private host(path: string): string {\n return join(this.root, ...repoPath(path).split('/'));\n }\n\n async read(path: string): Promise<string | null> {\n try {\n return await readFile(this.host(path), 'utf8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;\n throw error;\n }\n }\n\n async write(path: string, content: string): Promise<void> {\n const target = this.host(path);\n await mkdir(dirname(target), { recursive: true });\n // Written beside the target and renamed over it, so a reader never sees half a file.\n const temporary = join(dirname(target), `.${randomBytes(6).toString('hex')}.spec-brief.tmp`);\n // \"wx\" refuses a name that is taken rather than write over it. Six random\n // bytes are not drawn twice in a test, so none shows the flag at work.\n await writeFile(temporary, content, { flag: 'wx' });\n try {\n for (let attempt = 1; ; attempt += 1) {\n try {\n await rename(temporary, target);\n return;\n } catch (error) {\n // An error with no code is not in the set either.\n const code = (error as NodeJS.ErrnoException).code as string;\n if (!TRANSIENT.has(code) || attempt >= RENAME_ATTEMPTS) throw error;\n // How long it waits changes when the answer comes, not what it is.\n await pause(15 * attempt);\n }\n }\n } catch (error) {\n // Forced, so that a temporary file something else removed does not hide the rename's error.\n await rm(temporary, { force: true });\n throw error;\n }\n }\n\n async remove(path: string): Promise<void> {\n await rm(this.host(path));\n }\n\n async list(directory: string): Promise<string[] | null> {\n try {\n const entries = await readdir(this.host(directory), { withFileTypes: true });\n return entries.filter((e) => e.isFile()).map((e) => e.name);\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ENOENT' || code === 'ENOTDIR') return null;\n // A directory that may not be read must not read as \"no briefs\".\n throw error;\n }\n }\n\n async walk(): Promise<string[]> {\n const found: string[] = [];\n const visit = async (directory: string): Promise<void> => {\n const entries = await readdir(directory, { withFileTypes: true });\n for (const entry of entries) {\n const full = join(directory, entry.name);\n if (entry.isDirectory()) {\n if (!IGNORED_DIRECTORIES.has(entry.name)) await visit(full);\n } else if (entry.isFile()) {\n found.push(relative(this.root, full).split(sep).join('/'));\n }\n }\n };\n await visit(this.root);\n return found.sort();\n }\n\n async exists(path: string): Promise<boolean> {\n try {\n await stat(this.host(path));\n return true;\n } catch {\n return false;\n }\n }\n}\n\nexport type FileOperation = 'write' | 'remove';\n\n/**\n * Files in a `Map`. `fail` lets a test make one chosen operation throw, which\n * is the only honest way to prove a rollback restores what it claims to.\n */\nexport class MemoryFileSystem implements FileSystem {\n readonly files: Map<string, string>;\n fail: ((operation: FileOperation, path: string) => boolean) | undefined;\n\n constructor(files: Readonly<Record<string, string>> = {}) {\n this.files = new Map(Object.entries(files).map(([path, content]) => [repoPath(path), content]));\n this.fail = undefined;\n }\n\n read(path: string): Promise<string | null> {\n return Promise.resolve(this.files.get(repoPath(path)) ?? null);\n }\n\n write(path: string, content: string): Promise<void> {\n const normal = repoPath(path);\n if (this.fail?.('write', normal) === true) return Promise.reject(new Error(`injected failure writing ${normal}`));\n this.files.set(normal, content);\n return Promise.resolve();\n }\n\n remove(path: string): Promise<void> {\n const normal = repoPath(path);\n if (this.fail?.('remove', normal) === true) return Promise.reject(new Error(`injected failure removing ${normal}`));\n if (!this.files.delete(normal)) return Promise.reject(new Error(`${normal} does not exist`));\n return Promise.resolve();\n }\n\n list(directory: string): Promise<string[] | null> {\n const prefix = repoPath(directory);\n const names: string[] = [];\n let isDirectory = prefix === '';\n for (const path of this.files.keys()) {\n if (prefix !== '' && !path.startsWith(`${prefix}/`)) continue;\n isDirectory = true;\n const rest = prefix === '' ? path : path.slice(prefix.length + 1);\n if (!rest.includes('/')) names.push(rest);\n }\n return Promise.resolve(isDirectory ? names.sort() : null);\n }\n\n walk(): Promise<string[]> {\n const paths = [...this.files.keys()].filter((path) => !path.split('/').some((part) => IGNORED_DIRECTORIES.has(part)));\n return Promise.resolve(paths.sort());\n }\n\n exists(path: string): Promise<boolean> {\n const normal = repoPath(path);\n return Promise.resolve(\n this.files.has(normal) || [...this.files.keys()].some((p) => normal === '' || p.startsWith(`${normal}/`)),\n );\n }\n}\n"]}
1
+ {"version":3,"file":"fs.js","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The filesystem, behind an interface.\n *\n * Every path crossing this boundary is repository-relative and POSIX; the\n * Node implementation is the only place a host path exists. The in-memory\n * implementation is not a test double bolted on afterwards: it is how a\n * harness asks \"what would archiving this do\" against files that were never\n * written, and how the transaction's rollback is exercised with a failure on\n * exactly the operation that matters.\n */\n\nimport { createHash, randomBytes } from 'node:crypto';\nimport { mkdir, readdir, readFile, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';\nimport { dirname, join, relative, resolve, sep } from 'node:path';\n\nimport { normalisePath as repoPath } from './links.js';\n\nexport interface FileSystem {\n /**\n * Contents, or `null` when there is no such file. What is there and is not\n * a file is refused, and not read, unless `listed` says that `list` has\n * just named it a file.\n */\n read(path: string, listed?: boolean): Promise<string | null>;\n /** Writes a file, creating its directory. Replaces an existing file atomically where the platform allows. */\n write(path: string, content: string): Promise<void>;\n /** Removes a file. Removing a file that is absent is an error. */\n remove(path: string): Promise<void>;\n /** Names of the files directly inside a directory, or `null` when it does not exist. */\n list(directory: string): Promise<string[] | null>;\n /** Every file beneath the root, as sorted repository-relative paths, skipping {@link IGNORED_DIRECTORIES}. */\n walk(): Promise<string[]>;\n exists(path: string): Promise<boolean>;\n}\n\n/**\n * What a run read from the disk, by repository-relative path: each file with\n * the SHA-256 of its bytes, or `null` where it was looked for and was not\n * there, and each directory with the names of the files it listed, or\n * `null` where there was none. An answer that names every input it was read\n * from is one another tool may give again without starting this one\n * (ADR-0002, amended 2026-10-19).\n */\nexport interface Reads {\n readonly files: Map<string, string | null>;\n readonly directories: Map<string, readonly string[] | null>;\n}\n\n/** What stands at a path that is not a regular file, in the words a refusal says it in. */\nexport type NotAFile = 'a directory' | 'neither a file nor a directory';\n\n/**\n * What stands at a path where it is not a regular file, a link followed:\n * `null` for a file, and for a path where nothing is or that cannot be\n * asked about, which the read that follows reports as it always has. A\n * named pipe was read without end, a device as nothing or without end, and\n * a directory or a socket ended the run on Node's error and a stack\n * (ADR-0003, amended 2026-10-24).\n */\nexport async function notAFile(path: string): Promise<NotAFile | null> {\n const found = await stat(path).catch(() => null);\n if (found === null || found.isFile()) return null;\n return found.isDirectory() ? 'a directory' : 'neither a file nor a directory';\n}\n\n/** A path that is there and is not a regular file, refused before it is opened. */\nexport class NotAFileError extends Error {\n readonly path: string;\n readonly what: NotAFile;\n\n constructor(path: string, what: NotAFile) {\n super(`${path} is ${what}; only a file is read`);\n this.name = 'NotAFileError';\n this.path = path;\n this.what = what;\n }\n}\n\n/**\n * The path as the filesystem names it: links and junctions followed, and on\n * Windows short names such as `RUNNER~1` expanded. Git reports the top of a\n * work tree this way, so a root compared with it has to be named the same.\n * A path that does not exist is returned as given, for the caller to report.\n */\nexport async function canonicalPath(path: string): Promise<string> {\n try {\n return await realpath(path);\n } catch {\n return path;\n }\n}\n\n/** Directories a walk never enters: build output and other tools' state. */\nexport const IGNORED_DIRECTORIES: ReadonlySet<string> = new Set([\n '.git',\n '.hg',\n '.svn',\n 'node_modules',\n 'dist',\n 'build',\n 'coverage',\n 'target',\n '.venv',\n '__pycache__',\n '.stryker-tmp',\n]);\n\n/** Errors a rename on Windows raises while another process briefly holds the file. */\nconst TRANSIENT = new Set(['EPERM', 'EACCES', 'EBUSY']);\nconst RENAME_ATTEMPTS = 6;\n\nasync function pause(ms: number): Promise<void> {\n await new Promise((done) => setTimeout(done, ms));\n}\n\nexport class NodeFileSystem implements FileSystem {\n readonly root: string;\n private readonly reads: Reads | undefined;\n\n /** `reads`, where it is given, is told every file read and every directory listed. */\n constructor(root: string, reads?: Reads) {\n this.root = resolve(root);\n this.reads = reads;\n }\n\n private host(path: string): string {\n return join(this.root, ...repoPath(path).split('/'));\n }\n\n /** Contents, or `null` when there is no such file; a {@link NotAFileError} where what is there is not one. */\n async read(path: string, listed = false): Promise<string | null> {\n const host = this.host(path);\n // Asked before anything is opened: a path may come from the\n // configuration, as a template's does, or be one a plan writes over,\n // which no listing has said is a file. One a listing has just named a\n // file is not asked again, which cost `list` over 200 briefs 12 to 38 ms\n // on Windows.\n if (!listed) {\n const what = await notAFile(host);\n if (what !== null) throw new NotAFileError(repoPath(path), what);\n }\n let bytes: Buffer;\n try {\n bytes = await readFile(host);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n this.reads?.files.set(repoPath(path), null);\n return null;\n }\n // The digest is of the bytes the text is decoded from, so that nothing\n // written between a read and a digest of it can go unseen.\n this.reads?.files.set(repoPath(path), createHash('sha256').update(bytes).digest('hex'));\n return bytes.toString('utf8');\n }\n\n async write(path: string, content: string): Promise<void> {\n const target = this.host(path);\n await mkdir(dirname(target), { recursive: true });\n // Written beside the target and renamed over it, so a reader never sees half a file.\n const temporary = join(dirname(target), `.${randomBytes(6).toString('hex')}.spec-brief.tmp`);\n // \"wx\" refuses a name that is taken rather than write over it. Six random\n // bytes are not drawn twice in a test, so none shows the flag at work.\n await writeFile(temporary, content, { flag: 'wx' });\n try {\n for (let attempt = 1; ; attempt += 1) {\n try {\n await rename(temporary, target);\n return;\n } catch (error) {\n // An error with no code is not in the set either.\n const code = (error as NodeJS.ErrnoException).code as string;\n if (!TRANSIENT.has(code) || attempt >= RENAME_ATTEMPTS) throw error;\n // How long it waits changes when the answer comes, not what it is.\n await pause(15 * attempt);\n }\n }\n } catch (error) {\n // Forced, so that a temporary file something else removed does not hide the rename's error.\n await rm(temporary, { force: true });\n throw error;\n }\n }\n\n async remove(path: string): Promise<void> {\n await rm(this.host(path));\n }\n\n async list(directory: string): Promise<string[] | null> {\n try {\n const entries = await readdir(this.host(directory), { withFileTypes: true });\n const names = entries.filter((e) => e.isFile()).map((e) => e.name);\n this.reads?.directories.set(repoPath(directory), names);\n return names;\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ENOENT' || code === 'ENOTDIR') {\n this.reads?.directories.set(repoPath(directory), null);\n return null;\n }\n // A directory that may not be read must not read as \"no briefs\".\n throw error;\n }\n }\n\n async walk(): Promise<string[]> {\n const found: string[] = [];\n const visit = async (directory: string): Promise<void> => {\n const entries = await readdir(directory, { withFileTypes: true });\n for (const entry of entries) {\n const full = join(directory, entry.name);\n if (entry.isDirectory()) {\n if (!IGNORED_DIRECTORIES.has(entry.name)) await visit(full);\n } else if (entry.isFile()) {\n found.push(relative(this.root, full).split(sep).join('/'));\n }\n }\n };\n await visit(this.root);\n return found.sort();\n }\n\n async exists(path: string): Promise<boolean> {\n try {\n await stat(this.host(path));\n return true;\n } catch {\n return false;\n }\n }\n}\n\nexport type FileOperation = 'write' | 'remove';\n\n/**\n * Files in a `Map`. `fail` lets a test make one chosen operation throw, which\n * is the only honest way to prove a rollback restores what it claims to.\n */\nexport class MemoryFileSystem implements FileSystem {\n readonly files: Map<string, string>;\n fail: ((operation: FileOperation, path: string) => boolean) | undefined;\n\n constructor(files: Readonly<Record<string, string>> = {}) {\n this.files = new Map(Object.entries(files).map(([path, content]) => [repoPath(path), content]));\n this.fail = undefined;\n }\n\n read(path: string): Promise<string | null> {\n return Promise.resolve(this.files.get(repoPath(path)) ?? null);\n }\n\n write(path: string, content: string): Promise<void> {\n const normal = repoPath(path);\n if (this.fail?.('write', normal) === true) return Promise.reject(new Error(`injected failure writing ${normal}`));\n this.files.set(normal, content);\n return Promise.resolve();\n }\n\n remove(path: string): Promise<void> {\n const normal = repoPath(path);\n if (this.fail?.('remove', normal) === true) return Promise.reject(new Error(`injected failure removing ${normal}`));\n if (!this.files.delete(normal)) return Promise.reject(new Error(`${normal} does not exist`));\n return Promise.resolve();\n }\n\n list(directory: string): Promise<string[] | null> {\n const prefix = repoPath(directory);\n const names: string[] = [];\n let isDirectory = prefix === '';\n for (const path of this.files.keys()) {\n if (prefix !== '' && !path.startsWith(`${prefix}/`)) continue;\n isDirectory = true;\n const rest = prefix === '' ? path : path.slice(prefix.length + 1);\n if (!rest.includes('/')) names.push(rest);\n }\n return Promise.resolve(isDirectory ? names.sort() : null);\n }\n\n walk(): Promise<string[]> {\n const paths = [...this.files.keys()].filter((path) => !path.split('/').some((part) => IGNORED_DIRECTORIES.has(part)));\n return Promise.resolve(paths.sort());\n }\n\n exists(path: string): Promise<boolean> {\n const normal = repoPath(path);\n return Promise.resolve(\n this.files.has(normal) || [...this.files.keys()].some((p) => normal === '' || p.startsWith(`${normal}/`)),\n );\n }\n}\n"]}
package/dist/git.d.ts CHANGED
@@ -22,6 +22,11 @@ export interface FileChange {
22
22
  export interface Git {
23
23
  /** The commit a revision names, or `null` when it names none. */
24
24
  commit(revision: string): Promise<CommitInfo | null>;
25
+ /**
26
+ * The hash of the commit each revision names, or `null` for one that names
27
+ * none, asked once. A git without it is one that cannot say.
28
+ */
29
+ commits?(revisions: readonly string[]): Promise<(string | null)[]>;
25
30
  mergeBase(a: string, b: string): Promise<string | null>;
26
31
  /** Files changed from `from` to `to`; with no `from`, the files `to` changed against its first parent. */
27
32
  changes(from: string | null, to: string): Promise<FileChange[]>;
@@ -57,6 +62,12 @@ export declare class NodeGit implements Git {
57
62
  /** The working tree's top directory, canonical, or `null` outside a repository. */
58
63
  static toplevel(cwd: string): Promise<string | null>;
59
64
  commit(revision: string): Promise<CommitInfo | null>;
65
+ /**
66
+ * One process for every revision: `cat-file --batch-check` reads one a
67
+ * line, as `rev-parse --verify` reads one, and answers each with its
68
+ * object's name, or with the revision and why it names none.
69
+ */
70
+ commits(revisions: readonly string[]): Promise<(string | null)[]>;
60
71
  mergeBase(a: string, b: string): Promise<string | null>;
61
72
  changes(from: string | null, to: string): Promise<FileChange[]>;
62
73
  dirty(): Promise<string[]>;
package/dist/git.js CHANGED
@@ -22,11 +22,21 @@ export class GitError extends Error {
22
22
  this.name = 'GitError';
23
23
  }
24
24
  }
25
- function run(cwd, args) {
25
+ /** A git command, handed `input` on its standard input, and an end of it there either way. */
26
+ function run(cwd, args, input) {
26
27
  return new Promise((done) => {
27
- execFile('git', ['-c', 'core.quotepath=off', ...args],
28
+ const child = execFile('git', ['-c', 'core.quotepath=off', ...args],
28
29
  // windowsHide keeps a console window from flashing up on Windows; no other host has one to hide.
29
30
  { cwd, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024, windowsHide: true }, (error, stdout, stderr) => done({ ok: error === null, stdout, stderr }));
31
+ // A git that ends before it reads what it is handed, as one outside a
32
+ // repository does, closes the pipe under the write, and the EPIPE went
33
+ // uncaught and ended the process. What git printed is its answer, so the
34
+ // error is taken and nothing is done with it; the mutant that gives the
35
+ // handler a body of `undefined` does nothing either, and is equivalent.
36
+ child.stdin.on('error', () => { });
37
+ // A command that reads its input reads to the end of it, which only this
38
+ // gives it; the others read none.
39
+ child.stdin.end(input);
30
40
  });
31
41
  }
32
42
  /** A revision that starts with "-" would be read as an option. */
@@ -98,6 +108,26 @@ export class NodeGit {
98
108
  const [author, date] = fields;
99
109
  return { sha: hash, author, date };
100
110
  }
111
+ /**
112
+ * One process for every revision: `cat-file --batch-check` reads one a
113
+ * line, as `rev-parse --verify` reads one, and answers each with its
114
+ * object's name, or with the revision and why it names none.
115
+ */
116
+ async commits(revisions) {
117
+ // A line break would make one revision two questions, and its answer
118
+ // another's. On a line of its own a revision is never read as an option,
119
+ // so one that starts with "-" is asked, and names none.
120
+ if (revisions.some((revision) => revision.includes('\n')))
121
+ return revisions.map(() => null);
122
+ const asked = revisions.map((revision) => `${revision}^{commit}\n`).join('');
123
+ const lines = (await run(this.cwd, ['cat-file', '--batch-check=%(objectname)'], asked)).stdout.split('\n');
124
+ return revisions.map((_, index) => {
125
+ const line = lines[index];
126
+ // An object's name holds no space, and the answer for a revision that
127
+ // names none does; a git that answered nothing printed no line for it.
128
+ return line !== undefined && line !== '' && !line.includes(' ') ? line : null;
129
+ });
130
+ }
101
131
  async mergeBase(a, b) {
102
132
  const result = await run(this.cwd, ['merge-base', safeRevision(a), safeRevision(b)]);
103
133
  return result.ok ? result.stdout.trim() : null;
package/dist/git.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"git.js","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Git, read-only.\n *\n * spec-brief never stages, commits or rewrites history. It reads the commit a\n * round landed as, the files that commit touched, and whether the working tree\n * holds work the commit does not. What goes into a commit stays the decision\n * of whoever makes it, and archival is atomic over the files it writes rather\n * than over a repository state it would have to reverse.\n */\n\nimport { execFile } from 'node:child_process';\nimport { resolve } from 'node:path';\n\nimport { canonicalPath } from './fs.js';\nimport { relativePath } from './links.js';\n\nexport interface CommitInfo {\n readonly sha: string;\n readonly author: string;\n /** ISO 8601 author date. */\n readonly date: string;\n}\n\nexport interface FileChange {\n readonly path: string;\n /** `null` for a binary file, which has no line counts. */\n readonly insertions: number | null;\n readonly deletions: number | null;\n}\n\nexport interface Git {\n /** The commit a revision names, or `null` when it names none. */\n commit(revision: string): Promise<CommitInfo | null>;\n mergeBase(a: string, b: string): Promise<string | null>;\n /** Files changed from `from` to `to`; with no `from`, the files `to` changed against its first parent. */\n changes(from: string | null, to: string): Promise<FileChange[]>;\n /** Paths with uncommitted changes, untracked files included. */\n dirty(): Promise<string[]>;\n /** Every path git sees: tracked, or untracked and not ignored. A file a round just created counts. */\n files(): Promise<string[]>;\n remoteUrl(name: string): Promise<string | null>;\n}\n\n/**\n * A revision git cannot be handed, or a command of git's that failed: the\n * message is the whole report, since what failed is git or what it was given,\n * and no line of spec-brief's.\n */\nexport class GitError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'GitError';\n }\n}\n\ninterface Run {\n readonly ok: boolean;\n readonly stdout: string;\n readonly stderr: string;\n}\n\nfunction run(cwd: string, args: readonly string[]): Promise<Run> {\n return new Promise((done) => {\n execFile(\n 'git',\n ['-c', 'core.quotepath=off', ...args],\n // windowsHide keeps a console window from flashing up on Windows; no other host has one to hide.\n { cwd, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024, windowsHide: true },\n (error, stdout, stderr) => done({ ok: error === null, stdout, stderr }),\n );\n });\n}\n\n/** A revision that starts with \"-\" would be read as an option. */\nfunction safeRevision(revision: string): string {\n if (revision.startsWith('-') || revision.trim() === '') throw new GitError(`\"${revision}\" is not a revision`);\n return revision;\n}\n\nfunction records(output: string): string[] {\n return output.split('\\0').filter((r) => r !== '');\n}\n\n/** Parses `--numstat -z` output with renames off: `added<TAB>deleted<TAB>path<NUL>`. */\nexport function parseNumstat(output: string): FileChange[] {\n // `show --format=` leaves the empty header's newline in front of the first record.\n return records(output.replace(/^\\n+/, '')).map((record) => {\n const fields = record.split('\\t');\n // Cast on the name: what stands under an `as` is not mutated (ADR-0009).\n const [added, deleted, ...path] = fields as [string, string, ...string[]];\n return {\n path: path.join('\\t'),\n insertions: added === '-' ? null : Number(added),\n deletions: deleted === '-' ? null : Number(deleted),\n };\n });\n}\n\n/** Parses `status --porcelain=v1 -z`, where a rename's original path is a record of its own. */\nexport function parsePorcelain(output: string): string[] {\n const fields = records(output);\n const paths: string[] = [];\n for (let i = 0; i < fields.length; i += 1) {\n const field = fields[i] as string;\n paths.push(field.slice(3));\n const x = field.charAt(0);\n if (x === 'R' || x === 'C') i += 1;\n }\n return paths;\n}\n\n/**\n * Git reports paths from the top of the work tree; spec-brief's paths start at\n * the root, which may be a directory below it. Every path that crosses this\n * boundary is made relative to the root.\n */\nexport class NodeGit implements Git {\n readonly cwd: string;\n private prefix: Promise<string> | undefined;\n\n constructor(cwd: string) {\n this.cwd = cwd;\n }\n\n /** The root's path from the top of the work tree, `sub/dir/` or empty. */\n private async rootPrefix(): Promise<string> {\n // Asked only after a status that succeeded, so the repository exists.\n this.prefix ??= run(this.cwd, ['rev-parse', '--show-prefix']).then((r) => r.stdout.trim());\n return this.prefix;\n }\n\n /** The working tree's top directory, canonical, or `null` outside a repository. */\n static async toplevel(cwd: string): Promise<string | null> {\n const result = await run(cwd, ['rev-parse', '--show-toplevel']);\n return result.ok ? canonicalPath(resolve(result.stdout.trim())) : null;\n }\n\n async commit(revision: string): Promise<CommitInfo | null> {\n const sha = await run(this.cwd, ['rev-parse', '--verify', '--quiet', `${safeRevision(revision)}^{commit}`]);\n if (!sha.ok) return null;\n const hash = sha.stdout.trim();\n const show = await run(this.cwd, ['show', '-s', '--format=%an%x00%aI', hash]);\n const fields = show.stdout.trim().split('\\0');\n // Cast on the name, as in `parseNumstat`.\n const [author, date] = fields as [string, string];\n return { sha: hash, author, date };\n }\n\n async mergeBase(a: string, b: string): Promise<string | null> {\n const result = await run(this.cwd, ['merge-base', safeRevision(a), safeRevision(b)]);\n return result.ok ? result.stdout.trim() : null;\n }\n\n async changes(from: string | null, to: string): Promise<FileChange[]> {\n const target = safeRevision(to);\n let result: Run;\n if (from !== null) {\n result = await run(this.cwd, ['diff', '--numstat', '-z', '--no-renames', '--relative', safeRevision(from), target]);\n } else {\n const parent = await run(this.cwd, ['rev-parse', '--verify', '--quiet', `${target}^`]);\n result = parent.ok\n ? await run(this.cwd, ['diff', '--numstat', '-z', '--no-renames', '--relative', `${target}^`, target])\n : await run(this.cwd, ['show', '--numstat', '-z', '--no-renames', '--relative', '--format=', target]);\n }\n if (!result.ok) throw new GitError(`git could not diff ${from ?? `${to}^`}..${to}: ${result.stderr.trim()}`);\n return parseNumstat(result.stdout);\n }\n\n async dirty(): Promise<string[]> {\n const result = await run(this.cwd, ['status', '--porcelain=v1', '-z', '--untracked-files=all']);\n if (!result.ok) throw new GitError(`git status failed: ${result.stderr.trim()}`);\n const prefix = await this.rootPrefix();\n return parsePorcelain(result.stdout).map((path) => relativePath(prefix, path));\n }\n\n async files(): Promise<string[]> {\n const result = await run(this.cwd, ['ls-files', '-z', '--cached', '--others', '--exclude-standard']);\n if (!result.ok) throw new GitError(`git ls-files failed: ${result.stderr.trim()}`);\n return records(result.stdout).sort();\n }\n\n async remoteUrl(name: string): Promise<string | null> {\n const result = await run(this.cwd, ['remote', 'get-url', safeRevision(name)]);\n return result.ok ? result.stdout.trim() : null;\n }\n}\n\n/** The web address of a pull request, for remotes on github.com. */\nexport function pullRequestUrl(remote: string | null, number: number): string | null {\n // Said for the reader and the compiler: no remote reads as the text \"null\", which is no address either.\n if (remote === null) return null;\n const match = /github\\.com[:/]([^/\\s]+)\\/([^/\\s]+?)(?:\\.git)?\\/?$/.exec(remote);\n if (!match) return null;\n return `https://github.com/${match[1] as string}/${match[2] as string}/pull/${number}`;\n}\n"]}
1
+ {"version":3,"file":"git.js","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Git, read-only.\n *\n * spec-brief never stages, commits or rewrites history. It reads the commit a\n * round landed as, the files that commit touched, and whether the working tree\n * holds work the commit does not. What goes into a commit stays the decision\n * of whoever makes it, and archival is atomic over the files it writes rather\n * than over a repository state it would have to reverse.\n */\n\nimport { execFile } from 'node:child_process';\nimport { resolve } from 'node:path';\n\nimport { canonicalPath } from './fs.js';\nimport { relativePath } from './links.js';\n\nexport interface CommitInfo {\n readonly sha: string;\n readonly author: string;\n /** ISO 8601 author date. */\n readonly date: string;\n}\n\nexport interface FileChange {\n readonly path: string;\n /** `null` for a binary file, which has no line counts. */\n readonly insertions: number | null;\n readonly deletions: number | null;\n}\n\nexport interface Git {\n /** The commit a revision names, or `null` when it names none. */\n commit(revision: string): Promise<CommitInfo | null>;\n /**\n * The hash of the commit each revision names, or `null` for one that names\n * none, asked once. A git without it is one that cannot say.\n */\n commits?(revisions: readonly string[]): Promise<(string | null)[]>;\n mergeBase(a: string, b: string): Promise<string | null>;\n /** Files changed from `from` to `to`; with no `from`, the files `to` changed against its first parent. */\n changes(from: string | null, to: string): Promise<FileChange[]>;\n /** Paths with uncommitted changes, untracked files included. */\n dirty(): Promise<string[]>;\n /** Every path git sees: tracked, or untracked and not ignored. A file a round just created counts. */\n files(): Promise<string[]>;\n remoteUrl(name: string): Promise<string | null>;\n}\n\n/**\n * A revision git cannot be handed, or a command of git's that failed: the\n * message is the whole report, since what failed is git or what it was given,\n * and no line of spec-brief's.\n */\nexport class GitError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'GitError';\n }\n}\n\ninterface Run {\n readonly ok: boolean;\n readonly stdout: string;\n readonly stderr: string;\n}\n\n/** A git command, handed `input` on its standard input, and an end of it there either way. */\nfunction run(cwd: string, args: readonly string[], input?: string): Promise<Run> {\n return new Promise((done) => {\n const child = execFile(\n 'git',\n ['-c', 'core.quotepath=off', ...args],\n // windowsHide keeps a console window from flashing up on Windows; no other host has one to hide.\n { cwd, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024, windowsHide: true },\n (error, stdout, stderr) => done({ ok: error === null, stdout, stderr }),\n );\n // A git that ends before it reads what it is handed, as one outside a\n // repository does, closes the pipe under the write, and the EPIPE went\n // uncaught and ended the process. What git printed is its answer, so the\n // error is taken and nothing is done with it; the mutant that gives the\n // handler a body of `undefined` does nothing either, and is equivalent.\n child.stdin!.on('error', () => {});\n // A command that reads its input reads to the end of it, which only this\n // gives it; the others read none.\n child.stdin!.end(input);\n });\n}\n\n/** A revision that starts with \"-\" would be read as an option. */\nfunction safeRevision(revision: string): string {\n if (revision.startsWith('-') || revision.trim() === '') throw new GitError(`\"${revision}\" is not a revision`);\n return revision;\n}\n\nfunction records(output: string): string[] {\n return output.split('\\0').filter((r) => r !== '');\n}\n\n/** Parses `--numstat -z` output with renames off: `added<TAB>deleted<TAB>path<NUL>`. */\nexport function parseNumstat(output: string): FileChange[] {\n // `show --format=` leaves the empty header's newline in front of the first record.\n return records(output.replace(/^\\n+/, '')).map((record) => {\n const fields = record.split('\\t');\n // Cast on the name: what stands under an `as` is not mutated (ADR-0009).\n const [added, deleted, ...path] = fields as [string, string, ...string[]];\n return {\n path: path.join('\\t'),\n insertions: added === '-' ? null : Number(added),\n deletions: deleted === '-' ? null : Number(deleted),\n };\n });\n}\n\n/** Parses `status --porcelain=v1 -z`, where a rename's original path is a record of its own. */\nexport function parsePorcelain(output: string): string[] {\n const fields = records(output);\n const paths: string[] = [];\n for (let i = 0; i < fields.length; i += 1) {\n const field = fields[i] as string;\n paths.push(field.slice(3));\n const x = field.charAt(0);\n if (x === 'R' || x === 'C') i += 1;\n }\n return paths;\n}\n\n/**\n * Git reports paths from the top of the work tree; spec-brief's paths start at\n * the root, which may be a directory below it. Every path that crosses this\n * boundary is made relative to the root.\n */\nexport class NodeGit implements Git {\n readonly cwd: string;\n private prefix: Promise<string> | undefined;\n\n constructor(cwd: string) {\n this.cwd = cwd;\n }\n\n /** The root's path from the top of the work tree, `sub/dir/` or empty. */\n private async rootPrefix(): Promise<string> {\n // Asked only after a status that succeeded, so the repository exists.\n this.prefix ??= run(this.cwd, ['rev-parse', '--show-prefix']).then((r) => r.stdout.trim());\n return this.prefix;\n }\n\n /** The working tree's top directory, canonical, or `null` outside a repository. */\n static async toplevel(cwd: string): Promise<string | null> {\n const result = await run(cwd, ['rev-parse', '--show-toplevel']);\n return result.ok ? canonicalPath(resolve(result.stdout.trim())) : null;\n }\n\n async commit(revision: string): Promise<CommitInfo | null> {\n const sha = await run(this.cwd, ['rev-parse', '--verify', '--quiet', `${safeRevision(revision)}^{commit}`]);\n if (!sha.ok) return null;\n const hash = sha.stdout.trim();\n const show = await run(this.cwd, ['show', '-s', '--format=%an%x00%aI', hash]);\n const fields = show.stdout.trim().split('\\0');\n // Cast on the name, as in `parseNumstat`.\n const [author, date] = fields as [string, string];\n return { sha: hash, author, date };\n }\n\n /**\n * One process for every revision: `cat-file --batch-check` reads one a\n * line, as `rev-parse --verify` reads one, and answers each with its\n * object's name, or with the revision and why it names none.\n */\n async commits(revisions: readonly string[]): Promise<(string | null)[]> {\n // A line break would make one revision two questions, and its answer\n // another's. On a line of its own a revision is never read as an option,\n // so one that starts with \"-\" is asked, and names none.\n if (revisions.some((revision) => revision.includes('\\n'))) return revisions.map(() => null);\n const asked = revisions.map((revision) => `${revision}^{commit}\\n`).join('');\n const lines = (await run(this.cwd, ['cat-file', '--batch-check=%(objectname)'], asked)).stdout.split('\\n');\n return revisions.map((_, index) => {\n const line = lines[index];\n // An object's name holds no space, and the answer for a revision that\n // names none does; a git that answered nothing printed no line for it.\n return line !== undefined && line !== '' && !line.includes(' ') ? line : null;\n });\n }\n\n async mergeBase(a: string, b: string): Promise<string | null> {\n const result = await run(this.cwd, ['merge-base', safeRevision(a), safeRevision(b)]);\n return result.ok ? result.stdout.trim() : null;\n }\n\n async changes(from: string | null, to: string): Promise<FileChange[]> {\n const target = safeRevision(to);\n let result: Run;\n if (from !== null) {\n result = await run(this.cwd, ['diff', '--numstat', '-z', '--no-renames', '--relative', safeRevision(from), target]);\n } else {\n const parent = await run(this.cwd, ['rev-parse', '--verify', '--quiet', `${target}^`]);\n result = parent.ok\n ? await run(this.cwd, ['diff', '--numstat', '-z', '--no-renames', '--relative', `${target}^`, target])\n : await run(this.cwd, ['show', '--numstat', '-z', '--no-renames', '--relative', '--format=', target]);\n }\n if (!result.ok) throw new GitError(`git could not diff ${from ?? `${to}^`}..${to}: ${result.stderr.trim()}`);\n return parseNumstat(result.stdout);\n }\n\n async dirty(): Promise<string[]> {\n const result = await run(this.cwd, ['status', '--porcelain=v1', '-z', '--untracked-files=all']);\n if (!result.ok) throw new GitError(`git status failed: ${result.stderr.trim()}`);\n const prefix = await this.rootPrefix();\n return parsePorcelain(result.stdout).map((path) => relativePath(prefix, path));\n }\n\n async files(): Promise<string[]> {\n const result = await run(this.cwd, ['ls-files', '-z', '--cached', '--others', '--exclude-standard']);\n if (!result.ok) throw new GitError(`git ls-files failed: ${result.stderr.trim()}`);\n return records(result.stdout).sort();\n }\n\n async remoteUrl(name: string): Promise<string | null> {\n const result = await run(this.cwd, ['remote', 'get-url', safeRevision(name)]);\n return result.ok ? result.stdout.trim() : null;\n }\n}\n\n/** The web address of a pull request, for remotes on github.com. */\nexport function pullRequestUrl(remote: string | null, number: number): string | null {\n // Said for the reader and the compiler: no remote reads as the text \"null\", which is no address either.\n if (remote === null) return null;\n const match = /github\\.com[:/]([^/\\s]+)\\/([^/\\s]+?)(?:\\.git)?\\/?$/.exec(remote);\n if (!match) return null;\n return `https://github.com/${match[1] as string}/${match[2] as string}/pull/${number}`;\n}\n"]}