/** * Where a bundle's `guide` members land on disk (slice 1b of * docs/plans/context-bundle-authoring-plan.md). * * A guide materialized into `.auden/context//` is inert: that * directory is not a rules-discovery root, so no agent ever reads it as * guidance. A guide member must land at its **projection path** — its declared * `path` when it has one, else derived from its `origin` * (`file:{format}:{path}`, which records exactly the path it was imported * from), falling back to `.agents/.md` for dashboard-authored * guides that carry neither. * * **The path is resolved against the item's scope root, not always the repo** * (`docs/plans/guide-write-side-identity-plan.md` → slice 2). A `global` item * came from the user's home directory, and resolving it against `cwd` is how a * guide imported from `~/.claude/CLAUDE.md` came back down as * `/.claude/CLAUDE.md` — a fork of a file the user never asked to copy. * A caller that does not supply a home root gets a named refusal for such an * item instead. Slice 4 supplies one — `~/.auden/global/`, a directory Auden * owns — so a global guide now lands on the machine rather than being reported * as not materialized. It is deliberately not the home directory itself and not * another tool's config directory: the last hop into `~/.claude/` is a separate * question, and nothing callable may answer it. * * Paths are remote-supplied strings and are validated before a byte is * written. The rule originated in `auden guides export`'s writer, which is now * deleted (`unified-item-sync-plan.md` → Slice 5) — this is the surviving copy, * not a mirror of one: reject absolute * paths, parent traversal, and control characters; require lexical containment * in the scope root; and require realpath containment of the nearest existing * ancestor, so a symlinked directory cannot carry the write outside it. * A path that fails the guard is rejected and reported — never "fixed up". */ import type { GuideDiscoveryRoot } from '@auden.to/protocol'; /** * The roots a projection may resolve against, one per discovery scope. * * `project` is the repo checkout. `global` is where machine-wide guidance lands * — `~/.auden/global/`, a directory Auden owns, **not** the user's home * directory itself and not another tool's config directory. It stays optional in * the type because a caller may legitimately have no global root (a test * fixture, a scope check that is only asking about the project), and an item * declaring `root: 'global'` is then refused with a named reason rather than * silently rewritten repo-relative. * * Production callers should not build this object by hand — use * {@link guideScopeRoots}, which supplies the global root every time. */ export type GuideScopeRoots = { project: string; global?: string | undefined; }; /** * The scope roots for an ordinary sync or export in `repoRoot`. * * **One place, so no call site can forget the global root.** Both docs-phase * projection sites used to pass `{ project: root }` literally, which is how a * `global` item came back refused and — before that refusal existed — how it * got written into the project tree as a fork of the user's machine-wide * guidance. A helper that always supplies both is the difference between "this * call site remembered" and "this cannot be got wrong" * (CLAUDE.md → Simplicity: can a future call site forget this?). * * There is no consent parameter, deliberately. `~/.auden/global/` is Auden's own * directory, so writing there needs permission from nobody, and a *callable* * grant that widened this to `~/.claude/` would be a prompt-injection primitive * whose payoff is agent instructions on every future session on the machine. * The last hop into another tool's directory is a separate question and is not * answered by anything that can be invoked (`guide-write-side-identity-plan.md` * → slice 4, "the hard rule that survives"). */ export declare function guideScopeRoots(repoRoot: string): GuideScopeRoots; export type GuideProjectionDeps = { realpathFn?: (path: string) => Promise; lstatFn?: (path: string) => Promise<{ isSymbolicLink(): boolean; }>; }; export type GuideProjection = { ok: true; /** Absolute path to write. */ absolutePath: string; /** Scope-root-relative POSIX path (the sidecar's stored form). */ storedPath: string; /** Which root `storedPath` is relative to. */ scope: GuideDiscoveryRoot; } | { ok: false; reason: string; }; /** * Resolve the on-disk projection path for a `guide` bundle member. * * `usedStoredPaths` guards against two guides claiming one file in a single * pass (attach-time path uniqueness is `guide-versioned-identity`'s scope, so * nothing upstream prevents it yet) — the second claim is rejected, not * silently clobbered. */ export declare function resolveGuideProjectionPath(roots: GuideScopeRoots, item: { id: string; name: string; origin?: string | null | undefined; root?: GuideDiscoveryRoot | null | undefined; path?: string | null | undefined; }, usedStoredPaths: ReadonlySet, deps?: GuideProjectionDeps): Promise; /** * Checked on the **resolved** spelling, not the raw one (Codex, PR #564). * `docs/../.auden/config.json` does not start with `.auden/`, and * `.auden/context/../../.auden/config.json` looks like an allowed subtree — * both resolve to `.auden/config.json`, which is what actually gets written. * * Exported because `auden docs relink` repoints a mapping at a path a human * typed, and the sync then writes there — the same write, so the same rule. A * second copy of it in the command would be one symlink-escape's worth of drift * away from the first (Planning Principle 14). */ export declare function isReservedAudenPath(candidate: string): boolean; /** * Where a plain `document` (or `reference`) bundle member lands on its **first** * materialization — its own declared `path`, resolved against its own `root` * (`unified-item-sync-plan.md` → *Document landing paths become item-derived*). * * Docs were the last content type whose location came from a *container*: the * directory came from whichever bundle the sync happened to be processing, so an * item in two subscribed bundles landed wherever `docs.bundles` order put it, * and the sidecar then pinned that — stable on one machine, different on the * next. Guides project to a discovery root and eval criteria to the shadow tree; * this finishes the pattern. * * **Null, not a refusal, when the item declares no path.** A doc created through * the context layer or authored in the dashboard has no `path` and never had one * to lose, so the caller keeps today's bundle-directory default for it. This is * the difference from `resolveEvalProjectionPath`, which refuses a pathless item: * criteria are defined relative to the guide they grade, so a pathless one names * no guide, while a document is perfectly happy in the pull directory. * * Only the mapping-free case reaches here. Once a mapping exists its path is * authoritative and is never re-derived, so a doc moved (or renamed upstream) * cannot sprout a second file (`sync-docs.ts` → `writeMappedDoc`). */ export declare function resolveDocumentProjectionPath(roots: GuideScopeRoots, item: { id: string; name: string; root?: GuideDiscoveryRoot | null | undefined; path?: string | null | undefined; }, usedStoredPaths: ReadonlySet, deps?: GuideProjectionDeps): Promise; /** * Re-run the write-time guards on a path that arrived from the **committed** * placement file rather than from a projection this pass computed. * * Lexical containment is not enough for these (Codex, PR #564). A committed — * or locally planted — `docs/link` symlink pointing out of the repo passes * `isPathWithin` on the stored string, and `writeMappedDoc` then follows it: an * ordinary pull writes outside the repo, which is the one thing the whole design * promises cannot happen. A committed file is a remote-supplied string as much * as a projection is; it just arrives by git instead of by HTTP. * * Returns null when the target is safe, or a reason when it is not. */ export declare function refuseUnsafeMappedTarget(root: string, absolutePath: string, deps?: GuideProjectionDeps): Promise; /** * Resolve the on-disk path for an **eval criteria** bundle member * (`unified-item-sync-plan.md` slice 3 / `eval-criteria-on-disk-plan.md` slice 2). * * Criteria land in the shadow tree, `/.auden/evals/`, * which is committed, outside every discovery root, and therefore not injected * into an agent's context by default. The item's declared `path` already *is* * that shadow path — it is where the file lives — so this does not re-derive it * from a guide path; it validates that what the server sent is genuinely a * shadow-tree path and contains the write. * * **The containment rule this slice owes** (`unified-item-sync-plan.md` → * Dependencies: "slice 3's containment rule for `.auden/evals/` is still this * item's to build") is the `classifyLocalItemPath` check below, on top of the * shared guard. Lexical containment in the scope root is not enough on its own: * it would happily accept `.agents/style.md` as criteria and write a rubric into * a discovery root, which is the one outcome the shadow-tree design exists to * prevent — a rubric silently becoming guidance the agent reads as instructions. * * An item with no declared path is refused rather than defaulted. A guide can * fall back to `.agents/.md` because a guide belongs *somewhere*; criteria * are defined relative to the guide they grade, so criteria with no path name no * guide and have no home to invent. */ export declare function resolveEvalProjectionPath(roots: GuideScopeRoots, item: { id: string; name: string; root?: GuideDiscoveryRoot | null | undefined; path?: string | null | undefined; }, usedStoredPaths: ReadonlySet, deps?: GuideProjectionDeps): Promise; //# sourceMappingURL=guide-projection.d.ts.map