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