/**
 * LLM-driven document section classifier via Claude Haiku.
 *
 * Given the full text of an unstructured document and the loaded ontology
 * label set, returns a typed-section structure that `memory-ingest`
 * consumes to write typed graph nodes with natural anchor edges.
 *
 * Trust boundary: the document text comes from an external file the
 * operator uploaded. The system prompt sandboxes it as classification
 * input — any imperative verbs inside it are data, not instructions.
 * Mirrors the pattern in llm-ranker.ts.
 *
 * Auth: runs on Claude Code OAuth via `callOauthLlm`, never
 * the Anthropic API key. The API-key path is reserved for the public
 * agent.
 *
 * Hallucination defence: section `kind` is enforced as a closed enum in
 * `ClassifierOutputTool.input_schema`, so a hallucinated kind cannot reach
 * this module — Anthropic rejects the tool_use input at the API boundary.
 * For `related[].kind` and `documentEdges[].targetKind` (which the static
 * schema cannot constrain because the live ontology label set is a runtime
 * input), the post-validator drops any label not in `ontologyLabels`.
 * Failure of the LLM call (missing creds, network, schema-violating
 * `tool_use.input`) returns `{kind: "error", reason}`. The caller decides
 * whether to abort the ingest or degrade-on-error per session; classifier
 * never silently substitutes a degraded write.
 */
import { type OauthLlmTool } from "../../../../../lib/oauth-llm/dist/index.js";
/** Direction of the anchor edge relative to the typed node. */
export type AnchorEdgeDirection = "from-anchor" | "to-anchor";
/** Direction of a related-node edge relative to the typed node. */
export type RelatedEdgeDirection = "outgoing" | "incoming";
/** A related entity the typed node connects to (e.g. Position → Organization). */
export interface ClassifiedRelated {
    /** Ontology label — verified against the loaded label set. */
    kind: string;
    /** Properties on the related node. */
    properties: Record<string, unknown>;
    /** Edge from the typed node to the related node. */
    edge: {
        type: string;
        direction: RelatedEdgeDirection;
        properties?: Record<string, unknown>;
    };
    /**
     * When true, MERGE the related node on its identifying property
     * (e.g. Organization on `name`); when false, CREATE.
     * Defaults to true — entity reuse is the safer default.
     */
    merge?: boolean;
}
/** A single classified section of the document (every section is one `:Section` node). */
export interface ClassifiedSection {
    /**
     * Section kind from the closed enumeration declared in schema-base.md.
     * Becomes a secondary label on the `:Section` node (e.g. `:Section:Position`).
     *
     * Identity kinds (anchor edges to UserProfile/LocalBusiness):
     *   Position, Education, Credential, Skill, Biography
     * Document-structural kinds (HAS_SECTION + NEXT only):
     *   Preface, Abstract, Introduction, TableOfContents, Chapter,
     *   Conclusion, Appendix, Bibliography, Glossary, Acknowledgments
     * Contract-clause kinds (HAS_SECTION + NEXT, with special-case extras for Parties + Definitions):
     *   Parties, Recitals, Definitions, Scope, Term, Payment, Confidentiality,
     *   IntellectualProperty, Warranties, Indemnification, Liability,
     *   Termination, GoverningLaw, ForceMajeure, Notices, EntireAgreement,
     *   Amendment, Assignment, Severability, Signatures
     * Label fallback:
     *   Other (carries a `classifierReason` property naming what the classifier
     *   thought the section was about; ontology-growth signal)
     *
     * Standalone non-section node kind that may appear here:
     *   Project (written as a separate `:Project` node, anchored via CREATED)
     */
    kind: string;
    /** Short human-readable title for the section. */
    title: string;
    /**
     * The section's body text — embedded and stored on the section node.
     *
     * server-reconstructed via `documentText.slice(sourceStart, sourceEnd)`.
     * The LLM emits offsets, never the body text — output size becomes O(sections),
     * not O(input chars). Callers consume the same `body: string` shape as before.
     */
    body: string;
    /**
     * 1-3 sentence summary of the section, ≤500 chars (server-validated).
     * The LLM emits this; the server truncates if oversize. Stored as
     * `properties.summary` on the section node so adjacency search can
     * surface it without rehydrating the body.
     */
    summary: string;
    /**
     * Whole-document character offsets — inclusive start, exclusive end.
     * The LLM emits these; the server validates bounds and reconstructs
     * `body` via `documentText.slice(sourceStart, sourceEnd)`. In the
     * chunked-classify path these are translated from chunk-local to
     * whole-document coordinates so the merge step can detect boundary
     * straddlers across chunks.
     */
    sourceStart: number;
    sourceEnd: number;
    /** Properties on the section node (excluding accountId/embedding/provenance). */
    properties: Record<string, unknown>;
    /**
     * Edge from the document subject (anchor) to / from the section node.
     * Null for structural and contract-clause kinds — they anchor only via
     * the implicit `(:KnowledgeDocument)-[:HAS_SECTION]->(:Section)` + `:NEXT` chain.
     */
    anchorEdge: {
        type: string;
        direction: AnchorEdgeDirection;
        properties?: Record<string, unknown>;
    } | null;
    /** Other entities this section references (e.g. Position → Organization via AT). */
    related?: ClassifiedRelated[];
    /**
     * For `kind === "Other"`: one-line description of what the classifier thought
     * the section was about. Stamped on the `:Section:Other` node as the
     * `classifierReason` property so the ontology-growth review query
     * `MATCH (s:Section:Other) RETURN s.title, s.classifierReason, count(*)`
     * can surface candidates for new section-kind labels.
     */
    classifierReason?: string;
}
/**
 * A node the classifier emitted but could not edge naturally.
 * The skill surfaces these to the operator loudly so they decide whether
 * the orphan is intentional (rare, valid) or a classifier miss (bug).
 * Writer never synthesises its own edges to "fix" orphans.
 */
