@clivly/core 0.9.7 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +17 -10
  2. package/dist/builder.cjs +599 -0
  3. package/dist/builder.d.cts +336 -0
  4. package/dist/builder.d.mts +336 -0
  5. package/dist/builder.mjs +589 -0
  6. package/dist/drizzle.d.cts +1 -1
  7. package/dist/drizzle.d.mts +1 -1
  8. package/dist/{entity-config-25v_udeY.cjs → entity-config-C5jBmG2C.cjs} +83 -68
  9. package/dist/{entity-config-buOIVtMM.d.cts → entity-config-DCa785OE.d.cts} +27 -1
  10. package/dist/{entity-config-buOIVtMM.d.mts → entity-config-DCa785OE.d.mts} +27 -1
  11. package/dist/{entity-config-DwGXbUo_.mjs → entity-config-DpHPMFIS.mjs} +71 -38
  12. package/dist/entity-config.cjs +1 -1
  13. package/dist/entity-config.d.cts +1 -1
  14. package/dist/entity-config.d.mts +1 -1
  15. package/dist/entity-config.mjs +1 -1
  16. package/dist/entity-heuristics.cjs +34 -1
  17. package/dist/entity-heuristics.d.cts +8 -2
  18. package/dist/entity-heuristics.d.mts +8 -2
  19. package/dist/entity-heuristics.mjs +34 -2
  20. package/dist/index.cjs +136 -199
  21. package/dist/index.d.cts +135 -89
  22. package/dist/index.d.mts +135 -89
  23. package/dist/index.mjs +124 -188
  24. package/dist/mapping-form.d.cts +1 -1
  25. package/dist/mapping-form.d.mts +1 -1
  26. package/dist/mappings-config.cjs +2 -16
  27. package/dist/mappings-config.d.cts +1 -1
  28. package/dist/mappings-config.d.mts +1 -1
  29. package/dist/mappings-config.mjs +1 -15
  30. package/dist/{projection-OJYCYmS_.cjs → projection-DFgCoyZl.cjs} +0 -6
  31. package/dist/{projection-BXw_iiw5.mjs → projection-SCNrLJMV.mjs} +1 -1
  32. package/dist/schema-meta-Ac7938mT.mjs +46 -0
  33. package/dist/schema-meta-Cgi0MnHh.cjs +69 -0
  34. package/dist/sync-engine.cjs +1 -1
  35. package/dist/sync-engine.d.cts +1 -1
  36. package/dist/sync-engine.d.mts +1 -1
  37. package/dist/sync-engine.mjs +1 -1
  38. package/dist/types.cjs +11 -0
  39. package/dist/types.d.cts +28 -2
  40. package/dist/types.d.mts +28 -2
  41. package/dist/types.mjs +9 -1
  42. package/dist/{view-compiler-u3GGRgnv.mjs → view-compiler-CLvZ0wJW.mjs} +1 -1
  43. package/dist/{view-compiler-D30HNRnA.cjs → view-compiler-DP4Fbj7-.cjs} +1 -1
  44. package/dist/view-compiler.cjs +1 -1
  45. package/dist/view-compiler.d.cts +1 -1
  46. package/dist/view-compiler.d.mts +1 -1
  47. package/dist/view-compiler.mjs +1 -1
  48. package/package.json +11 -1
