@bjornpagen/bumbledb 0.1.0

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 (108) hide show
  1. package/LICENSE +12 -0
  2. package/README.md +84 -0
  3. package/dist/brand.d.ts +59 -0
  4. package/dist/brand.d.ts.map +1 -0
  5. package/dist/brand.js +47 -0
  6. package/dist/brand.js.map +1 -0
  7. package/dist/closed.d.ts +97 -0
  8. package/dist/closed.d.ts.map +1 -0
  9. package/dist/closed.js +107 -0
  10. package/dist/closed.js.map +1 -0
  11. package/dist/count.d.ts +55 -0
  12. package/dist/count.d.ts.map +1 -0
  13. package/dist/count.js +92 -0
  14. package/dist/count.js.map +1 -0
  15. package/dist/db.d.ts +341 -0
  16. package/dist/db.d.ts.map +1 -0
  17. package/dist/db.js +1016 -0
  18. package/dist/db.js.map +1 -0
  19. package/dist/exhume.d.ts +130 -0
  20. package/dist/exhume.d.ts.map +1 -0
  21. package/dist/exhume.js +145 -0
  22. package/dist/exhume.js.map +1 -0
  23. package/dist/face.d.ts +91 -0
  24. package/dist/face.d.ts.map +1 -0
  25. package/dist/face.js +69 -0
  26. package/dist/face.js.map +1 -0
  27. package/dist/fields.d.ts +187 -0
  28. package/dist/fields.d.ts.map +1 -0
  29. package/dist/fields.js +204 -0
  30. package/dist/fields.js.map +1 -0
  31. package/dist/index.d.ts +49 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +31 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/lower.d.ts +34 -0
  36. package/dist/lower.d.ts.map +1 -0
  37. package/dist/lower.js +105 -0
  38. package/dist/lower.js.map +1 -0
  39. package/dist/marshal.d.ts +79 -0
  40. package/dist/marshal.d.ts.map +1 -0
  41. package/dist/marshal.js +167 -0
  42. package/dist/marshal.js.map +1 -0
  43. package/dist/native.d.ts +532 -0
  44. package/dist/native.d.ts.map +1 -0
  45. package/dist/native.js +64 -0
  46. package/dist/native.js.map +1 -0
  47. package/dist/query/atom.d.ts +285 -0
  48. package/dist/query/atom.d.ts.map +1 -0
  49. package/dist/query/atom.js +281 -0
  50. package/dist/query/atom.js.map +1 -0
  51. package/dist/query/lower.d.ts +145 -0
  52. package/dist/query/lower.d.ts.map +1 -0
  53. package/dist/query/lower.js +604 -0
  54. package/dist/query/lower.js.map +1 -0
  55. package/dist/query/predicate.d.ts +101 -0
  56. package/dist/query/predicate.d.ts.map +1 -0
  57. package/dist/query/predicate.js +85 -0
  58. package/dist/query/predicate.js.map +1 -0
  59. package/dist/query/run.d.ts +30 -0
  60. package/dist/query/run.d.ts.map +1 -0
  61. package/dist/query/run.js +94 -0
  62. package/dist/query/run.js.map +1 -0
  63. package/dist/query/scope.d.ts +168 -0
  64. package/dist/query/scope.d.ts.map +1 -0
  65. package/dist/query/scope.js +134 -0
  66. package/dist/query/scope.js.map +1 -0
  67. package/dist/query/select.d.ts +106 -0
  68. package/dist/query/select.d.ts.map +1 -0
  69. package/dist/query/select.js +69 -0
  70. package/dist/query/select.js.map +1 -0
  71. package/dist/relation.d.ts +120 -0
  72. package/dist/relation.d.ts.map +1 -0
  73. package/dist/relation.js +108 -0
  74. package/dist/relation.js.map +1 -0
  75. package/dist/schema.d.ts +50 -0
  76. package/dist/schema.d.ts.map +1 -0
  77. package/dist/schema.js +235 -0
  78. package/dist/schema.js.map +1 -0
  79. package/dist/spec.d.ts +218 -0
  80. package/dist/spec.d.ts.map +1 -0
  81. package/dist/spec.js +154 -0
  82. package/dist/spec.js.map +1 -0
  83. package/dist/statements.d.ts +91 -0
  84. package/dist/statements.d.ts.map +1 -0
  85. package/dist/statements.js +101 -0
  86. package/dist/statements.js.map +1 -0
  87. package/package.json +66 -0
  88. package/src/brand.ts +82 -0
  89. package/src/closed.ts +228 -0
  90. package/src/count.ts +117 -0
  91. package/src/db.ts +1519 -0
  92. package/src/exhume.ts +243 -0
  93. package/src/face.ts +161 -0
  94. package/src/fields.ts +385 -0
  95. package/src/index.ts +185 -0
  96. package/src/lower.ts +118 -0
  97. package/src/marshal.ts +220 -0
  98. package/src/native.ts +576 -0
  99. package/src/query/atom.ts +556 -0
  100. package/src/query/lower.ts +855 -0
  101. package/src/query/predicate.ts +195 -0
  102. package/src/query/run.ts +106 -0
  103. package/src/query/scope.ts +301 -0
  104. package/src/query/select.ts +140 -0
  105. package/src/relation.ts +252 -0
  106. package/src/schema.ts +297 -0
  107. package/src/spec.ts +325 -0
  108. package/src/statements.ts +148 -0
