import type { AnnotationStatus } from "./annotationProvenance";
export type ConversationArcEventType = "suggestion-shown" | "suggestion-chosen" | "audience-set" | "chart-rendered" | "chart-edited" | "chart-replaced" | "chart-exported" | "chart-abandoned" | "interrogation-asked" | "interrogation-answered" | "nav-node-focused" | "nav-branch-expanded" | "annotation-status-changed";
interface ConversationArcEventBase {
    /** Discriminator for the event variant. */
    type: ConversationArcEventType;
    /** `Date.now()` at the moment the event was recorded. Stamped by the store. */
    timestamp: number;
    /** Stable ID for the enabled session — survives until `disableConversationArc()` or `reset()`. */
    sessionId: string;
    /** Optional opaque correlation key that threads a single arc together. */
    arcId?: string;
    /** Free-form bag for context the talk-track doesn't need a typed slot for. */
    meta?: Record<string, unknown>;
}
export interface SuggestionShownEvent extends ConversationArcEventBase {
    type: "suggestion-shown";
    /**
     * Intent label fed into `suggestCharts` (e.g. "trend", "distribution").
     * Accepts a single intent or an array — mirrors `SuggestChartsOptions.intent`.
     */
    intent?: string | ReadonlyArray<string>;
    /** Ranked component names in the order the suggester returned them. */
    components: string[];
    /** Top suggestion's composite score, if known. */
    topScore?: number;
    /** Audience target active when the suggestion ran. */
    audience?: string;
}
export interface SuggestionChosenEvent extends ConversationArcEventBase {
    type: "suggestion-chosen";
    component: string;
    /** 1-based rank in the matching `suggestion-shown` event, if known. */
    rank?: number;
    /** Who picked the suggestion: a human, an agent loop, or a default-fall-through. */
    source?: "user" | "agent" | "auto";
}
export interface AudienceSetEvent extends ConversationArcEventBase {
    type: "audience-set";
    audience: string;
    previous?: string;
}
export interface ChartRenderedEvent extends ConversationArcEventBase {
    type: "chart-rendered";
    component: string;
    chartId?: string;
}
export interface ChartEditedEvent extends ConversationArcEventBase {
    type: "chart-edited";
    component: string;
    chartId?: string;
    /** Names of props that changed in this edit. */
    changedProps?: string[];
}
export interface ChartReplacedEvent extends ConversationArcEventBase {
    type: "chart-replaced";
    from: string;
    to: string;
    /** Why the swap happened — `"repair"`, `"variant"`, `"user-rejected"`, etc. */
    reason?: string;
}
export interface ChartExportedEvent extends ConversationArcEventBase {
    type: "chart-exported";
    component: string;
    /** What was exported: `"jsx"`, `"svg"`, `"png"`, `"json"`, `"url"`, etc. */
    format: string;
}
export interface ChartAbandonedEvent extends ConversationArcEventBase {
    type: "chart-abandoned";
    component?: string;
    reason?: string;
}
export interface InterrogationAskedEvent extends ConversationArcEventBase {
    type: "interrogation-asked";
    /** Chart the question was directed at, if known. */
    component?: string;
    /**
     * Question text. The `useChartInterrogation` instrumentation
     * truncates to ~500 chars before recording so the ring buffer
     * stays bounded; callers stamping their own events should do the
     * same.
     */
    query: string;
    /** Optional payload size hint (e.g. summary token count) for diagnostics. */
    contextSize?: number;
}
export interface InterrogationAnsweredEvent extends ConversationArcEventBase {
    type: "interrogation-answered";
    /** Chart the answer was directed at, if known. */
    component?: string;
    /**
     * Answer text. The `useChartInterrogation` instrumentation
     * truncates to ~2000 chars before recording so multi-kilobyte LLM
     * responses don't bloat the ring buffer. Callers stamping their
     * own events should follow the same convention.
     */
    answer?: string;
    /** Number of annotations the response attached, if known. */
    annotationCount?: number;
    /**
     * Round-trip latency in ms from ask to answer, clamped to ≥ 0.
     * The instrumentation measures via `performance.now()` when
     * available; the `Date.now()` fallback can produce negative
     * deltas under clock changes, hence the clamp.
     */
    latencyMs?: number;
    /** Set when the response was an error rather than a successful answer. */
    error?: boolean;
}
/**
 * A reader focused a node in an `AccessibleNavTree` (keyboard or click). The
 * first *reception*-side behavioral signal in the arc — which structural nodes
 * a non-visual (or AI) reader actually visits, the dependent measure visualization-
 * literacy studies usually lack. Emitted only on genuine tree interaction, not
 * when the active node is driven externally (canvas → tree sync).
 */