export interface OrphanCandidate {
    /** Ontology label of the would-be node. */
    kind: string;
    /** Short human-readable label naming the entity (e.g. organisation name). */
    label: string;
    /** Why the classifier could not find a natural edge for this node. */
    reason: string;
}
/** The classifier's full output. */
export interface ClassifierOutput {
    /** 1–3 sentence summary of the whole document. */
    documentSummary: string;
    /** Topic keywords for the document (for retrieval and filing). */
    documentKeywords: string[];
    /** Per-section structure. */
    sections: ClassifiedSection[];
    /** Nodes the classifier could not edge naturally — operator decides per case. */
    orphanCandidates: OrphanCandidate[];
    /** Document-level edges the classifier proposes off the KnowledgeDocument
     * (e.g. PARTY edges to Person/Organization for a contract). The writer
     * applies these; never invents its own. */
    documentEdges?: Array<{
        type: string;
        direction: "outgoing" | "incoming";
        targetKind: string;
        targetProperties: Record<string, unknown>;
        /** Default true — MERGE Person/Organization on identifying property. */
        merge?: boolean;
    }>;
    /** Number of related-node `kind` values dropped as hallucinations (diagnostic). */
    hallucinatedRelated: number;
}
export type ClassifyResult = {
    kind: "ok";
    output: ClassifierOutput;
} | {
    kind: "error";
    reason: string;
};
/**
 * Closed enumeration of section `kind` values. Each becomes a secondary
 * label on the `:Section` node (e.g. `:Section:Position`). The `kind` field
 * is enforced as an `enum` in `ClassifierOutputTool.input_schema`, so a
 * hallucinated kind cannot reach the post-validator — Anthropic rejects the
 * tool_use input at the API boundary.
 *
 * Source of truth: schema-base.md "Section kinds" table. Changes here MUST
 * be mirrored there or the validator will reject the secondary label.
 */
export declare const SECTION_KIND_OTHER = "Other";
export declare const IDENTITY_SECTION_KINDS: readonly ["Position", "Education", "Credential", "Skill", "Biography"];
export declare const STRUCTURAL_SECTION_KINDS: readonly ["Preface", "Abstract", "Introduction", "TableOfContents", "Chapter", "Conclusion", "Appendix", "Bibliography", "Glossary", "Acknowledgments", "Conversation"];
export declare const CONTRACT_SECTION_KINDS: readonly ["Parties", "Recitals", "Definitions", "Scope", "Term", "Payment", "Confidentiality", "IntellectualProperty", "Warranties", "Indemnification", "Liability", "Termination", "GoverningLaw", "ForceMajeure", "Notices", "EntireAgreement", "Amendment", "Assignment", "Severability", "Signatures"];
/** Standalone (non-Section) node kind the classifier may emit per section. */
export declare const STANDALONE_NODE_KINDS: readonly ["Project"];
export declare const ALL_SECTION_KINDS: ReadonlySet<string>;
export declare const ClassifierOutputTool: OauthLlmTool;
export interface ClassifyParams {
    /** Account scope, for log prefixing. */
    accountId: string;
    /**
     * Classification mode.
     *   'document' (default) — unstructured document → typed `:Section` chunks.
     *   'chat'              — one chat session → `:Section` chunks under a
     *                         :ConversationArchive parent (Task 397) keyed on
     *                         `conversationIdentity`. Bypasses anchor /
     *                         related / documentEdges / orphan logic; the only
     *                         legal `kind` is `Conversation`.
     * Defaults to 'document' so existing PDF/CV/web callers are unchanged.
     */
    mode?: "document" | "chat";
    /**
     * Anchor description — a short human sentence the classifier reads to
     * decide which ontology edges fit. Example:
     *   "subject = UserProfile (the account owner); edges from UserProfile."
     *   "subject = LocalBusiness (the operator's business); edges from LocalBusiness."
     * The skill writer composes this from the operator's confirmation step.
     * In chat mode this is included for log context only — the chat prompt
     * has no anchor logic.
     */
    anchorDescription: string;
    /**
     * Ontology label set the graph supports — only these are legal `kind`
     * values. Live ∪ declared ∪ markdown labels passed in by the caller
     * so the classifier never needs filesystem access. In chat mode this is
     * effectively unused (only `Conversation` is legal) but accepted for
     * symmetry with document mode.
     */
    ontologyLabels: ReadonlySet<string>;
    /**
     * Natural-edge map — pasted into the prompt verbatim. The skill renders
     * this from the schema-base.md document-ingestion typed-node table so
     * the classifier sees the exact edges the validator accepts. In chat
     * mode this is omitted from the user message (the chat prompt has no
     * edge proposals).
     */
    naturalEdgeMap: string;
    /** Full text of the document, or one chat session in turn-attributed form. */
    documentText: string;
}
/**
 * Classify a document into typed sections via Haiku.
 *
 * Returns:
 *   { kind: "ok", output } on success — every section's `kind` is in the
 *     closed enumeration (identity / structural / contract-clause / Other).
 *     Sections the classifier could not natural-edge appear in
 *     `output.orphanCandidates`. The skill surfaces orphans loudly to
 *     the operator.
 *   { kind: "error", reason } when the LLM is unavailable, returns
 *     malformed JSON, or hits an input-too-large guard. The caller
 *     decides whether to abort the ingest entirely (document mode) or
 *     degrade-on-error per session (chat mode). Classifier
 *     never silently substitutes a degraded write.
 */
export declare function classifyDocument(params: ClassifyParams): Promise<ClassifyResult>;
//# sourceMappingURL=llm-classifier.d.ts.map