import { R as ProjectionMapping, m as DiscoveredSchemaTable } from "./entity-config-OhC4Otkz.mjs"; //#region src/relationship-graph.d.ts /** * A bidirectional relationship graph derived from discovery metadata. * * The retired builder's join suggester scanned only the *joined* table for a * key pointing at the base table — the parent-to-child direction — so it * auto-filled exactly the joins that fan out and could never fill the safe * child-to-parent ones. (That helper was removed with the builder it served; * this note records the mistake so it is not repeated.) This graph walks a * table's own FKs (child -> parent) *and* every other table's FKs that point * at it (parent -> child), so both directions are navigable. * * An edge is identified by its complete FK column pair, not by its table * pair: `tickets.assignee_id -> users.id` and `tickets.reviewer_id -> * users.id` are two distinct edges, not one collapsed relationship. * * Absent metadata is UNKNOWN, never a negative fact (see `schema-meta.ts`). * A table that fails `hasRichMeta` cannot be proven to have any — or no — * relationships, so it is marked `offerable: false` with a reason rather * than silently rendered as a legitimately isolated node with zero edges. */ type RelationshipDirection = "child-to-parent" | "parent-to-child"; type RelationshipCardinality = "to-one" | "to-many"; interface RelationshipEdge { cardinality: RelationshipCardinality; direction: RelationshipDirection; /** The FK column on the child side of the relationship. */ fromColumn: string; /** The table this edge is being viewed from. */ fromTable: string; /** The referenced column on the parent side of the relationship. */ toColumn: string; /** The table this edge points to. */ toTable: string; } /** * Why a table (or, later, a path) cannot be offered. * * A closed discriminated union, not free text. Every case has to be rendered * as its own piece of copy for a non-technical user, and copy belongs to the * UI, not here — so this carries the machine-readable case only. Closing the * union is what makes the UI's switch exhaustive: a case added here without a * matching message is a type error, rather than a new case reaching a user * untranslated. * * Task 4 added the starting-record eligibility cases (`composite-pk`, * `no-primary-key`). Add members; do not widen the type back to `string`. */ /** Discovery reported no `columnsMeta`/`uniqueConstraints` for the table. */ interface NoRichMetaReason { code: "no-rich-meta"; } /** * The table has more than one primary-key column. * * This is a UI limitation, not a platform one: `projectionIdentitySchema` * already supports `{ kind: "composite", parts }` and `checkIdentitySafety` * already validates it — the builder simply does not emit that identity kind * in v1. The main casualty is the junction table, which usually carries a * composite PK and is exactly the shape someone reaches for when modelling a * many-to-many, so the UI copy for this case must say "not yet" and must not * imply the data cannot support it. */ interface CompositePrimaryKeyReason { code: "composite-pk"; } /** The table has rich metadata but no column is flagged as a primary key. */ interface NoPrimaryKeyReason { code: "no-primary-key"; } type IneligibilityReason = NoRichMetaReason | CompositePrimaryKeyReason | NoPrimaryKeyReason; interface RelationshipNode { /** False when the table's metadata is too thin to prove anything about it. */ offerable: boolean; /** Present only when `offerable` is false; names the machine-readable case. */ reason?: IneligibilityReason; table: string; } interface RelationshipGraph { edges: RelationshipEdge[]; nodes: RelationshipNode[]; } /** Every edge whose `fromTable` is `table`, in either direction. */ export declare function edgesFrom(graph: RelationshipGraph, table: string): RelationshipEdge[]; /** * Build the bidirectional relationship graph for a discovered schema. * * No table, column, or relationship is ever named in this function — the * only input is the schema itself. */ export declare function buildRelationshipGraph(schema: DiscoveredSchemaTable[]): RelationshipGraph; //#endregion //#region src/path-enumeration.d.ts /** * Breadth-first enumeration of the paths reachable from a starting table. * * This is the "browse outward" engine behind the derived-object builder: the * user picks a starting list and we show them what else is reachable, without * ever naming a join, a base table, or an ON condition. * * Three properties matter enough to be spelled out: * * 1. **Cycles terminate.** A table already on a path is not revisited, so a * self-referencing table yields its one-hop neighbours and stops. Parallel * edges are still distinct: edge identity is the FK column pair, not the * table pair, so two FKs into the same table stay two relationships. * * 2. **To-many is sticky.** Once a path crosses a to-many edge, everything at * or beyond it is a *collection* — a list of rows, not a single value. Those * paths are not pickable columns and must never be compiled into joins; the * UI shows them with a "start a list of X" action instead. * * 3. **Truncation is reported, never silent.** Hitting a budget is part of the * return value so the UI can say deeper paths are hidden and offer a * narrower search, rather than quietly showing a partial answer as if it * were the whole one. * * No table, column, or relationship is ever named here — the only inputs are * the graph and the starting table. */ /** Spec-mandated: how many edges deep a path may go. */ export declare const MAX_HOPS = 3; /** Spec-mandated: how many candidate paths may be considered before stopping. */ export declare const MAX_CANDIDATES = 200; /** Spec-mandated: how many ranked results may be displayed. */ export declare const MAX_RESULTS = 50; /** Which budget stopped the walk. Absent when nothing was cut. */ type PathTruncationCause = "candidate-budget" | "result-limit"; interface EnumeratedPath { /** * The edges walked from the starting table, in order. Also the path's * identity: two paths differ if any edge differs, including which FK * column was followed. */ edges: RelationshipEdge[]; /** * True once the path has crossed a to-many edge. Such a path resolves to * many rows, so it is a collection to start a new list from, never a * column to pick. */ isCollection: boolean; /** * A stable identifier for the path, derived only from the edges walked. * Deterministic across runs, which is what keeps Task 3's generated join * keys stable. */ key: string; /** The table the path ends at. */ toTable: string; } interface PathEnumeration { /** How many candidate paths were considered, capped at `MAX_CANDIDATES`. */ candidatesConsidered: number; /** Ranked, capped at `MAX_RESULTS`. */ paths: EnumeratedPath[]; /** True when a budget hid paths that would otherwise have been returned. */ truncated: boolean; /** Which budget did the cutting. Absent when `truncated` is false. */ truncatedBy?: PathTruncationCause; } interface EnumeratePathsOptions { /** Column names per table, used only to resolve `field`. */ columnsByTable?: Map; /** * A field the user is looking for. Paths ending at a table that has a * column of exactly this name rank above their peers at the same depth. * Purely a ranking hint — it never filters. */ field?: string; maxCandidates?: number; maxHops?: number; maxResults?: number; } /** * Enumerate every path reachable from `startTable`, ranked and budgeted. * * Breadth-first, so the candidate budget always cuts the deepest frontier * rather than an arbitrary slice of a half-explored level. * * Note the order of operations: ranking happens *after* the candidate budget * has already stopped the walk. A path that would have ranked highly but sits * beyond the 200th candidate is therefore never seen, and no amount of * ranking recovers it. That is inherent to bounding the search rather than a * defect — and it is precisely why truncation is reported, so the UI can * offer a narrower search instead of implying the list is complete. */ export declare function enumeratePaths(graph: RelationshipGraph, startTable: string, options?: EnumeratePathsOptions): PathEnumeration; //#endregion //#region src/path-compiler.d.ts /** A field the user picked, optionally reached through a path. */ interface PickedField { /** The column on the path's destination table (or on the start table). */ column: string; /** The renameable name this field is exposed as. Becomes a binding key. */ name: string; /** * How the field is reached. Absent (or a zero-edge path) means the field is * a column of the starting table itself. */ path?: EnumeratedPath; } interface CompilePathsOptions { entityKey: string; pickedFields: PickedField[]; schema: DiscoveredSchemaTable[]; startTable: string; } /** * Thrown when picks cannot compile to a valid projection. * * A distinct class so a builder UI can tell "this cannot be expressed" apart * from a genuine bug, and so callers can catch it narrowly. Failing loudly is * the point: the alternative — emitting a plausible-looking mapping — is the * defect class this whole body of work exists to close. */ export declare class PathCompileError extends Error { constructor(message: string); } /** * Compile picked fields into a projection mapping. * * Throws `PathCompileError` when the picks cannot be expressed — a collection * path, a missing primary key, a reserved or duplicated field name. Every * mapping it returns satisfies `projectionSchema` and, given a schema whose * tables carry rich metadata, `validateProjection`. */ export declare function compilePaths(options: CompilePathsOptions): ProjectionMapping; //#endregion //#region src/starting-record-change.d.ts /** * What a proposed starting-record change costs, in picked fields. * * ## Why this lives in core and not in the builder UI * * "Which of my fields survive if I change what this is a list of?" reads like * a set operation on what the user already picked, and it is not. Reachability * is DIRECTIONAL: `unit.property_id -> property.id` makes a property one thing * when seen from a unit, and makes units many things when seen from a * property. So a field that is a single value — a pickable column — from one * starting record can be a *collection* from another, and a collection is not * pickable at all. * * That is why the obvious implementation is wrong. Intersecting the two * starting records' path keys says "a route to `unit` exists from `property`, * so `unit.label` survives", which is false: that route fans out. Worse, path * keys are derived from the edges walked, so the key spaces of two different * starting records barely overlap even where the same field genuinely does * survive — the intersection is both too generous and too stingy, in different * places. * * The only sound answer is to re-enumerate from the proposed starting record * and ask, per pick, whether that same column is reachable there as a single * value. That requires the relationship graph, so it belongs to the engine. * * ## Why picks come back repointed rather than merely flagged * * A surviving pick's old `path` describes a route from the OLD starting * record. Handing it back unchanged would emit a join anchored to a table no * longer in the query. A survivor is therefore returned with the route it has * from the new starting record — which may be shorter, longer, or absent * entirely when the field is now a column of the starting record itself. */ interface StartingRecordChangeImpact { /** * Picks that survive, each carrying the route it has from the NEW starting * record. Order follows the input. */ keptFields: PickedField[]; /** * How many distinct connections to other information fall away. * * Counted over routes, not fields: several removed fields can share one * route, and a field that was a column of the old starting record was * never reached by a connection at all. Neither fact is derivable from * `removedFields.length`, which is why this is counted here rather than in * the UI. */ removedConnectionCount: number; /** Picks not reachable as a single value from the new starting record. */ removedFields: PickedField[]; } interface ClassifyStartingRecordChangeOptions { /** The starting record in force now. */ from: string; /** The fields the user has picked, as `compilePaths` consumes them. */ picks: PickedField[]; schema: DiscoveredSchemaTable[]; /** The starting record being proposed. */ to: string; } /** * Classify what changing the starting record does to a set of picked fields. * * Pure and total: an unknown `to` table simply has no reachable paths, so * every pick that is not one of its own columns is reported removed. It never * throws, because its caller is a confirmation dialog that must be able to * render an answer for any proposed change, including a nonsensical one. */ export declare function classifyStartingRecordChange(options: ClassifyStartingRecordChangeOptions): StartingRecordChangeImpact; //#endregion //#region src/starting-record-eligibility.d.ts /** * Whether a table may be offered as a derived object's "starting record" — * the grain choice where one output row represents one row of that table — * and why not, when it may not. * * `path-compiler.ts`'s `basePrimaryKey` enforces the same single-column-PK * requirement, but as a thrown `PathCompileError` deep inside compilation. * This module answers the earlier question the builder UI actually needs: * before the user has picked anything, which tables can even be offered as * a starting point, and what do we tell them about the ones that can't. * * Absent metadata is UNKNOWN, never a negative fact (see `schema-meta.ts`). * A table that fails `hasRichMeta` cannot be proven to have — or lack — a * primary key, so it is reported unofferable with its own reason rather * than folded into (or confused with) the no-primary-key case, which is a * known, proven fact about a fully-described table. */ interface StartingRecordOption { /** False when this table cannot be the grain of a derived object (yet). */ offerable: boolean; /** Present only when `offerable` is false; names the machine-readable case. */ reason?: IneligibilityReason; table: string; } /** * List every table in the discovered schema with its starting-record * eligibility, in schema order. * * No table, column, or relationship is ever named in this function — the * only input is the schema itself. */ export declare function listStartingRecordOptions(schema: DiscoveredSchemaTable[]): StartingRecordOption[]; //#endregion export type { ClassifyStartingRecordChangeOptions, CompilePathsOptions, CompositePrimaryKeyReason, EnumeratePathsOptions, EnumeratedPath, IneligibilityReason, NoPrimaryKeyReason, NoRichMetaReason, PathEnumeration, PathTruncationCause, PickedField, RelationshipCardinality, RelationshipDirection, RelationshipEdge, RelationshipGraph, RelationshipNode, StartingRecordChangeImpact, StartingRecordOption };