import type { TaprootDb } from '../db/client.js'; import { type ContentItem } from './items.js'; /** * Copying a subtree — a page and everything beneath it, as drafts. * * What a versioned section of a site needs: duplicate `/handbook/2026-27` and every page beneath it * to `/handbook/2027-28`, edit the copy, publish it. Freezing the old one is then structural rather * than enforced — its pages are *different rows*, so nothing anybody does to the copy can reach * them. That is the whole argument for copy-forward over effective-dating or a version column. * * Note what copy-forward does **not** freeze: anything the copies resolve at read time from a shared * row — a reusable block, a text snippet — still points at one central row, so editing that row * changes every copy, including ones meant to stand as a record. Media is exempt by construction: a * media row's bytes are immutable because the storage key derives from the asset id. */ /** What to call the copy, and where to put it. */ export interface DuplicateSubtreeInput { /** The copy's slug. Defaults to the source's, disambiguated by `createItem` against siblings. */ slug?: string; /** * Where the copy's root goes. Defaults to the source's own parent, which makes the copy a sibling * — the shape a new version takes, since `/handbook/2026-27` and `/handbook/2027-28` share one. */ parentId?: string | null; /** Title for the copy's root. Descendants always keep theirs. */ title?: string; userId?: string | null; /** * How many items to write before returning. Defaults to everything. * * **Chunking is not optional at scale and this is how it is expressed.** A catalog year is ~280 * items, each needing its own batch — D1 caps statements per batch, and every item carries path * rewrites, a revision, a taxonomy plan and two derived indexes — so one request cannot do it * inside a Worker's budget. A caller loops until `remaining` is 0. */ limit?: number; } export interface DuplicateSubtreeResult { /** The copy's root, whether it was created by this call or an earlier one. */ root: ContentItem; /** Items written by *this* call. */ created: number; /** Items still to copy. Zero means done; anything else means call again. */ remaining: number; } export declare class DuplicateError extends Error { readonly code: 'not_found' | 'invalid_target'; name: string; constructor(message: string, code?: 'not_found' | 'invalid_target'); } /** * Copy an item and everything beneath it. * * **Resumable with no bookkeeping table, because the destination paths are derivable.** An item is * already copied when something exists at its mapped path, which is the same lookup-then-create * shape `seed.ts` uses to stay idempotent. So a caller can loop, a failed run can be re-run, and * nothing has to be cleaned up first — where a job table would need its own lifecycle, its own * sweep, and a story for what happens when a row outlives the content it describes. * * **Reads first, then compute, then write** — the batch rule, applied one item at a time rather than * once, since `createItem` owns its own batch and there is no transaction spanning N of them. The * consequence is honest and worth stating: an interrupted copy leaves a *partial* subtree of drafts, * not a corrupt one. Nothing is published, so nothing a visitor sees is affected, and the next call * carries on where this one stopped. * * Order is by depth, so a parent always exists before its child needs it as a `parentId`. */ export declare function duplicateSubtree(handle: TaprootDb, rootId: string, input?: DuplicateSubtreeInput): Promise; /** * Rewrite every reference that points inside the copied subtree, and re-mint every instance id. * * **Five field types carry an item reference, not one.** The obvious version of this remaps * `relation` fields and stops — and then a `link` field's button, a rich-text paragraph's internal * link, and anything of either kind nested inside a block or a repeater row all keep pointing at * last year's pages. None of those breaks visibly: every link works and lands on a real page that * looks almost the same, which is exactly why it would survive review. * * Structural rather than definition-driven, deliberately. `collectReferences` walks by field type * and needs the schema; this runs over stored `data` where the shapes are unambiguous on their own — * a `{ kind: 'item', id }` object is a link wherever it sits, and a string containing * `taproot:item:` is prose wherever it sits. That means one walk covers every depth without loading * a block registry, and it cannot be defeated by a field whose definition has since changed. * * **Media, taxonomy terms and query rules are left alone.** They point *outside* the subtree by * design: a copy shares the library's assets and the site's vocabulary, and a saved query is a rule * rather than a set of ids. Remapping them would be a bug, not a missing feature. * * Block instance ids and repeater row ids are re-minted, because an id should identify one thing — * and `queryKey` is `${containerId}:${fieldApiId}`, so two pages sharing a block instance id is a * collision waiting for somebody to put a query field in that block. */ export declare function remapData(value: Record, idMap: ReadonlyMap, options?: { mintIds?: boolean; }): Record;