@ldclabs/kip-lang 2.0.2 → 2.2.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.
package/src/lower.ts CHANGED
@@ -32,14 +32,11 @@ import type {
32
32
  SetStructuralClause,
33
33
  UnsetStructuralClause,
34
34
  UnsetField,
35
- RetractAssertionStatement,
36
- SupersedeAssertionStatement,
37
- CorrectEvidenceStatement,
38
- TransitionActivityStatement,
35
+ ExpectVersionClause,
36
+ TransitionStatement,
39
37
  SetRetentionStatement,
40
- ArchiveStatement,
41
- TombstoneStatement,
42
38
  PurgeStatement,
39
+ PurgePayloadStatement,
43
40
  MergeConceptStatement,
44
41
  DescribeStatement,
45
42
  ListStatement,
@@ -49,7 +46,6 @@ import type {
49
46
  PreviewStatement,
50
47
  HistoryStatement,
51
48
  ChangesStatement,
52
- SnapshotStatement,
53
49
  ExportCapsuleStatement,
54
50
  NumberLiteral
55
51
  } from './ast.js'
@@ -67,6 +63,9 @@ import type {
67
63
  DotPathVar,
68
64
  ElementRef,
69
65
  EnsureProposition,
66
+ ExpectVersion,
67
+ VersionPlane,
68
+ Transition,
70
69
  FacetAssignment,
71
70
  FacetUnset,
72
71
  FilterExpression,
@@ -245,14 +244,10 @@ export function lowerStatement(stmt: Statement): Command {
245
244
  case 'CreateAssertionStatement':
246
245
  case 'CreateActivityStatement':
247
246
  case 'UpdateStatement':
248
- case 'RetractAssertionStatement':
249
- case 'SupersedeAssertionStatement':
250
- case 'CorrectEvidenceStatement':
251
- case 'TransitionActivityStatement':
247
+ case 'TransitionStatement':
252
248
  case 'SetRetentionStatement':
253
- case 'ArchiveStatement':
254
- case 'TombstoneStatement':
255
249
  case 'PurgeStatement':
250
+ case 'PurgePayloadStatement':
256
251
  case 'MergeConceptStatement':
257
252
  const clauses = lowerMutationClause(stmt, 0)
258
253
  assertUniqueHandles(clauses, stmt.range)
@@ -292,15 +287,50 @@ function lowerFind(stmt: FindStatement): KqlQuery {
292
287
  }
293
288
 
294
289
  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
- }
290
+ return { Seq: lowerScalar(clause.value) }
291
+ }
292
+
293
+ /**
294
+ * `{ expect_version_clause }` → at most one guard per plane. Two guards on one
295
+ * plane cannot both be meant, so the duplicate is rejected here rather than
296
+ * left for the engine to pick one.
297
+ */
298
+ function lowerExpectVersions(clauses: ExpectVersionClause[]): ExpectVersion[] {
299
+ const seen = new Set<string>()
300
+ return clauses.map((clause) => {
301
+ let plane: VersionPlane | null = null
302
+ let key = 'element'
303
+ if (clause.plane) {
304
+ switch (clause.plane.kind) {
305
+ case 'ATTRIBUTES':
306
+ plane = 'Attributes'
307
+ key = 'attributes'
308
+ break
309
+ case 'STRUCTURAL':
310
+ plane = 'Structural'
311
+ key = 'structural'
312
+ break
313
+ case 'RETENTION':
314
+ plane = 'Retention'
315
+ key = 'retention'
316
+ break
317
+ case 'FACET': {
318
+ const facet = lowerSymbol(clause.plane.facet)
319
+ plane = { Facet: facet }
320
+ key = 'facet:' + ('Name' in facet ? facet.Name : ':' + facet.Param)
321
+ break
322
+ }
323
+ }
324
+ }
325
+ if (seen.has(key)) {
326
+ throw invalidSyntax(
327
+ `duplicate EXPECT VERSION guard on the same ${key === 'element' ? 'element' : 'plane'}`,
328
+ clause.range
329
+ )
330
+ }
331
+ seen.add(key)
332
+ return { version: lowerScalar(clause.value), plane }
333
+ })
304
334
  }
305
335
 
306
336
  function lowerFindExpression(expr: Expression): FindExpression {
@@ -862,45 +892,8 @@ function lowerMutationClause(
862
892
  return [{ CreateActivity: lowerRecordCreate(stmt) }]
863
893
  case 'UpdateStatement':
864
894
  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) }]