package/src/spec.ts ADDED
@@ -0,0 +1,325 @@
1
+ /**
2
+ * The lowered wire shapes — a 1:1 TypeScript mirror of bumbledb's
3
+ * `SchemaSpec` bindings contract (PRD-01;
4
+ * `bumbledb/crates/bumbledb/src/schema/spec.rs`): a schema as named plain
5
+ * data, relations and dependency statements each in declaration order (the
6
+ * declaration-order law that mints every id). The SDK's `lower()` emits
7
+ * these values; the napi bridge (PRD-04) marshals them into the Rust
8
+ * `SchemaSpec` verbatim, and the engine's own judge (`SchemaSpec::descriptor`
9
+ * name resolution + `SchemaDescriptor::validate` at `Db.create`/`Db.open`)
10
+ * stays the single semantic authority — the SDK lowers, it never re-judges.
11
+ *
12
+ * Every u64 crosses as `bigint`, never `number` (PRD-04's marshaling law: no
13
+ * 53-bit hazards, no branch). Object keys are always written in one fixed
14
+ * literal order per shape, so serialization of a lowered schema is
15
+ * deterministic (byte-stable) by construction.
16
+ */
17
+
18
+ /**
19
+ * A structural value type — the one type vocabulary of the `schema!` field
20
+ * grammar (`ValueType` in Rust): `bool`, `u64`, `i64`, `str` (`string`
21
+ * here), `bytes<N>` (`fixedBytes`), and the interval family (`width:
22
+ * undefined` is the general 16-byte encoding with rays representable;
23
+ * `width: w` is the fixed-width `interval<E, w>` whose encoding stores only
24
+ * the start).
25
+ */
26
+ type ValueTypeSpec =
27
+ | { readonly kind: "bool" }
28
+ | { readonly kind: "u64" }
29
+ | { readonly kind: "i64" }
30
+ | { readonly kind: "string" }
31
+ | { readonly kind: "fixedBytes"; readonly len: number }
32
+ | {
33
+ readonly kind: "interval"
34
+ readonly element: "u64" | "i64"
35
+ readonly width: bigint | undefined
36
+ }
37
+
38
+ /**
39
+ * One plain engine value as carried by a lowered literal — the mirror of
40
+ * `bumbledb::Value` restricted to the schema-literal vocabulary (Allen masks
41
+ * are query-side values, never schema literals). Intervals are half-open
42
+ * `[start, end)`.
43
+ */
44
+ type ValueSpec =
45
+ | { readonly kind: "bool"; readonly value: boolean }
46
+ | { readonly kind: "u64"; readonly value: bigint }
47
+ | { readonly kind: "i64"; readonly value: bigint }
48
+ | { readonly kind: "string"; readonly value: string }
49
+ | { readonly kind: "fixedBytes"; readonly value: Uint8Array }
50
+ | { readonly kind: "intervalU64"; readonly start: bigint; readonly end: bigint }
51
+ | { readonly kind: "intervalI64"; readonly start: bigint; readonly end: bigint }
52
+
53
+ /**
54
+ * One literal as spelled: a plain value, or a closed relation's handle by
55
+ * name (the `| status == Frozen` spelling) — resolved by the engine through
56
+ * the selected field's newtype to the handle's declaration-order row id,
57
+ * exactly as the macro resolves it at expansion.
58
+ */
59
+ type LiteralSpec =
60
+ | { readonly kind: "value"; readonly value: ValueSpec }
61
+ | { readonly kind: "handle"; readonly handle: string }
62
+
63
+ /**
64
+ * One σ binding's right side: a single literal or a literal set (read
65
+ * disjunctively). The SDK's selection constructors make the degenerate sets
66
+ * unwritable (`oneOf` demands two leading literals), so a lowered `many`
67
+ * always carries ≥ 2 literals.
68
+ */
69
+ type LiteralSetSpec =
70
+ | { readonly kind: "one"; readonly literal: LiteralSpec }
71
+ | { readonly kind: "many"; readonly literals: readonly LiteralSpec[] }
72
+
73
+ /**
74
+ * One side of a containment or window: `R(fields… | field == literal…)`,
75
+ * all names. `projection` is π in the statement's written order (positional
76
+ * pairing with the other side); `selection` is σ as (field, literal-or-set)
77
+ * pairs, read conjunctively.
78
+ */
79
+ interface SideSpec {
80
+ readonly relation: string
81
+ readonly projection: readonly string[]
82
+ readonly selection: ReadonlyArray<readonly [string, LiteralSetSpec]>
83
+ }
84
+
85
+ /**
86
+ * A cardinality window's bounds — the canonical-utterance law's surviving
87
+ * spellings only, since the SDK's `Count` constructors make every banned
88
+ * spelling unwritable or a construction error: `exact` is `{n}` (`{0}` the
89
+ * exclusion), `range` is `{lo..hi}` with lo < hi, `floor` is `{lo..*}` with
90
+ * lo ≥ 2.
91
+ */
92
+ type WindowSpec =
93
+ | { readonly kind: "exact"; readonly n: bigint }
94
+ | { readonly kind: "range"; readonly lo: bigint; readonly hi: bigint }
95
+ | { readonly kind: "floor"; readonly lo: bigint }
96
+
97
+ /**
98
+ * One field: name, structural type, host newtype name (carried for handle
99
+ * resolution only — dropped by the engine at descriptor lowering), and the
100
+ * `fresh` mint mark.
101
+ */
102
+ interface FieldSpec {
103
+ readonly name: string
104
+ readonly valueType: ValueTypeSpec
105
+ readonly newtype: string | undefined
106
+ readonly fresh: boolean
107
+ }
108
+
109
+ /**
110
+ * One ground axiom of a closed relation: the handle plus one literal per
111
+ * declared intrinsic column, in field-declaration order (row id = index).
112
+ */
113
+ interface RowSpec {
114
+ readonly handle: string
115
+ readonly values: readonly LiteralSpec[]
116
+ }
117
+
118
+ /**
119
+ * One relation. `extension: rows` declares it closed (the option is the
120
+ * kind); a closed relation's `fields` are its declared intrinsic columns
121
+ * only — the synthetic (`id`, u64) handle field is materialized by the
122
+ * engine's schema validation. `newtype` is the handle newtype of a closed
123
+ * relation (the SDK uses the closed relation's own name), undefined on an
124
+ * ordinary one.
125
+ */
126
+ interface RelationSpec {
127
+ readonly name: string
128
+ readonly newtype: string | undefined
129
+ readonly fields: readonly FieldSpec[]
130
+ readonly extension: readonly RowSpec[] | undefined
131
+ }
132
+
133
+ /**
134
+ * One dependency statement, tagged by form. `==` is not a variant: a
135
+ * bidirectional containment is `containment` with `bidirectional: true`,
136
+ * lowered by the engine to the two adjacent containments (`source <=
137
+ * target` first). `cardinality` is B-family, target-left: the target is the
138
+ * per-group parent, the source is counted.
139
+ */
140
+ type StatementSpec =
141
+ | { readonly kind: "fd"; readonly relation: string; readonly projection: readonly string[] }
142
+ | {
143
+ readonly kind: "containment"
144
+ readonly source: SideSpec
145
+ readonly target: SideSpec
146
+ readonly bidirectional: boolean
147
+ }
148
+ | {
149
+ readonly kind: "cardinality"
150
+ readonly target: SideSpec
151
+ readonly window: WindowSpec
152
+ readonly source: SideSpec
153
+ }
154
+
155
+ /**
156
+ * The whole theory as named plain data — what `lower()` produces and what
157
+ * the bridge's `dbCreate`/`dbOpen` take. Both lists are in declaration
158
+ * order. Only DECLARED statements appear: the engine materializes the
159
+ * fresh-implied and closed auto-keys itself
160
+ * (`SchemaDescriptor::materialized_statements` — fresh keys first, closed
161
+ * auto-keys second, declared statements last), so re-stating them here
162
+ * would double them and change the fingerprint.
163
+ */
164
+ interface SchemaSpec {
165
+ readonly relations: readonly RelationSpec[]
166
+ readonly statements: readonly StatementSpec[]
167
+ }
168
+
169
+ /**
170
+ * The characters Rust's `char::escape_debug` (the engine renderer's string
171
+ * formatter, `schema/render.rs` `literal`) escapes as `\u{…}`: everything
172
+ * non-printable per rustc's generated tables — the C categories (Cc, Cf,
173
+ * Cs, Co, Cn) and the Z separators (Zs, Zl, Zp) except U+0020 itself
174
+ * (`library/core/src/unicode/printable.py`).
175
+ */
176
+ const NON_PRINTABLE = /[\p{C}\p{Z}]/u
177
+
178
+ /** Grapheme-extending characters, which `char::escape_debug` always escapes even when printable. */
179
+ const GRAPHEME_EXTEND = /\p{Grapheme_Extend}/u
180
+
181
+ /**
182
+ * One char exactly as Rust's `char::escape_debug` spells it (the engine
183
+ * renders strings char by char through it): `\0`, `\t`, `\r`, `\n`,
184
+ * backslash-escaped `\\`/`\'`/`\"`, `\u{hex}` (lowercase, unpadded) for
185
+ * grapheme-extending and non-printable chars, the char itself otherwise.
186
+ */
187
+ function escapeDebugChar(ch: string): string {
188
+ if (ch === "\0") {
189
+ return "\\0"
190
+ }
191
+ if (ch === "\t") {
192
+ return "\\t"
193
+ }
194
+ if (ch === "\r") {
195
+ return "\\r"
196
+ }
197
+ if (ch === "\n") {
198
+ return "\\n"
199
+ }
200
+ if (ch === "\\" || ch === "'" || ch === '"') {
201
+ return `\\${ch}`
202
+ }
203
+ if (GRAPHEME_EXTEND.test(ch) || (ch !== " " && NON_PRINTABLE.test(ch))) {
204
+ const codePoint = ch.codePointAt(0)
205
+ if (codePoint === undefined) {
206
+ return ch
207
+ }
208
+ return `\\u{${codePoint.toString(16)}}`
209
+ }
210
+ return ch
211
+ }
212
+
213
+ /**
214
+ * One byte exactly as Rust's `u8::escape_ascii` spells it (the engine
215
+ * renders `bytes<N>` literals byte by byte through it): `\t`, `\r`, `\n`,
216
+ * `\\`, `\'`, `\"` as two-char escapes, printable ASCII (0x20–0x7e)
217
+ * verbatim, everything else `\xNN` lowercase.
218
+ */
219
+ function escapeAsciiByte(byte: number): string {
220
+ if (byte === 0x09) {
221
+ return "\\t"
222
+ }
223
+ if (byte === 0x0d) {
224
+ return "\\r"
225
+ }
226
+ if (byte === 0x0a) {
227
+ return "\\n"
228
+ }
229
+ if (byte === 0x5c) {
230
+ return "\\\\"
231
+ }
232
+ if (byte === 0x27) {
233
+ return "\\'"
234
+ }
235
+ if (byte === 0x22) {
236
+ return '\\"'
237
+ }
238
+ if (byte >= 0x20 && byte <= 0x7e) {
239
+ return String.fromCharCode(byte)
240
+ }
241
+ return `\\x${byte.toString(16).padStart(2, "0")}`
242
+ }
243
+
244
+ /**
245
+ * Renders one lowered literal in the exact macro spelling the engine's own
246
+ * renderer uses (`schema/render.rs` `literal`): handles bare by name,
247
+ * integers as digits, `true`/`false`, intervals as `start..end`, strings
248
+ * char-escaped through the `char::escape_debug` mirror, bytes as `b"…"`
249
+ * byte-escaped through the `u8::escape_ascii` mirror — byte-for-byte the
250
+ * engine's violation canonicals, so TS-side construction errors and
251
+ * engine-side violations read identically and `renderStatement` equals the
252
+ * violation's `canonical`.
253
+ */
254
+ function renderLiteral(literal: LiteralSpec): string {
255
+ if (literal.kind === "handle") {
256
+ return literal.handle
257
+ }
258
+ const value = literal.value
259
+ switch (value.kind) {
260
+ case "bool":
261
+ return value.value ? "true" : "false"
262
+ case "u64":
263
+ case "i64":
264
+ return value.value.toString()
265
+ case "string": {
266
+ let out = '"'
267
+ for (const ch of value.value) {
268
+ out += escapeDebugChar(ch)
269
+ }
270
+ return `${out}"`
271
+ }
272
+ case "fixedBytes": {
273
+ let out = 'b"'
274
+ for (const byte of value.value) {
275
+ out += escapeAsciiByte(byte)
276
+ }
277
+ return `${out}"`
278
+ }
279
+ case "intervalU64":
280
+ case "intervalI64":
281
+ return `${value.start}..${value.end}`
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Renders one σ binding's right side: a bare literal, or a disjunctive
287
+ * literal set in braces (`{A, B}`).
288
+ */
289
+ function renderLiteralSet(set: LiteralSetSpec): string {
290
+ if (set.kind === "one") {
291
+ return renderLiteral(set.literal)
292
+ }
293
+ return `{${set.literals.map(renderLiteral).join(", ")}}`
294
+ }
295
+
296
+ /**
297
+ * Renders window bounds in their one canonical spelling: `{n}` exact
298
+ * (`{0}` the exclusion), `{lo..hi}`, `{lo..*}` — the spelling set the
299
+ * engine's renderer emits for sealed statements.
300
+ */
301
+ function renderWindow(window: WindowSpec): string {
302
+ switch (window.kind) {
303
+ case "exact":
304
+ return `{${window.n}}`
305
+ case "range":
306
+ return `{${window.lo}..${window.hi}}`
307
+ case "floor":
308
+ return `{${window.lo}..*}`
309
+ }
310
+ }
311
+
312
+ export type {
313
+ FieldSpec,
314
+ LiteralSetSpec,
315
+ LiteralSpec,
316
+ RelationSpec,
317
+ RowSpec,
318
+ SchemaSpec,
319
+ SideSpec,
320
+ StatementSpec,
321
+ ValueSpec,
322
+ ValueTypeSpec,
323
+ WindowSpec
324
+ }
325
+ export { renderLiteral, renderLiteralSet, renderWindow }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Dependency statements as typed values (`docs/architecture/30-dependencies.md`
3
+ * owns the semantics; `docs/architecture/70-api.md` the surface): the FD key
4
+ * form, conditional containment, the bidirectional `==` abbreviation, and
5
+ * the cardinality window. A statement value is opaque and inert — no
6
+ * methods, no fluent continuation: a fact about the theory, not a builder.
7
+ */
8
+
9
+ import * as errors from "@superbuilders/errors"
10
+ import { phantom } from "#brand.ts"
11
+ import type { Count } from "#count.ts"
12
+ import { type AnyFace, type FaceData, renderFace, type SameArity } from "#face.ts"
13
+ import type { AnyRelation, RelationFields } from "#relation.ts"
14
+ import { renderWindow, type WindowSpec } from "#spec.ts"
15
+
16
+ /** One statement's runtime description, tagged by form. */
17
+ type StatementData =
18
+ | { readonly kind: "key"; readonly owner: AnyRelation; readonly projection: readonly string[] }
19
+ | {
20
+ readonly kind: "containment"
21
+ readonly source: FaceData
22
+ readonly target: FaceData
23
+ readonly bidirectional: boolean
24
+ }
25
+ | {
26
+ readonly kind: "window"
27
+ readonly target: FaceData
28
+ readonly window: WindowSpec
29
+ readonly source: FaceData
30
+ }
31
+
32
+ /** An opaque statement value — what `schema()` assembles into a theory. */
33
+ interface Statement {
34
+ readonly data: StatementData
35
+ }
36
+
37
+ /**
38
+ * A `key()` statement as a TYPED value: the statement plus a phantom
39
+ * carrying its owner and projection tuple — what the key-statement-selected
40
+ * `get(relation, keyStatement, key)` overload types its key object by
41
+ * (`docs/architecture/70-api.md` § the freeze, the multi-key typed get).
42
+ * Structurally still a plain {@link Statement}; the phantom is never present
43
+ * at runtime.
44
+ */
45
+ interface KeyStatement<R extends AnyRelation, Projection extends readonly string[]> extends Statement {
46
+ readonly [phantom]?: { readonly owner: R; readonly projection: Projection }
47
+ }
48
+
49
+ /**
50
+ * `R(X) -> R` — the FD key form, composite keys as tuples. No selection
51
+ * parameter exists (the FD-with-selection shape is unrepresentable, as in
52
+ * the grammar), and only ordinary relations are accepted: a closed
53
+ * relation's key `R(id) -> R` is materialized by the engine, so an
54
+ * explicit one would only ever be a duplicate. The projection tuple is
55
+ * carried in the returned value's type ({@link KeyStatement}), so keyed
56
+ * point reads through THIS statement are typed field-for-field.
57
+ */
58
+ function key<
59
+ R extends AnyRelation,
60
+ const Projection extends readonly [keyof RelationFields<R> & string, ...(keyof RelationFields<R> & string)[]]
61
+ >(relation: R, fields: Projection): KeyStatement<R, Projection> {
62
+ if (!("fields" in relation.data)) {
63
+ throw errors.new(
64
+ `key(${relation.name}, ...): closedness already materializes ${relation.name}(id) -> ${relation.name} — an explicit key on a closed relation is rejected as a duplicate`
65
+ )
66
+ }
67
+ const data: StatementData = Object.freeze({
68
+ kind: "key",
69
+ owner: relation,
70
+ projection: Object.freeze([...fields])
71
+ })
72
+ return Object.freeze({ data })
73
+ }
74
+
75
+ /**
76
+ * `A(X|φ) <= B(Y|ψ)` — conditional inclusion, source left. The target
77
+ * side must resolve a declared key of B — DELIBERATELY judged by the
78
+ * engine at `Db.create`/`Db.open` (`SchemaError`), never re-checked here.
79
+ * Arity mismatch between the two faces is a type error ({@link SameArity}).
80
+ */
81
+ function contained<A extends AnyFace, B extends AnyFace>(source: A, target: B & SameArity<A, B>): Statement {
82
+ const data: StatementData = Object.freeze({
83
+ kind: "containment",
84
+ source: source.data,
85
+ target: target.data,
86
+ bidirectional: false
87
+ })
88
+ return Object.freeze({ data })
89
+ }
90
+
91
+ /**
92
+ * `A(X|φ) == B(Y|ψ)` — the bidirectional abbreviation, one utterance. It
93
+ * lowers to the two adjacent containments in the `A <= B` first order
94
+ * (macro parity) and renders as `==` once, in the written orientation.
95
+ */
96
+ function mirrors<A extends AnyFace, B extends AnyFace>(source: A, target: B & SameArity<A, B>): Statement {
97
+ const data: StatementData = Object.freeze({
98
+ kind: "containment",
99
+ source: source.data,
100
+ target: target.data,
101
+ bidirectional: true
102
+ })
103
+ return Object.freeze({ data })
104
+ }
105
+
106
+ /**
107
+ * `B(Y|ψ) <={window} A(X|φ)` — the cardinality window. READ CAREFULLY: the
108
+ * LEFT face is the window's TARGET, the per-group parent (B-family,
109
+ * target-left — macro parity), and the RIGHT face is the counted source.
110
+ * `window(on(Holder, "id"), atMost(3n), on(Account, "holder"))` says: each
111
+ * Holder id groups at most three Account rows by holder.
112
+ */
113
+ function window<B extends AnyFace, A extends AnyFace>(target: B, count: Count, source: A & SameArity<B, A>): Statement {
114
+ const data: StatementData = Object.freeze({
115
+ kind: "window",
116
+ target: target.data,
117
+ window: count.window,
118
+ source: source.data
119
+ })
120
+ return Object.freeze({ data })
121
+ }
122
+
123
+ /**
124
+ * Renders one statement in the CANONICAL macro spelling
125
+ * (`docs/architecture/70-api.md`; the engine's `schema/render.rs` emits the
126
+ * same shapes for violations) — `Account(id) -> Account`,
127
+ * `Account(holder) <= Holder(id)`,
128
+ * `Account(id | kind == Savings) == SavingsTerms(account)`,
129
+ * `Holder(id) <={0..3} Account(holder)` — so TS-side errors and
130
+ * engine-side diagnostics read identically. A renderer, never a parser:
131
+ * strings are output-only.
132
+ */
133
+ function renderStatement(statement: Statement): string {
134
+ const data = statement.data
135
+ switch (data.kind) {
136
+ case "key":
137
+ return `${data.owner.name}(${data.projection.join(", ")}) -> ${data.owner.name}`
138
+ case "containment": {
139
+ const operator = data.bidirectional ? "==" : "<="
140
+ return `${renderFace(data.source)} ${operator} ${renderFace(data.target)}`
141
+ }
142
+ case "window":
143
+ return `${renderFace(data.target)} <=${renderWindow(data.window)} ${renderFace(data.source)}`
144
+ }
145
+ }
146
+
147
+ export type { KeyStatement, Statement, StatementData }
148
+ export { contained, key, mirrors, renderStatement, window }