export interface NavNodeFocusedEvent extends ConversationArcEventBase {
    type: "nav-node-focused";
    /** `chartId` of the chart the tree describes, when correlated. */
    chartId?: string;
    /** Tree node id that gained focus. */
    nodeId: string;
    /** Node role: `"chart" | "axis" | "series" | "datum"`. */
    role: string;
    /** 1-based depth (aria-level). */
    level: number;
    /** The node's announced label (the emitter truncates to ~200 chars). */
    label?: string;
}
/** A reader expanded or collapsed a branch in an `AccessibleNavTree`. */
export interface NavBranchExpandedEvent extends ConversationArcEventBase {
    type: "nav-branch-expanded";
    /** `chartId` of the chart the tree describes, when correlated. */
    chartId?: string;
    /** Tree node id that was toggled. */
    nodeId: string;
    /** Node role of the toggled branch. */
    role: string;
    /** 1-based depth (aria-level). */
    level: number;
    /** `true` on expand, `false` on collapse. */
    expanded: boolean;
}
/**
 * An annotation's editorial status transitioned (M7). The accept / dispute /
 * retract / propose flow is what turns an annotation into the durable,
 * observable node of the conversation arc (IDID §13.4): the note is the unit
 * the arc is *about*, not chart chrome.
 *
 * `fromStatus`/`toStatus` are deliberately not named `from`/`to` — `summarizeArc`
 * reads `from`/`to` as chart-component names (the `chart-replaced` shape), so a
 * status value there would pollute `componentsSeen`.
 */
export interface AnnotationStatusChangedEvent extends ConversationArcEventBase {
    type: "annotation-status-changed";
    /** `provenance.stableId` of the annotation whose status changed, when known. */
    annotationId?: string;
    /** Previous editorial status, if known. */
    fromStatus?: AnnotationStatus;
    /** New editorial status. */
    toStatus: AnnotationStatus;
    /** `chartId` of the chart carrying the annotation, when correlated. */
    chartId?: string;
}
export type ConversationArcEvent = SuggestionShownEvent | SuggestionChosenEvent | AudienceSetEvent | ChartRenderedEvent | ChartEditedEvent | ChartReplacedEvent | ChartExportedEvent | ChartAbandonedEvent | InterrogationAskedEvent | InterrogationAnsweredEvent | NavNodeFocusedEvent | NavBranchExpandedEvent | AnnotationStatusChangedEvent;
/**
 * Input shape accepted by `record()`: the event variant without the
 * stamped fields (`timestamp` and `sessionId`). Callers may still
 * provide them to backfill historical events.
 *
 * Implemented as a distributive conditional so each member of the
 * discriminated union keeps its variant-specific payload (e.g.
 * `SuggestionShownEvent.components`). A non-distributive
 * `Omit<ConversationArcEvent, ...>` collapses to the union's common
 * fields and rejects every variant-specific key.
 */
