import { EntityOverlayMap, EntityTypeToken, EntityOverlayEntry, ErrorSink, AssociationsRpcError, AssociationsDataSource, AssociationsIdentity, AssociationTargetType, AssociationsRpcResult, AssociationEdge, AssociationTargetEdge, AssociationSourceEdge, CategoryDimension, PlatformCategory, UserStateKind, UserEntityState, CommentAnchor, PlatformComment, AssociationsEntry, CategoriesEntry, CommentsEntry, AssociationsConfig } from '../contracts.js'; import 'react'; import '@ai-matrx/data/db'; /** The universal ownership column post-2026-reorg. */ declare const DEFAULT_OWNER_COLUMN = "created_by"; /** The universal org-scoping column. */ declare const DEFAULT_ORG_COLUMN = "organization_id"; type ContentRole = "utility" | "source" | "destination" | "hybrid" | "container"; /** Runtime guard for the DB's free-text `content_role` column. */ declare function isContentRole(value: string | null): value is ContentRole; /** * Normalize a raw `kind`/`type` string onto its canonical entity token. * Returns the input unchanged when it is already canonical or unknown — the * caller's existing "no registry entry → plain text" path still applies. */ declare function resolveEntityToken(raw: string): string; /** * Fully-resolved entity descriptor — generated metadata + host overlay, with * safe fallbacks so an un-overlaid token still renders (derived plural, no * door, `Icon: null` → the consumer's default) even if it can't be queried * for candidates yet. */ interface EntityInfo { token: EntityTypeToken; label: string; labelPlural: string; /** Postgres schema of the backing table (from the generated registry). */ schema: string; /** Backing table name (from the generated registry). */ table: string; /** Title column for the picker, or null when none is registered. */ titleColumn: string | null; /** Ownership column to scope candidate reads to the current user. */ ownerColumn: string; /** Org-scoping column. */ orgColumn: string; /** Host-supplied icon component, or null (consumer renders its default). */ Icon: EntityOverlayEntry["Icon"] | null; hrefFor: ((id: string) => string) | null; scopeable: boolean; category: string | null; /** Knowledge-model grouping bucket (resource surfaces group by this). */ contentRole: ContentRole; /** Candidate-source override (rag-backed tokens etc.); null = generic read. */ listCandidates: EntityOverlayEntry["listCandidates"] | null; /** True when a picker can list real candidates (title column OR override). */ canListCandidates: boolean; } interface EntityRegistry { /** Merge more overlay entries in (host binding, W4 pickers, tests). */ registerEntityOverlay(overlay: EntityOverlayMap): void; /** Resolve a KNOWN token to its full descriptor. */ getEntityInfo(token: EntityTypeToken): EntityInfo; /** Safe variant for raw strings (e.g. an edge's `otherType`). */ tryGetEntityInfo(token: string): EntityInfo | null; /** Resolve from a live `(schema, table)` pair, or null. */ tryGetEntityInfoByTable(schema: string, table: string): EntityInfo | null; /** Resolve a raw table name only when it maps to exactly one entity. */ tryGetEntityInfoByUniqueTableName(table: string): EntityInfo | null; /** * Tokens the DB classifies as knowledge resources via a valid * `content_role` AND that can list candidates. The default set for card * grids, resource sections, and attach pickers. */ curatedTokens(): EntityTypeToken[]; /** * Tokens offered as reference "Allowed types" — DB-driven via * `platform.entity_types.reference_pickable`. A pickable token with no * title column and no candidate override is a config defect: excluded and * screamed to the errorSink, never silently shown broken. */ listableTokens(): EntityTypeToken[]; } /** Build the merge engine over the host overlay (empty overlay is legal). */ declare function createEntityRegistry(errorSink: ErrorSink, initialOverlay?: EntityOverlayMap): EntityRegistry; /** Return the first failing check, or null if all pass. Pure. */ declare function firstError(...checks: (AssociationsRpcError | null)[]): AssociationsRpcError | null; interface AssociationGuards { /** A single id must be a real UUID. Kills the "cute text id" bug class. */ checkUuid(field: string, value: unknown): AssociationsRpcError | null; /** Every id in an array must be a real UUID. */ checkUuidArray(field: string, values: readonly unknown[]): AssociationsRpcError | null; /** A type token must be registered in `platform.entity_types`. */ checkToken(field: string, value: unknown): AssociationsRpcError | null; /** * Map a legacy/phantom token to its registered canonical token. * Pass-through for anything not in the alias table. Loud by doctrine — the * alias firing is a bug at the callsite, recovered here, never silent. */ normalizeEntityToken(value: string): string; } /** Bind the guard set to the host's errorSink (the loud seam). */ declare function createAssociationGuards(errorSink: ErrorSink): AssociationGuards; declare function err(code: AssociationsRpcError["code"], message: string, detail?: unknown, hint?: string): { ok: false; error: AssociationsRpcError; }; declare function ok(data: T): { ok: true; data: T; }; type PgErrorMapper = (e: unknown) => AssociationsRpcError; type PgErrorPairMapper = (e: unknown) => [AssociationsRpcError["code"], string, unknown, string | undefined]; interface RpcResultHelpers { ok: typeof ok; err: typeof err; mapPgError: PgErrorMapper; mapPgErrorPair: PgErrorPairMapper; } declare function createRpcResultHelpers(errorSink: ErrorSink, sessionAttached?: () => boolean): RpcResultHelpers; interface CoreDeps { dataSource: AssociationsDataSource; identity: AssociationsIdentity; errorSink: ErrorSink; guards: AssociationGuards; rpc: RpcResultHelpers; registry: EntityRegistry; } interface ConversationFileLink { fileId: string; label: string | null; metadata: unknown; createdAt: string; } interface AddAssociationArgs { sourceType: string; sourceId: string; targetType: AssociationTargetType; targetId: string; orgId?: string; label?: string; metadata?: unknown; /** * Exact file → conversation updates preserve existing metadata unless the * caller explicitly opts into replacement. Invalid for every other pair. */ replaceMetadata?: boolean; role?: string; position?: number; /** * Typed edge payload (Edge Payload System). `payloadKind` must be a * registered `platform.edge_payload_kind`; the DB trigger validates * `payload` against its JSON Schema and hard-fails on any mismatch. Real * logic an edge carries goes HERE — `metadata` is for loose annotations. */ payloadKind?: string; payload?: unknown; } /** Edges per `addMany` call — one transaction of ~12 ms/edge, far under the API's 8 s statement cap. */ declare const ASSOC_ADD_MANY_CHUNK = 100; interface AssociationsServiceApi { listForEntity(rawType: string, id: string): Promise>; listConversationFiles(conversationId: string): Promise>; listForTargets(rawTargetType: string, targetIds: string[]): Promise>; listForTargetsVisible(rawTargetType: string, targetIds: string[]): Promise>; listForSources(rawSourceType: string, sourceIds: string[], rawTargetType?: string): Promise>; add(args: AddAssociationArgs): Promise>; /** * Attach MANY edges in ONE database transaction (`platform.assoc_add_many`): every edge is * applied exactly as {@link add} applies it, the ids come back in input order, and if ANY edge * is refused the whole call fails and NOTHING is applied. Keep a call under * {@link ASSOC_ADD_MANY_CHUNK} edges (the helpers' `linkEdges` chunks for you). The * file → conversation pair is not batchable (it has its own stricter door): use {@link add}. */ addMany(args: AddAssociationArgs[]): Promise>; remove(args: { sourceType: string; sourceId: string; targetType: string; targetId: string; role?: string; }): Promise>; setTargets(args: { sourceType: string; sourceId: string; targetType: AssociationTargetType; targetIds: string[]; orgId?: string; role?: string; }): Promise>; addAgentResource(args: { agentId: string; sourceType: string; sourceId: string; label?: string; metadata?: unknown; }): Promise>; removeAgentResource(args: { agentId: string; sourceType: string; sourceId: string; }): Promise>; removeForEntity(rawType: string, id: string): Promise>; } declare function createAssociationsService(deps: CoreDeps): AssociationsServiceApi; interface CategoriesServiceApi { list(dimension?: CategoryDimension): Promise>; create(args: { dimension: CategoryDimension; name: string; orgId: string; parentId?: string | null; color?: string | null; icon?: string | null; slug?: string | null; }): Promise>; update(args: { id: string; name: string; slug: string | null; color: string | null; icon: string | null; position: number | null; }): Promise>; reparent(args: { id: string; parentId: string | null; }): Promise>; delete(id: string): Promise>; } declare function createCategoriesService(deps: CoreDeps): CategoriesServiceApi; interface FavoritesServiceApi { setState(args: { entityType: string; entityId: string; isFavorite?: boolean; isPinned?: boolean; isHidden?: boolean; }): Promise>; setFavorite(entityType: string, entityId: string, isFavorite: boolean): Promise>; setPinned(entityType: string, entityId: string, isPinned: boolean): Promise>; setHidden(entityType: string, entityId: string, isHidden: boolean): Promise>; list(kind?: UserStateKind): Promise>; getBulk(entityType: string, entityIds: string[]): Promise>; touch(entityType: string, entityId: string): Promise>; } declare function createFavoritesService(deps: CoreDeps): FavoritesServiceApi; interface CandidateRecord { id: string; title: string; } interface ListCandidatesArgs { token: EntityTypeToken; /** Case-insensitive title filter. */ search?: string; limit?: number; } type CandidatesResult = { ok: true; data: CandidateRecord[]; } | { ok: false; error: string; }; interface UniversalCandidate extends CandidateRecord { token: EntityTypeToken; } interface SearchAcrossTokensArgs { /** Tokens to search. Defaults to every curated token in the registry. */ tokens?: EntityTypeToken[]; search: string; /** Cap per token — a universal search shows a few of everything. */ perTokenLimit?: number; /** Max in-flight token queries. */ concurrency?: number; } /** One token whose candidate read failed during a cross-token search. */ interface TokenSearchFailure { token: EntityTypeToken; error: string; } /** * A cross-token search's honest answer: what could be read, AND what could * not. An empty `results` with non-empty `failures` is "we couldn't look", * never "there is nothing". */ interface SearchAcrossTokensResult { results: UniversalCandidate[]; failures: TokenSearchFailure[]; } interface ReferenceSearchCandidatesFn { (args: { p_token: string; p_search?: string; p_limit?: number; p_ids?: string[]; }): Promise<{ data: Array<{ id: string; title: string | null; }>; error: null; } | { data: null; error: { message: string; }; }>; } interface CandidatesServiceApi { /** * The universal candidate reader RPC (`platform.reference_search_candidates`) * — shared by candidate search here and title resolution in titles.ts. */ callReferenceSearchCandidates: ReferenceSearchCandidatesFn; listAssociationCandidates(args: ListCandidatesArgs): Promise; /** * ONE search over EVERY listable entity type — the "attach anything from * anywhere" primitive. Registry-driven client fan-out, capped and * concurrency-limited. One broken table never blanks the search: a failed * token is screamed (errorSink) AND reported in `failures` (registry * order), so a face can say what it could not search instead of showing * "no results". */ searchCandidatesAcrossTokens(args: SearchAcrossTokensArgs): Promise; } declare function createCandidatesService(deps: CoreDeps): CandidatesServiceApi; declare function entityTitleCacheKey(token: string, id: string): string; interface TitlesServiceApi { /** Read the cache synchronously (for render paths after a fetch settled). */ get(token: string, id: string): string | null; /** * Push a known-fresh title into the cache (after a rename or a create), so * surfaces that resolve through this service never show a stale name. */ prime(token: string, id: string, title: string): void; /** Display fallback when no label and no fetched title exist. */ fallback(token: string): string; /** * Resolve titles for `ids` of one token. Returns ONLY the resolved entries * (cache + fresh reads); missing rows (deleted, no access) stay absent. */ fetch(token: string, ids: string[]): Promise>; } declare function createTitlesService(deps: CoreDeps, candidates: CandidatesServiceApi): TitlesServiceApi; type EntityRowResult = { ok: true; data: { id: string; title: string; }; } | { ok: false; error: string; }; interface CreateEntityRowArgs { /** The new row's human name — written to the registry `titleColumn`. */ title: string; /** * The organization the new row belongs to. REQUIRED by * `platform.entity_row_create`: every creatable token's table carries * `organization_id`, and the door refuses a row with nobody owning it. */ orgId?: string | null; /** * 🚨 NO LONGER WRITABLE THROUGH THIS SERVICE (DOORS-ONLY-4). The door writes the * name, the owner and the organization and nothing else, so a non-empty value here * is REFUSED with the columns named rather than dropped on the floor. A token whose * table needs more than a name is created by its own feature's create service; this * generic path then only renames it. */ extraColumns?: Record; } interface EntityRowsServiceApi { createEntityRow(token: EntityTypeToken, args: CreateEntityRowArgs): Promise; renameEntityRow(token: EntityTypeToken, id: string, title: string): Promise<{ ok: true; } | { ok: false; error: string; }>; } declare function createEntityRowsService(deps: CoreDeps, titles: TitlesServiceApi): EntityRowsServiceApi; /** A typed reference to a single row — `{ type, id }`. */ interface EntityRef { type: EntityTypeToken; id: string; } /** A container reference — the deliberate set an edge may point at. */ interface ContainerRef { type: AssociationTargetType; id: string; } /** Per-edge attributes shared by the link helpers. */ interface EdgeAttrs { orgId?: string; label?: string; metadata?: unknown; role?: string; position?: number; } /** One fully-specified edge for {@link AssociationHelpersApi.linkEdges}. */ interface EdgeSpec extends EdgeAttrs { source: EntityRef; target: ContainerRef; } interface AssociationHelpersApi { /** * Add an arbitrary batch of edges in one call (idempotent per edge). * Written through the batch door (`platform.assoc_add_many`) in sequential chunks of * {@link ASSOC_ADD_MANY_CHUNK}; each chunk is ONE transaction (all or nothing). The result * is `ok` with the ids in input order only when ALL succeed, otherwise `err` whose * `detail` lists a failure for every edge that was not written (chunks that DID succeed * remain applied — re-running is safe via ON CONFLICT). */ linkEdges(edges: EdgeSpec[]): Promise>; /** Attach ONE source to MANY containers in a single shot. */ linkOneToMany(args: { source: EntityRef; targets: ContainerRef[]; attrs?: EdgeAttrs; }): Promise>; /** Attach MANY sources to ONE container in a single shot. */ linkManyToOne(args: { sources: EntityRef[]; target: ContainerRef; attrs?: EdgeAttrs; }): Promise>; /** * REPLACE the full set of a source's edges of one container type (adds * missing, removes extras) in one DB transaction — the set-semantics * one-shot. Thin, named pass-through to `assoc_set_targets`. */ replaceTargets(args: { source: EntityRef; targetType: AssociationTargetType; targetIds: string[]; orgId?: string; role?: string; }): Promise>; /** * Wire edges onto a JUST-CREATED entity. The caller inserts its own row * (entity tables are feature-owned), then passes the new id here to attach * it to any number of containers (orphan-on-partial-fail is harmless and * re-runnable). `created` is the new entity; it becomes the edges' SOURCE. */ linkCreated(args: { created: EntityRef; targets: ContainerRef[]; attrs?: EdgeAttrs; }): Promise>; /** Remove an arbitrary batch of edges (best-effort; no-op per missing edge). */ unlinkEdges(edges: { source: EntityRef; target: ContainerRef; role?: string; }[]): Promise>; } declare function createAssociationHelpers(service: AssociationsServiceApi): AssociationHelpersApi; interface AddCommentArgs { /** Registered entity-type token the comment hangs on (writes-strict, C18). */ entityType: string; entityId: string; body: string; /** Reply target — an existing comment's id. Top-level when omitted. */ parentId?: string | null; /** Org override; the RPC resolves task-org/personal-org when omitted. */ orgId?: string | null; /** Where on the record the comment points (a `part_anchor`, a `text_anchor`); whole record when omitted. */ anchor?: CommentAnchor | null; } interface CommentsServiceApi { listForEntity(entityType: string, entityId: string): Promise>; add(args: AddCommentArgs): Promise>; edit(id: string, body: string): Promise>; remove(id: string): Promise>; } declare function createCommentsService(deps: CoreDeps): CommentsServiceApi; /** Cache key for one association endpoint. */ declare function associationsKey(type: string, id: string): string; interface AssociationWriteResult { ok: boolean; /** Set on a successful single-edge `add`. */ id?: string; error?: string; } interface CategoryMutationResult { ok: boolean; /** Set on a successful mutation. */ id?: string; error?: string; } interface CommentMutationResult { ok: boolean; /** Set on a successful `addComment`. */ id?: string; error?: string; } interface AssociationsStore { /** Stable, sync read of one endpoint's entry (idle default when uncached). */ getEdges(type: string, id: string): AssociationsEntry; /** * Lazy load of every edge touching `${type}:${id}` (both directions). * Deduped per key; `status === "ready"` short-circuits unless `force`. */ load(type: string, id: string, opts?: { force?: boolean; }): Promise; /** External-store contract: notified whenever the key's entry changes. */ subscribe(key: string, cb: () => void): () => void; /** Attach source → target; on success reloads BOTH endpoints. */ add(args: { sourceType: string; sourceId: string; targetType: AssociationTargetType; targetId: string; orgId?: string; label?: string; role?: string; metadata?: unknown; replaceMetadata?: boolean; }): Promise; /** Detach source → target; on success reloads BOTH endpoints. */ remove(args: { sourceType: string; sourceId: string; targetType: string; targetId: string; role?: string; }): Promise; /** Replace the source's `targetType` edge set; reloads the SOURCE. */ setTargets(args: { sourceType: string; sourceId: string; targetType: AssociationTargetType; targetIds: string[]; orgId?: string; role?: string; }): Promise; /** Reset one endpoint to idle and notify its subscribers. */ invalidate(type: string, id: string): void; /** Reset every association endpoint, category facet, and comment thread. */ invalidateAll(): void; getCategories(dimension: CategoryDimension): CategoriesEntry; loadCategories(dimension: CategoryDimension, opts?: { force?: boolean; }): Promise; subscribeCategories(dimension: CategoryDimension, cb: () => void): () => void; /** Create + echo-insert + authoritative force reload. Returns the new id. */ createCategory(args: { dimension: CategoryDimension; name: string; orgId: string; parentId?: string | null; color?: string | null; icon?: string | null; slug?: string | null; }): Promise; updateCategory(args: { dimension: CategoryDimension; id: string; name: string; slug: string | null; color: string | null; icon: string | null; position: number | null; }): Promise; reparentCategory(args: { dimension: CategoryDimension; id: string; parentId: string | null; }): Promise; deleteCategory(args: { dimension: CategoryDimension; id: string; }): Promise; /** Stable, sync read of one entity's comment thread (idle when uncached). */ getComments(type: string, id: string): CommentsEntry; /** * Lazy load of every comment on `${type}:${id}` (oldest→newest; thread by * `parentId`). Deduped per key; `status === "ready"` short-circuits unless * `force`. */ loadComments(type: string, id: string, opts?: { force?: boolean; }): Promise; /** External-store contract for one entity's comment thread. */ subscribeComments(key: string, cb: () => void): () => void; /** Post a comment (or a reply); on success force-reloads the thread. */ addComment(args: { entityType: string; entityId: string; body: string; parentId?: string | null; orgId?: string | null; anchor?: CommentAnchor | null; }): Promise; /** Edit a comment body (author-only in the RPC); reloads the thread. */ editComment(args: { entityType: string; entityId: string; id: string; body: string; }): Promise; /** Soft-delete a comment (author/org member in the RPC); reloads the thread. */ deleteComment(args: { entityType: string; entityId: string; id: string; }): Promise; /** Reset one entity's comment thread to idle and notify its subscribers. */ invalidateComments(type: string, id: string): void; /** Batched title resolution + the session cache + prime. */ titles: TitlesServiceApi; /** Per-user favorite/pinned/hidden state (`ues_*`). */ favorites: FavoritesServiceApi; /** Candidate reads for the pickers. */ candidates: CandidatesServiceApi; /** Generic registry-convention row create/rename. */ entityRows: EntityRowsServiceApi; /** Composite link helpers over the chokepoint. */ helpers: AssociationHelpersApi; /** The registry merge engine (generated metadata + host overlay). */ registry: EntityRegistry; /** * The raw chokepoint services — every RPC family, results-enveloped. * `associations` writes here do NOT touch the cache; use the store-level * `add`/`remove`/`setTargets` when a cached surface must stay fresh. */ services: { associations: AssociationsServiceApi; categories: CategoriesServiceApi; comments: CommentsServiceApi; }; /** Merge more host overlay entries in (icons/routes/candidate loaders). */ registerEntityOverlay: EntityRegistry["registerEntityOverlay"]; /** * The bound scream seam (W3, additive): /react surfaces route their loud * recoveries here (the originals' console.error sites) without a second * sink binding. Same function the host passed at construction. */ errorSink: ErrorSink; /** * The bound data seam (0.6.0, additive): the provider's demanded-schema * boot probe reads it so hosts stop re-exporting their dataSource just to * run a check the package owns. */ dataSource: AssociationsDataSource; /** * The bound identity port (W3, additive): /react hooks that FILTER by the * current user (candidate owner scoping) read it leniently; write paths * keep going through the services, where `requireUserId` failures stay * loud. Same object the host passed at construction. */ identity: AssociationsConfig["identity"]; } /** * Build the one store a host binds. The REQUIRED ports (dataSource / * identity — true app identity, C23) throw at construction when absent. * Every other port has a SHIPPED DEFAULT (errorSink since 0.6.0) or a * documented degradation; nothing is ever silently missing. */ declare function createAssociationsStore(config: AssociationsConfig): AssociationsStore; /** Source tokens that are structure, never container content. */ declare const NON_CONTENT_SOURCE_TYPES: ReadonlySet; /** True when edge metadata marks a container-membership edge. */ declare function isMembershipMetadata(metadata: unknown): boolean; /** * True when an edge into a container represents CONTENT the container holds. * `sourceType` is the edge's raw `source_type` token; `metadata` the edge's * jsonb (pass it when available so membership edges are excluded even if a * structural token is ever missed). */ declare function isContentSourceEdge(sourceType: string, metadata?: unknown): boolean; interface CategoryHierarchyItem { category: PlatformCategory; depth: 0 | 1; parent: PlatformCategory | null; displayName: string; } interface CategoryHierarchy { items: CategoryHierarchyItem[]; roots: PlatformCategory[]; hasHierarchy: boolean; } /** * Turns the RPC's ordered flat rows into the canonical two-level display order. * Flat facets retain their exact input order. Invalid/orphaned rows remain * visible at the root; the database guard prevents new ones from being made. */ declare function buildCategoryHierarchy(categories: PlatformCategory[]): CategoryHierarchy; /** * Build the default sink. `announce` is called once, on the first event, so * the absence of a bound sink is itself loud (never a silent substitution). */ declare function createDefaultErrorSink(): ErrorSink; declare function pluralizeLabel(label: string): string; export { ASSOC_ADD_MANY_CHUNK, DEFAULT_ORG_COLUMN, DEFAULT_OWNER_COLUMN, NON_CONTENT_SOURCE_TYPES, associationsKey, buildCategoryHierarchy, createAssociationGuards, createAssociationHelpers, createAssociationsService, createAssociationsStore, createCandidatesService, createCategoriesService, createCommentsService, createDefaultErrorSink, createEntityRegistry, createEntityRowsService, createFavoritesService, createRpcResultHelpers, createTitlesService, entityTitleCacheKey, err, firstError, isContentRole, isContentSourceEdge, isMembershipMetadata, ok, pluralizeLabel, resolveEntityToken }; export type { AddAssociationArgs, AddCommentArgs, AssociationGuards, AssociationHelpersApi, AssociationWriteResult, AssociationsServiceApi, AssociationsStore, CandidateRecord, CandidatesResult, CandidatesServiceApi, CategoriesServiceApi, CategoryHierarchy, CategoryHierarchyItem, CategoryMutationResult, CommentMutationResult, CommentsServiceApi, ContainerRef, ContentRole, ConversationFileLink, CoreDeps, CreateEntityRowArgs, EdgeAttrs, EdgeSpec, EntityInfo, EntityRef, EntityRegistry, EntityRowResult, EntityRowsServiceApi, FavoritesServiceApi, ListCandidatesArgs, PgErrorMapper, RpcResultHelpers, SearchAcrossTokensArgs, SearchAcrossTokensResult, TitlesServiceApi, TokenSearchFailure, UniversalCandidate };