895
+ case 'TransitionStatement':
896
+ return [{ Transition: lowerTransition(stmt) }]
904
897
  case 'SetRetentionStatement':
905
898
  return [
906
899
  {
@@ -909,18 +902,14 @@ function lowerMutationClause(
909
902
  values: lowerAssignments(stmt.assignments, null),
910
903
  where_clauses: stmt.where ? lowerWhere(stmt.where) : null,
911
904
  limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
912
- expect_version: stmt.expectVersion
913
- ? lowerScalar(stmt.expectVersion.value)
914
- : null
905
+ expect_versions: lowerExpectVersions(stmt.expectVersions)
915
906
  }
916
907
  }
917
908
  ]
918
- case 'ArchiveStatement':
919
- return [{ Archive: lowerRemoval(stmt) }]
920
- case 'TombstoneStatement':
921
- return [{ Tombstone: lowerRemoval(stmt) }]
922
909
  case 'PurgeStatement':
923
910
  return [{ Purge: lowerPurge(stmt) }]
911
+ case 'PurgePayloadStatement':
912
+ return [{ PurgePayload: lowerPurgePayload(stmt) }]
924
913
  case 'MergeConceptStatement':
925
914
  return [
926
915
  {
@@ -928,9 +917,7 @@ function lowerMutationClause(
928
917
  source: lowerElementRef(stmt.source),
929
918
  into: lowerElementRef(stmt.into),
930
919
  where_clauses: stmt.where ? lowerWhere(stmt.where) : null,
931
- expect_version: stmt.expectVersion
932
- ? lowerScalar(stmt.expectVersion.value)
933
- : null
920
+ expect_versions: lowerExpectVersions(stmt.expectVersions)
934
921
  }
935
922
  }
936
923
  ]
