@ldclabs/kip-lang 0.4.0 → 2.0.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 (54) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/LICENSE +21 -0
  3. package/README.md +89 -64
  4. package/dist/ast.d.ts +511 -145
  5. package/dist/ast.d.ts.map +1 -1
  6. package/dist/diagnostics.d.ts +8 -2
  7. package/dist/diagnostics.d.ts.map +1 -1
  8. package/dist/diagnostics.js +32 -3
  9. package/dist/diagnostics.js.map +1 -1
  10. package/dist/exec-ast.d.ts +514 -149
  11. package/dist/exec-ast.d.ts.map +1 -1
  12. package/dist/exec-ast.js +8 -7
  13. package/dist/exec-ast.js.map +1 -1
  14. package/dist/formatter.d.ts.map +1 -1
  15. package/dist/formatter.js +870 -479
  16. package/dist/formatter.js.map +1 -1
  17. package/dist/index.d.ts +4 -4
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +2 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/lexer.d.ts.map +1 -1
  22. package/dist/lexer.js +12 -30
  23. package/dist/lexer.js.map +1 -1
  24. package/dist/lower.d.ts +1 -2
  25. package/dist/lower.d.ts.map +1 -1
  26. package/dist/lower.js +1355 -594
  27. package/dist/lower.js.map +1 -1
  28. package/dist/parser.d.ts.map +1 -1
  29. package/dist/parser.js +2410 -1300
  30. package/dist/parser.js.map +1 -1
  31. package/dist/semantics.d.ts +11 -7
  32. package/dist/semantics.d.ts.map +1 -1
  33. package/dist/semantics.js +295 -180
  34. package/dist/semantics.js.map +1 -1
  35. package/dist/token.d.ts +130 -40
  36. package/dist/token.d.ts.map +1 -1
  37. package/dist/token.js +264 -83
  38. package/dist/token.js.map +1 -1
  39. package/dist/version.d.ts +2 -2
  40. package/dist/version.js +2 -2
  41. package/package.json +35 -5
  42. package/src/ast.ts +914 -0
  43. package/src/budget.ts +108 -0
  44. package/src/diagnostics.ts +182 -0
  45. package/src/errors.ts +42 -0
  46. package/src/exec-ast.ts +614 -0
  47. package/src/formatter.ts +1339 -0
  48. package/src/index.ts +226 -0
  49. package/src/lexer.ts +459 -0
  50. package/src/lower.ts +2094 -0
  51. package/src/parser.ts +3506 -0
  52. package/src/semantics.ts +392 -0
  53. package/src/token.ts +408 -0
  54. package/src/version.ts +13 -0
