/** * Core of `auden docs relink ` — the declared fallback for a move * detection honestly cannot settle (`docs-in-place-plan.md` → slice 4). * * Slice 3 narrowed automatic relinking to **git-detected renames only**: `??` * does not mean "appeared since the last sync", so a unique baseline match among * untracked files is reported with its candidate rather than adopted. Everything * the sync reports as `ambiguous`, `no-candidate`, `no-git` or `unstaged-move` * therefore arrives here, and the unstaged move — a plain `mv` nobody staged — * is the common one. The command's job is to let a person confirm what detection * refused to assume. * * **It repoints a placement record and writes nothing else.** No file is * created, moved, or removed: the user already moved the file, and the CLI is * catching up. The next `auden sync` reconciles at the new path. * * **This is the one path that repoints a mapping on a human's say-so rather than * on git's evidence, so the guards are the whole design:** * * - **Containment and symlinks.** The target is guarded exactly as a mapped * write target is (`refuseUnsafeMappedTarget`), because it *becomes* one the * moment the next sync runs — a `docs/link → /etc` symlink passes every * lexical check and still carries the write out of the repo. Auden's own state * files are refused for the same reason a document may not declare them * (`isReservedAudenPath`): a mapping at `.auden/config.json` would have the * sync overwrite the subscription state that decides what syncs at all. * - **Write-once.** A file already sitting at the target is adopted only when its * bytes match the recorded baseline — the proof the reconciler uses everywhere * else that a file is ours. That is exactly the candidate the sync report names * for an unstaged move, so confirming a reported candidate always works, and * pointing a mapping at an unrelated file never does. An **absent** target is * refused too: the sync cannot fill it (`sync-docs.ts:1277-1289` reports * `relink-needed` and writes nothing), so the mapping would be stuck. Deleting * the doc's placement line is the remedy for a deletion, and it is a different * action from confirming a move. * - **One path, one mapping.** A target another mapping already claims is * refused — by resolved path, not by spelling, so a repo-internal directory * symlink cannot alias two mappings onto one file. Two mappings at one path is * the duplication the one-path rule exists to prevent, and it would reconcile * one item's bytes into the other. * - **Ordinary documents only**, the rule the sync's own prepass applies * (`sync-docs.ts:1049`) restated in terms of the path, which is all a command * that never saw the item has. A guide's location is its projection into a * discovery root and a rubric's is its shadow-tree path — neither is a * placement a human owns, and a doc sent *to* one of them is read as guidance. * * A move whose file was also *edited* is therefore refused unless git saw the * rename — and that refusal names the remedy, because git's rename detection is * similarity-based and survives editing: stage the move and let slice 3 do it. * Loosening the rule instead would let a mistyped path push an unrelated file's * contents into a canonical doc under `expectedVersion`. * * Kept free of citty and console so it unit-tests against a temp dir. */ import { type DocMapping, type PlacementConflict, type Sidecar } from './sidecar.js'; /** Why a relink was refused. One code per guard, so the tests name the guard. */ export type RelinkRefusal = /** No placement record names the old path. */ 'no-mapping' /** The placement file disagrees about this doc; only the user can settle it. */ | 'placement-conflict' /** Old and new path are the same file — nothing to rewrite. */ | 'same-path' /** The target is not inside the repo, lexically. */ | 'outside-repo' /** The target is Auden's own state, which sync rewrites. */ | 'reserved-path' /** The target is a symlink, or resolves outside the repo through one. */ | 'unsafe-target' /** The old path is a projection (a guide's or a rubric's), not a placement. */ | 'projected-source' /** The new path is a discovery root or the eval shadow tree. */ | 'projected-target' /** Another mapping already owns the target path. */ | 'claimed' /** A file sits at the target and its bytes are not provably ours. */ | 'unprovable-target' /** Something is at the target but could not be read to check. */ | 'unreadable-target' /** Nothing is at the target — a mapping there is one sync can only report. */ | 'missing-target'; export type RelinkMappingResult = { ok: true; itemId: string; bundle: string; /** Repo-relative POSIX paths, the form the placement file stores. */ from: string; to: string; /** * True when the arguments were read relative to the repo root rather than * the working directory — which is what copying the sync report's * suggested command out of a subdirectory does. Said out loud, never * inferred: the user typed one spelling and the command acted on another. */ resolvedFromRepoRoot: boolean; /** * True when a file *also* still sits at the old path. Not an error — the * user may have copied rather than moved — but the sync will report it as * unmanaged from now on, so saying so beats letting them discover it. */ sourceStillPresent: boolean; } | { ok: false; code: RelinkRefusal; message: string; }; export type RelinkMappingDeps = { readSidecarFn?: (root: string) => Promise; writePlacementFn?: (root: string, docs: readonly DocMapping[], conflicts: readonly PlacementConflict[]) => Promise; readFileFn?: (path: string, encoding: 'utf8') => Promise; statFn?: (path: string) => Promise<{ size: number; }>; realpathFn?: (path: string) => Promise; lstatFn?: (path: string) => Promise<{ isSymbolicLink(): boolean; }>; /** * Re-key this machine's record of a file that moved. Injected so tests can * pin the machine cache inside their fixture instead of the real * `~/.auden/state/`. */ moveCacheEntryFn?: (fromAbsolute: string, toAbsolute: string) => Promise; }; /** * Move one entry in the machine cache, leaving every other repo's alone. * * A no-op when this machine has no record of the source — a fresh clone relinks * a doc it never wrote, and inventing an entry would claim a write that did not * happen. */ export declare function moveMachineCacheEntry(fromAbsolute: string, toAbsolute: string, cachePath?: string): Promise; /** * Repoint the placement record for the doc mapped at `from` to `to`. * * `from` and `to` are what the user typed, resolved here rather than by the * command, because **which directory they are relative to is not obvious and the * answer depends on the lookup.** A shell's tab completion produces paths * relative to the working directory; the sync report prints them relative to the * repo root (`sync-docs.ts:1851-1857`), and copying its suggested command out of * a subdirectory would otherwise resolve `docs/a.md` to `//docs/a.md` * and fail as `no-mapping` — on the one path this command exists to serve. * Raised by Codex on PR #578. * * So both frames are tried, working directory first, and **the frame that finds * the mapping decides how `to` is read as well** — the pair is interpreted * together or not at all, and the result says which frame won. When the command * is run from the repo root, as it usually is, the two frames are the same * directory and nothing here does anything. */ export declare function relinkDocMapping(params: { root: string; cwd?: string; from: string; to: string; }, deps?: RelinkMappingDeps): Promise; /** * Render the outcome as plain lines. * * A silent success here is indistinguishable from a no-op, so a success always * names the doc, both paths, and what the next sync will do with the file. Paths * are escaped: they come from the committed placement file and from the command * line, neither of which this module wrote. */ export declare function formatRelinkResult(result: RelinkMappingResult): string; //# sourceMappingURL=relink-mapping.d.ts.map