export type ConversationArcEventInput = ConversationArcEvent extends infer E ? E extends ConversationArcEvent ? Omit<E, "timestamp" | "sessionId"> & Partial<Pick<E, "timestamp" | "sessionId">> : never : never;
export type ConversationArcListener = (event: ConversationArcEvent) => void;
export interface ConversationArcSink {
    /**
     * Persist a newly-recorded event. Called only after the store is enabled and
     * the event is accepted into the ring buffer.
     */
    record?(event: ConversationArcEvent): void | Promise<void>;
    /**
     * Optional hook for consumers that treat `flush()` as an export boundary.
     * The in-memory buffer is still cleared by the store after this call.
     */
    flush?(events: ReadonlyArray<ConversationArcEvent>): void | Promise<void>;
    /** Clear durable state owned by this sink. */
    clear?(): void | Promise<void>;
    /** Load previously persisted events for replay / hydration. */
    load?(): ReadonlyArray<ConversationArcEvent> | Promise<ReadonlyArray<ConversationArcEvent>>;
}
export interface ConversationArcStorageLike {
    getItem(key: string): string | null;
    setItem(key: string, value: string): void;
    removeItem(key: string): void;
}
export interface LocalStorageConversationArcSinkOptions {
    /** Storage key. Defaults to `semiotic:conversation-arc`. */
    key?: string;
    /** Test hook or alternate Storage implementation. Defaults to `window.localStorage`. */
    storage?: ConversationArcStorageLike;
    /** Maximum events retained in storage. Defaults to 1000. */
    maxEvents?: number;
}
export interface IndexedDBConversationArcSinkOptions {
    /** Database name. Defaults to `semiotic-conversation-arc`. */
    dbName?: string;
    /** Object store name. Defaults to `events`. */
    storeName?: string;
    /** Test hook or alternate IndexedDB factory. Defaults to `globalThis.indexedDB`. */
    indexedDB?: IDBFactory;
    /** Maximum events retained in the object store. Defaults to 1000. */
    maxEvents?: number;
}
export type ConversationArcWebhookFetch = (input: string, init?: RequestInit) => Promise<unknown>;
export interface WebhookConversationArcSinkOptions {
    url: string;
    method?: "POST" | "PUT";
    headers?: Record<string, string>;
    fetch?: ConversationArcWebhookFetch;
    mapEvent?: (event: ConversationArcEvent) => unknown;
}
export interface LoadConversationArcOptions {
    /** Capacity of the hydrated ring buffer. Defaults to max(existing, events.length, 1000). */
    capacity?: number;
    /** Active session id for future recordings. Replayed event session ids are preserved. */
    sessionId?: string;
    /**
     * Whether the store should accept new events after hydration. Defaults to false
     * so replaying an artifact cannot accidentally start telemetry.
     */
    enabled?: boolean;
    /** Append to the current buffer instead of replacing it. Defaults to false. */
    append?: boolean;
}
export interface ConversationArcStore {
    readonly enabled: boolean;
    readonly sessionId: string | null;
    readonly capacity: number;
    /**
     * Records an event. Returns the stamped event on success, or `null`
     * if the store is disabled. Stamps `timestamp` and `sessionId` if
     * the caller didn't.
     */
    record(input: ConversationArcEventInput): ConversationArcEvent | null;
    /** Returns the current buffer (newest last) and clears it. */
    flush(): ConversationArcEvent[];
    /**
     * Returns a frozen, referentially-stable snapshot of the current
     * buffer. Stable across consecutive calls until the next mutation,
     * so it can drive `useSyncExternalStore`. Read-only — callers that
     * need a mutable copy should `.slice()` the result.
     */
    getEvents(): ReadonlyArray<ConversationArcEvent>;
    /**
     * Subscribe to new events. Returns an unsubscribe function.
     *
     * Subscriptions persist across enable/disable transitions — a
     * subscriber registered before `enableConversationArc()` still
     * receives events once recording starts. Cleared by `reset()`.
     */
    subscribe(listener: ConversationArcListener): () => void;
    /** Empties the buffer without disabling the store. */
    clear(): void;
    /** Disables the store and drops the buffer + subscribers. */
    reset(): void;
}
export interface EnableConversationArcOptions {
    /** Maximum events retained in the ring buffer. Defaults to 1000. */
    capacity?: number;
    /** Override the generated session ID. Useful for cross-tab correlation. */
    sessionId?: string;
}
/**
 * Subscribe to *any* state mutation in the conversation-arc store —
 * including `clear`, `flush`, `reset`, and `enable`, in addition to
 * recorded events. Intended for React hooks that need to re-render
 * on snapshot changes; event sinks should use `subscribe()` instead.
 *
 * Returns an unsubscribe callback.
 */
export declare function subscribeToConversationArcChange(listener: () => void): () => void;
/**
 * Register an opt-in persistence sink. Sinks receive only accepted events:
 * disabled telemetry remains a no-op and replay hydration does not re-emit
 * historical events into durable stores.
 */
