/** * Decide what a local file *is*, relative to the canonical doc it materializes. * * The docs phase needs one question answered before it may overwrite anything: * **did the user edit this file, or is it just an old copy of ours?** * * The recorded baseline in `.auden/docs.json` answers it for free whenever the * file is still the newest copy we wrote, and the sync path asks that first — * the sidecar has to carry that hash for the orphan sweep regardless, so * reaching for the network ahead of it would cost a request on the commonest * path and give up working offline, for nothing. * * This module covers what the baseline *cannot* answer, which is everything * about a file it did not itself write: * * - a file left by `auden pull`, a colleague's commit, or a fresh clone — * no baseline exists, and before this it had to be called a conflict; * - a file reverted to an earlier version, or restored from an older commit — * the baseline knows only the newest copy, so this read as an edit and * blocked the doc from ever updating again. * * The server saves a `contentHash` on every version of a doc * (`ContextItemVersionSchema`, and `computeContentHash` is a plain * `sha256(content, 'utf8')` hex digest — the same function `hashContent` is). * So both cases are answerable directly: hash the local bytes and look for that * hash in the doc's history. * * matches the current content → `current` — nothing to do * matches some older version N → `stale` — our copy, safe to overwrite * matches nothing in the history → `edited` — the user's work, never touch * could not be established → `unknown` — assume the user's work * * **`unknown` is not a failure mode, it is the safe answer**, and no caller may * overwrite on it. But it comes in two kinds, and they are not equally safe to * *push*: `unreadable` carries no evidence at all, while `lookback-exhausted` * carries evidence pointing the wrong way — the file matched none of the * versions we managed to check, yet older ones went unchecked, so it may be one * of our own ancient copies. Pushing that republishes stale content as canonical * for every subscriber. See `blocksPush`. * * Kept free of citty/console so it unit-tests against a mocked client. */ import type { ContextClient } from '../mcp/client.js'; import type { ContextItem } from '@auden.to/protocol'; /** * How many versions back to look before giving up. * * A bound is required: history is unbounded and paging all of it to classify one * file would turn an ordinary sync into an arbitrary number of requests. Giving * up resolves to `unknown`, which callers treat as "the user's work" — so the * cost of the cap is a refused overwrite, never a lost edit. */ export declare const MAX_VERSION_LOOKBACK = 100; export type LocalClassification = { state: 'current'; } | { state: 'stale'; version: number; } | { state: 'edited'; } /** * We could not establish what the file is. `cause` matters, because the two * ways of not knowing carry different evidence: * * - `unreadable` — the history could not be fetched at all, so there is no * evidence either way and the caller's own baseline is the best it has. * - `lookback-exhausted` — the history *was* read and the file matched none of * the newest `MAX_VERSION_LOOKBACK` versions, but older ones went unchecked. * Partial evidence, and pointed the wrong way: the file could still be one of * our very old copies. */ | { state: 'unknown'; cause: 'unreadable' | 'lookback-exhausted'; reason: string; }; /** * The stored contents a local file could have been materialized from. * * The server hashes the doc's content; the writer puts `content + "\n"` on disk * when the content does not already end in a newline. So a local file ending in * a newline is consistent with *two* stored contents — itself, and itself minus * that newline — and both have to be tried or every doc whose content lacks a * trailing newline would classify as edited the moment it was written. * * Offering both is safe rather than ambiguous: this is an equality test against * a sha256, so a wrong candidate matching would require a collision. */ export declare function storedContentCandidates(localBytes: string): string[]; /** True when the local bytes hash to `contentHash` under either candidate form. */ export declare function matchesStoredHash(localBytes: string, contentHash: string): boolean; /** * Classify `localBytes` against `item`. * * The current content is checked first and costs nothing — the caller already * holds it from the pull — so a file that is already up to date never touches * the network. Everything else walks the history. * * There is deliberately no "the doc hasn't moved, so skip the walk" shortcut. * It looks safe and is not: it assumes a file differing from the newest copy can * only be the user's work, when it can equally be an *older* copy — reverted by * hand, or restored with the rest of an old commit — which is the exact case * this module was added to recognize. The shortcut turned that file into a push * of stale content back over the canonical doc. */ export declare function classifyLocalFile(client: ContextClient, bundle: string, item: ContextItem, localBytes: string): Promise; /** True when a classification means "the user's work — do not overwrite". */ export declare function isUserOwned(classification: LocalClassification): boolean; /** * True when these bytes must not be sent back to the server. * * Only `lookback-exhausted` blocks. It is the one classification carrying * evidence that points the wrong way: the file matched none of the versions we * looked at, but older ones went unchecked, so it may well be one of our own * ancient copies. Pushing it would republish stale content as the newest * version — for every repo subscribed to that bundle, from an unattended Stop * hook, with no one watching. * * The asymmetry with `isUserOwned` is deliberate. Refusing to *overwrite* on any * uncertainty protects the user's file; refusing to *push* protects everyone * else's. Without this the same user action — reverting a doc to an earlier * version — resolved opposite ways depending only on how long that doc's history * happened to be: inside the lookback it was recognized and pulled, past it, it * was pushed over the canonical copy. */ export declare function blocksPush(classification: LocalClassification): classification is { state: 'unknown'; cause: 'lookback-exhausted'; reason: string; }; //# sourceMappingURL=classify.d.ts.map