@ldclabs/kip-lang 2.1.0 → 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,13 +32,9 @@ 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,
43
39
  PurgePayloadStatement,
44
40
  MergeConceptStatement,
@@ -50,7 +46,6 @@ import type {
50
46
  PreviewStatement,
51
47
  HistoryStatement,
52
48
  ChangesStatement,
53
- SnapshotStatement,
54
49
  ExportCapsuleStatement,
55
50
  NumberLiteral
56
51
  } from './ast.js'
@@ -68,6 +63,9 @@ import type {
68
63
  DotPathVar,
69
64
  ElementRef,
70
65
  EnsureProposition,
66
+ ExpectVersion,
67
+ VersionPlane,
68
+ Transition,
71
69
  FacetAssignment,
72
70
  FacetUnset,
73
71
  FilterExpression,
@@ -246,13 +244,8 @@ export function lowerStatement(stmt: Statement): Command {
246
244
  case 'CreateAssertionStatement':
247
245
  case 'CreateActivityStatement':
248
246
  case 'UpdateStatement':
249
- case 'RetractAssertionStatement':
250
- case 'SupersedeAssertionStatement':
251
- case 'CorrectEvidenceStatement':
252
- case 'TransitionActivityStatement':
247
+ case 'TransitionStatement':
253
248
  case 'SetRetentionStatement':
254
- case 'ArchiveStatement':
255
- case 'TombstoneStatement':
256
249
  case 'PurgeStatement':
257
250
  case 'PurgePayloadStatement':
258
251
  case 'MergeConceptStatement':
@@ -294,15 +287,50 @@ function lowerFind(stmt: FindStatement): KqlQuery {
294
287
  }
295
288
 
296
289
  function lowerAsOf(clause: AsOfClause): AsOf {
297
- const value = lowerScalar(clause.value)
298
- switch (clause.basis) {
299
- case 'SEQ':
300
- return { Seq: value }
301
- case 'TX':
302
- return { Tx: value }
303
- case 'TIME':
304
- return { Time: value }
305
- }
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
+ })
306
334
  }
307
335
 
308
336
  function lowerFindExpression(expr: Expression): FindExpression {
@@ -864,45 +892,8 @@ function lowerMutationClause(
864
892
  return [{ CreateActivity: lowerRecordCreate(stmt) }]
865
893
  case 'UpdateStatement':
866
894
  return [{ Update: lowerUpdate(stmt) }]
867
- case 'RetractAssertionStatement':
868
- return [
869
- {
870
- RetractAssertion: {
871
- target: lowerElementRef(stmt.target),
872
- where_clauses: stmt.where ? lowerWhere(stmt.where) : null,
873
- limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
874
- expect_state: stmt.expectState
875
- ? lowerScalar(stmt.expectState.value)
876
- : null
877
- }
878
- }
879
- ]
880
- case 'SupersedeAssertionStatement':
881
- return [
882
- {
883
- SupersedeAssertion: {
884
- target: lowerElementRef(stmt.target),
885
- by: lowerElementRef(stmt.by),
886
- expect_state: stmt.expectState
887
- ? lowerScalar(stmt.expectState.value)
888
- : null
889
- }
890
- }
891
- ]
892
- case 'CorrectEvidenceStatement':
893
- return [
894
- {
895
- CorrectEvidence: {
896
- target: lowerElementRef(stmt.target),
897
- by: lowerElementRef(stmt.by),
898
- expect_state: stmt.expectState
899
- ? lowerScalar(stmt.expectState.value)
900
- : null
901
- }
902
- }
903
- ]
904
- case 'TransitionActivityStatement':
905
- return [{ TransitionActivity: lowerTransition(stmt) }]
895
+ case 'TransitionStatement':
896
+ return [{ Transition: lowerTransition(stmt) }]
906
897
  case 'SetRetentionStatement':
907
898
  return [
908
899
  {
@@ -911,16 +902,10 @@ function lowerMutationClause(
911
902
  values: lowerAssignments(stmt.assignments, null),
912
903
  where_clauses: stmt.where ? lowerWhere(stmt.where) : null,
913
904
  limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
914
- expect_version: stmt.expectVersion
915
- ? lowerScalar(stmt.expectVersion.value)
916
- : null
905
+ expect_versions: lowerExpectVersions(stmt.expectVersions)
917
906
  }
918
907
  }
919
908
  ]
920
- case 'ArchiveStatement':
921
- return [{ Archive: lowerRemoval(stmt) }]
922
- case 'TombstoneStatement':
923
- return [{ Tombstone: lowerRemoval(stmt) }]
924
909
  case 'PurgeStatement':
925
910
  return [{ Purge: lowerPurge(stmt) }]
926
911
  case 'PurgePayloadStatement':
@@ -932,9 +917,7 @@ function lowerMutationClause(
932
917
  source: lowerElementRef(stmt.source),
933
918
  into: lowerElementRef(stmt.into),
934
919
  where_clauses: stmt.where ? lowerWhere(stmt.where) : null,
935
- expect_version: stmt.expectVersion
936
- ? lowerScalar(stmt.expectVersion.value)
937
- : null
920
+ expect_versions: lowerExpectVersions(stmt.expectVersions)
938
921
  }
939
922
  }
940
923
  ]
