import { type HookRegistration, type ResolvedHookRegistration } from "./schema.js"; /** * The native hook shape: what AIH emits, what it reads back, and the key that * decides whether those two are the same entry. * * The rule this module exists to keep: a projected `hooks` key is written as a * WHOLE-KEY REPLACE, so anything the flattening cannot see is destroyed. That * makes faithfulness a correctness property. AIH therefore carries every native * field it did not author — a group's `matcher` above all — verbatim through * capture, receipt and re-emission, exactly as it already carries the command. * It interprets none of them: AIH implements no scoping grammar. */ /** A hook entry as it appears in the client's own configuration. */ export interface ProjectedHookCommand { type?: unknown; command: string; timeout?: number; [field: string]: unknown; } export interface ProjectedHookGroup { hooks: ProjectedHookCommand[]; [field: string]: unknown; } export interface ProjectedHookSettings { hooks: Record; } /** The captured native envelope of one entry, and everything needed to re-emit it. */ export interface NativeHookEntry { event: string; command: string; timeout?: number; /** The group's own fields, minus `hooks`. */ nativeGroup?: Record; /** The hook object's own fields, minus `command` and `timeout`. */ nativeHook?: Record; } /** One native group carrying one hook — the shape every projected entry takes. */ export declare function projectedHookGroup(entry: NativeHookEntry): ProjectedHookGroup; /** * The ownership key for one native entry. It covers EVERY field * {@link projectedHookGroup} emits, because the projection replaces the whole * `hooks` key: a field left out of this key is a field the destination can * hold, AIH can then call already-known, and the replace can drop — a silent * rewrite of a hook AIH never emitted. * * `stableJson` sorts with `localeCompare`, which returns 0 for some distinct * strings, so this key is NOT guaranteed insensitive to the order a writer used: * two objects carrying the same fields in different orders can serialize * differently and read as two different entries. That direction is fail-closed — * the entry is reported unowned rather than silently rewritten — so it is a * false-drift risk, never a deletion risk. */ export declare function nativeHookEntryKey(entry: NativeHookEntry): string; /** * Validate a registration set at the boundary, and prove every third-party * launcher still matches its pin. A launcher whose hash moved is DRIFT — it is * refused here rather than projected. The checks are the grammar's own * `hookRegistrationSetIssues` — one copy, shared with `governance.hookRegistrations`. */ export declare function assertHookRegistrations(registrations: readonly HookRegistration[]): ResolvedHookRegistration[]; /** The native entry one registration projects into. */ export declare function registrationNativeEntry(registration: ResolvedHookRegistration): NativeHookEntry; export declare function registrationKey(registration: ResolvedHookRegistration): string; /** * Occurrence counts, not a membership set. N identical entries on disk against * one owned entry means N-1 of them are unowned: a plain set matched them all * against the same key and let the whole-key replace delete the rest unreported. */ export declare function occurrenceCounts(keys: readonly string[]): Map; /** * The most copies of each entry either source vouches for — never their sum. * The receipt records what the last projection wrote and the selection records * what this one will write, so the same entry is normally in BOTH: adding them * would silently vouch for a second copy nobody owns. */ export declare function mergedMaxCounts(left: Map, right: Map): Map; /** Consume one occurrence of `key`, or report that none is left to claim. */ export declare function claimOccurrence(counts: Map, key: string): boolean; /** True only for a timeout the registration grammar will accept as its own field. */ export declare function isAuthorableTimeout(value: unknown): boolean; /** * Groups that yield no entry, kept per event exactly as found. A group object is * CONTENT — it carries `matcher`, `id`, `description`, the very fields this * projector exists to preserve — so producing no entry must not mean the * whole-key replace deletes it. The projection carries these through unchanged * and the receipt records them, so revocation puts them back rather than * subtracting them along with what AIH owned. An event whose group list is empty * is carried the same way, so the event key itself survives. */ export declare function entrylessGroups(destination: unknown): Record; /** * The `hooks` value a set of owned entries plus carried-through content makes. * ONE composer, used by the projection and by the receipt's expectation, so the * bytes AIH writes and the bytes it later proves it owns cannot diverge. */ export declare function composeProjectedHooks(entries: readonly NativeHookEntry[], carried?: Record): Record; /** * Refuse a `__proto__` member ANYWHERE in the destination text, whatever its * value type. The parse result cannot answer this: a member with an object or * null value poisons the prototype, and one with a string or number value is * discarded outright — no own property, no prototype change, nothing left to * observe. So this reads the SOURCE, walking the JSONC parse tree, where the * property name survives regardless of what it was set to. A `__proto__` * appearing inside a string VALUE is not a member and does not trip it. * * Without this, an operator's `"__proto__": "content"` is destroyed by the * whole-key replace with no refusal, no unowned report, and a verdict of active. */ export declare function assertNoProtoMember(text: string, where: string): void; /** * True when the destination TEXT carries a JSONC comment INSIDE the `hooks` key. * * Read from the SOURCE, the same way {@link assertNoProtoMember} is: a comment * is not a value, so nothing in the parse result remembers it. The writer edits * only the keys it changes, so a comment elsewhere in the file survives a write * untouched and refusing over it would withhold a subtraction for content that * was never at risk. `hooks` is the exception: it is replaced whole, so a * comment within its span cannot survive. Callers refuse that case rather than * silently strip it — the governing ruling on this destination is that refusing * beats stripping. * * Offsets, not a re-parse: a comment has no node of its own, so the only way to * place one relative to a key is to compare its offset against that key's span. */ export declare function carriesJsoncComments(text: string): boolean; /** * Top-level names this text duplicates, when it ALSO carries a comment anywhere * — otherwise empty. * * A duplicated name forces the writer off its in-place path onto the whole-file * render, and that render drops every comment in the file, not just the ones * inside the owned span. So the narrower guard above stops being sufficient * exactly here, and the write has to refuse on the same ruling rather than * quietly strip comments the span check just declared safe. Without a comment * to lose there is nothing to refuse: collapsing a duplicated key is the * pre-existing normalization of a file no reader agrees on. */ export declare function shadowedCommentKeys(text: string): string[]; /** * Parse the destination the one safe way: the shared JSONC parse, plus the * source-level `__proto__` refusal the parse result cannot express. */ export declare function parseDestinationSettings(text: string): unknown; /** * Flatten a native hook configuration into entries. * * It refuses ONLY structure that cannot be interpreted at all. Content that is * merely RICHER than AIH can author — a group scoped by `matcher`, a hook * carrying `async` — is captured and surfaced as an entry, so the unowned check * names it and adoption can capture it. Making that fatal here pre-empted the * machinery the contract already gives for unowned content (A3: refusal beats * absorption, with adoption as the way out) and left the projector with no * capability at all on real client configurations. * * A group with no hooks, and an event with no groups, hold no entry: nothing can * be silently deleted and nothing needs re-emitting, so neither refuses. */ export declare function destinationHookEntries(destination: unknown): NativeHookEntry[]; /** How many destination-read entries may reach one message or report field. */ export declare const MAX_REPORTED_HOOK_ENTRIES = 100; /** * Make one destination-read string safe to PRINT. A policy-authored launcher is * bounded and control-character-free by the grammar; a string read from the * destination is bounded only by file size, and it reaches the operator's * terminal, the `--json` envelope and the governance digest — where control * characters repaint the screen, a CR/LF forges a digest row, and a megabyte of * launcher makes one error message unreadable. * * Display only. Every hash, comparison, ownership key and captured launcher * keeps the ORIGINAL bytes: ownership turns on exactness, so neutralizing what * is shown must never touch what is compared. */ export declare function displayableDestinationText(value: string): string; /** * Bound a reported LIST as well as each string in it. Per-string bounding alone * still let a legal destination produce a multi-million character message and a * multi-megabyte JSON payload, because the count was unbounded. */ export declare function boundedReportedEntries(entries: readonly T[]): { shown: T[]; omitted: number; };