@@ -0,0 +1,336 @@
1
+ import { F as ProjectionMapping, m as DiscoveredSchemaTable } from "./entity-config-DCa785OE.cjs";
2
+ //#region src/relationship-graph.d.ts
3
+ /**
4
+ * A bidirectional relationship graph derived from discovery metadata.
5
+ *
6
+ * The retired builder's join suggester scanned only the *joined* table for a
7
+ * key pointing at the base table — the parent-to-child direction — so it
8
+ * auto-filled exactly the joins that fan out and could never fill the safe
9
+ * child-to-parent ones. (That helper was removed with the builder it served;
10
+ * this note records the mistake so it is not repeated.) This graph walks a
11
+ * table's own FKs (child -> parent) *and* every other table's FKs that point
12
+ * at it (parent -> child), so both directions are navigable.
13
+ *
14
+ * An edge is identified by its complete FK column pair, not by its table
15
+ * pair: `tickets.assignee_id -> users.id` and `tickets.reviewer_id ->
16
+ * users.id` are two distinct edges, not one collapsed relationship.
17
+ *
18
+ * Absent metadata is UNKNOWN, never a negative fact (see `schema-meta.ts`).
19
+ * A table that fails `hasRichMeta` cannot be proven to have any — or no —
20
+ * relationships, so it is marked `offerable: false` with a reason rather
21
+ * than silently rendered as a legitimately isolated node with zero edges.
22
+ */
23
+ type RelationshipDirection = "child-to-parent" | "parent-to-child";
24
+ type RelationshipCardinality = "to-one" | "to-many";
25
+ interface RelationshipEdge {
26
+ cardinality: RelationshipCardinality;
27
+ direction: RelationshipDirection;
28
+ /** The FK column on the child side of the relationship. */
29
+ fromColumn: string;
30
+ /** The table this edge is being viewed from. */
31
+ fromTable: string;
32
+ /** The referenced column on the parent side of the relationship. */
33
+ toColumn: string;
34
+ /** The table this edge points to. */
35
+ toTable: string;
36
+ }
37
+ /**
38
+ * Why a table (or, later, a path) cannot be offered.
39
+ *
40
+ * A closed discriminated union, not free text. Every case has to be rendered
41
+ * as its own piece of copy for a non-technical user, and copy belongs to the
42
+ * UI, not here — so this carries the machine-readable case only. Closing the
43
+ * union is what makes the UI's switch exhaustive: a case added here without a
44
+ * matching message is a type error, rather than a new case reaching a user
45
+ * untranslated.
46
+ *
47
+ * Task 4 added the starting-record eligibility cases (`composite-pk`,
48
+ * `no-primary-key`). Add members; do not widen the type back to `string`.
49
+ */
50
+ /** Discovery reported no `columnsMeta`/`uniqueConstraints` for the table. */
51
+ interface NoRichMetaReason {
52
+ code: "no-rich-meta";
53
+ }
54
+ /**
55
+ * The table has more than one primary-key column.
56
+ *
57
+ * This is a UI limitation, not a platform one: `projectionIdentitySchema`
58
+ * already supports `{ kind: "composite", parts }` and `checkIdentitySafety`
59
+ * already validates it — the builder simply does not emit that identity kind
60
+ * in v1. The main casualty is the junction table, which usually carries a
61
+ * composite PK and is exactly the shape someone reaches for when modelling a
62
+ * many-to-many, so the UI copy for this case must say "not yet" and must not
63
+ * imply the data cannot support it.
64
+ */
65
+ interface CompositePrimaryKeyReason {
66
+ code: "composite-pk";
67
+ }
68
+ /** The table has rich metadata but no column is flagged as a primary key. */
69
+ interface NoPrimaryKeyReason {
70
+ code: "no-primary-key";
71
+ }
72
+ type IneligibilityReason = NoRichMetaReason | CompositePrimaryKeyReason | NoPrimaryKeyReason;
73
+ interface RelationshipNode {
74
+ /** False when the table's metadata is too thin to prove anything about it. */
75
+ offerable: boolean;
76
+ /** Present only when `offerable` is false; names the machine-readable case. */
77
+ reason?: IneligibilityReason;
78
+ table: string;
79
+ }
80
+ interface RelationshipGraph {
81
+ edges: RelationshipEdge[];
82
+ nodes: RelationshipNode[];
83
+ }
84
+ /** Every edge whose `fromTable` is `table`, in either direction. */
85
+ declare function edgesFrom(graph: RelationshipGraph, table: string): RelationshipEdge[];
86
+ /**
87
+ * Build the bidirectional relationship graph for a discovered schema.
88
+ *
89
+ * No table, column, or relationship is ever named in this function — the
90
+ * only input is the schema itself.
91
+ */
92
+ declare function buildRelationshipGraph(schema: DiscoveredSchemaTable[]): RelationshipGraph;
93
+ //#endregion
94
+ //#region src/path-enumeration.d.ts
95
+ /**
96
+ * Breadth-first enumeration of the paths reachable from a starting table.
97
+ *
98
+ * This is the "browse outward" engine behind the derived-object builder: the
99
+ * user picks a starting list and we show them what else is reachable, without
100
+ * ever naming a join, a base table, or an ON condition.
101
+ *
102
+ * Three properties matter enough to be spelled out:
103
+ *
104
+ * 1. **Cycles terminate.** A table already on a path is not revisited, so a
105
+ * self-referencing table yields its one-hop neighbours and stops. Parallel
106
+ * edges are still distinct: edge identity is the FK column pair, not the
107
+ * table pair, so two FKs into the same table stay two relationships.
108
+ *
109
+ * 2. **To-many is sticky.** Once a path crosses a to-many edge, everything at
110
+ * or beyond it is a *collection* — a list of rows, not a single value. Those
111
+ * paths are not pickable columns and must never be compiled into joins; the
112
+ * UI shows them with a "start a list of X" action instead.
113
+ *
114
+ * 3. **Truncation is reported, never silent.** Hitting a budget is part of the
115
+ * return value so the UI can say deeper paths are hidden and offer a
116
+ * narrower search, rather than quietly showing a partial answer as if it
117
+ * were the whole one.
118
+ *
119
+ * No table, column, or relationship is ever named here — the only inputs are
120
+ * the graph and the starting table.
121
+ */
122
+ /** Spec-mandated: how many edges deep a path may go. */
123
+ declare const MAX_HOPS = 3;
124
+ /** Spec-mandated: how many candidate paths may be considered before stopping. */
125
+ declare const MAX_CANDIDATES = 200;
126
+ /** Spec-mandated: how many ranked results may be displayed. */
127
+ declare const MAX_RESULTS = 50;
128
+ /** Which budget stopped the walk. Absent when nothing was cut. */
129
+ type PathTruncationCause = "candidate-budget" | "result-limit";
130
+ interface EnumeratedPath {
131
+ /**
132
+ * The edges walked from the starting table, in order. Also the path's
133
+ * identity: two paths differ if any edge differs, including which FK
134
+ * column was followed.
135
+ */
136
+ edges: RelationshipEdge[];
137
+ /**
138
+ * True once the path has crossed a to-many edge. Such a path resolves to
139
+ * many rows, so it is a collection to start a new list from, never a
140
+ * column to pick.
141
+ */
142
+ isCollection: boolean;
143
+ /**
144
+ * A stable identifier for the path, derived only from the edges walked.
145
+ * Deterministic across runs, which is what keeps Task 3's generated join
146
+ * keys stable.
147
+ */
148
+ key: string;
149
+ /** The table the path ends at. */
150
+ toTable: string;
151
+ }
152
+ interface PathEnumeration {
153
+ /** How many candidate paths were considered, capped at `MAX_CANDIDATES`. */
154
+ candidatesConsidered: number;
155
+ /** Ranked, capped at `MAX_RESULTS`. */
156
+ paths: EnumeratedPath[];
157
+ /** True when a budget hid paths that would otherwise have been returned. */
158
+ truncated: boolean;
159
+ /** Which budget did the cutting. Absent when `truncated` is false. */
160
+ truncatedBy?: PathTruncationCause;
161
+ }
162
+ interface EnumeratePathsOptions {
163
+ /** Column names per table, used only to resolve `field`. */
164
+ columnsByTable?: Map<string, readonly string[]>;
165
+ /**
166
+ * A field the user is looking for. Paths ending at a table that has a
167
+ * column of exactly this name rank above their peers at the same depth.
168
+ * Purely a ranking hint — it never filters.
169
+ */
170
+ field?: string;
171
+ maxCandidates?: number;
172
+ maxHops?: number;
173
+ maxResults?: number;
174
+ }
175
+ /**
176
+ * Enumerate every path reachable from `startTable`, ranked and budgeted.
177
+ *
178
+ * Breadth-first, so the candidate budget always cuts the deepest frontier
179
+ * rather than an arbitrary slice of a half-explored level.
180
+ *
181
+ * Note the order of operations: ranking happens *after* the candidate budget
182
+ * has already stopped the walk. A path that would have ranked highly but sits
183
+ * beyond the 200th candidate is therefore never seen, and no amount of
184
+ * ranking recovers it. That is inherent to bounding the search rather than a
185
+ * defect — and it is precisely why truncation is reported, so the UI can
186
+ * offer a narrower search instead of implying the list is complete.
187
+ */
188
+ declare function enumeratePaths(graph: RelationshipGraph, startTable: string, options?: EnumeratePathsOptions): PathEnumeration;
189
+ //#endregion
190
+ //#region src/path-compiler.d.ts
191
+ /** A field the user picked, optionally reached through a path. */
192
+ interface PickedField {
193
+ /** The column on the path's destination table (or on the start table). */
194
+ column: string;
195
+ /** The renameable name this field is exposed as. Becomes a binding key. */
196
+ name: string;
197
+ /**
198
+ * How the field is reached. Absent (or a zero-edge path) means the field is
199
+ * a column of the starting table itself.
200
+ */
201
+ path?: EnumeratedPath;
202
+ }
203
+ interface CompilePathsOptions {
204
+ entityKey: string;
205
+ pickedFields: PickedField[];
206
+ schema: DiscoveredSchemaTable[];
207
+ startTable: string;
208
+ }
209
+ /**
210
+ * Thrown when picks cannot compile to a valid projection.
211
+ *
212
+ * A distinct class so a builder UI can tell "this cannot be expressed" apart
213
+ * from a genuine bug, and so callers can catch it narrowly. Failing loudly is
214
+ * the point: the alternative — emitting a plausible-looking mapping — is the
215
+ * defect class this whole body of work exists to close.
216
+ */
217
+ declare class PathCompileError extends Error {
218
+ constructor(message: string);
219
+ }
220
+ /**
221
+ * Compile picked fields into a projection mapping.
222
+ *
223
+ * Throws `PathCompileError` when the picks cannot be expressed — a collection
224
+ * path, a missing primary key, a reserved or duplicated field name. Every
225
+ * mapping it returns satisfies `projectionSchema` and, given a schema whose
226
+ * tables carry rich metadata, `validateProjection`.
227
+ */
228
+ declare function compilePaths(options: CompilePathsOptions): ProjectionMapping;
229
+ //#endregion
230
+ //#region src/starting-record-change.d.ts
231
+ /**
232
+ * What a proposed starting-record change costs, in picked fields.
233
+ *
234
+ * ## Why this lives in core and not in the builder UI
235
+ *
236
+ * "Which of my fields survive if I change what this is a list of?" reads like
237
+ * a set operation on what the user already picked, and it is not. Reachability
238
+ * is DIRECTIONAL: `unit.property_id -> property.id` makes a property one thing
239
+ * when seen from a unit, and makes units many things when seen from a
240
+ * property. So a field that is a single value — a pickable column — from one
241
+ * starting record can be a *collection* from another, and a collection is not
242
+ * pickable at all.
243
+ *
244
+ * That is why the obvious implementation is wrong. Intersecting the two
245
+ * starting records' path keys says "a route to `unit` exists from `property`,
246
+ * so `unit.label` survives", which is false: that route fans out. Worse, path
247
+ * keys are derived from the edges walked, so the key spaces of two different
248
+ * starting records barely overlap even where the same field genuinely does
249
+ * survive — the intersection is both too generous and too stingy, in different
250
+ * places.
251
+ *
252
+ * The only sound answer is to re-enumerate from the proposed starting record
253
+ * and ask, per pick, whether that same column is reachable there as a single
254
+ * value. That requires the relationship graph, so it belongs to the engine.
255
+ *
256
+ * ## Why picks come back repointed rather than merely flagged
257
+ *
258
+ * A surviving pick's old `path` describes a route from the OLD starting
259
+ * record. Handing it back unchanged would emit a join anchored to a table no
260
+ * longer in the query. A survivor is therefore returned with the route it has
261
+ * from the new starting record — which may be shorter, longer, or absent
262
+ * entirely when the field is now a column of the starting record itself.
263
+ */
264
+ interface StartingRecordChangeImpact {
265
+ /**
266
+ * Picks that survive, each carrying the route it has from the NEW starting
267
+ * record. Order follows the input.
268
+ */
269
+ keptFields: PickedField[];
270
+ /**
271
+ * How many distinct connections to other information fall away.
272
+ *
273
+ * Counted over routes, not fields: several removed fields can share one
274
+ * route, and a field that was a column of the old starting record was
275
+ * never reached by a connection at all. Neither fact is derivable from
276
+ * `removedFields.length`, which is why this is counted here rather than in
277
+ * the UI.
278
+ */
279
+ removedConnectionCount: number;
280
+ /** Picks not reachable as a single value from the new starting record. */
281
+ removedFields: PickedField[];
282
+ }
283
+ interface ClassifyStartingRecordChangeOptions {
284
+ /** The starting record in force now. */
285
+ from: string;
286
+ /** The fields the user has picked, as `compilePaths` consumes them. */
287
+ picks: PickedField[];
288
+ schema: DiscoveredSchemaTable[];
289
+ /** The starting record being proposed. */
290
+ to: string;
291
+ }
292
+ /**
293
+ * Classify what changing the starting record does to a set of picked fields.
294
+ *
295
+ * Pure and total: an unknown `to` table simply has no reachable paths, so
296
+ * every pick that is not one of its own columns is reported removed. It never
297
+ * throws, because its caller is a confirmation dialog that must be able to
298
+ * render an answer for any proposed change, including a nonsensical one.
299
+ */
300
+ declare function classifyStartingRecordChange(options: ClassifyStartingRecordChangeOptions): StartingRecordChangeImpact;
301
+ //#endregion
302
+ //#region src/starting-record-eligibility.d.ts
303
+ /**
304
+ * Whether a table may be offered as a derived object's "starting record" —
305
+ * the grain choice where one output row represents one row of that table —
306
+ * and why not, when it may not.
307
+ *
308
+ * `path-compiler.ts`'s `basePrimaryKey` enforces the same single-column-PK
309
+ * requirement, but as a thrown `PathCompileError` deep inside compilation.
310
+ * This module answers the earlier question the builder UI actually needs:
311
+ * before the user has picked anything, which tables can even be offered as
312
+ * a starting point, and what do we tell them about the ones that can't.
313
+ *
314
+ * Absent metadata is UNKNOWN, never a negative fact (see `schema-meta.ts`).
315
+ * A table that fails `hasRichMeta` cannot be proven to have — or lack — a
316
+ * primary key, so it is reported unofferable with its own reason rather
317
+ * than folded into (or confused with) the no-primary-key case, which is a
318
+ * known, proven fact about a fully-described table.
319
+ */
320
+ interface StartingRecordOption {
321
+ /** False when this table cannot be the grain of a derived object (yet). */
322
+ offerable: boolean;
323
+ /** Present only when `offerable` is false; names the machine-readable case. */
324
+ reason?: IneligibilityReason;
325
+ table: string;
326
+ }
327
+ /**
328
+ * List every table in the discovered schema with its starting-record
329
+ * eligibility, in schema order.
330
+ *
331
+ * No table, column, or relationship is ever named in this function — the
332
+ * only input is the schema itself.
333
+ */
334
+ declare function listStartingRecordOptions(schema: DiscoveredSchemaTable[]): StartingRecordOption[];
335
+ //#endregion
336
+ export { type ClassifyStartingRecordChangeOptions, type CompilePathsOptions, type CompositePrimaryKeyReason, type EnumeratePathsOptions, type EnumeratedPath, type IneligibilityReason, MAX_CANDIDATES, MAX_HOPS, MAX_RESULTS, type NoPrimaryKeyReason, type NoRichMetaReason, PathCompileError, type PathEnumeration, type PathTruncationCause, type PickedField, type RelationshipCardinality, type RelationshipDirection, type RelationshipEdge, type RelationshipGraph, type RelationshipNode, type StartingRecordChangeImpact, type StartingRecordOption, buildRelationshipGraph, classifyStartingRecordChange, compilePaths, edgesFrom, enumeratePaths, listStartingRecordOptions };
@@ -0,0 +1,336 @@
1
+ import { F as ProjectionMapping, m as DiscoveredSchemaTable } from "./entity-config-DCa785OE.mjs";
2
+ //#region src/relationship-graph.d.ts
3
+ /**
4
+ * A bidirectional relationship graph derived from discovery metadata.
5
+ *
6
+ * The retired builder's join suggester scanned only the *joined* table for a
7
+ * key pointing at the base table — the parent-to-child direction — so it
8
+ * auto-filled exactly the joins that fan out and could never fill the safe
9
+ * child-to-parent ones. (That helper was removed with the builder it served;
10
+ * this note records the mistake so it is not repeated.) This graph walks a
11
+ * table's own FKs (child -> parent) *and* every other table's FKs that point
12
+ * at it (parent -> child), so both directions are navigable.
13
+ *
14
+ * An edge is identified by its complete FK column pair, not by its table
15
+ * pair: `tickets.assignee_id -> users.id` and `tickets.reviewer_id ->
16
+ * users.id` are two distinct edges, not one collapsed relationship.
17
+ *
18
+ * Absent metadata is UNKNOWN, never a negative fact (see `schema-meta.ts`).
19
+ * A table that fails `hasRichMeta` cannot be proven to have any — or no —
20
+ * relationships, so it is marked `offerable: false` with a reason rather
21
+ * than silently rendered as a legitimately isolated node with zero edges.
22
+ */
23
+ type RelationshipDirection = "child-to-parent" | "parent-to-child";
24
+ type RelationshipCardinality = "to-one" | "to-many";
25
+ interface RelationshipEdge {
26
+ cardinality: RelationshipCardinality;
27
+ direction: RelationshipDirection;
28
+ /** The FK column on the child side of the relationship. */
29
+ fromColumn: string;
30
+ /** The table this edge is being viewed from. */
31
+ fromTable: string;
32
+ /** The referenced column on the parent side of the relationship. */
33
+ toColumn: string;
34
+ /** The table this edge points to. */
35
+ toTable: string;
36
+ }
37
+ /**
38
+ * Why a table (or, later, a path) cannot be offered.
39
+ *
40
+ * A closed discriminated union, not free text. Every case has to be rendered
41
+ * as its own piece of copy for a non-technical user, and copy belongs to the
42
+ * UI, not here — so this carries the machine-readable case only. Closing the
43
+ * union is what makes the UI's switch exhaustive: a case added here without a
44
+ * matching message is a type error, rather than a new case reaching a user
45
+ * untranslated.
46
+ *
47
+ * Task 4 added the starting-record eligibility cases (`composite-pk`,
48
+ * `no-primary-key`). Add members; do not widen the type back to `string`.
49
+ */
50
+ /** Discovery reported no `columnsMeta`/`uniqueConstraints` for the table. */
51
+ interface NoRichMetaReason {
52
+ code: "no-rich-meta";
53
+ }
54
+ /**
55
+ * The table has more than one primary-key column.
56
+ *
57
+ * This is a UI limitation, not a platform one: `projectionIdentitySchema`
58
+ * already supports `{ kind: "composite", parts }` and `checkIdentitySafety`
59
+ * already validates it — the builder simply does not emit that identity kind
60
+ * in v1. The main casualty is the junction table, which usually carries a
61
+ * composite PK and is exactly the shape someone reaches for when modelling a
62
+ * many-to-many, so the UI copy for this case must say "not yet" and must not
63
+ * imply the data cannot support it.
64
+ */
65
+ interface CompositePrimaryKeyReason {
66
+ code: "composite-pk";
67
+ }
68
+ /** The table has rich metadata but no column is flagged as a primary key. */
69
+ interface NoPrimaryKeyReason {
70
+ code: "no-primary-key";
71
+ }
72
+ type IneligibilityReason = NoRichMetaReason | CompositePrimaryKeyReason | NoPrimaryKeyReason;
73
+ interface RelationshipNode {
74
+ /** False when the table's metadata is too thin to prove anything about it. */
75
+ offerable: boolean;
76
+ /** Present only when `offerable` is false; names the machine-readable case. */
77
+ reason?: IneligibilityReason;
78
+ table: string;
79
+ }
80
+ interface RelationshipGraph {
81
+ edges: RelationshipEdge[];
82
+ nodes: RelationshipNode[];
83
+ }
84
+ /** Every edge whose `fromTable` is `table`, in either direction. */
85
+ declare function edgesFrom(graph: RelationshipGraph, table: string): RelationshipEdge[];
86
+ /**
87
+ * Build the bidirectional relationship graph for a discovered schema.
88
+ *
89
+ * No table, column, or relationship is ever named in this function — the
90
+ * only input is the schema itself.
91
+ */
92
+ declare function buildRelationshipGraph(schema: DiscoveredSchemaTable[]): RelationshipGraph;
93
+ //#endregion
94
+ //#region src/path-enumeration.d.ts
95
+ /**
96
+ * Breadth-first enumeration of the paths reachable from a starting table.
97
+ *
98
+ * This is the "browse outward" engine behind the derived-object builder: the
99
+ * user picks a starting list and we show them what else is reachable, without
100
+ * ever naming a join, a base table, or an ON condition.
101
+ *
102
+ * Three properties matter enough to be spelled out:
103
+ *
104
+ * 1. **Cycles terminate.** A table already on a path is not revisited, so a
105
+ * self-referencing table yields its one-hop neighbours and stops. Parallel
106
+ * edges are still distinct: edge identity is the FK column pair, not the
107
+ * table pair, so two FKs into the same table stay two relationships.
108
+ *
109
+ * 2. **To-many is sticky.** Once a path crosses a to-many edge, everything at
110
+ * or beyond it is a *collection* — a list of rows, not a single value. Those
111
+ * paths are not pickable columns and must never be compiled into joins; the
112
+ * UI shows them with a "start a list of X" action instead.
113
+ *
114
+ * 3. **Truncation is reported, never silent.** Hitting a budget is part of the
115
+ * return value so the UI can say deeper paths are hidden and offer a
116
+ * narrower search, rather than quietly showing a partial answer as if it
117
+ * were the whole one.
118
+ *
119
+ * No table, column, or relationship is ever named here — the only inputs are
120
+ * the graph and the starting table.
121
+ */
122
+ /** Spec-mandated: how many edges deep a path may go. */
123
+ declare const MAX_HOPS = 3;
124
+ /** Spec-mandated: how many candidate paths may be considered before stopping. */
125
+ declare const MAX_CANDIDATES = 200;
126
+ /** Spec-mandated: how many ranked results may be displayed. */
127
+ declare const MAX_RESULTS = 50;
128
+ /** Which budget stopped the walk. Absent when nothing was cut. */
129
+ type PathTruncationCause = "candidate-budget" | "result-limit";
130
+ interface EnumeratedPath {
131
+ /**
132
+ * The edges walked from the starting table, in order. Also the path's
133
+ * identity: two paths differ if any edge differs, including which FK
134
+ * column was followed.
135
+ */
136
+ edges: RelationshipEdge[];
137
+ /**
138
+ * True once the path has crossed a to-many edge. Such a path resolves to
139
+ * many rows, so it is a collection to start a new list from, never a
140
+ * column to pick.
141
+ */
142
+ isCollection: boolean;
143
+ /**
144
+ * A stable identifier for the path, derived only from the edges walked.
145
+ * Deterministic across runs, which is what keeps Task 3's generated join
146
+ * keys stable.
147
+ */
148
+ key: string;
149
+ /** The table the path ends at. */
150
+ toTable: string;
151
+ }
152
+ interface PathEnumeration {
153
+ /** How many candidate paths were considered, capped at `MAX_CANDIDATES`. */
154
+ candidatesConsidered: number;
155
+ /** Ranked, capped at `MAX_RESULTS`. */
156
+ paths: EnumeratedPath[];
157
+ /** True when a budget hid paths that would otherwise have been returned. */
158
+ truncated: boolean;
159
+ /** Which budget did the cutting. Absent when `truncated` is false. */
160
+ truncatedBy?: PathTruncationCause;
161
+ }
162
+ interface EnumeratePathsOptions {
163
+ /** Column names per table, used only to resolve `field`. */
164
+ columnsByTable?: Map<string, readonly string[]>;
165
+ /**
166
+ * A field the user is looking for. Paths ending at a table that has a
167
+ * column of exactly this name rank above their peers at the same depth.
168
+ * Purely a ranking hint — it never filters.
169
+ */
170
+ field?: string;
171
+ maxCandidates?: number;
172
+ maxHops?: number;
173
+ maxResults?: number;
174
+ }
175
+ /**
176
+ * Enumerate every path reachable from `startTable`, ranked and budgeted.
177
+ *
178
+ * Breadth-first, so the candidate budget always cuts the deepest frontier
179
+ * rather than an arbitrary slice of a half-explored level.
180
+ *
181
+ * Note the order of operations: ranking happens *after* the candidate budget
182
+ * has already stopped the walk. A path that would have ranked highly but sits
183
+ * beyond the 200th candidate is therefore never seen, and no amount of
184
+ * ranking recovers it. That is inherent to bounding the search rather than a
185
+ * defect — and it is precisely why truncation is reported, so the UI can
186
+ * offer a narrower search instead of implying the list is complete.
187
+ */
188
+ declare function enumeratePaths(graph: RelationshipGraph, startTable: string, options?: EnumeratePathsOptions): PathEnumeration;
189
+ //#endregion
190
+ //#region src/path-compiler.d.ts
191
+ /** A field the user picked, optionally reached through a path. */
192
+ interface PickedField {
193
+ /** The column on the path's destination table (or on the start table). */
194
+ column: string;
195
+ /** The renameable name this field is exposed as. Becomes a binding key. */
196
+ name: string;
197
+ /**
198
+ * How the field is reached. Absent (or a zero-edge path) means the field is
199
+ * a column of the starting table itself.
200
+ */
201
+ path?: EnumeratedPath;
202
+ }
203
+ interface CompilePathsOptions {
204
+ entityKey: string;
205
+ pickedFields: PickedField[];
206
+ schema: DiscoveredSchemaTable[];
207
+ startTable: string;
208
+ }
209
+ /**
210
+ * Thrown when picks cannot compile to a valid projection.
211
+ *
212
+ * A distinct class so a builder UI can tell "this cannot be expressed" apart
213
+ * from a genuine bug, and so callers can catch it narrowly. Failing loudly is
214
+ * the point: the alternative — emitting a plausible-looking mapping — is the
215
+ * defect class this whole body of work exists to close.
216
+ */
217
+ declare class PathCompileError extends Error {
218
+ constructor(message: string);
219
+ }
220
+ /**
221
+ * Compile picked fields into a projection mapping.
222
+ *
223
+ * Throws `PathCompileError` when the picks cannot be expressed — a collection
224
+ * path, a missing primary key, a reserved or duplicated field name. Every
225
+ * mapping it returns satisfies `projectionSchema` and, given a schema whose
226
+ * tables carry rich metadata, `validateProjection`.
227
+ */
228
+ declare function compilePaths(options: CompilePathsOptions): ProjectionMapping;
229
+ //#endregion
230
+ //#region src/starting-record-change.d.ts
231
+ /**
232
+ * What a proposed starting-record change costs, in picked fields.
233
+ *
234
+ * ## Why this lives in core and not in the builder UI
235
+ *
236
+ * "Which of my fields survive if I change what this is a list of?" reads like
237
+ * a set operation on what the user already picked, and it is not. Reachability
238
+ * is DIRECTIONAL: `unit.property_id -> property.id` makes a property one thing
239
+ * when seen from a unit, and makes units many things when seen from a
240
+ * property. So a field that is a single value — a pickable column — from one
241
+ * starting record can be a *collection* from another, and a collection is not
242
+ * pickable at all.
243
+ *
244
+ * That is why the obvious implementation is wrong. Intersecting the two
245
+ * starting records' path keys says "a route to `unit` exists from `property`,
246
+ * so `unit.label` survives", which is false: that route fans out. Worse, path
247
+ * keys are derived from the edges walked, so the key spaces of two different
248
+ * starting records barely overlap even where the same field genuinely does
249
+ * survive — the intersection is both too generous and too stingy, in different
250
+ * places.
251
+ *
252
+ * The only sound answer is to re-enumerate from the proposed starting record
253
+ * and ask, per pick, whether that same column is reachable there as a single
254
+ * value. That requires the relationship graph, so it belongs to the engine.
255
+ *
256
+ * ## Why picks come back repointed rather than merely flagged
257
+ *
258
+ * A surviving pick's old `path` describes a route from the OLD starting
259
+ * record. Handing it back unchanged would emit a join anchored to a table no
260
+ * longer in the query. A survivor is therefore returned with the route it has
261
+ * from the new starting record — which may be shorter, longer, or absent
262
+ * entirely when the field is now a column of the starting record itself.
263
+ */
264
+ interface StartingRecordChangeImpact {
265
+ /**
266
+ * Picks that survive, each carrying the route it has from the NEW starting
267
+ * record. Order follows the input.
268
+ */
269
+ keptFields: PickedField[];
270
+ /**
271
+ * How many distinct connections to other information fall away.
272
+ *
273
+ * Counted over routes, not fields: several removed fields can share one
274
+ * route, and a field that was a column of the old starting record was
275
+ * never reached by a connection at all. Neither fact is derivable from
276
+ * `removedFields.length`, which is why this is counted here rather than in
277
+ * the UI.
278
+ */
279
+ removedConnectionCount: number;
280
+ /** Picks not reachable as a single value from the new starting record. */
281
+ removedFields: PickedField[];
282
+ }
283
+ interface ClassifyStartingRecordChangeOptions {
284
+ /** The starting record in force now. */
285
+ from: string;
286
+ /** The fields the user has picked, as `compilePaths` consumes them. */
287
+ picks: PickedField[];
288
+ schema: DiscoveredSchemaTable[];
289
+ /** The starting record being proposed. */
290
+ to: string;
291
+ }
292
+ /**
293
+ * Classify what changing the starting record does to a set of picked fields.
294
+ *
295
+ * Pure and total: an unknown `to` table simply has no reachable paths, so
296
+ * every pick that is not one of its own columns is reported removed. It never
297
+ * throws, because its caller is a confirmation dialog that must be able to
298
+ * render an answer for any proposed change, including a nonsensical one.
299
+ */
300
+ declare function classifyStartingRecordChange(options: ClassifyStartingRecordChangeOptions): StartingRecordChangeImpact;
301
+ //#endregion
302
+ //#region src/starting-record-eligibility.d.ts
303
+ /**
304
+ * Whether a table may be offered as a derived object's "starting record" —
305
+ * the grain choice where one output row represents one row of that table —
306
+ * and why not, when it may not.
307
+ *
308
+ * `path-compiler.ts`'s `basePrimaryKey` enforces the same single-column-PK
309
+ * requirement, but as a thrown `PathCompileError` deep inside compilation.
310
+ * This module answers the earlier question the builder UI actually needs:
311
+ * before the user has picked anything, which tables can even be offered as
312
+ * a starting point, and what do we tell them about the ones that can't.
313
+ *
314
+ * Absent metadata is UNKNOWN, never a negative fact (see `schema-meta.ts`).
315
+ * A table that fails `hasRichMeta` cannot be proven to have — or lack — a
316
+ * primary key, so it is reported unofferable with its own reason rather
317
+ * than folded into (or confused with) the no-primary-key case, which is a
318
+ * known, proven fact about a fully-described table.
319
+ */
320
+ interface StartingRecordOption {
321
+ /** False when this table cannot be the grain of a derived object (yet). */
322
+ offerable: boolean;
323
+ /** Present only when `offerable` is false; names the machine-readable case. */
324
+ reason?: IneligibilityReason;
325
+ table: string;
326
+ }
327
+ /**
328
+ * List every table in the discovered schema with its starting-record
329
+ * eligibility, in schema order.
330
+ *
331
+ * No table, column, or relationship is ever named in this function — the
332
+ * only input is the schema itself.
333
+ */
334
+ declare function listStartingRecordOptions(schema: DiscoveredSchemaTable[]): StartingRecordOption[];
335
+ //#endregion
336
+ export { type ClassifyStartingRecordChangeOptions, type CompilePathsOptions, type CompositePrimaryKeyReason, type EnumeratePathsOptions, type EnumeratedPath, type IneligibilityReason, MAX_CANDIDATES, MAX_HOPS, MAX_RESULTS, type NoPrimaryKeyReason, type NoRichMetaReason, PathCompileError, type PathEnumeration, type PathTruncationCause, type PickedField, type RelationshipCardinality, type RelationshipDirection, type RelationshipEdge, type RelationshipGraph, type RelationshipNode, type StartingRecordChangeImpact, type StartingRecordOption, buildRelationshipGraph, classifyStartingRecordChange, compilePaths, edgesFrom, enumeratePaths, listStartingRecordOptions };