@@ -1003,9 +986,7 @@ function lowerUpsertConcept(stmt: UpsertConceptStatement): ConceptUpsert {
1003
986
  return {
1004
987
  handle: varName(stmt.handle.name, stmt.handle.range),
1005
988
  match,
1006
- expect_version: stmt.expectVersion
1007
- ? lowerScalar(stmt.expectVersion.value)
1008
- : null,
989
+ expect_versions: lowerExpectVersions(stmt.expectVersions),
1009
990
  set_fields: stmt.setFields
1010
991
  ? lowerAssignments(stmt.setFields.assignments, null)
1011
992
  : null,
@@ -1036,9 +1017,7 @@ function lowerEnsureProposition(
1036
1017
  return {
1037
1018
  handle: stmt.handle ? varName(stmt.handle.name, stmt.handle.range) : null,
1038
1019
  ...triple,
1039
- expect_version: stmt.expectVersion
1040
- ? lowerScalar(stmt.expectVersion.value)
1041
- : null
1020
+ expect_versions: lowerExpectVersions(stmt.expectVersions)
1042
1021
  }
1043
1022
  }
1044
1023
 
@@ -1064,7 +1043,7 @@ function lowerRecordCreate(
1064
1043
 
1065
1044
  /**
1066
1045
  * Desugars `ASSERT` into exactly what the Spec defines it as (§55.1):
1067
- * `ENSURE PROPOSITION` + `CREATE ASSERTION`, plus `SUPERSEDE` when written.
1046
+ * `ENSURE PROPOSITION` + `CREATE ASSERTION`, plus `TRANSITION ... TO "superseded"` when `SUPERSEDING` is written.
1068
1047
  *
1069
1048
  * Nothing else is fabricated. The sugar exists because recording an
1070
1049
  * attributed claim is the hot path, not because it means anything new.
@@ -1135,7 +1114,7 @@ function lowerAssertSugar(
1135
1114
  EnsureProposition: {
1136
1115
  handle: propositionHandle,
1137
1116
  ...triple,
1138
- expect_version: null
1117
+ expect_versions: []
1139
1118
  }
1140
1119
  }
1141
1120
  ]
@@ -1193,10 +1172,15 @@ function lowerAssertSugar(
1193
1172
 
1194
1173
  if (stmt.superseding) {
1195
1174
  clauses.push({
1196
- SupersedeAssertion: {
1175
+ Transition: {
1197
1176
  target: lowerElementRef(stmt.superseding),
1177
+ to: { Literal: { String: 'superseded' } },
1198
1178
  by: { Handle: assertionHandle },
1199
- expect_state: null
1179
+ set_fields: null,
1180
+ set_structural: null,
1181
+ where_clauses: null,
1182
+ limit: null,
1183
+ expect_versions: []
1200
1184
  }
1201
1185
  })
1202
1186
  }
@@ -1204,7 +1188,33 @@ function lowerAssertSugar(
1204
1188
  return clauses
1205
1189
  }
1206
1190
 
1207
- 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
+ }
1208
1218
  let setFields: Assignments | null = null
1209
1219
  let setStructural: StructuralEdge[] | null = null
1210
1220
  for (const clause of stmt.finalize) {
@@ -1223,18 +1233,12 @@ function lowerTransition(stmt: TransitionActivityStatement) {
1223
1233
  return {
1224
1234
  target: lowerElementRef(stmt.target),
1225
1235
  to: lowerScalar(stmt.to),
1236
+ by: stmt.by ? lowerElementRef(stmt.by) : null,
1226
1237
  set_fields: setFields,
1227
1238
  set_structural: setStructural,
1228
- expect_state: stmt.expectState ? lowerScalar(stmt.expectState.value) : null
1229
- }
1230
- }
1231
-
1232
- function lowerRemoval(stmt: ArchiveStatement | TombstoneStatement) {
1233
- return {
1234
- target: lowerElementRef(stmt.target),
1235
1239
  where_clauses: stmt.where ? lowerWhere(stmt.where) : null,
1236
1240
  limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
1237
- expect_state: stmt.expectState ? lowerScalar(stmt.expectState.value) : null
1241
+ expect_versions: lowerExpectVersions(stmt.expectVersions)
1238
1242
  }
1239
1243
  }
1240
1244
 
@@ -1249,6 +1253,7 @@ function lowerPurge(stmt: PurgeStatement) {
1249
1253
  target: lowerElementRef(stmt.target),
1250
1254
  where_clauses: stmt.where ? lowerWhere(stmt.where) : null,
1251
1255
  limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
1256
+ expect_versions: lowerExpectVersions(stmt.expectVersions),
1252
1257
  reference_policy: stmt.referencePolicy
1253
1258
  ? lowerScalar(stmt.referencePolicy)
1254
1259
  : null,
@@ -1267,6 +1272,7 @@ function lowerPurgePayload(stmt: PurgePayloadStatement) {
1267
1272
  target: lowerElementRef(stmt.target),
1268
1273
  where_clauses: stmt.where ? lowerWhere(stmt.where) : null,
1269
1274
  limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
1275
+ expect_versions: lowerExpectVersions(stmt.expectVersions),
1270
1276
  confirm: 'PURGE'
1271
1277
  }
1272
1278
  }
@@ -1294,12 +1300,10 @@ function lowerUpdate(stmt: CstUpdateStatement) {
1294
1300
 
1295
1301
  return {
1296
1302
  target,
1297
- expect_version: stmt.expectVersion
1298
- ? lowerScalar(stmt.expectVersion.value)
1299
- : null,
1300
1303
  actions,
1301
1304
  where_clauses: stmt.where ? lowerWhere(stmt.where) : null,
1302
- limit: stmt.limit ? lowerScalar(stmt.limit.value) : null
1305
+ limit: stmt.limit ? lowerScalar(stmt.limit.value) : null,
1306
+ expect_versions: lowerExpectVersions(stmt.expectVersions)
1303
1307
  }
1304
1308
  }
1305
1309
 
@@ -1396,7 +1400,7 @@ function lowerUpdateAction(
1396
1400
  * Structural mutation reaches mutable Concept topology only (Spec §17.5).
1397
1401
  * Record kinds keep their topology: an Assertion's citations and an
1398
1402
  * Evidence's lineage are immutable payload, a Proposition has no structural
1399
- * fields, and a pending Activity finalizes through TRANSITION ACTIVITY.
1403
+ * fields, and a pending Activity finalizes through TRANSITION.
1400
1404
  */
1401
1405
  function guardStructuralMutation(
1402
1406
  verb: string,
@@ -1411,7 +1415,7 @@ function guardStructuralMutation(
1411
1415
  )
1412
1416
  case 'evidence':
1413
1417
  throw invalidSyntax(
1414
- `${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`,
1415
1419
  range
1416
1420
  )
1417
1421
  case 'proposition':
@@ -1421,7 +1425,7 @@ function guardStructuralMutation(
1421
1425
  )
1422
1426
  case 'activity':
1423
1427
  throw invalidSyntax(
1424
- `${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`,
1425
1429
  range
1426
1430
  )
1427
1431
  default:
@@ -1455,7 +1459,7 @@ function guardImmutableField(
1455
1459
  }
1456
1460
  if (kind === 'evidence' && EVIDENCE_IMMUTABLE.has(field)) {
1457
1461
  throw invalidSyntax(
1458
- `${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`,
1459
1463
  range
1460
1464
  )
1461
1465
  }
@@ -1753,8 +1757,6 @@ function lowerMeta(stmt: Statement): MetaCommand {
1753
1757
  }
1754
1758
  }
1755
1759
  }
1756
- case 'SnapshotStatement':
1757
- return { Snapshot: { as_of: stmt.asOf ? lowerAsOf(stmt.asOf) : null } }
1758
1760
  case 'ExportCapsuleStatement':
1759
1761
  return { ExportCapsule: lowerExport(stmt) }
1760
1762
  default:
@@ -1817,8 +1819,6 @@ function lowerDescribe(stmt: DescribeStatement): DescribeTarget {
1817
1819
  return { Primer: { mode: stmt.mode ? lowerScalar(stmt.mode) : null } }
1818
1820
  case 'PROTOCOL':
1819
1821
  return 'Protocol'
1820
- case 'EXECUTION_CONTEXT':
1821
- return 'ExecutionContext'
1822
1822
  case 'CAPABILITIES':
1823
1823
  return 'Capabilities'
1824
1824
  case 'SPACE':
@@ -1854,15 +1854,18 @@ function lowerDescribe(stmt: DescribeStatement): DescribeTarget {
1854
1854
  case 'TRANSACTION_BY_IDEMPOTENCY_KEY':
1855
1855
  return { TransactionByIdempotencyKey: value() }
1856
1856
  case 'SNAPSHOT':
1857
- 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
+ }
1858
1863
  case 'CAPSULE':
1859
1864
  return { Capsule: value() }
1860
1865
  case 'EPISTEMIC_POLICY':
1861
1866
  return {
1862
1867
  EpistemicPolicy: { value: stmt.value ? lowerScalar(stmt.value) : null }
1863
1868
  }
1864
- case 'PROJECTION_CAPABILITY':
1865
- return 'ProjectionCapability'
1866
1869
  case 'TRUST':
1867
1870
  return { Trust: { value: stmt.value ? lowerScalar(stmt.value) : null } }
1868
1871
  case 'ACCESS':