@ldclabs/kip-lang 2.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/parser.ts CHANGED
@@ -74,6 +74,7 @@ import type {
74
74
  ArchiveStatement,
75
75
  TombstoneStatement,
76
76
  PurgeStatement,
77
+ PurgePayloadStatement,
77
78
  MergeConceptStatement,
78
79
  DescribeStatement,
79
80
  DescribeTargetKind,
@@ -1907,10 +1908,13 @@ class Parser {
1907
1908
  }
1908
1909
  }
1909
1910
 
1910
- private parsePurgeStatement(): PurgeStatement {
1911
+ private parsePurgeStatement(): PurgeStatement | PurgePayloadStatement {
1911
1912
  const leadingComments = this.collectLeadingComments()
1912
1913
  const start = this.currentPos()
1913
- this.expectKeywordWithSpace(TokenType.Purge)
1914
+ const purge = this.expectKeywordWithSpace(TokenType.Purge)
1915
+ if (this.check(TokenType.Payload)) {
1916
+ return this.parsePurgePayloadTail(start, purge, leadingComments)
1917
+ }
1914
1918
  const target = this.parseTargetRef()
1915
1919
 
1916
1920
  let where: WhereClause | undefined
@@ -1954,6 +1958,49 @@ class Parser {
1954
1958
  }
1955
1959
  }
1956
1960
 
1961
+ /**
1962
+ * `PURGE PAYLOAD` erases Evidence bytes while the element survives
1963
+ * (Spec §60.6), so there is no REFERENCE POLICY clause; the confirmation
1964
+ * literal is the same as element purge.
1965
+ */
1966
+ private parsePurgePayloadTail(
1967
+ start: Position,
1968
+ purge: Token,
1969
+ leadingComments: string[]
1970
+ ): PurgePayloadStatement {
1971
+ this.expectSecondWord(TokenType.Payload, purge)
1972
+ const target = this.parseTargetRef()
1973
+
1974
+ let where: WhereClause | undefined
1975
+ if (this.check(TokenType.Where)) {
1976
+ this.expectKeywordWithSpace(TokenType.Where)
1977
+ this.dialect = 'raw'
1978
+ where = this.parseWhereClause()
1979
+ }
1980
+
1981
+ const limit = this.check(TokenType.Limit) ? this.parseLimitClause() : undefined
1982
+
1983
+ this.expect(TokenType.Confirm)
1984
+ const confirmTok = this.current()
1985
+ const confirm = this.parseStringLiteral()
1986
+ if (confirm.parsed !== 'PURGE') {
1987
+ this.error(
1988
+ `PURGE PAYLOAD must be confirmed with the exact literal "PURGE", got ${confirmTok.value}`,
1989
+ confirmTok
1990
+ )
1991
+ }
1992
+
1993
+ return {
1994
+ kind: 'PurgePayloadStatement',
1995
+ target,
1996
+ where,
1997
+ limit,
1998
+ confirm,
1999
+ range: { start, end: this.endPos() },
2000
+ leadingComments: leadingComments.length ? leadingComments : undefined
2001
+ }
2002
+ }
2003
+
1957
2004
  private parseMergeConcept(): MergeConceptStatement {
1958
2005
  const leadingComments = this.collectLeadingComments()
1959
2006
  const start = this.currentPos()
@@ -2123,6 +2170,8 @@ class Parser {
2123
2170
 
2124
2171
  let target: ListTargetKind
2125
2172
  let status: ScalarValue | undefined
2173
+ let element: ScalarValue | undefined
2174
+ let depth: ScalarValue | undefined
2126
2175
 
2127
2176
  switch (tok.type) {
2128
2177
  case TokenType.Spaces:
@@ -2160,6 +2209,14 @@ class Parser {
2160
2209
  target = 'EPISTEMIC_POLICIES'
2161
2210
  break
2162
2211
  }
2212
+ case TokenType.Dependents:
2213
+ // Bounded reverse provenance closure (Spec §63.5): the operand is
2214
+ // required, the DEPTH bound is optional and defaults engine-side to 1.
2215
+ this.expectSecondWord(TokenType.Dependents, list)
2216
+ target = 'DEPENDENTS'
2217
+ element = this.parseScalarValue()
2218
+ if (this.match(TokenType.Depth)) depth = this.parseScalarValue()
2219
+ break
2163
2220
  default:
2164
2221
  this.error(`Unknown LIST target '${tok.value}'`, tok)
2165
2222
  throw new ParseAbort()
@@ -2174,6 +2231,8 @@ class Parser {
2174
2231
  kind: 'ListStatement',
2175
2232
  target,
2176
2233
  status,
2234
+ element,
2235
+ depth,
2177
2236
  limit,
2178
2237
  cursor,
2179
2238
  range: { start, end: this.endPos() },
package/src/semantics.ts CHANGED
@@ -11,7 +11,8 @@ import type {
11
11
  ScalarValue,
12
12
  MutateStatement,
13
13
  AssertStatement,
14
- SetFacetClause
14
+ SetFacetClause,
15
+ SetStructuralClause
15
16
  } from './ast.js'
16
17
  import type { Range } from './token.js'
17
18
 
@@ -54,6 +55,7 @@ const ASSERTION_STATES = new Set([
54
55
  'expired'
55
56
  ])
56
57
  const ACTIVITY_TERMINAL = new Set(['completed', 'failed', 'cancelled'])
58
+ const EVIDENCE_ROLES = new Set(['support', 'challenge', 'context'])
57
59
 
58
60
  /** Signals the Profile fixes to `[0,1]`; none of them is truth. */
59
61
  const UNIT_INTERVAL_FIELDS = new Set([
@@ -96,11 +98,11 @@ function analyzeMutationClause(stmt: Statement, diags: Diagnostic[]): void {
96
98
  break
97
99
 
98
100
  case 'CreateConceptStatement':
99
- for (const facet of stmt.setFacets) checkFacet(facet, diags)
100
- break
101
-
102
101
  case 'UpsertConceptStatement':
102
+ case 'CreateEvidenceStatement':
103
+ case 'CreateActivityStatement':
103
104
  for (const facet of stmt.setFacets) checkFacet(facet, diags)
105
+ checkStructural(stmt.setStructural, diags)
104
106
  break
105
107
 
106
108
  case 'CreateAssertionStatement':
@@ -108,30 +110,62 @@ function analyzeMutationClause(stmt: Statement, diags: Diagnostic[]): void {
108
110
  checkAssignmentValues(stmt.setFields.assignments, diags)
109
111
  }
110
112
  for (const facet of stmt.setFacets) checkFacet(facet, diags)
111
- break
112
-
113
- case 'CreateEvidenceStatement':
114
- case 'CreateActivityStatement':
115
- for (const facet of stmt.setFacets) checkFacet(facet, diags)
113
+ checkStructural(stmt.setStructural, diags)
116
114
  break
117
115
 
118
116
  case 'UpdateStatement':
119
117
  for (const action of stmt.actions) {
120
118
  if (action.kind === 'SetFacetClause') checkFacet(action, diags)
119
+ // Core fields are Core-typed wherever they are written, so an UPDATE
120
+ // that sets one gets the same check `CREATE ASSERTION` already gets.
121
+ else if (action.kind === 'SetFieldsClause') {
122
+ checkAssignmentValues(action.assignments, diags)
123
+ } else if (action.kind === 'SetStructuralClause') {
124
+ checkStructural(action, diags)
125
+ }
121
126
  }
122
127
  if (stmt.where) checkWhere(stmt.where, !!stmt.limit, diags)
123
128
  break
124
129
 
130
+ // `EXPECT STATE` is checkable only where the target's kind is fixed by the
131
+ // statement itself. RETRACT and SUPERSEDE always name an Assertion, so the
132
+ // Assertion lifecycle registry applies. ARCHIVE and TOMBSTONE take any
133
+ // element — a Concept is not `retracted` and may well be `archived` — and
134
+ // Core registers no element-lifecycle vocabulary, so checking them here
135
+ // rejected commands the Specification admits.
125
136
  case 'RetractAssertionStatement':
126
- case 'ArchiveStatement':
127
- case 'TombstoneStatement':
137
+ if (stmt.expectState) {
138
+ checkEnum(stmt.expectState.value, ASSERTION_STATES, 'EXPECT STATE', diags)
139
+ }
140
+ if (stmt.where) checkWhere(stmt.where, !!stmt.limit, diags)
141
+ break
142
+
143
+ case 'SupersedeAssertionStatement':
128
144
  if (stmt.expectState) {
129
145
  checkEnum(stmt.expectState.value, ASSERTION_STATES, 'EXPECT STATE', diags)
130
146
  }
131
147
  break
132
148
 
149
+ case 'ArchiveStatement':
150
+ case 'TombstoneStatement':
151
+ if (stmt.where) checkWhere(stmt.where, !!stmt.limit, diags)
152
+ break
153
+
133
154
  case 'TransitionActivityStatement':
134
155
  checkEnum(stmt.to, ACTIVITY_TERMINAL, 'TRANSITION ACTIVITY TO', diags)
156
+ checkStructural(stmt.finalize.find(
157
+ (c): c is SetStructuralClause => c.kind === 'SetStructuralClause'
158
+ ), diags)
159
+ break
160
+
161
+ // Spec §52.7 names seven statements whose WHERE can select an unbounded
162
+ // set and which SHOULD therefore carry a LIMIT. Warning on the read and on
163
+ // UPDATE while staying silent on the removal ladder had it backwards: an
164
+ // over-broad PURGE is the one that cannot be undone.
165
+ case 'PurgeStatement':
166
+ case 'PurgePayloadStatement':
167
+ case 'SetRetentionStatement':
168
+ if (stmt.where) checkWhere(stmt.where, !!stmt.limit, diags)
135
169
  break
136
170
  }
137
171
  }
@@ -176,6 +210,26 @@ function checkFacet(clause: SetFacetClause, diags: Diagnostic[]): void {
176
210
  checkAssignmentValues(clause.assignments, diags)
177
211
  }
178
212
 
213
+ /**
214
+ * `role` on an `("evidence", ...)` citation comes from the Core registry
215
+ * (Spec §20.13, §56.2), so a misspelling is as checkable here as a bad
216
+ * `stance`. Other structural fields carry engine- or package-defined options,
217
+ * which only the Schema Environment can judge.
218
+ */
219
+ function checkStructural(
220
+ clause: SetStructuralClause | undefined,
221
+ diags: Diagnostic[]
222
+ ): void {
223
+ if (!clause) return
224
+ for (const edge of clause.assignments) {
225
+ if (edge.field.kind === 'ParameterRef' || edge.field.parsed !== 'evidence') {
226
+ continue
227
+ }
228
+ const role = edge.options?.entries.find((e) => e.key === 'role')
229
+ if (role) checkEnum(role.value, EVIDENCE_ROLES, 'Evidence role', diags)
230
+ }
231
+ }
232
+
179
233
  function checkAssignmentValues(
180
234
  object: ObjectLiteral,
181
235
  diags: Diagnostic[]
package/src/token.ts CHANGED
@@ -65,6 +65,7 @@ export enum TokenType {
65
65
  Archive = 'ARCHIVE',
66
66
  Tombstone = 'TOMBSTONE',
67
67
  Purge = 'PURGE',
68
+ Payload = 'PAYLOAD',
68
69
  Confirm = 'CONFIRM',
69
70
  Reference = 'REFERENCE',
70
71
  Policy = 'POLICY',
@@ -113,6 +114,8 @@ export enum TokenType {
113
114
  Facets = 'FACETS',
114
115
  Types = 'TYPES',
115
116
  Policies = 'POLICIES',
117
+ Dependents = 'DEPENDENTS',
118
+ Depth = 'DEPTH',
116
119
  Compatibility = 'COMPATIBILITY',
117
120
  From = 'FROM',
118
121
  Error = 'ERROR',
@@ -245,6 +248,7 @@ export const KEYWORDS: ReadonlyMap<string, TokenType> = new Map([
245
248
  ['ARCHIVE', TokenType.Archive],
246
249
  ['TOMBSTONE', TokenType.Tombstone],
247
250
  ['PURGE', TokenType.Purge],
251
+ ['PAYLOAD', TokenType.Payload],
248
252
  ['CONFIRM', TokenType.Confirm],
249
253
  ['REFERENCE', TokenType.Reference],
250
254
  ['POLICY', TokenType.Policy],
@@ -293,6 +297,8 @@ export const KEYWORDS: ReadonlyMap<string, TokenType> = new Map([
293
297
  ['FACETS', TokenType.Facets],
294
298
  ['TYPES', TokenType.Types],
295
299
  ['POLICIES', TokenType.Policies],
300
+ ['DEPENDENTS', TokenType.Dependents],
301
+ ['DEPTH', TokenType.Depth],
296
302
  ['COMPATIBILITY', TokenType.Compatibility],
297
303
  ['FROM', TokenType.From],
298
304
  ['ERROR', TokenType.Error],
package/src/version.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * has to load in runtimes with no filesystem; `test/lower.test.mjs` asserts the
8
8
  * two stay in step.
9
9
  */
10
- export const PARSER_VERSION = '2.0.1'
10
+ export const PARSER_VERSION = '2.1.0'
11
11
 
12
12
  /** The KIP specification revision this grammar targets. */
13
13
  export const KIP_SPEC_REVISION = '2.0-draft'