/** * Find a mapped doc whose file has moved, before anything is written * (`docs-in-place-plan.md` → "Relinking is automatic; the command is only the * fallback"). * * A missing mapped file used to be an ordinary pull: re-materialize at the * mapped path. That is exactly right while docs live in a directory nobody * hand-edits, and exactly wrong once one lives at `docs/architecture.md` — move * it, sync, and Auden puts a second copy back where it used to be. Making the * user run a command to prevent that is charging them for our bookkeeping. * * **The evidence is git's, not the hash's.** Content equality proves *these * bytes are ours*; it does not prove *this file is that doc*. Move a doc and * edit it and the moved file drops out of the hash match entirely — so an * unrelated file that happens to equal the old baseline (a copy the user made, a * template, a fixture) would become the sole candidate and be adopted silently, * after which an upstream update overwrites it while the real doc sits unmanaged. * Worse, that case is not even *recognized* as ambiguous, because there * genuinely is only one match. So: * * - A **rename git already detects** is the answer, and it is the strong signal * precisely because it survives editing: a moved-and-edited doc is reported as * a rename with its old path attached. The content difference is then not this * module's problem — the mapping repoints and the ordinary reconcile * classification sees a local edit and pushes it under `expectedVersion`. * - Otherwise the baseline hash may match a file git reports as added or * untracked — but that is **a suggestion to report, never an adoption**. The * plan's rule was "match only against files git reports as newly appeared", * which rested on a premise that does not hold: `git status` prints `??` for * *every* untracked path on *every* invocation, so a template that has sat in * the repo untracked for months is indistinguishable from a file that appeared * a second ago (verified against git 2.43.0; raised by Codex on PR #566). The * counterexample the plan set out to disarm therefore survived the rule written * to disarm it — move a doc *and edit it* and the real file drops out of the * hash match, leaving a long-standing unrelated file as the sole "match", which * a later upstream update would then overwrite. Git records nothing about when * an untracked file appeared, so no amount of parsing recovers the evidence. * The hash match is still worth *saying* — it is almost always right, and it * turns a bare "I lost this doc" into "did it move to `reference/api.md`?" — so * it is reported for `auden docs relink` to confirm. * - **No git work tree, no candidate set.** Report and stop. `project-id.ts` sets * the precedent for both the `spawnSync('git', …)` pattern and for refusing to * guess when git cannot answer. * * **Only a git-detected rename changes a mapping automatically.** That is the one * signal carrying git's own account of identity rather than a hash coincidence, * and it is also the one that survives editing. * * Candidates come from git's change report — a handful of paths — not from * `git ls-files` or a directory walk, so there is no repo-wide scan here, and * this runs only when a mapped path is missing, which is rare on top of that. * * Kept free of citty/console so it unit-tests against a fake spawn. */ /** What git says changed in the work tree, reduced to what a relink can use. */ export type GitChangeReport = { /** Renames git itself detected, old repo-relative path → new one. */ renames: Map; /** * Repo-relative paths git reports as added to the index or untracked. * * **Not "newly appeared"** — git prints `??` for every untracked path on every * invocation, so this set includes files that have been sitting there for * months. It is therefore the candidate set for a *suggestion*, never for an * automatic adoption; see the module comment. */ appeared: string[]; }; export type SpawnGitFn = (args: readonly string[], cwd: string) => { status: number | null; stdout: string; }; /** * Ask git what moved. Null — not an empty report — when there is no work tree: * "git has nothing to say" and "git says nothing changed" license opposite * conclusions, and collapsing them would let a non-git checkout silently take * the no-candidates path as if detection had run. */ export declare function readGitChangeReport(root: string, spawnGit?: SpawnGitFn): GitChangeReport | null; /** * Parse `git status --porcelain=v1 -z`. * * The NUL framing is the reason this is hand-parsed rather than split on * newlines: a rename record is `XYnewold`, so the old path is * its own field, and any path may legally contain a newline. Splitting on `\n` * both loses the pairing and lets a crafted filename forge a record. */ export declare function parseGitStatus(raw: string): GitChangeReport; export type RelinkOutcome = /** * Git reported a rename away from the mapped path, and the destination is * readable. The **only** outcome that changes a mapping by itself: content * need not match, because git's own rename detection is the evidence. */ { kind: 'renamed'; to: string; } /** * Exactly one added-or-untracked file matches the recorded baseline. * * Reported, **not adopted** — `??` does not mean "appeared since the last * sync", so the bytes are a coincidence this module cannot rule out. The user * confirms with `auden docs relink`. */ | { kind: 'suggested'; to: string; } /** Several candidates match — two identical copies is not a move. */ | { kind: 'ambiguous'; candidates: string[]; } /** Nothing matched, or there was no git work tree to ask. */ | { kind: 'unmatched'; reason: 'no-candidate' | 'no-git'; }; /** * Decide where a missing mapped doc went. * * `hashOf` reads and hashes a candidate, returning null when it cannot be read — * an unreadable candidate is not a match, and must never be adopted on the * strength of having failed to be inspected. * * `isClaimed` rejects a candidate another mapping or this pass already owns: * adopting one would point two items at one file, which is the invariant the * whole reconciler is built on. */ export declare function resolveRelinkTarget(params: { mappedPath: string; baselineHash: string | undefined; report: GitChangeReport | null; hashOf: (path: string) => Promise; isClaimed: (path: string) => boolean; }): Promise; //# sourceMappingURL=relink.d.ts.map