export declare function registerConversationArcSink(sink: ConversationArcSink): () => void;
/**
 * Browser-local durable sink backed by `localStorage`. It appends each accepted
 * event to a JSON array and exposes `load()` so a session can be replayed later.
 * When `localStorage` is unavailable (SSR, private browser failures), operations
 * degrade to no-ops and `load()` returns `[]`.
 */
export declare function createLocalStorageConversationArcSink(options?: LocalStorageConversationArcSinkOptions): ConversationArcSink & {
    load(): ConversationArcEvent[];
};
/**
 * Browser durable sink backed by IndexedDB. Writes are asynchronous and
 * fire-and-forget from the recorder; callers that need replay can await
 * `sink.load()`.
 */
export declare function createIndexedDBConversationArcSink(options?: IndexedDBConversationArcSinkOptions): ConversationArcSink & {
    load(): Promise<ConversationArcEvent[]>;
};
/**
 * Minimal webhook sink for app-owned analytics ingestion. The sink posts one
 * JSON payload per accepted event; retry, batching, authentication refresh, and
 * sampling policy remain application concerns.
 */
export declare function createWebhookConversationArcSink(options: WebhookConversationArcSinkOptions): ConversationArcSink;
/**
 * Hydrate the in-memory store from a saved arc without replaying side effects.
 * The loaded events become visible through `getEvents()` / `useConversationArc`,
 * but listeners and sinks are not called. Pass `{ enabled: true }` when you want
 * to resume recording after loading; the default is replay-only.
 */
export declare function loadConversationArc(events: ReadonlyArray<ConversationArcEvent>, options?: LoadConversationArcOptions): ReadonlyArray<ConversationArcEvent>;
/** Alias with the word replay for fixture-driven callers. */
export declare function replayConversationArc(events: ReadonlyArray<ConversationArcEvent>, options?: LoadConversationArcOptions): ReadonlyArray<ConversationArcEvent>;
/**
 * Record an audience-set event. Convenience wrapper around `record`
 * for the common pattern: "the user picked a new audience profile
 * and I want to put that in the arc."
 *
 * `previous` is optional but recommended — it lets downstream
 * analytics see the transition rather than just the new state. Pass
 * `null` when there was no prior audience.
 *
 * Returns the stamped event, or `null` if recording is disabled.
 *
 * ```ts
 * import { recordAudienceChange } from "semiotic/ai"
 *
 * function AudiencePicker({ value, onChange }) {
 *   return (
 *     <select value={value} onChange={(e) => {
 *       const next = e.target.value
 *       recordAudienceChange(next, value)
 *       onChange(next)
 *     }} />
 *   )
 * }
 * ```
 */
export declare function recordAudienceChange(audience: string, previous?: string | null, extra?: {
    arcId?: string;
    meta?: Record<string, unknown>;
}): ConversationArcEvent | null;
/**
 * Sugar for an `annotation-status-changed` event (M7). Call it from the
 * accept / dispute / retract / propose UI so the arc records how an
 * annotation's editorial standing moved — the durable, observable node of the
 * conversation. No-op (returns null) until `enableConversationArc()`.
 */
export declare function recordAnnotationStatusChange(toStatus: AnnotationStatus, opts?: {
    annotationId?: string;
    fromStatus?: AnnotationStatus;
    chartId?: string;
    arcId?: string;
    meta?: Record<string, unknown>;
}): ConversationArcEvent | null;
/**
 * Opt in to conversation-arc telemetry. Safe to call multiple times —
 * subsequent calls reuse the existing session unless `sessionId` is
 * explicitly provided.
 */
export declare function enableConversationArc(options?: EnableConversationArcOptions): ConversationArcStore;
/** Turn the store off without dropping the buffered events. */
export declare function disableConversationArc(): void;
/**
 * Always returns a store façade. Methods are safe to call when
 * disabled — `record()` returns null, `getEvents()`/`flush()` return
 * empty arrays, `subscribe()` returns a no-op unsubscriber.
 */
export declare function getConversationArcStore(): ConversationArcStore;
export {};