package/src/lower.ts ADDED
@@ -0,0 +1,2094 @@
1
+ import type {
2
+ Program,
3
+ Statement,
4
+ MutationClause as CstMutationClause,
5
+ FindStatement,
6
+ AsOfClause,
7
+ OrderItem,
8
+ WhereClause as CstWhereClause,
9
+ WherePattern,
10
+ PropositionTuple,
11
+ Term as CstTerm,
12
+ PredicateAtom,
13
+ RawPredicateExpression,
14
+ ObjectPattern,
15
+ ObjectLiteral,
16
+ ObjectEntry,
17
+ Expression,
18
+ ScalarValue,
19
+ SchemaSymbol,
20
+ TargetRef,
21
+ MutateStatement,
22
+ CreateConceptStatement,
23
+ UpsertConceptStatement,
24
+ EnsurePropositionStatement,
25
+ AssertStatement,
26
+ CreateEvidenceStatement,
27
+ CreateAssertionStatement,
28
+ CreateActivityStatement,
29
+ UpdateStatement as CstUpdateStatement,
30
+ UpdateAction as CstUpdateAction,
31
+ SetFacetClause,
32
+ SetStructuralClause,
33
+ UnsetStructuralClause,
34
+ UnsetField,
35
+ RetractAssertionStatement,
36
+ SupersedeAssertionStatement,
37
+ CorrectEvidenceStatement,
38
+ TransitionActivityStatement,
39
+ SetRetentionStatement,
40
+ ArchiveStatement,
41
+ TombstoneStatement,
42
+ PurgeStatement,
43
+ MergeConceptStatement,
44
+ DescribeStatement,
45
+ ListStatement,
46
+ SearchStatement,
47
+ VerifyStatement,
48
+ ValidateStatement,
49
+ PreviewStatement,
50
+ HistoryStatement,
51
+ ChangesStatement,
52
+ SnapshotStatement,
53
+ ExportCapsuleStatement,
54
+ NumberLiteral
55
+ } from './ast.js'
56
+ import { invalidSyntax } from './errors.js'
57
+ import type {
58
+ AggregationFunction,
59
+ Assignments,
60
+ AsOf,
61
+ BeliefTarget,
62
+ Command,
63
+ ComparisonOperator,
64
+ ConceptCreate,
65
+ ConceptUpsert,
66
+ DescribeTarget,
67
+ DotPathVar,
68
+ ElementRef,
69
+ EnsureProposition,
70
+ FacetAssignment,
71
+ FacetUnset,
72
+ FilterExpression,
73
+ FilterFunction,
74
+ FilterOperand,
75
+ FindExpression,
76
+ HistoryCommand,
77
+ BoundValue,
78
+ KipValue,
79
+ KmlStatement,
80
+ KqlQuery,
81
+ ListTarget,
82
+ MatchValue,
83
+ MetaCommand,
84
+ MutationClause,
85
+ MutationValue,
86
+ ObjectMatcher,
87
+ OrderByItem,
88
+ PathStep,
89
+ PredAtom,
90
+ PredPathAtom,
91
+ PredTerm,
92
+ PropositionMatcher,
93
+ RecordCreate,
94
+ Scalar,
95
+ SearchTarget,
96
+ StructuralEdge,
97
+ StructuralRemoval,
98
+ SymbolRef,
99
+ Term,
100
+ UpdateAction,
101
+ UpdateExpr,
102
+ UpdateFunction,
103
+ ValidateTarget,
104
+ VerifyTarget,
105
+ WhereClause
106
+ } from './exec-ast.js'
107
+ import type { Range } from './token.js'
108
+
109
+ const AGGREGATIONS = new Map<string, AggregationFunction>([
110
+ ['COUNT', 'Count'],
111
+ ['SUM', 'Sum'],
112
+ ['AVG', 'Avg'],
113
+ ['MIN', 'Min'],
114
+ ['MAX', 'Max']
115
+ ])
116
+
117
+ const FILTER_FUNCTIONS = new Map<string, FilterFunction>([
118
+ ['CONTAINS', 'Contains'],
119
+ ['STARTS_WITH', 'StartsWith'],
120
+ ['ENDS_WITH', 'EndsWith'],
121
+ ['REGEX', 'Regex'],
122
+ ['IN', 'In'],
123
+ ['IS_NULL', 'IsNull'],
124
+ ['IS_NOT_NULL', 'IsNotNull'],
125
+ ['IS_LITERAL', 'IsLiteral'],
126
+ ['IS_ELEMENT', 'IsElement'],
127
+ ['IS_KIND', 'IsKind'],
128
+ ['LITERAL_TYPE', 'LiteralType']
129
+ ])
130
+
131
+ const UPDATE_FUNCTIONS = new Map<string, UpdateFunction>([
132
+ ['ADD', 'Add'],
133
+ ['MUL', 'Mul'],
134
+ ['CLAMP', 'Clamp'],
135
+ ['COALESCE', 'Coalesce']
136
+ ])
137
+
138
+ const UPDATE_ARITY: Record<UpdateFunction, number> = {
139
+ Add: 2,
140
+ Mul: 2,
141
+ Coalesce: 2,
142
+ Clamp: 3
143
+ }
144
+
145
+ const COMPARISONS = new Map<string, ComparisonOperator>([
146
+ ['==', 'Equal'],
147
+ ['!=', 'NotEqual'],
148
+ ['<', 'LessThan'],
149
+ ['>', 'GreaterThan'],
150
+ ['<=', 'LessEqual'],
151
+ ['>=', 'GreaterEqual']
152
+ ])
153
+
154
+ /**
155
+ * Engine-owned state no cognitive mutation may write (Spec §6.3, §2.11).
156
+ *
157
+ * These are checked by name on every mutation, not just on UPDATE: author
158
+ * content that could rewrite engine truth or its own authority is exactly
159
+ * what "external cognition cannot self-escalate authority" forbids.
160
+ */
161
+ const PROTECTED_FIELDS = new Set([
162
+ '_system',
163
+ 'governance',
164
+ 'space_id',
165
+ 'space_seq'
166
+ ])
167
+
168
+ /**
169
+ * Assertion payload that is immutable after creation (Spec §13.7).
170
+ *
171
+ * Changing epistemic commitment means a new Assertion plus supersession, so
172
+ * an UPDATE naming one of these is the `EpistemicRevisionRequired` mistake
173
+ * caught statically wherever the WHERE block says the target is an Assertion.
174
+ */
175
+ const ASSERTION_IMMUTABLE = new Set([
176
+ 'proposition_id',
177
+ 'proposition',
178
+ 'asserted_by',
179
+ 'stance',
180
+ 'mode',
181
+ 'confidence',
182
+ 'asserted_at',
183
+ 'valid_time',
184
+ 'evidence_refs'
185
+ ])
186
+
187
+ /** Evidence payload and observation identity are immutable (Spec §15.5). */
188
+ const EVIDENCE_IMMUTABLE = new Set([
189
+ 'evidence_class',
190
+ 'payload',
191
+ 'content_digest',
192
+ 'media_type',
193
+ 'observed_at'
194
+ ])
195
+
196
+ /** A Proposition tuple is immutable after creation (Spec §12.5). */
197
+ const PROPOSITION_IMMUTABLE = new Set(['subject', 'predicate', 'object'])
198
+
199
+ /**
200
+ * Lowers a parsed program carrying exactly one command.
201
+ *
202
+ * KIP's request envelope binds one command to one result, so a source text
203
+ * that holds two statements is not a command — it is a batch, and silently
204
+ * running the first would answer a question the caller did not ask. Use
205
+ * {@link lowerAll} for multi-statement text such as a schema capsule.
206
+ *
207
+ * @throws {KipSyntaxError} on anything that is not one executable command.
208
+ */
209
+ export function lower(program: Program): Command {
210
+ const [first, second] = program.statements
211
+ if (!first) {
212
+ throw invalidSyntax('expected a KIP command, found none')
213
+ }
214
+ if (second) {
215
+ throw invalidSyntax(
216
+ 'expected one KIP command, found several: wrap consecutive mutations in ' +
217
+ 'MUTATE { ... } to make them one transaction, or use lowerAll for a batch',
218
+ second.range
219
+ )
220
+ }
221
+ return lowerStatement(first)
222
+ }
223
+
224
+ /** Lowers every statement in a multi-command source text. */
225
+ export function lowerAll(program: Program): Command[] {
226
+ if (program.statements.length === 0) {
227
+ throw invalidSyntax('expected at least one KIP command, found none')
228
+ }
229
+ return program.statements.map(lowerStatement)
230
+ }
231
+
232
+ export function lowerStatement(stmt: Statement): Command {
233
+ switch (stmt.kind) {
234
+ case 'FindStatement':
235
+ return { Kql: lowerFind(stmt) }
236
+
237
+ case 'MutateStatement':
238
+ return { Kml: lowerMutate(stmt) }
239
+
240
+ case 'CreateConceptStatement':
241
+ case 'UpsertConceptStatement':
242
+ case 'EnsurePropositionStatement':
243
+ case 'AssertStatement':
244
+ case 'CreateEvidenceStatement':
245
+ case 'CreateAssertionStatement':
246
+ case 'CreateActivityStatement':
247
+ case 'UpdateStatement':
248
+ case 'RetractAssertionStatement':
249
+ case 'SupersedeAssertionStatement':
250
+ case 'CorrectEvidenceStatement':
251
+ case 'TransitionActivityStatement':
252
+ case 'SetRetentionStatement':
253
+ case 'ArchiveStatement':
254
+ case 'TombstoneStatement':
255
+ case 'PurgeStatement':
256
+ case 'MergeConceptStatement':
257
+ const clauses = lowerMutationClause(stmt, 0)
258
+ assertUniqueHandles(clauses, stmt.range)
259
+ assertResolvedHandles(clauses, stmt.range)
260
+ return {
261
+ Kml: {
262
+ explicit_transaction: false,
263
+ clauses
264
+ }
265
+ }
266
+
267
+ default:
268
+ return { Meta: lowerMeta(stmt) }
269
+ }
270
+ }
271
+
272
+ // ---------------------------------------------------------------------------
273
+ // KQL
274
+ // ---------------------------------------------------------------------------
275
+
276
+ function lowerFind(stmt: FindStatement): KqlQuery {
277
+ if (stmt.projections.length === 0) {
278
+ throw invalidSyntax('FIND requires at least one projection', stmt.range)
279
+ }
280
+ return {
281
+ find_clause: {
282
+ expressions: stmt.projections.map((p) => lowerFindExpression(p))
283
+ },
284
+ where_clauses: lowerWhere(stmt.where),
285
+ as_of: stmt.asOf ? lowerAsOf(stmt.asOf) : null,
286
+ for_time: stmt.forTime ? lowerScalar(stmt.forTime.value) : null,
287
+ epistemic: stmt.epistemic ? lowerBoundObject(stmt.epistemic.options) : null,
288
+ order_by: stmt.orderBy ? stmt.orderBy.items.map(lowerOrderItem) : null,
289
+ limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
290
+ cursor: stmt.cursor ? lowerScalar(stmt.cursor.value) : null
291
+ }
292
+ }
293
+
294
+ function lowerAsOf(clause: AsOfClause): AsOf {
295
+ const value = lowerScalar(clause.value)
296
+ switch (clause.basis) {
297
+ case 'SEQ':
298
+ return { Seq: value }
299
+ case 'TX':
300
+ return { Tx: value }
301
+ case 'TIME':
302
+ return { Time: value }
303
+ }
304
+ }
305
+
306
+ function lowerFindExpression(expr: Expression): FindExpression {
307
+ if (expr.kind === 'AggregateExpr') {
308
+ const func = AGGREGATIONS.get(expr.name.toUpperCase())
309
+ if (!func) {
310
+ throw invalidSyntax(`unknown aggregate ${expr.name}`, expr.range)
311
+ }
312
+ return {
313
+ Aggregation: {
314
+ func,
315
+ var: lowerDotPath(expr.argument),
316
+ distinct: expr.distinct
317
+ }
318
+ }
319
+ }
320
+ return { Variable: lowerDotPath(expr) }
321
+ }
322
+
323
+ function lowerOrderItem(item: OrderItem): OrderByItem {
324
+ const direction = item.direction === 'DESC' ? 'Desc' : 'Asc'
325
+ if (item.expression.kind === 'AggregateExpr') {
326
+ const func = AGGREGATIONS.get(item.expression.name.toUpperCase())
327
+ if (!func) {
328
+ throw invalidSyntax(
329
+ `unknown aggregate ${item.expression.name}`,
330
+ item.expression.range
331
+ )
332
+ }
333
+ return {
334
+ variable: lowerDotPath(item.expression.argument),
335
+ direction,
336
+ aggregation: func
337
+ }
338
+ }
339
+ return {
340
+ variable: lowerDotPath(item.expression),
341
+ direction,
342
+ aggregation: null
343
+ }
344
+ }
345
+
346
+ /** A projection or sort key must resolve to one variable plus a path. */
347
+ function lowerDotPath(expr: Expression): DotPathVar {
348
+ if (expr.kind === 'VariableRef') {
349
+ return { var: varName(expr.name, expr.range), path: [] }
350
+ }
351
+ if (expr.kind === 'FieldAccess') {
352
+ const path: PathStep[] = expr.steps.map((step) =>
353
+ step.kind === 'DotStep'
354
+ ? { Field: step.name }
355
+ : { Key: step.key.parsed }
356
+ )
357
+ return { var: varName(expr.base.name, expr.base.range), path }
358
+ }
359
+ throw invalidSyntax(
360
+ `expected a variable or a dot path, found ${describeExpression(expr)}`,
361
+ expr.range
362
+ )
363
+ }
364
+
365
+ function lowerWhere(clause: { patterns: WherePattern[] }): WhereClause[] {
366
+ return clause.patterns.map(lowerWherePattern)
367
+ }
368
+
369
+ function lowerWherePattern(pattern: WherePattern): WhereClause {
370
+ switch (pattern.kind) {
371
+ case 'ConceptPattern':
372
+ return {
373
+ Concept: {
374
+ variable: varName(pattern.variable.name, pattern.variable.range),
375
+ matcher: lowerObjectMatcher(pattern.matcher)
376
+ }
377
+ }
378
+
379
+ case 'PropositionPattern':
380
+ return {
381
+ Proposition: {
382
+ variable: pattern.variable
383
+ ? varName(pattern.variable.name, pattern.variable.range)
384
+ : null,
385
+ matcher: lowerPropositionMatcher(pattern.tuple)
386
+ }
387
+ }
388
+
389
+ case 'AssertionPattern':
390
+ return {
391
+ Assertion: {
392
+ variable: varName(pattern.variable.name, pattern.variable.range),
393
+ matcher: lowerObjectMatcher(pattern.matcher)
394
+ }
395
+ }
396
+
397
+ case 'EvidencePattern':
398
+ return {
399
+ Evidence: {
400
+ variable: varName(pattern.variable.name, pattern.variable.range),
401
+ matcher: lowerObjectMatcher(pattern.matcher)
402
+ }
403
+ }
404
+
405
+ case 'ActivityPattern':
406
+ return {
407
+ Activity: {
408
+ variable: varName(pattern.variable.name, pattern.variable.range),
409
+ matcher: lowerObjectMatcher(pattern.matcher)
410
+ }
411
+ }
412
+
413
+ case 'StructuralPattern':
414
+ return {
415
+ Structural: {
416
+ variable: pattern.variable
417
+ ? varName(pattern.variable.name, pattern.variable.range)
418
+ : null,
419
+ subject: lowerTerm(pattern.subject),
420
+ field: lowerSymbol(pattern.field),
421
+ object: lowerTerm(pattern.object)
422
+ }
423
+ }
424
+
425
+ case 'BeliefPattern': {
426
+ let target: BeliefTarget
427
+ if (pattern.proposition) {
428
+ target = {
429
+ Proposition: varName(
430
+ pattern.proposition.name,
431
+ pattern.proposition.range
432
+ )
433
+ }
434
+ } else if (pattern.propositionId) {
435
+ // Same slot, same reference form as `?p PROPOSITION (id: ...)`.
436
+ target = { Id: lowerScalar(pattern.propositionId) }
437
+ } else {
438
+ if (!pattern.subject || !pattern.predicate || !pattern.object) {
439
+ throw invalidSyntax(
440
+ 'BELIEF requires one bound Proposition, an (id: ...) reference, or a full (subject, predicate, object) tuple',
441
+ pattern.range
442
+ )
443
+ }
444
+ target = {
445
+ Tuple: {
446
+ subject: lowerPropositionSubject(pattern.subject),
447
+ predicate: { Atom: lowerPredAtom(pattern.predicate) },
448
+ object: lowerTerm(pattern.object)
449
+ }
450
+ }
451
+ }
452
+ return {
453
+ Belief: {
454
+ variable: varName(pattern.variable.name, pattern.variable.range),
455
+ target
456
+ }
457
+ }
458
+ }
459
+
460
+ case 'BeliefSlotPattern':
461
+ return {
462
+ BeliefSlot: {
463
+ variable: varName(pattern.variable.name, pattern.variable.range),
464
+ subject: lowerPropositionSubject(pattern.subject),
465
+ predicate: lowerPredAtom(pattern.predicate)
466
+ }
467
+ }
468
+
469
+ case 'FilterClause':
470
+ return { Filter: { expression: lowerFilter(pattern.expression) } }
471
+
472
+ case 'NotClause':
473
+ return { Not: pattern.patterns.map(lowerWherePattern) }
474
+
475
+ case 'OptionalClause':
476
+ return { Optional: pattern.patterns.map(lowerWherePattern) }
477
+
478
+ case 'UnionClause':
479
+ return { Union: pattern.patterns.map(lowerWherePattern) }
480
+ }
481
+ }
482
+
483
+ function lowerPropositionMatcher(tuple: PropositionTuple): PropositionMatcher {
484
+ if (tuple.id) return { Id: lowerScalar(tuple.id) }
485
+ if (!tuple.subject || !tuple.predicate || !tuple.object) {
486
+ throw invalidSyntax(
487
+ 'a Proposition expression is either (subject, predicate, object) or (id: ...)',
488
+ tuple.range
489
+ )
490
+ }
491
+ return {
492
+ Tuple: {
493
+ subject: lowerPropositionSubject(tuple.subject),
494
+ predicate: lowerPredicate(tuple.predicate),
495
+ object: lowerTerm(tuple.object)
496
+ }
497
+ }
498
+ }
499
+
500
+ /**
501
+ * Resolves the tuple a resolve-or-create statement needs.
502
+ *
503
+ * `(id: ...)` is match-only: it names a Proposition that must already exist,
504
+ * so it cannot drive ENSURE PROPOSITION — or the ASSERT sugar that desugars
505
+ * through it — whose job is to create the tuple when it is absent.
506
+ */
507
+ function requireStructuralTuple(
508
+ tuple: PropositionTuple,
509
+ statement: string
510
+ ): { subject: Term; predicate: PredAtom; object: Term } {
511
+ if (tuple.id) {
512
+ throw invalidSyntax(
513
+ `${statement} needs a (subject, predicate, object) tuple: (id: ...) only matches an ` +
514
+ 'existing Proposition, and no structure can be created from an id',
515
+ tuple.range
516
+ )
517
+ }
518
+ const predicateExpression = tuple.predicate!
519
+ const predicate = lowerPredicate(predicateExpression)
520
+ if (!('Atom' in predicate)) {
521
+ throw invalidSyntax(
522
+ `${statement} needs one exact predicate; alternation and hop quantifiers are KQL traversal forms`,
523
+ tuple.predicate!.range
524
+ )
525
+ }
526
+ if ('Variable' in predicate.Atom) {
527
+ throw invalidSyntax(
528
+ `${statement} needs an exact quoted predicate or :parameter; ?variables are KQL read-pattern syntax`,
529
+ predicateExpression.range
530
+ )
531
+ }
532
+ return {
533
+ subject: lowerPropositionSubject(tuple.subject!),
534
+ predicate: predicate.Atom,
535
+ object: lowerTerm(tuple.object!)
536
+ }
537
+ }
538
+
539
+ function lowerPredicate(expr: RawPredicateExpression): PredTerm {
540
+ const [only] = expr.atoms
541
+ if (expr.atoms.length === 1 && only && !only.quantifier) {
542
+ return { Atom: lowerPredAtom(only.atom) }
543
+ }
544
+ const path: PredPathAtom[] = expr.atoms.map((atom) => ({
545
+ predicate: lowerPredAtom(atom.atom),
546
+ hops: atom.quantifier
547
+ ? { min: atom.quantifier.min, max: atom.quantifier.max ?? null }
548
+ : null
549
+ }))
550
+ return { Path: path }
551
+ }
552
+
553
+ function lowerPredAtom(atom: PredicateAtom): PredAtom {
554
+ switch (atom.kind) {
555
+ case 'StringLiteral':
556
+ return { Literal: atom.parsed }
557
+ case 'ParameterRef':
558
+ return { Param: paramName(atom.name) }
559
+ case 'VariableRef':
560
+ return { Variable: varName(atom.name, atom.range) }
561
+ }
562
+ }
563
+
564
+ function lowerTerm(term: CstTerm): Term {
565
+ switch (term.kind) {
566
+ case 'VariableRef':
567
+ return { Variable: varName(term.name, term.range) }
568
+ case 'ParameterRef':
569
+ return { Param: paramName(term.name) }
570
+ case 'ObjectPattern':
571
+ return { Match: lowerObjectMatcher(term) }
572
+ case 'PropositionTuple':
573
+ return { Proposition: lowerPropositionMatcher(term) }
574
+ default:
575
+ return { Literal: lowerKipValue(term) }
576
+ }
577
+ }
578
+
579
+ /** A Proposition subject is always an Element reference, never a Literal. */
580
+ function lowerPropositionSubject(term: CstTerm): Term {
581
+ switch (term.kind) {
582
+ case 'StringLiteral':
583
+ case 'NumberLiteral':
584
+ case 'BooleanLiteral':
585
+ case 'NullLiteral':
586
+ throw invalidSyntax(
587
+ 'a Proposition subject must be a local Element reference, never a Literal',
588
+ term.range
589
+ )
590
+ default:
591
+ return lowerTerm(term)
592
+ }
593
+ }
594
+
595
+ function lowerObjectMatcher(pattern: ObjectPattern): ObjectMatcher {
596
+ const matcher: ObjectMatcher = {}
597
+ for (const member of pattern.members) {
598
+ if (Object.prototype.hasOwnProperty.call(matcher, member.key)) {
599
+ throw invalidSyntax(
600
+ `duplicate match field ${member.key}`,
601
+ member.range
602
+ )
603
+ }
604
+ matcher[member.key] = lowerMatchValue(member.value)
605
+ }
606
+ return matcher
607
+ }
608
+
609
+ function lowerMatchValue(expr: Expression): MatchValue {
610
+ switch (expr.kind) {
611
+ case 'VariableRef':
612
+ return { Variable: varName(expr.name, expr.range) }
613
+ case 'ParameterRef':
614
+ return { Param: paramName(expr.name) }
615
+ case 'ArrayLiteral':
616
+ return { Array: expr.elements.map(lowerMatchValue) }
617
+ case 'ObjectPattern':
618
+ return { Match: lowerObjectMatcher(expr) }
619
+ case 'PropositionTuple':
620
+ return { Proposition: lowerPropositionMatcher(expr) }
621
+ default:
622
+ return { Literal: lowerKipValue(expr) }
623
+ }
624
+ }
625
+
626
+ // ---------------------------------------------------------------------------
627
+ // Filters
628
+ // ---------------------------------------------------------------------------
629
+
630
+ function lowerFilter(expr: Expression): FilterExpression {
631
+ switch (expr.kind) {
632
+ case 'BinaryExpression': {
633
+ if (expr.operator === '&&' || expr.operator === '||') {
634
+ return {
635
+ Logical: {
636
+ left: lowerFilter(expr.left),
637
+ operator: expr.operator === '&&' ? 'And' : 'Or',
638
+ right: lowerFilter(expr.right)
639
+ }
640
+ }
641
+ }
642
+ const operator = COMPARISONS.get(expr.operator)
643
+ if (!operator) {
644
+ throw invalidSyntax(
645
+ `unknown comparison operator ${expr.operator}`,
646
+ expr.range
647
+ )
648
+ }
649
+ return {
650
+ Comparison: {
651
+ left: lowerFilterOperand(expr.left),
652
+ operator,
653
+ right: lowerFilterOperand(expr.right)
654
+ }
655
+ }
656
+ }
657
+
658
+ case 'UnaryExpression':
659
+ if (expr.operator === '!') {
660
+ return { Not: lowerFilter(expr.operand) }
661
+ }
662
+ throw invalidSyntax(
663
+ 'a filter must be a comparison, a logical combination, a negation or a function call',
664
+ expr.range
665
+ )
666
+
667
+ case 'FunctionCallExpr': {
668
+ const func = FILTER_FUNCTIONS.get(expr.name.toUpperCase())
669
+ if (!func) {
670
+ throw invalidSyntax(
671
+ `${expr.name} is not a KIP filter function`,
672
+ expr.range
673
+ )
674
+ }
675
+ return {
676
+ Function: { func, args: expr.args.map(lowerFilterOperand) }
677
+ }
678
+ }
679
+
680
+ case 'AggregateExpr':
681
+ // An aggregate summarizes a solution set; a filter runs per candidate
682
+ // row, so there is no set for it to summarize yet.
683
+ throw invalidSyntax(
684
+ `${expr.name} is an aggregate and cannot appear inside FILTER`,
685
+ expr.range
686
+ )
687
+
688
+ default:
689
+ throw invalidSyntax(
690
+ `a filter must be a comparison, a logical combination, a negation or a function call, found ${describeExpression(expr)}`,
691
+ expr.range
692
+ )
693
+ }
694
+ }
695
+
696
+ function lowerFilterOperand(expr: Expression): FilterOperand {
697
+ switch (expr.kind) {
698
+ case 'VariableRef':
699
+ case 'FieldAccess':
700
+ return { Variable: lowerDotPath(expr) }
701
+ case 'ParameterRef':
702
+ return { Param: paramName(expr.name) }
703
+ case 'ArrayLiteral':
704
+ if (expr.trailingComma) {
705
+ throw invalidSyntax(
706
+ 'a filter list does not allow a trailing comma',
707
+ expr.range
708
+ )
709
+ }
710
+ return { List: expr.elements.map(lowerFilterOperand) }
711
+ case 'UnaryExpression':
712
+ if (expr.operator === '-') {
713
+ return { Negate: lowerFilterOperand(expr.operand) }
714
+ }
715
+ throw invalidSyntax(
716
+ `expected a filter operand, found ${describeExpression(expr)}`,
717
+ expr.range
718
+ )
719
+ case 'AggregateExpr':
720
+ // An aggregate summarizes a solution set; a filter runs per candidate
721
+ // row, so there is no set for it to summarize yet.
722
+ throw invalidSyntax(
723
+ `${expr.name} is an aggregate and cannot appear inside FILTER`,
724
+ expr.range
725
+ )
726
+ case 'FunctionCallExpr':
727
+ case 'BinaryExpression':
728
+ throw invalidSyntax(
729
+ `expected a filter operand, found ${describeExpression(expr)}`,
730
+ expr.range
731
+ )
732
+ default:
733
+ return { Literal: lowerKipValue(expr) }
734
+ }
735
+ }
736
+
737
+ // ---------------------------------------------------------------------------
738
+ // KML
739
+ // ---------------------------------------------------------------------------
740
+
741
+ function lowerMutate(stmt: MutateStatement): KmlStatement {
742
+ if (stmt.clauses.length === 0) {
743
+ throw invalidSyntax('MUTATE requires at least one mutation', stmt.range)
744
+ }
745
+ const clauses = stmt.clauses.flatMap((clause, i) =>
746
+ lowerMutationClause(clause, i)
747
+ )
748
+ assertUniqueHandles(clauses, stmt.range)
749
+ assertResolvedHandles(clauses, stmt.range)
750
+ return { explicit_transaction: true, clauses }
751
+ }
752
+
753
+ /**
754
+ * Handles are block-local names. Two clauses claiming the same handle make
755
+ * every forward reference to it ambiguous, so the whole plan is rejected
756
+ * rather than resolved by position.
757
+ */
758
+ function assertUniqueHandles(clauses: MutationClause[], range: Range): void {
759
+ const seen = new Set<string>()
760
+ for (const clause of clauses) {
761
+ const handle = handleOf(clause)
762
+ if (handle === null) continue
763
+ if (seen.has(handle)) {
764
+ throw invalidSyntax(
765
+ `duplicate local handle ?${handle} in one mutation plan`,
766
+ range
767
+ )
768
+ }
769
+ seen.add(handle)
770
+ }
771
+ }
772
+
773
+ function handleOf(clause: MutationClause): string | null {
774
+ if ('CreateConcept' in clause) return clause.CreateConcept.handle
775
+ if ('UpsertConcept' in clause) return clause.UpsertConcept.handle
776
+ if ('CreateEvidence' in clause) return clause.CreateEvidence.handle
777
+ if ('CreateAssertion' in clause) return clause.CreateAssertion.handle
778
+ if ('CreateActivity' in clause) return clause.CreateActivity.handle
779
+ if ('EnsureProposition' in clause) return clause.EnsureProposition.handle
780
+ return null
781
+ }
782
+
783
+ /**
784
+ * Every executable `Handle` must be created by this mutation plan or bound by
785
+ * that clause's WHERE. Parameters remain runtime bindings and are unaffected.
786
+ */
787
+ function assertResolvedHandles(clauses: MutationClause[], range: Range): void {
788
+ const planHandles = new Set<string>()
789
+ for (const clause of clauses) {
790
+ const handle = handleOf(clause)
791
+ if (handle !== null) planHandles.add(handle)
792
+ }
793
+
794
+ for (const clause of clauses) {
795
+ const allowed = new Set(planHandles)
796
+ const body = Object.values(clause)[0] as Record<string, unknown>
797
+ collectWhereVariables(body.where_clauses, allowed)
798
+
799
+ const referenced = new Set<string>()
800
+ collectTaggedHandles(body, referenced)
801
+ for (const handle of referenced) {
802
+ if (!allowed.has(handle)) {
803
+ throw invalidSyntax(
804
+ `?${handle} is not bound by this command's mutation outputs or WHERE clause`,
805
+ range
806
+ )
807
+ }
808
+ }
809
+ }
810
+ }
811
+
812
+ function collectWhereVariables(value: unknown, out: Set<string>): void {
813
+ if (Array.isArray(value)) {
814
+ for (const item of value) collectWhereVariables(item, out)
815
+ return
816
+ }
817
+ if (!value || typeof value !== 'object') return
818
+ const record = value as Record<string, unknown>
819
+ if (typeof record.variable === 'string') out.add(record.variable)
820
+ // Pattern terms and predicate atoms use the tagged `{ Variable: name }`
821
+ // shape. Filter operands also use `Variable`, but carry a DotPathVar object
822
+ // rather than a string, so they cannot accidentally introduce a binding.
823
+ if (typeof record.Variable === 'string') out.add(record.Variable)
824
+ for (const child of Object.values(record)) collectWhereVariables(child, out)
825
+ }
826
+
827
+ function collectTaggedHandles(value: unknown, out: Set<string>): void {
828
+ if (Array.isArray(value)) {
829
+ for (const item of value) collectTaggedHandles(item, out)
830
+ return
831
+ }
832
+ if (!value || typeof value !== 'object') return
833
+ const record = value as Record<string, unknown>
834
+ if (typeof record.Handle === 'string') out.add(record.Handle)
835
+ for (const child of Object.values(record)) collectTaggedHandles(child, out)
836
+ }
837
+
838
+ /**
839
+ * One source statement may lower to several clauses; `ASSERT` is the case.
840
+ *
841
+ * `seq` is the clause's position in its plan, used only to keep synthetic
842
+ * handles distinct between two handle-less ASSERTs in the same transaction.
843
+ */
844
+ function lowerMutationClause(
845
+ stmt: CstMutationClause,
846
+ seq: number
847
+ ): MutationClause[] {
848
+ switch (stmt.kind) {
849
+ case 'CreateConceptStatement':
850
+ return [{ CreateConcept: lowerCreateConcept(stmt) }]
851
+ case 'UpsertConceptStatement':
852
+ return [{ UpsertConcept: lowerUpsertConcept(stmt) }]
853
+ case 'EnsurePropositionStatement':
854
+ return [{ EnsureProposition: lowerEnsureProposition(stmt) }]
855
+ case 'AssertStatement':
856
+ return lowerAssertSugar(stmt, seq)
857
+ case 'CreateEvidenceStatement':
858
+ return [{ CreateEvidence: lowerRecordCreate(stmt) }]
859
+ case 'CreateAssertionStatement':
860
+ return [{ CreateAssertion: lowerRecordCreate(stmt) }]
861
+ case 'CreateActivityStatement':
862
+ return [{ CreateActivity: lowerRecordCreate(stmt) }]
863
+ case 'UpdateStatement':
864
+ return [{ Update: lowerUpdate(stmt) }]
865
+ case 'RetractAssertionStatement':
866
+ return [
867
+ {
868
+ RetractAssertion: {
869
+ target: lowerElementRef(stmt.target),
870
+ where_clauses: stmt.where ? lowerWhere(stmt.where) : null,
871
+ limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
872
+ expect_state: stmt.expectState
873
+ ? lowerScalar(stmt.expectState.value)
874
+ : null
875
+ }
876
+ }
877
+ ]
878
+ case 'SupersedeAssertionStatement':
879
+ return [
880
+ {
881
+ SupersedeAssertion: {
882
+ target: lowerElementRef(stmt.target),
883
+ by: lowerElementRef(stmt.by),
884
+ expect_state: stmt.expectState
885
+ ? lowerScalar(stmt.expectState.value)
886
+ : null
887
+ }
888
+ }
889
+ ]
890
+ case 'CorrectEvidenceStatement':
891
+ return [
892
+ {
893
+ CorrectEvidence: {
894
+ target: lowerElementRef(stmt.target),
895
+ by: lowerElementRef(stmt.by),
896
+ expect_state: stmt.expectState
897
+ ? lowerScalar(stmt.expectState.value)
898
+ : null
899
+ }
900
+ }
901
+ ]
902
+ case 'TransitionActivityStatement':
903
+ return [{ TransitionActivity: lowerTransition(stmt) }]
904
+ case 'SetRetentionStatement':
905
+ return [
906
+ {
907
+ SetRetention: {
908
+ target: lowerElementRef(stmt.target),
909
+ values: lowerAssignments(stmt.assignments, null),
910
+ where_clauses: stmt.where ? lowerWhere(stmt.where) : null,
911
+ limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
912
+ expect_version: stmt.expectVersion
913
+ ? lowerScalar(stmt.expectVersion.value)
914
+ : null
915
+ }
916
+ }
917
+ ]
918
+ case 'ArchiveStatement':
919
+ return [{ Archive: lowerRemoval(stmt) }]
920
+ case 'TombstoneStatement':
921
+ return [{ Tombstone: lowerRemoval(stmt) }]
922
+ case 'PurgeStatement':
923
+ return [{ Purge: lowerPurge(stmt) }]
924
+ case 'MergeConceptStatement':
925
+ return [
926
+ {
927
+ MergeConcept: {
928
+ source: lowerElementRef(stmt.source),
929
+ into: lowerElementRef(stmt.into),
930
+ where_clauses: stmt.where ? lowerWhere(stmt.where) : null,
931
+ expect_version: stmt.expectVersion
932
+ ? lowerScalar(stmt.expectVersion.value)
933
+ : null
934
+ }
935
+ }
936
+ ]
937
+ }
938
+ }
939
+
940
+ function lowerCreateConcept(stmt: CreateConceptStatement): ConceptCreate {
941
+ return {
942
+ handle: varName(stmt.handle.name, stmt.handle.range),
943
+ type: stmt.type ? lowerSymbol(stmt.type.value) : null,
944
+ client_key: stmt.clientKey ? lowerScalar(stmt.clientKey.value) : null,
945
+ name: stmt.name ? lowerScalar(stmt.name.value) : null,
946
+ set_fields: stmt.setFields
947
+ ? lowerAssignments(stmt.setFields.assignments, null)
948
+ : null,
949
+ set_attributes: stmt.setAttributes
950
+ ? lowerAssignments(stmt.setAttributes.assignments, null)
951
+ : null,
952
+ set_facets: stmt.setFacets.map((f) => lowerFacet(f, null)),
953
+ set_structural: stmt.setStructural
954
+ ? lowerStructural(stmt.setStructural, null)
955
+ : null
956
+ }
957
+ }
958
+
959
+ /**
960
+ * Whether a matcher pins exactly one Concept.
961
+ *
962
+ * Only `id` and `key` identify, and only when they carry a value that is one
963
+ * value: a literal, or a parameter the runtime binds to one. Anything else —
964
+ * a variable, a nested pattern, a list — describes candidates.
965
+ */
966
+ function hasStableIdentity(match: ObjectMatcher): boolean {
967
+ for (const field of ['id', 'key']) {
968
+ const value = match[field]
969
+ if (value && ('Literal' in value || 'Param' in value)) return true
970
+ }
971
+ return false
972
+ }
973
+
974
+ function lowerUpsertConcept(stmt: UpsertConceptStatement): ConceptUpsert {
975
+ const match = stmt.match ? lowerObjectMatcher(stmt.match.pattern) : null
976
+
977
+ // Identity for an upsert is `id` or `key`, spelled as a literal or a
978
+ // parameter. Three things are refused here, and they are the same mistake at
979
+ // different depths:
980
+ //
981
+ // - no MATCH at all, which would make UPSERT mean "create, always";
982
+ // - a name-only match, because names are mutable grounding state with
983
+ // duplicates allowed, so "the Concept named X" can silently address a
984
+ // different node over time;
985
+ // - an identity whose value is a variable, which is a *set* of candidates
986
+ // rather than one element — an upsert resolving it would pick a winner.
987
+ //
988
+ // A match may carry other fields beside the identity; they narrow, they do
989
+ // not identify.
990
+ if (!match || !hasStableIdentity(match)) {
991
+ throw invalidSyntax(
992
+ 'UPSERT CONCEPT requires a MATCH on a stable identity: {id: <literal-or-parameter>} ' +
993
+ 'or {key: <literal-or-parameter>} — name is mutable grounding state and never ' +
994
+ 'identifies a Concept, and a variable names a set rather than an element',
995
+ stmt.match ? stmt.match.range : stmt.range
996
+ )
997
+ }
998
+
999
+ return {
1000
+ handle: varName(stmt.handle.name, stmt.handle.range),
1001
+ match,
1002
+ expect_version: stmt.expectVersion
1003
+ ? lowerScalar(stmt.expectVersion.value)
1004
+ : null,
1005
+ set_fields: stmt.setFields
1006
+ ? lowerAssignments(stmt.setFields.assignments, null)
1007
+ : null,
1008
+ set_attributes: stmt.setAttributes
1009
+ ? lowerAssignments(stmt.setAttributes.assignments, null)
1010
+ : null,
1011
+ set_facets: stmt.setFacets.map((f) => lowerFacet(f, null)),
1012
+ unset_attributes: stmt.unsetAttributes
1013
+ ? lowerUnsetFields(stmt.unsetAttributes.fields)
1014
+ : null,
1015
+ unset_facets: stmt.unsetFacets.map((f) => ({
1016
+ facet: lowerSymbol(f.facet),
1017
+ fields: lowerUnsetFields(f.fields)
1018
+ })),
1019
+ set_structural: stmt.setStructural
1020
+ ? lowerStructural(stmt.setStructural, null)
1021
+ : null,
1022
+ unset_structural: stmt.unsetStructural
1023
+ ? lowerStructuralRemovals(stmt.unsetStructural, null)
1024
+ : null
1025
+ }
1026
+ }
1027
+
1028
+ function lowerEnsureProposition(
1029
+ stmt: EnsurePropositionStatement
1030
+ ): EnsureProposition {
1031
+ const triple = requireStructuralTuple(stmt.tuple, 'ENSURE PROPOSITION')
1032
+ return {
1033
+ handle: stmt.handle ? varName(stmt.handle.name, stmt.handle.range) : null,
1034
+ ...triple,
1035
+ expect_version: stmt.expectVersion
1036
+ ? lowerScalar(stmt.expectVersion.value)
1037
+ : null
1038
+ }
1039
+ }
1040
+
1041
+ function lowerRecordCreate(
1042
+ stmt:
1043
+ | CreateEvidenceStatement
1044
+ | CreateAssertionStatement
1045
+ | CreateActivityStatement
1046
+ ): RecordCreate {
1047
+ const fields = stmt.setFields
1048
+ ? lowerAssignments(stmt.setFields.assignments, null)
1049
+ : null
1050
+ return {
1051
+ handle: varName(stmt.handle.name, stmt.handle.range),
1052
+ client_key: stmt.clientKey ? lowerScalar(stmt.clientKey.value) : null,
1053
+ set_fields: fields,
1054
+ set_facets: stmt.setFacets.map((f) => lowerFacet(f, null)),
1055
+ set_structural: stmt.setStructural
1056
+ ? lowerStructural(stmt.setStructural, null)
1057
+ : null
1058
+ }
1059
+ }
1060
+
1061
+ /**
1062
+ * Desugars `ASSERT` into exactly what the Spec defines it as (§55.1):
1063
+ * `ENSURE PROPOSITION` + `CREATE ASSERTION`, plus `SUPERSEDE` when written.
1064
+ *
1065
+ * Nothing else is fabricated. The sugar exists because recording an
1066
+ * attributed claim is the hot path, not because it means anything new.
1067
+ */
1068
+ function lowerAssertSugar(
1069
+ stmt: AssertStatement,
1070
+ seq: number
1071
+ ): MutationClause[] {
1072
+ const members = new Map<string, Expression>()
1073
+ for (const entry of stmt.assignments.entries) {
1074
+ if (members.has(entry.key)) {
1075
+ throw invalidSyntax(`duplicate ASSERT member ${entry.key}`, entry.range)
1076
+ }
1077
+ members.set(entry.key, entry.value)
1078
+ }
1079
+
1080
+ const known = new Set([
1081
+ 'by',
1082
+ 'mode',
1083
+ 'stance',
1084
+ 'confidence',
1085
+ 'at',
1086
+ 'valid',
1087
+ 'evidence',
1088
+ 'key'
1089
+ ])
1090
+ for (const [key, value] of members) {
1091
+ if (!known.has(key)) {
1092
+ throw invalidSyntax(
1093
+ `${key} is not an ASSERT member; expected one of ${[...known].join(', ')}`,
1094
+ value.range
1095
+ )
1096
+ }
1097
+ }
1098
+
1099
+ // `by` names whose stance this is, and `mode` says how it was arrived at.
1100
+ // Neither has a safe default: guessing the actor would forge attribution,
1101
+ // and guessing the mode would turn hearsay into observation.
1102
+ const by = members.get('by')
1103
+ if (!by) {
1104
+ throw invalidSyntax(
1105
+ 'ASSERT requires by: <semantic actor> — an Assertion without an assertor has no epistemic owner',
1106
+ stmt.assignments.range
1107
+ )
1108
+ }
1109
+ const mode = members.get('mode')
1110
+ if (!mode) {
1111
+ throw invalidSyntax(
1112
+ 'ASSERT requires mode: one of observed, stated, inferred, predicted, hypothetical, imported',
1113
+ stmt.assignments.range
1114
+ )
1115
+ }
1116
+
1117
+ // The Proposition handle is synthesized, so it must collide with neither a
1118
+ // user handle nor another ASSERT in the same plan. `#` cannot occur in a KIP
1119
+ // identifier, which rules out the first; `seq` is the clause position, which
1120
+ // rules out the second — two handle-less ASSERTs in one MUTATE are ordinary
1121
+ // input, not a name clash.
1122
+ const assertionHandle = stmt.handle
1123
+ ? varName(stmt.handle.name, stmt.handle.range)
1124
+ : `#assert${seq}`
1125
+ const propositionHandle = `${assertionHandle}#proposition`
1126
+
1127
+ const triple = requireStructuralTuple(stmt.tuple, 'ASSERT')
1128
+
1129
+ const clauses: MutationClause[] = [
1130
+ {
1131
+ EnsureProposition: {
1132
+ handle: propositionHandle,
1133
+ ...triple,
1134
+ expect_version: null
1135
+ }
1136
+ }
1137
+ ]
1138
+
1139
+ const fields: Assignments = [
1140
+ ['proposition', { Handle: propositionHandle }],
1141
+ ['asserted_by', lowerMutationValue(by, null)],
1142
+ ['mode', lowerMutationValue(mode, null)],
1143
+ // The normative expansion carries a stance even when the source omitted
1144
+ // one, so the default is materialized here rather than left for the
1145
+ // engine to re-derive.
1146
+ [
1147
+ 'stance',
1148
+ members.has('stance')
1149
+ ? lowerMutationValue(members.get('stance')!, null)
1150
+ : { Value: { String: 'support' } }
1151
+ ]
1152
+ ]
1153
+ const optional: [string, string][] = [
1154
+ ['confidence', 'confidence'],
1155
+ ['at', 'asserted_at'],
1156
+ ['valid', 'valid_time']
1157
+ ]
1158
+ for (const [member, field] of optional) {
1159
+ const value = members.get(member)
1160
+ if (value) fields.push([field, lowerMutationValue(value, null)])
1161
+ }
1162
+
1163
+ // `evidence` is a reserved Core *structural* field, not a plain one: the
1164
+ // normative desugaring emits `("evidence", ref) {role: "support"}`. An array
1165
+ // cites several artifacts, so it becomes one role-qualified edge each.
1166
+ const evidenceExpr = members.get('evidence')
1167
+ const evidenceEdges: StructuralEdge[] =
1168
+ evidenceExpr === undefined
1169
+ ? []
1170
+ : (evidenceExpr.kind === 'ArrayLiteral'
1171
+ ? evidenceExpr.elements
1172
+ : [evidenceExpr]
1173
+ ).map((ref) => ({
1174
+ field: { Name: 'evidence' },
1175
+ value: lowerMutationValue(ref, null),
1176
+ options: { role: { Value: { String: 'support' } } }
1177
+ }))
1178
+
1179
+ const clientKeyExpr = members.get('key')
1180
+ clauses.push({
1181
+ CreateAssertion: {
1182
+ handle: assertionHandle,
1183
+ client_key: clientKeyExpr ? lowerScalarExpression(clientKeyExpr) : null,
1184
+ set_fields: fields,
1185
+ set_facets: [],
1186
+ set_structural: evidenceEdges.length > 0 ? evidenceEdges : null
1187
+ }
1188
+ })
1189
+
1190
+ if (stmt.superseding) {
1191
+ clauses.push({
1192
+ SupersedeAssertion: {
1193
+ target: lowerElementRef(stmt.superseding),
1194
+ by: { Handle: assertionHandle },
1195
+ expect_state: null
1196
+ }
1197
+ })
1198
+ }
1199
+
1200
+ return clauses
1201
+ }
1202
+
1203
+ function lowerTransition(stmt: TransitionActivityStatement) {
1204
+ let setFields: Assignments | null = null
1205
+ let setStructural: StructuralEdge[] | null = null
1206
+ for (const clause of stmt.finalize) {
1207
+ if (clause.kind === 'SetFieldsClause') {
1208
+ if (setFields) {
1209
+ throw invalidSyntax('duplicate SET FIELDS clause', clause.range)
1210
+ }
1211
+ setFields = lowerAssignments(clause.assignments, null)
1212
+ } else {
1213
+ if (setStructural) {
1214
+ throw invalidSyntax('duplicate SET STRUCTURAL clause', clause.range)
1215
+ }
1216
+ setStructural = lowerStructural(clause, null)
1217
+ }
1218
+ }
1219
+ return {
1220
+ target: lowerElementRef(stmt.target),
1221
+ to: lowerScalar(stmt.to),
1222
+ set_fields: setFields,
1223
+ set_structural: setStructural,
1224
+ expect_state: stmt.expectState ? lowerScalar(stmt.expectState.value) : null
1225
+ }
1226
+ }
1227
+
1228
+ function lowerRemoval(stmt: ArchiveStatement | TombstoneStatement) {
1229
+ return {
1230
+ target: lowerElementRef(stmt.target),
1231
+ where_clauses: stmt.where ? lowerWhere(stmt.where) : null,
1232
+ limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
1233
+ expect_state: stmt.expectState ? lowerScalar(stmt.expectState.value) : null
1234
+ }
1235
+ }
1236
+
1237
+ function lowerPurge(stmt: PurgeStatement) {
1238
+ if (stmt.confirm.parsed !== 'PURGE') {
1239
+ throw invalidSyntax(
1240
+ 'PURGE must be confirmed with the exact literal "PURGE"',
1241
+ stmt.confirm.range
1242
+ )
1243
+ }
1244
+ return {
1245
+ target: lowerElementRef(stmt.target),
1246
+ where_clauses: stmt.where ? lowerWhere(stmt.where) : null,
1247
+ limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
1248
+ reference_policy: stmt.referencePolicy
1249
+ ? lowerScalar(stmt.referencePolicy)
1250
+ : null,
1251
+ confirm: 'PURGE'
1252
+ }
1253
+ }
1254
+
1255
+ // ---------------------------------------------------------------------------
1256
+ // UPDATE
1257
+ // ---------------------------------------------------------------------------
1258
+
1259
+ function lowerUpdate(stmt: CstUpdateStatement) {
1260
+ if (stmt.actions.length === 0) {
1261
+ throw invalidSyntax(
1262
+ 'UPDATE requires at least one SET or UNSET action',
1263
+ stmt.range
1264
+ )
1265
+ }
1266
+
1267
+ const target = lowerElementRef(stmt.target)
1268
+ const targetVar = 'Handle' in target ? target.Handle : null
1269
+ const kind =
1270
+ targetVar && stmt.where ? boundKindOf(targetVar, stmt.where.patterns) : null
1271
+
1272
+ const actions = stmt.actions.map((action) =>
1273
+ lowerUpdateAction(action, targetVar, kind)
1274
+ )
1275
+
1276
+ return {
1277
+ target,
1278
+ expect_version: stmt.expectVersion
1279
+ ? lowerScalar(stmt.expectVersion.value)
1280
+ : null,
1281
+ actions,
1282
+ where_clauses: stmt.where ? lowerWhere(stmt.where) : null,
1283
+ limit: stmt.limit ? lowerScalar(stmt.limit.value) : null
1284
+ }
1285
+ }
1286
+
1287
+ /** Which Core kind the UPDATE target is bound to, when the WHERE block says. */
1288
+ type BoundKind = 'assertion' | 'evidence' | 'proposition' | 'concept' | 'activity'
1289
+
1290
+ function boundKindOf(
1291
+ variable: string,
1292
+ patterns: WherePattern[]
1293
+ ): BoundKind | null {
1294
+ for (const pattern of patterns) {
1295
+ switch (pattern.kind) {
1296
+ case 'AssertionPattern':
1297
+ if (varName(pattern.variable.name, pattern.variable.range) === variable) {
1298
+ return 'assertion'
1299
+ }
1300
+ break
1301
+ case 'EvidencePattern':
1302
+ if (varName(pattern.variable.name, pattern.variable.range) === variable) {
1303
+ return 'evidence'
1304
+ }
1305
+ break
1306
+ case 'ActivityPattern':
1307
+ if (varName(pattern.variable.name, pattern.variable.range) === variable) {
1308
+ return 'activity'
1309
+ }
1310
+ break
1311
+ case 'ConceptPattern':
1312
+ if (varName(pattern.variable.name, pattern.variable.range) === variable) {
1313
+ return 'concept'
1314
+ }
1315
+ break
1316
+ case 'PropositionPattern':
1317
+ if (
1318
+ pattern.variable &&
1319
+ varName(pattern.variable.name, pattern.variable.range) === variable
1320
+ ) {
1321
+ return 'proposition'
1322
+ }
1323
+ break
1324
+ case 'NotClause':
1325
+ case 'OptionalClause':
1326
+ case 'UnionClause': {
1327
+ const nested = boundKindOf(variable, pattern.patterns)
1328
+ if (nested) return nested
1329
+ break
1330
+ }
1331
+ }
1332
+ }
1333
+ return null
1334
+ }
1335
+
1336
+ function lowerUpdateAction(
1337
+ action: CstUpdateAction,
1338
+ targetVar: string | null,
1339
+ kind: BoundKind | null
1340
+ ): UpdateAction {
1341
+ switch (action.kind) {
1342
+ case 'SetFieldsClause': {
1343
+ const assignments = lowerAssignments(action.assignments, targetVar)
1344
+ for (const entry of action.assignments.entries) {
1345
+ guardImmutableField(entry.key, kind, entry.range)
1346
+ }
1347
+ return { SetFields: assignments }
1348
+ }
1349
+ case 'SetAttributesClause': {
1350
+ const assignments = lowerAssignments(action.assignments, targetVar)
1351
+ for (const entry of action.assignments.entries) {
1352
+ guardProtectedField(entry.key, entry.range)
1353
+ }
1354
+ return { SetAttributes: assignments }
1355
+ }
1356
+ case 'SetFacetClause':
1357
+ return { SetFacet: lowerFacet(action, targetVar) }
1358
+ case 'UnsetAttributesClause':
1359
+ return { UnsetAttributes: lowerUnsetFields(action.fields) }
1360
+ case 'UnsetFacetClause':
1361
+ return {
1362
+ UnsetFacet: {
1363
+ facet: lowerSymbol(action.facet),
1364
+ fields: lowerUnsetFields(action.fields)
1365
+ }
1366
+ }
1367
+ case 'SetStructuralClause':
1368
+ guardStructuralMutation('SET STRUCTURAL', kind, action.range)
1369
+ return { SetStructural: lowerStructural(action, targetVar) }
1370
+ case 'UnsetStructuralClause':
1371
+ guardStructuralMutation('UNSET STRUCTURAL', kind, action.range)
1372
+ return { UnsetStructural: lowerStructuralRemovals(action, targetVar) }
1373
+ }
1374
+ }
1375
+
1376
+ /**
1377
+ * Structural mutation reaches mutable Concept topology only (Spec §17.5).
1378
+ * Record kinds keep their topology: an Assertion's citations and an
1379
+ * Evidence's lineage are immutable payload, a Proposition has no structural
1380
+ * fields, and a pending Activity finalizes through TRANSITION ACTIVITY.
1381
+ */
1382
+ function guardStructuralMutation(
1383
+ verb: string,
1384
+ kind: BoundKind | null,
1385
+ range: Range
1386
+ ): void {
1387
+ switch (kind) {
1388
+ case 'assertion':
1389
+ throw invalidSyntax(
1390
+ `${verb} cannot change an Assertion's citations: they are immutable payload — record a new Assertion with SUPERSEDING`,
1391
+ range
1392
+ )
1393
+ case 'evidence':
1394
+ throw invalidSyntax(
1395
+ `${verb} cannot change Evidence topology: correct it with CORRECT EVIDENCE :old BY :new`,
1396
+ range
1397
+ )
1398
+ case 'proposition':
1399
+ throw invalidSyntax(
1400
+ `${verb} has no target on a Proposition: a Proposition is its tuple and carries no structural fields`,
1401
+ range
1402
+ )
1403
+ case 'activity':
1404
+ throw invalidSyntax(
1405
+ `${verb} cannot change Activity topology: finalize a pending Activity with TRANSITION ACTIVITY ... SET STRUCTURAL; a terminal Activity is immutable`,
1406
+ range
1407
+ )
1408
+ default:
1409
+ return
1410
+ }
1411
+ }
1412
+
1413
+ /** Engine-owned state is never author-writable, whatever the element kind. */
1414
+ function guardProtectedField(field: string, range: Range): void {
1415
+ if (PROTECTED_FIELDS.has(field)) {
1416
+ throw invalidSyntax(
1417
+ `${field} is engine-maintained state and cannot be written by a mutation`,
1418
+ range
1419
+ )
1420
+ }
1421
+ }
1422
+
1423
+ function guardImmutableField(
1424
+ field: string,
1425
+ kind: BoundKind | null,
1426
+ range: Range
1427
+ ): void {
1428
+ guardProtectedField(field, range)
1429
+
1430
+ if (kind === 'assertion' && ASSERTION_IMMUTABLE.has(field)) {
1431
+ throw invalidSyntax(
1432
+ `${field} is immutable Assertion payload: record the change as a new Assertion with SUPERSEDING, ` +
1433
+ 'never by rewriting the old one',
1434
+ range
1435
+ )
1436
+ }
1437
+ if (kind === 'evidence' && EVIDENCE_IMMUTABLE.has(field)) {
1438
+ throw invalidSyntax(
1439
+ `${field} is immutable Evidence payload: correct it with CORRECT EVIDENCE :old BY :new`,
1440
+ range
1441
+ )
1442
+ }
1443
+ if (kind === 'proposition' && PROPOSITION_IMMUTABLE.has(field)) {
1444
+ throw invalidSyntax(
1445
+ `${field} is part of the immutable Proposition tuple: a different tuple is a different Proposition`,
1446
+ range
1447
+ )
1448
+ }
1449
+ }
1450
+
1451
+ // ---------------------------------------------------------------------------
1452
+ // Assignments, facets, structural edges
1453
+ // ---------------------------------------------------------------------------
1454
+
1455
+ function lowerAssignments(
1456
+ object: ObjectLiteral,
1457
+ targetVar: string | null
1458
+ ): Assignments {
1459
+ const seen = new Set<string>()
1460
+ const out: Assignments = []
1461
+ for (const entry of object.entries) {
1462
+ guardProtectedField(entry.key, entry.range)
1463
+ if (seen.has(entry.key)) {
1464
+ throw invalidSyntax(`duplicate assignment for ${entry.key}`, entry.range)
1465
+ }
1466
+ seen.add(entry.key)
1467
+ out.push([entry.key, lowerMutationValue(entry.value, targetVar)])
1468
+ }
1469
+ return out
1470
+ }
1471
+
1472
+ function lowerFacet(
1473
+ clause: SetFacetClause,
1474
+ targetVar: string | null
1475
+ ): FacetAssignment {
1476
+ return {
1477
+ facet: lowerSymbol(clause.facet),
1478
+ values: lowerAssignments(clause.assignments, targetVar)
1479
+ }
1480
+ }
1481
+
1482
+ function lowerStructural(
1483
+ clause: SetStructuralClause,
1484
+ targetVar: string | null
1485
+ ): StructuralEdge[] {
1486
+ return clause.assignments.map((assignment) => ({
1487
+ field: lowerSymbol(assignment.field),
1488
+ value: lowerMutationValue(assignment.value, targetVar),
1489
+ options: assignment.options ? lowerBoundObject(assignment.options) : null
1490
+ }))
1491
+ }
1492
+
1493
+ function lowerStructuralRemovals(
1494
+ clause: UnsetStructuralClause,
1495
+ targetVar: string | null
1496
+ ): StructuralRemoval[] {
1497
+ if (clause.removals.length === 0) {
1498
+ throw invalidSyntax(
1499
+ 'UNSET STRUCTURAL removes named references; list at least one (field, target)',
1500
+ clause.range
1501
+ )
1502
+ }
1503
+ return clause.removals.map((removal) => ({
1504
+ field: lowerSymbol(removal.field),
1505
+ value: lowerMutationValue(removal.value, targetVar)
1506
+ }))
1507
+ }
1508
+
1509
+ function lowerUnsetFields(fields: UnsetField[]): string[] {
1510
+ const seen = new Set<string>()
1511
+ for (const field of fields) {
1512
+ if (seen.has(field.name)) {
1513
+ throw invalidSyntax(`duplicate field ${field.name}`, field.range)
1514
+ }
1515
+ guardProtectedField(field.name, field.range)
1516
+ seen.add(field.name)
1517
+ }
1518
+ return [...seen]
1519
+ }
1520
+
1521
+ function lowerMutationValue(
1522
+ expr: Expression,
1523
+ targetVar: string | null
1524
+ ): MutationValue {
1525
+ if (expr.kind === 'FunctionCallExpr') {
1526
+ return { Expr: lowerUpdateExpr(expr, targetVar) }
1527
+ }
1528
+ if (expr.kind === 'AggregateExpr') {
1529
+ throw invalidSyntax(
1530
+ `${expr.name} is an aggregate and cannot appear in an assignment`,
1531
+ expr.range
1532
+ )
1533
+ }
1534
+ return lowerBoundValue(expr, targetVar)
1535
+ }
1536
+
1537
+ /**
1538
+ * Lowers a `data_value`, keeping structure only where something still needs
1539
+ * binding. A wholly literal subtree collapses to one `Value`, so an engine
1540
+ * that has nothing to substitute never walks a binding tree.
1541
+ */
1542
+ function lowerBoundValue(
1543
+ expr: Expression,
1544
+ targetVar: string | null
1545
+ ): BoundValue {
1546
+ switch (expr.kind) {
1547
+ case 'ParameterRef':
1548
+ return { Param: paramName(expr.name) }
1549
+
1550
+ case 'VariableRef':
1551
+ return { Handle: varName(expr.name, expr.range) }
1552
+
1553
+ case 'FieldAccess': {
1554
+ const path = lowerDotPath(expr)
1555
+ guardOwnField(path, targetVar, expr.range)
1556
+ return { Variable: path }
1557
+ }
1558
+
1559
+ case 'ArrayLiteral':
1560
+ return isFullyLiteral(expr)
1561
+ ? { Value: lowerKipValue(expr) }
1562
+ : { Array: expr.elements.map((e) => lowerBoundValue(e, targetVar)) }
1563
+
1564
+ case 'ObjectLiteral':
1565
+ return isFullyLiteral(expr)
1566
+ ? { Value: lowerKipValue(expr) }
1567
+ : {
1568
+ Object: expr.entries.map(
1569
+ (e) =>
1570
+ [e.key, lowerBoundValue(e.value, targetVar)] as [
1571
+ string,
1572
+ BoundValue
1573
+ ]
1574
+ )
1575
+ }
1576
+
1577
+ default:
1578
+ return { Value: lowerKipValue(expr) }
1579
+ }
1580
+ }
1581
+
1582
+ /** True when nothing in the subtree needs binding at execution time. */
1583
+ function isFullyLiteral(expr: Expression): boolean {
1584
+ switch (expr.kind) {
1585
+ case 'StringLiteral':
1586
+ case 'NumberLiteral':
1587
+ case 'BooleanLiteral':
1588
+ case 'NullLiteral':
1589
+ return true
1590
+ case 'ArrayLiteral':
1591
+ return expr.elements.every(isFullyLiteral)
1592
+ case 'ObjectLiteral':
1593
+ return expr.entries.every((e) => isFullyLiteral(e.value))
1594
+ case 'UnaryExpression':
1595
+ return expr.operator === '-' && isFullyLiteral(expr.operand)
1596
+ default:
1597
+ return false
1598
+ }
1599
+ }
1600
+
1601
+ function lowerUpdateExpr(
1602
+ expr: Expression,
1603
+ targetVar: string | null
1604
+ ): UpdateExpr {
1605
+ switch (expr.kind) {
1606
+ case 'FunctionCallExpr': {
1607
+ const func = UPDATE_FUNCTIONS.get(expr.name.toUpperCase())
1608
+ if (!func) {
1609
+ throw invalidSyntax(
1610
+ `${expr.name} is not a KIP update function; expected ADD, MUL, CLAMP or COALESCE`,
1611
+ expr.range
1612
+ )
1613
+ }
1614
+ const arity = UPDATE_ARITY[func]
1615
+ if (expr.args.length !== arity) {
1616
+ throw invalidSyntax(
1617
+ `${expr.name} takes ${arity} arguments, found ${expr.args.length}`,
1618
+ expr.range
1619
+ )
1620
+ }
1621
+ return {
1622
+ Function: {
1623
+ func,
1624
+ args: expr.args.map((arg) => lowerUpdateExpr(arg, targetVar))
1625
+ }
1626
+ }
1627
+ }
1628
+
1629
+ case 'ParameterRef':
1630
+ return { Param: paramName(expr.name) }
1631
+
1632
+ case 'NumberLiteral':
1633
+ return { Number: numberValue(expr) }
1634
+
1635
+ case 'UnaryExpression':
1636
+ if (expr.operator === '-' && expr.operand.kind === 'NumberLiteral') {
1637
+ return { Number: -numberValue(expr.operand) }
1638
+ }
1639
+ throw invalidSyntax(
1640
+ `expected a number, a parameter, the target's own field or a registered function, found ${describeExpression(expr)}`,
1641
+ expr.range
1642
+ )
1643
+
1644
+ case 'VariableRef':
1645
+ case 'FieldAccess': {
1646
+ const path = lowerDotPath(expr)
1647
+ guardOwnField(path, targetVar, expr.range)
1648
+ return { Variable: path }
1649
+ }
1650
+
1651
+ default:
1652
+ throw invalidSyntax(
1653
+ `expected a number, a parameter, the target's own field or a registered function, found ${describeExpression(expr)}`,
1654
+ expr.range
1655
+ )
1656
+ }
1657
+ }
1658
+
1659
+ /**
1660
+ * An update expression may read only the element being updated.
1661
+ *
1662
+ * Reading another variable would make the result depend on a join the
1663
+ * statement never declared, so each matched element must be computable from
1664
+ * its own row.
1665
+ */
1666
+ function guardOwnField(
1667
+ path: DotPathVar,
1668
+ targetVar: string | null,
1669
+ range: Range
1670
+ ): void {
1671
+ if (targetVar !== null && path.var !== targetVar) {
1672
+ throw invalidSyntax(
1673
+ `an update expression may read only the target ?${targetVar}, found ?${path.var}`,
1674
+ range
1675
+ )
1676
+ }
1677
+ }
1678
+
1679
+ // ---------------------------------------------------------------------------
1680
+ // META
1681
+ // ---------------------------------------------------------------------------
1682
+
1683
+ function lowerMeta(stmt: Statement): MetaCommand {
1684
+ switch (stmt.kind) {
1685
+ case 'DescribeStatement':
1686
+ return { Describe: lowerDescribe(stmt) }
1687
+ case 'ListStatement':
1688
+ return { List: lowerList(stmt) }
1689
+ case 'SearchStatement':
1690
+ return { Search: lowerSearch(stmt) }
1691
+ case 'VerifyStatement':
1692
+ return {
1693
+ Verify: {
1694
+ target: VERIFY_TARGETS[stmt.target],
1695
+ value: lowerScalar(stmt.value)
1696
+ }
1697
+ }
1698
+ case 'ValidateStatement':
1699
+ return {
1700
+ Validate: {
1701
+ target: VALIDATE_TARGETS[stmt.target],
1702
+ value: lowerScalar(stmt.value),
1703
+ options: stmt.options ? lowerBoundObject(stmt.options) : null
1704
+ }
1705
+ }
1706
+ case 'PreviewStatement':
1707
+ return {
1708
+ Preview:
1709
+ stmt.target === 'KML'
1710
+ ? { Kml: lowerScalar(stmt.value) }
1711
+ : {
1712
+ ImportCapsule: {
1713
+ capsule: lowerScalar(stmt.value),
1714
+ into: lowerScalar(stmt.into!)
1715
+ }
1716
+ }
1717
+ }
1718
+ case 'HistoryStatement':
1719
+ return { History: lowerHistory(stmt) }
1720
+ case 'ChangesStatement':
1721
+ return {
1722
+ Changes:
1723
+ stmt.mode === 'SINCE'
1724
+ ? {
1725
+ Since: {
1726
+ cursor: lowerScalar(stmt.value),
1727
+ limit: stmt.limit ? lowerScalar(stmt.limit.value) : null
1728
+ }
1729
+ }
1730
+ : {
1731
+ AfterSeq: {
1732
+ seq: lowerScalar(stmt.value),
1733
+ limit: stmt.limit ? lowerScalar(stmt.limit.value) : null
1734
+ }
1735
+ }
1736
+ }
1737
+ case 'SnapshotStatement':
1738
+ return { Snapshot: { as_of: stmt.asOf ? lowerAsOf(stmt.asOf) : null } }
1739
+ case 'ExportCapsuleStatement':
1740
+ return { ExportCapsule: lowerExport(stmt) }
1741
+ default:
1742
+ throw invalidSyntax(
1743
+ `${stmt.kind} is not an executable KIP command`,
1744
+ stmt.range
1745
+ )
1746
+ }
1747
+ }
1748
+
1749
+ const VERIFY_TARGETS: Record<VerifyStatement['target'], VerifyTarget> = {
1750
+ CAPSULE: 'Capsule',
1751
+ SCHEMA_PACKAGE: 'SchemaPackage',
1752
+ RECEIPT: 'Receipt',
1753
+ BLOB: 'Blob',
1754
+ CHECKPOINT: 'Checkpoint'
1755
+ }
1756
+
1757
+ const VALIDATE_TARGETS: Record<ValidateStatement['target'], ValidateTarget> = {
1758
+ KQL: 'Kql',
1759
+ KML: 'Kml',
1760
+ CAPSULE: 'Capsule',
1761
+ SCHEMA_PACKAGE: 'SchemaPackage',
1762
+ IMPORT_PLAN: 'ImportPlan'
1763
+ }
1764
+
1765
+ const LIST_TARGETS: Record<ListStatement['target'], ListTarget> = {
1766
+ SPACES: 'Spaces',
1767
+ SCHEMA_PACKAGES: 'SchemaPackages',
1768
+ TYPES: 'Types',
1769
+ PREDICATES: 'Predicates',
1770
+ FACETS: 'Facets',
1771
+ STRUCTURAL_FIELDS: 'StructuralFields',
1772
+ EPISTEMIC_POLICIES: 'EpistemicPolicies'
1773
+ }
1774
+
1775
+ const SEARCH_TARGETS: Record<SearchStatement['searchKind'], SearchTarget> = {
1776
+ CONCEPT: 'Concept',
1777
+ PROPOSITION: 'Proposition',
1778
+ ASSERTION: 'Assertion',
1779
+ EVIDENCE: 'Evidence',
1780
+ ACTIVITY: 'Activity',
1781
+ COGNITION: 'Cognition'
1782
+ }
1783
+
1784
+ function lowerDescribe(stmt: DescribeStatement): DescribeTarget {
1785
+ const value = () => {
1786
+ if (!stmt.value) {
1787
+ throw invalidSyntax(
1788
+ `DESCRIBE ${stmt.target} requires an operand`,
1789
+ stmt.range
1790
+ )
1791
+ }
1792
+ return lowerScalar(stmt.value)
1793
+ }
1794
+
1795
+ switch (stmt.target) {
1796
+ case 'PRIMER':
1797
+ return { Primer: { mode: stmt.mode ? lowerScalar(stmt.mode) : null } }
1798
+ case 'PROTOCOL':
1799
+ return 'Protocol'
1800
+ case 'EXECUTION_CONTEXT':
1801
+ return 'ExecutionContext'
1802
+ case 'CAPABILITIES':
1803
+ return 'Capabilities'
1804
+ case 'SPACE':
1805
+ return { Space: { value: stmt.value ? lowerScalar(stmt.value) : null } }
1806
+ case 'SCHEMA_ENVIRONMENT':
1807
+ return {
1808
+ SchemaEnvironment: { as_of: stmt.asOf ? lowerAsOf(stmt.asOf) : null }
1809
+ }
1810
+ case 'PACKAGE':
1811
+ return { Package: value() }
1812
+ case 'TYPE':
1813
+ return { Type: value() }
1814
+ case 'PREDICATE':
1815
+ return { Predicate: value() }
1816
+ case 'FACET':
1817
+ return { Facet: value() }
1818
+ case 'STRUCTURAL_FIELD':
1819
+ return { StructuralField: value() }
1820
+ case 'COMPATIBILITY':
1821
+ if (!stmt.from || !stmt.to) {
1822
+ throw invalidSyntax(
1823
+ 'DESCRIBE COMPATIBILITY requires FROM and TO',
1824
+ stmt.range
1825
+ )
1826
+ }
1827
+ return {
1828
+ Compatibility: { from: lowerScalar(stmt.from), to: lowerScalar(stmt.to) }
1829
+ }
1830
+ case 'ERROR':
1831
+ return { Error: value() }
1832
+ case 'TRANSACTION':
1833
+ return { Transaction: value() }
1834
+ case 'TRANSACTION_BY_IDEMPOTENCY_KEY':
1835
+ return { TransactionByIdempotencyKey: value() }
1836
+ case 'SNAPSHOT':
1837
+ return { Snapshot: { as_of: stmt.asOf ? lowerAsOf(stmt.asOf) : null } }
1838
+ case 'CAPSULE':
1839
+ return { Capsule: value() }
1840
+ case 'EPISTEMIC_POLICY':
1841
+ return {
1842
+ EpistemicPolicy: { value: stmt.value ? lowerScalar(stmt.value) : null }
1843
+ }
1844
+ case 'PROJECTION_CAPABILITY':
1845
+ return 'ProjectionCapability'
1846
+ case 'TRUST':
1847
+ return { Trust: { value: stmt.value ? lowerScalar(stmt.value) : null } }
1848
+ case 'ACCESS':
1849
+ return {
1850
+ Access: { with: stmt.with ? lowerBoundObject(stmt.with) : null }
1851
+ }
1852
+ }
1853
+ }
1854
+
1855
+ function lowerList(stmt: ListStatement) {
1856
+ return {
1857
+ target: LIST_TARGETS[stmt.target],
1858
+ status: stmt.status ? lowerScalar(stmt.status) : null,
1859
+ limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
1860
+ cursor: stmt.cursor ? lowerScalar(stmt.cursor.value) : null
1861
+ }
1862
+ }
1863
+
1864
+ function lowerSearch(stmt: SearchStatement) {
1865
+ return {
1866
+ target: SEARCH_TARGETS[stmt.searchKind],
1867
+ term: lowerScalar(stmt.term),
1868
+ with_type: stmt.withType ? lowerScalar(stmt.withType) : null,
1869
+ with_predicate: stmt.withPredicate ? lowerScalar(stmt.withPredicate) : null,
1870
+ mode: stmt.mode ? lowerScalar(stmt.mode) : null,
1871
+ threshold: stmt.threshold ? lowerScalar(stmt.threshold) : null,
1872
+ as_of_seq: stmt.asOfSeq ? lowerScalar(stmt.asOfSeq) : null,
1873
+ limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
1874
+ cursor: stmt.cursor ? lowerScalar(stmt.cursor.value) : null
1875
+ }
1876
+ }
1877
+
1878
+ function lowerHistory(stmt: HistoryStatement): HistoryCommand {
1879
+ const paging = {
1880
+ from_seq: stmt.fromSeq ? lowerScalar(stmt.fromSeq) : null,
1881
+ to_seq: stmt.toSeq ? lowerScalar(stmt.toSeq) : null,
1882
+ limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
1883
+ cursor: stmt.cursor ? lowerScalar(stmt.cursor.value) : null
1884
+ }
1885
+ if (stmt.target === 'SPACE') {
1886
+ return { Space: paging }
1887
+ }
1888
+ if (!stmt.value) {
1889
+ throw invalidSyntax('HISTORY ELEMENT requires an element id', stmt.range)
1890
+ }
1891
+ return { Element: { value: lowerScalar(stmt.value), ...paging } }
1892
+ }
1893
+
1894
+ function lowerExport(stmt: ExportCapsuleStatement) {
1895
+ const where_clauses = lowerWhere(stmt.where)
1896
+ // A Capsule is a bounded, self-contained excerpt. `WHERE { }` selects the
1897
+ // whole Space, which is not a smaller thing to hand somebody — it is the
1898
+ // Brain, exported by accident.
1899
+ if (where_clauses.length === 0) {
1900
+ throw invalidSyntax(
1901
+ 'expected at least one selection pattern: an unbounded EXPORT is not a Capsule',
1902
+ stmt.where.range
1903
+ )
1904
+ }
1905
+ return {
1906
+ target: lowerElementRef(stmt.target),
1907
+ where_clauses,
1908
+ options: stmt.options ? lowerBoundObject(stmt.options) : null,
1909
+ as_of: stmt.asOf ? lowerAsOf(stmt.asOf) : null
1910
+ }
1911
+ }
1912
+
1913
+ // ---------------------------------------------------------------------------
1914
+ // Leaf conversions
1915
+ // ---------------------------------------------------------------------------
1916
+
1917
+ function lowerScalar(value: ScalarValue): Scalar {
1918
+ if (value.kind === 'ParameterRef') {
1919
+ return { Param: paramName(value.name) }
1920
+ }
1921
+ return { Literal: lowerKipValue(value) }
1922
+ }
1923
+
1924
+ /** An ASSERT member used where the grammar needs a scalar, e.g. `key:`. */
1925
+ function lowerScalarExpression(expr: Expression): Scalar {
1926
+ if (expr.kind === 'ParameterRef') {
1927
+ return { Param: paramName(expr.name) }
1928
+ }
1929
+ if (
1930
+ expr.kind === 'StringLiteral' ||
1931
+ expr.kind === 'NumberLiteral' ||
1932
+ expr.kind === 'BooleanLiteral' ||
1933
+ expr.kind === 'NullLiteral'
1934
+ ) {
1935
+ return { Literal: lowerKipValue(expr) }
1936
+ }
1937
+ throw invalidSyntax(
1938
+ `expected a literal or :parameter, found ${describeExpression(expr)}`,
1939
+ expr.range
1940
+ )
1941
+ }
1942
+
1943
+ function lowerSymbol(symbol: SchemaSymbol): SymbolRef {
1944
+ return symbol.kind === 'ParameterRef'
1945
+ ? { Param: paramName(symbol.name) }
1946
+ : { Name: symbol.parsed }
1947
+ }
1948
+
1949
+ function lowerElementRef(ref: TargetRef): ElementRef {
1950
+ switch (ref.kind) {
1951
+ case 'VariableRef':
1952
+ return { Handle: varName(ref.name, ref.range) }
1953
+ case 'ParameterRef':
1954
+ return { Param: paramName(ref.name) }
1955
+ case 'StringLiteral':
1956
+ return { Id: ref.parsed }
1957
+ }
1958
+ }
1959
+
1960
+ // ---------------------------------------------------------------------------
1961
+ // Numeric literals
1962
+ // ---------------------------------------------------------------------------
1963
+
1964
+ /** `i64::MIN` — the most negative integer a KIP number literal may spell. */
1965
+ const INT_MIN = -(2n ** 63n)
1966
+ /** `u64::MAX` — the largest. */
1967
+ const INT_MAX = 2n ** 64n - 1n
1968
+
1969
+ /** An integer literal: no fraction, no exponent, so it is read as an integer. */
1970
+ const INTEGER_FORM = /^-?\d+$/
1971
+
1972
+ /**
1973
+ * The value of a number literal, refusing the ones that cannot survive being
1974
+ * one.
1975
+ *
1976
+ * A JavaScript number is a double, so `18446744073709551617` silently becomes
1977
+ * `18446744073709551616` on the way in. Accepting that would be the worst
1978
+ * possible outcome: the command does not fail, it *executes with a different
1979
+ * number than it says*, and no engine downstream can detect it — by the time
1980
+ * an executable AST exists the digits are gone. So the check happens here,
1981
+ * against the raw text, which is the only place the original is still around.
1982
+ *
1983
+ * The bounds are the reference grammar's: an integer literal is read as an
1984
+ * `i64` or a `u64` and must fit one of them, and any other form must parse to a
1985
+ * finite double. `18446744073709551616.0` is therefore accepted where
1986
+ * `18446744073709551616` is not — the float form is claiming an approximation,
1987
+ * and the integer form is claiming an exact value it cannot deliver.
1988
+ *
1989
+ * Integers above 2^53 still lose precision in this implementation's `value`
1990
+ * even though they are accepted, because a double cannot hold them. That is a
1991
+ * property of the host, not a disagreement about the language: both engines
1992
+ * agree the command is legal, and a runtime that needs the exact digits has
1993
+ * `raw`.
1994
+ */
1995
+ function numberValue(node: NumberLiteral): number {
1996
+ if (INTEGER_FORM.test(node.raw)) {
1997
+ const exact = BigInt(node.raw)
1998
+ if (exact < INT_MIN || exact > INT_MAX) {
1999
+ throw invalidSyntax(
2000
+ `${node.raw} is outside the range a KIP integer literal can represent ` +
2001
+ `(${INT_MIN} to ${INT_MAX})`,
2002
+ node.range
2003
+ )
2004
+ }
2005
+ return node.value
2006
+ }
2007
+ if (!Number.isFinite(node.value)) {
2008
+ throw invalidSyntax(
2009
+ `only finite numbers are valid KIP literals, found ${node.raw}`,
2010
+ node.range
2011
+ )
2012
+ }
2013
+ return node.value
2014
+ }
2015
+
2016
+ function lowerKipValue(expr: Expression): KipValue {
2017
+ switch (expr.kind) {
2018
+ case 'StringLiteral':
2019
+ return { String: expr.parsed }
2020
+ case 'NumberLiteral':
2021
+ return { Number: numberValue(expr) }
2022
+ case 'BooleanLiteral':
2023
+ return { Bool: expr.value }
2024
+ case 'NullLiteral':
2025
+ return 'Null'
2026
+ case 'ArrayLiteral':
2027
+ return { Array: expr.elements.map(lowerKipValue) }
2028
+ case 'ObjectLiteral':
2029
+ case 'ObjectPattern': {
2030
+ const entries =
2031
+ expr.kind === 'ObjectLiteral' ? expr.entries : expr.members
2032
+ const out: Record<string, KipValue> = {}
2033
+ for (const entry of entries) {
2034
+ out[entry.key] = lowerKipValue(entry.value)
2035
+ }
2036
+ return { Object: out }
2037
+ }
2038
+ case 'UnaryExpression':
2039
+ if (expr.operator === '-' && expr.operand.kind === 'NumberLiteral') {
2040
+ return { Number: -numberValue(expr.operand) }
2041
+ }
2042
+ throw invalidSyntax(
2043
+ `expected a value, found ${describeExpression(expr)}`,
2044
+ expr.range
2045
+ )
2046
+ default:
2047
+ throw invalidSyntax(
2048
+ `expected a value, found ${describeExpression(expr)}`,
2049
+ expr.range
2050
+ )
2051
+ }
2052
+ }
2053
+
2054
+ /**
2055
+ * Option and epistemic blocks are `data_value`s, not plain JSON: the grammar
2056
+ * lets a parameter stand anywhere inside them.
2057
+ */
2058
+ function lowerBoundObject(object: ObjectLiteral): Record<string, BoundValue> {
2059
+ const out: Record<string, BoundValue> = {}
2060
+ for (const entry of object.entries) {
2061
+ out[entry.key] = lowerBoundValue(entry.value, null)
2062
+ }
2063
+ return out
2064
+ }
2065
+
2066
+ /** Strips the `?` sigil; the executable form carries bare names. */
2067
+ function varName(name: string, range: Range): string {
2068
+ if (!name.startsWith('?')) {
2069
+ throw invalidSyntax(`expected a variable, found ${name}`, range)
2070
+ }
2071
+ return name.slice(1)
2072
+ }
2073
+
2074
+ /** Strips the `:` sigil; the executable form carries bare names. */
2075
+ function paramName(name: string): string {
2076
+ return name.startsWith(':') ? name.slice(1) : name
2077
+ }
2078
+
2079
+ function describeExpression(expr: Expression): string {
2080
+ switch (expr.kind) {
2081
+ case 'ParameterRef':
2082
+ return `the parameter ${expr.name}`
2083
+ case 'VariableRef':
2084
+ return `the variable ${expr.name}`
2085
+ case 'FunctionCallExpr':
2086
+ return `a call to ${expr.name}`
2087
+ case 'AggregateExpr':
2088
+ return `the aggregate ${expr.name}`
2089
+ case 'BinaryExpression':
2090
+ return `the operator ${expr.operator}`
2091
+ default:
2092
+ return expr.kind
2093
+ }
2094
+ }