@@ -999,9 +986,7 @@ function lowerUpsertConcept(stmt: UpsertConceptStatement): ConceptUpsert {
999
986
  return {
1000
987
  handle: varName(stmt.handle.name, stmt.handle.range),
1001
988
  match,
1002
- expect_version: stmt.expectVersion
1003
- ? lowerScalar(stmt.expectVersion.value)
1004
- : null,
989
+ expect_versions: lowerExpectVersions(stmt.expectVersions),
1005
990
  set_fields: stmt.setFields
1006
991
  ? lowerAssignments(stmt.setFields.assignments, null)
1007
992
  : null,
@@ -1032,9 +1017,7 @@ function lowerEnsureProposition(
1032
1017
  return {
1033
1018
  handle: stmt.handle ? varName(stmt.handle.name, stmt.handle.range) : null,
1034
1019
  ...triple,
1035
- expect_version: stmt.expectVersion
1036
- ? lowerScalar(stmt.expectVersion.value)
1037
- : null
1020
+ expect_versions: lowerExpectVersions(stmt.expectVersions)
1038
1021
  }
1039
1022
  }
1040
1023
 
@@ -1060,7 +1043,7 @@ function lowerRecordCreate(
1060
1043
 
1061
1044
  /**
1062
1045
  * Desugars `ASSERT` into exactly what the Spec defines it as (§55.1):
1063
- * `ENSURE PROPOSITION` + `CREATE ASSERTION`, plus `SUPERSEDE` when written.
1046
+ * `ENSURE PROPOSITION` + `CREATE ASSERTION`, plus `TRANSITION ... TO "superseded"` when `SUPERSEDING` is written.
1064
1047
  *
1065
1048
  * Nothing else is fabricated. The sugar exists because recording an
1066
1049
  * attributed claim is the hot path, not because it means anything new.
@@ -1131,7 +1114,7 @@ function lowerAssertSugar(
1131
1114
  EnsureProposition: {
1132
1115
  handle: propositionHandle,
1133
1116
  ...triple,
1134
- expect_version: null
1117
+ expect_versions: []
1135
1118
  }
1136
1119
  }
1137
1120
  ]
@@ -1189,10 +1172,15 @@ function lowerAssertSugar(
1189
1172
 
1190
1173
  if (stmt.superseding) {
1191
1174
  clauses.push({
1192
- SupersedeAssertion: {
1175
+ Transition: {
1193
1176
  target: lowerElementRef(stmt.superseding),
1177
+ to: { Literal: { String: 'superseded' } },
1194
1178
  by: { Handle: assertionHandle },
1195
- expect_state: null
1179
+ set_fields: null,
1180
+ set_structural: null,
1181
+ where_clauses: null,
1182
+ limit: null,
1183
+ expect_versions: []
1196
1184
  }
1197
1185
  })
1198
1186
  }
@@ -1200,7 +1188,33 @@ function lowerAssertSugar(
1200
1188
  return clauses
1201
1189
  }
1202
1190
 
1203
- function lowerTransition(stmt: TransitionActivityStatement) {
1191
+ /** Moves that name the replacing element with `BY` (Spec §52.5). */
1192
+ const TRANSITION_WITH_BY = new Set(['superseded', 'corrected'])
1193
+ /** Activity status moves: the only ones that may finalize fields or topology. */
1194
+ const TRANSITION_ACTIVITY_STATES = new Set(['running', 'completed', 'failed', 'cancelled'])
1195
+
1196
+ function lowerTransition(stmt: TransitionStatement): Transition {
1197
+ // Spec §52.5: BY exactly for superseded / corrected, SET clauses only on
1198
+ // Activity states. These are syntax errors, so they belong to the
1199
+ // executability contract, not only to the editor diagnostics.
1200
+ if (stmt.to.kind === 'StringLiteral') {
1201
+ const to = stmt.to.parsed
1202
+ if (TRANSITION_WITH_BY.has(to) && !stmt.by) {
1203
+ throw invalidSyntax(
1204
+ `TRANSITION TO "${to}" names the replacing element with BY`,
1205
+ stmt.to.range
1206
+ )
1207
+ }
1208
+ if (!TRANSITION_WITH_BY.has(to) && stmt.by) {
1209
+ throw invalidSyntax(`TRANSITION TO "${to}" takes no BY`, stmt.by.range)
1210
+ }
1211
+ if (stmt.finalize.length > 0 && !TRANSITION_ACTIVITY_STATES.has(to)) {
1212
+ throw invalidSyntax(
1213
+ `TRANSITION TO "${to}" cannot finalize fields or topology; only a pending Activity does`,
1214
+ stmt.finalize[0].range
1215
+ )
1216
+ }
1217
+ }
1204
1218
  let setFields: Assignments | null = null
1205
1219
  let setStructural: StructuralEdge[] | null = null
1206
1220
  for (const clause of stmt.finalize) {
@@ -1219,18 +1233,12 @@ function lowerTransition(stmt: TransitionActivityStatement) {
1219
1233
  return {
1220
1234
  target: lowerElementRef(stmt.target),
1221
1235
  to: lowerScalar(stmt.to),
1236
+ by: stmt.by ? lowerElementRef(stmt.by) : null,
1222
1237
  set_fields: setFields,
1223
1238
  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
1239
  where_clauses: stmt.where ? lowerWhere(stmt.where) : null,
1232
1240
  limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
1233
- expect_state: stmt.expectState ? lowerScalar(stmt.expectState.value) : null
1241
+ expect_versions: lowerExpectVersions(stmt.expectVersions)
1234
1242
  }
1235
1243
  }
1236
1244
 
@@ -1245,6 +1253,7 @@ function lowerPurge(stmt: PurgeStatement) {
1245
1253
  target: lowerElementRef(stmt.target),
1246
1254
  where_clauses: stmt.where ? lowerWhere(stmt.where) : null,
1247
1255
  limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
1256
+ expect_versions: lowerExpectVersions(stmt.expectVersions),
1248
1257
  reference_policy: stmt.referencePolicy
1249
1258
  ? lowerScalar(stmt.referencePolicy)
1250
1259
  : null,
@@ -1252,6 +1261,22 @@ function lowerPurge(stmt: PurgeStatement) {
1252
1261
  }
1253
1262
  }
1254
1263
 
1264
+ function lowerPurgePayload(stmt: PurgePayloadStatement) {
1265
+ if (stmt.confirm.parsed !== 'PURGE') {
1266
+ throw invalidSyntax(
1267
+ 'PURGE PAYLOAD must be confirmed with the exact literal "PURGE"',
1268
+ stmt.confirm.range
1269
+ )
1270
+ }
1271
+ return {
1272
+ target: lowerElementRef(stmt.target),
1273
+ where_clauses: stmt.where ? lowerWhere(stmt.where) : null,
1274
+ limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
1275
+ expect_versions: lowerExpectVersions(stmt.expectVersions),
1276
+ confirm: 'PURGE'
1277
+ }
1278
+ }
1279
+
1255
1280
  // ---------------------------------------------------------------------------
1256
1281
  // UPDATE
1257
1282
  // ---------------------------------------------------------------------------
@@ -1275,12 +1300,10 @@ function lowerUpdate(stmt: CstUpdateStatement) {
1275
1300
 
1276
1301
  return {
1277
1302
  target,
1278
- expect_version: stmt.expectVersion
1279
- ? lowerScalar(stmt.expectVersion.value)
1280
- : null,
1281
1303
  actions,
1282
1304
  where_clauses: stmt.where ? lowerWhere(stmt.where) : null,
1283
- limit: stmt.limit ? lowerScalar(stmt.limit.value) : null
1305
+ limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
1306
+ expect_versions: lowerExpectVersions(stmt.expectVersions)
1284
1307
  }
1285
1308
  }
1286
1309
 
@@ -1377,7 +1400,7 @@ function lowerUpdateAction(
1377
1400
  * Structural mutation reaches mutable Concept topology only (Spec §17.5).
1378
1401
  * Record kinds keep their topology: an Assertion's citations and an
1379
1402
  * Evidence's lineage are immutable payload, a Proposition has no structural
1380
- * fields, and a pending Activity finalizes through TRANSITION ACTIVITY.
1403
+ * fields, and a pending Activity finalizes through TRANSITION.
1381
1404
  */
1382
1405
  function guardStructuralMutation(
1383
1406
  verb: string,
@@ -1392,7 +1415,7 @@ function guardStructuralMutation(
1392
1415
  )
1393
1416
  case 'evidence':
1394
1417
  throw invalidSyntax(
1395
- `${verb} cannot change Evidence topology: correct it with CORRECT EVIDENCE :old BY :new`,
1418
+ `${verb} cannot change Evidence topology: correct it with TRANSITION :old TO "corrected" BY :new`,
1396
1419
  range
1397
1420
  )
1398
1421
  case 'proposition':
@@ -1402,7 +1425,7 @@ function guardStructuralMutation(
1402
1425
  )
1403
1426
  case 'activity':
1404
1427
  throw invalidSyntax(
1405
- `${verb} cannot change Activity topology: finalize a pending Activity with TRANSITION ACTIVITY ... SET STRUCTURAL; a terminal Activity is immutable`,
1428
+ `${verb} cannot change Activity topology: finalize a pending Activity with TRANSITION ... TO "completed" SET STRUCTURAL; a terminal Activity is immutable`,
1406
1429
  range
1407
1430
  )
1408
1431
  default:
@@ -1436,7 +1459,7 @@ function guardImmutableField(
1436
1459
  }
1437
1460
  if (kind === 'evidence' && EVIDENCE_IMMUTABLE.has(field)) {
1438
1461
  throw invalidSyntax(
1439
- `${field} is immutable Evidence payload: correct it with CORRECT EVIDENCE :old BY :new`,
1462
+ `${field} is immutable Evidence payload: correct it with TRANSITION :old TO "corrected" BY :new`,
1440
1463
  range
1441
1464
  )
1442
1465
  }
@@ -1734,8 +1757,6 @@ function lowerMeta(stmt: Statement): MetaCommand {
1734
1757
  }
1735
1758
  }
1736
1759
  }
1737
- case 'SnapshotStatement':
1738
- return { Snapshot: { as_of: stmt.asOf ? lowerAsOf(stmt.asOf) : null } }
1739
1760
  case 'ExportCapsuleStatement':
1740
1761
  return { ExportCapsule: lowerExport(stmt) }
1741
1762
  default:
@@ -1769,7 +1790,8 @@ const LIST_TARGETS: Record<ListStatement['target'], ListTarget> = {
1769
1790
  PREDICATES: 'Predicates',
1770
1791
  FACETS: 'Facets',
1771
1792
  STRUCTURAL_FIELDS: 'StructuralFields',
1772
- EPISTEMIC_POLICIES: 'EpistemicPolicies'
1793
+ EPISTEMIC_POLICIES: 'EpistemicPolicies',
1794
+ DEPENDENTS: 'Dependents'
1773
1795
  }
1774
1796
 
1775
1797
  const SEARCH_TARGETS: Record<SearchStatement['searchKind'], SearchTarget> = {
@@ -1797,8 +1819,6 @@ function lowerDescribe(stmt: DescribeStatement): DescribeTarget {
1797
1819
  return { Primer: { mode: stmt.mode ? lowerScalar(stmt.mode) : null } }
1798
1820
  case 'PROTOCOL':
1799
1821
  return 'Protocol'
1800
- case 'EXECUTION_CONTEXT':
1801
- return 'ExecutionContext'
1802
1822
  case 'CAPABILITIES':
1803
1823
  return 'Capabilities'
1804
1824
  case 'SPACE':
@@ -1834,15 +1854,18 @@ function lowerDescribe(stmt: DescribeStatement): DescribeTarget {
1834
1854
  case 'TRANSACTION_BY_IDEMPOTENCY_KEY':
1835
1855
  return { TransactionByIdempotencyKey: value() }
1836
1856
  case 'SNAPSHOT':
1837
- return { Snapshot: { as_of: stmt.asOf ? lowerAsOf(stmt.asOf) : null } }
1857
+ return {
1858
+ Snapshot: {
1859
+ as_of: stmt.asOf ? lowerAsOf(stmt.asOf) : null,
1860
+ at_time: stmt.atTime ? lowerScalar(stmt.atTime) : null
1861
+ }
1862
+ }
1838
1863
  case 'CAPSULE':
1839
1864
  return { Capsule: value() }
1840
1865
  case 'EPISTEMIC_POLICY':
1841
1866
  return {
1842
1867
  EpistemicPolicy: { value: stmt.value ? lowerScalar(stmt.value) : null }
1843
1868
  }
1844
- case 'PROJECTION_CAPABILITY':
1845
- return 'ProjectionCapability'
1846
1869
  case 'TRUST':
1847
1870
  return { Trust: { value: stmt.value ? lowerScalar(stmt.value) : null } }
1848
1871
  case 'ACCESS':
@@ -1853,9 +1876,14 @@ function lowerDescribe(stmt: DescribeStatement): DescribeTarget {
1853
1876
  }
1854
1877
 
1855
1878
  function lowerList(stmt: ListStatement) {
1879
+ if (stmt.target === 'DEPENDENTS' && !stmt.element) {
1880
+ throw invalidSyntax('LIST DEPENDENTS requires an element operand', stmt.range)
1881
+ }
1856
1882
  return {
1857
1883
  target: LIST_TARGETS[stmt.target],
1858
1884
  status: stmt.status ? lowerScalar(stmt.status) : null,
1885
+ element: stmt.element ? lowerScalar(stmt.element) : null,
1886
+ depth: stmt.depth ? lowerScalar(stmt.depth) : null,
1859
1887
  limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
1860
1888
  cursor: stmt.cursor ? lowerScalar(stmt.cursor.value) : null
1861
1889
  }