@ldclabs/kip-lang 0.3.1 → 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 (64) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/LICENSE +21 -0
  3. package/README.md +133 -44
  4. package/dist/ast.d.ts +519 -144
  5. package/dist/ast.d.ts.map +1 -1
  6. package/dist/budget.d.ts +37 -0
  7. package/dist/budget.d.ts.map +1 -0
  8. package/dist/budget.js +105 -0
  9. package/dist/budget.js.map +1 -0
  10. package/dist/diagnostics.d.ts +8 -2
  11. package/dist/diagnostics.d.ts.map +1 -1
  12. package/dist/diagnostics.js +32 -3
  13. package/dist/diagnostics.js.map +1 -1
  14. package/dist/errors.d.ts +29 -0
  15. package/dist/errors.d.ts.map +1 -0
  16. package/dist/errors.js +27 -0
  17. package/dist/errors.js.map +1 -0
  18. package/dist/exec-ast.d.ts +679 -0
  19. package/dist/exec-ast.d.ts.map +1 -0
  20. package/dist/exec-ast.js +24 -0
  21. package/dist/exec-ast.js.map +1 -0
  22. package/dist/formatter.d.ts.map +1 -1
  23. package/dist/formatter.js +870 -479
  24. package/dist/formatter.js.map +1 -1
  25. package/dist/index.d.ts +9 -3
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +6 -2
  28. package/dist/index.js.map +1 -1
  29. package/dist/lexer.d.ts.map +1 -1
  30. package/dist/lexer.js +46 -33
  31. package/dist/lexer.js.map +1 -1
  32. package/dist/lower.d.ts +17 -0
  33. package/dist/lower.d.ts.map +1 -0
  34. package/dist/lower.js +1566 -0
  35. package/dist/lower.js.map +1 -0
  36. package/dist/parser.d.ts.map +1 -1
  37. package/dist/parser.js +2476 -1152
  38. package/dist/parser.js.map +1 -1
  39. package/dist/semantics.d.ts +11 -7
  40. package/dist/semantics.d.ts.map +1 -1
  41. package/dist/semantics.js +295 -180
  42. package/dist/semantics.js.map +1 -1
  43. package/dist/token.d.ts +130 -40
  44. package/dist/token.d.ts.map +1 -1
  45. package/dist/token.js +264 -83
  46. package/dist/token.js.map +1 -1
  47. package/dist/version.d.ts +13 -0
  48. package/dist/version.d.ts.map +1 -0
  49. package/dist/version.js +13 -0
  50. package/dist/version.js.map +1 -0
  51. package/package.json +36 -6
  52. package/src/ast.ts +914 -0
  53. package/src/budget.ts +108 -0
  54. package/src/diagnostics.ts +182 -0
  55. package/src/errors.ts +42 -0
  56. package/src/exec-ast.ts +614 -0
  57. package/src/formatter.ts +1339 -0
  58. package/src/index.ts +226 -0
  59. package/src/lexer.ts +459 -0
  60. package/src/lower.ts +2011 -0
  61. package/src/parser.ts +3506 -0
  62. package/src/semantics.ts +392 -0
  63. package/src/token.ts +408 -0
  64. package/src/version.ts +13 -0
@@ -0,0 +1,392 @@
1
+ import type { Diagnostic } from './diagnostics.js'
2
+ import type {
3
+ Program,
4
+ Statement,
5
+ MutationClause,
6
+ WhereClause,
7
+ WherePattern,
8
+ Expression,
9
+ ObjectLiteral,
10
+ ObjectPattern,
11
+ ScalarValue,
12
+ MutateStatement,
13
+ AssertStatement,
14
+ SetFacetClause
15
+ } from './ast.js'
16
+ import type { Range } from './token.js'
17
+
18
+ /**
19
+ * Best-effort static checks layered on top of the syntax parser.
20
+ *
21
+ * These encode the KIP 2.0 rules that are decidable without a live Schema:
22
+ * - Core registry values written as literals (stance, mode, search mode);
23
+ * - the `[0,1]` ranges Core and the Cognitive Memory Profile fix;
24
+ * - unbounded recall lacking a LIMIT;
25
+ * - local handles referenced inside a mutation plan that nothing binds.
26
+ *
27
+ * Everything requiring the graph's Schema — whether a symbol resolves, whether
28
+ * a field exists, whether a Projection is sufficiently bounded — is left to
29
+ * the engine, which is the only party that knows the active Schema Environment.
30
+ */
31
+ export function analyzeSemantics(program: Program): Diagnostic[] {
32
+ const diags: Diagnostic[] = []
33
+ for (const stmt of program.statements) {
34
+ analyzeStatement(stmt, diags)
35
+ }
36
+ return diags
37
+ }
38
+
39
+ /** Core registries (Spec §20.13). A Schema Package may not shadow these. */
40
+ const STANCES = new Set(['support', 'reject', 'uncertain'])
41
+ const MODES = new Set([
42
+ 'observed',
43
+ 'stated',
44
+ 'inferred',
45
+ 'predicted',
46
+ 'hypothetical',
47
+ 'imported'
48
+ ])
49
+ const SEARCH_MODES = new Set(['keyword', 'semantic', 'hybrid'])
50
+ const ASSERTION_STATES = new Set([
51
+ 'active',
52
+ 'retracted',
53
+ 'superseded',
54
+ 'expired'
55
+ ])
56
+ const ACTIVITY_TERMINAL = new Set(['completed', 'failed', 'cancelled'])
57
+
58
+ /** Signals the Profile fixes to `[0,1]`; none of them is truth. */
59
+ const UNIT_INTERVAL_FIELDS = new Set([
60
+ 'confidence',
61
+ 'memory_strength',
62
+ 'salience',
63
+ 'utility',
64
+ 'threshold'
65
+ ])
66
+
67
+ function analyzeStatement(stmt: Statement, diags: Diagnostic[]): void {
68
+ switch (stmt.kind) {
69
+ case 'FindStatement':
70
+ checkWhere(stmt.where, !!stmt.limit, diags)
71
+ break
72
+
73
+ case 'MutateStatement':
74
+ checkMutate(stmt, diags)
75
+ for (const clause of stmt.clauses) analyzeMutationClause(clause, diags)
76
+ break
77
+
78
+ case 'SearchStatement':
79
+ if (stmt.mode) {
80
+ checkEnum(stmt.mode, SEARCH_MODES, 'SEARCH MODE', diags)
81
+ }
82
+ if (stmt.threshold && stmt.threshold.kind === 'NumberLiteral') {
83
+ checkUnitInterval('THRESHOLD', stmt.threshold.value, stmt.threshold.range, diags)
84
+ }
85
+ break
86
+
87
+ default:
88
+ analyzeMutationClause(stmt as MutationClause, diags)
89
+ }
90
+ }
91
+
92
+ function analyzeMutationClause(stmt: Statement, diags: Diagnostic[]): void {
93
+ switch (stmt.kind) {
94
+ case 'AssertStatement':
95
+ checkAssert(stmt, diags)
96
+ break
97
+
98
+ case 'CreateConceptStatement':
99
+ for (const facet of stmt.setFacets) checkFacet(facet, diags)
100
+ break
101
+
102
+ case 'UpsertConceptStatement':
103
+ for (const facet of stmt.setFacets) checkFacet(facet, diags)
104
+ break
105
+
106
+ case 'CreateAssertionStatement':
107
+ if (stmt.setFields) {
108
+ checkAssignmentValues(stmt.setFields.assignments, diags)
109
+ }
110
+ 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)
116
+ break
117
+
118
+ case 'UpdateStatement':
119
+ for (const action of stmt.actions) {
120
+ if (action.kind === 'SetFacetClause') checkFacet(action, diags)
121
+ }
122
+ if (stmt.where) checkWhere(stmt.where, !!stmt.limit, diags)
123
+ break
124
+
125
+ case 'RetractAssertionStatement':
126
+ case 'ArchiveStatement':
127
+ case 'TombstoneStatement':
128
+ if (stmt.expectState) {
129
+ checkEnum(stmt.expectState.value, ASSERTION_STATES, 'EXPECT STATE', diags)
130
+ }
131
+ break
132
+
133
+ case 'TransitionActivityStatement':
134
+ checkEnum(stmt.to, ACTIVITY_TERMINAL, 'TRANSITION ACTIVITY TO', diags)
135
+ break
136
+ }
137
+ }
138
+
139
+ /**
140
+ * `by` and `mode` carry the whole epistemic commitment, so a literal that is
141
+ * not in the Core registry is a mistake the toolkit can name now rather than
142
+ * letting the engine reject the whole transaction later.
143
+ */
144
+ function checkAssert(stmt: AssertStatement, diags: Diagnostic[]): void {
145
+ for (const entry of stmt.assignments.entries) {
146
+ if (entry.key === 'mode') {
147
+ checkEnum(entry.value, MODES, 'ASSERT mode', diags)
148
+ } else if (entry.key === 'stance') {
149
+ checkEnum(entry.value, STANCES, 'ASSERT stance', diags)
150
+ } else if (entry.key === 'confidence' && entry.value.kind === 'NumberLiteral') {
151
+ checkUnitInterval('confidence', entry.value.value, entry.value.range, diags)
152
+ }
153
+ }
154
+
155
+ // An observation with no cited artifact is still a valid Assertion, but it
156
+ // is the shape that most often should have carried Evidence.
157
+ const mode = stmt.assignments.entries.find((e) => e.key === 'mode')
158
+ const hasEvidence = stmt.assignments.entries.some((e) => e.key === 'evidence')
159
+ if (
160
+ mode &&
161
+ mode.value.kind === 'StringLiteral' &&
162
+ mode.value.parsed === 'observed' &&
163
+ !hasEvidence
164
+ ) {
165
+ diags.push({
166
+ range: stmt.assignments.range,
167
+ severity: 'info',
168
+ message:
169
+ 'mode: "observed" without evidence: an observation normally cites the artifact it was observed from',
170
+ code: 'KIP_2101'
171
+ })
172
+ }
173
+ }
174
+
175
+ function checkFacet(clause: SetFacetClause, diags: Diagnostic[]): void {
176
+ checkAssignmentValues(clause.assignments, diags)
177
+ }
178
+
179
+ function checkAssignmentValues(
180
+ object: ObjectLiteral,
181
+ diags: Diagnostic[]
182
+ ): void {
183
+ for (const entry of object.entries) {
184
+ if (
185
+ UNIT_INTERVAL_FIELDS.has(entry.key) &&
186
+ entry.value.kind === 'NumberLiteral'
187
+ ) {
188
+ checkUnitInterval(entry.key, entry.value.value, entry.value.range, diags)
189
+ }
190
+ if (entry.key === 'stance') checkEnum(entry.value, STANCES, 'stance', diags)
191
+ if (entry.key === 'mode') checkEnum(entry.value, MODES, 'mode', diags)
192
+ }
193
+ }
194
+
195
+ function checkEnum(
196
+ value: Expression | ScalarValue,
197
+ allowed: Set<string>,
198
+ label: string,
199
+ diags: Diagnostic[]
200
+ ): void {
201
+ // A parameter is bound at execution time; only a written literal is checkable.
202
+ if (value.kind !== 'StringLiteral') return
203
+ if (allowed.has(value.parsed)) return
204
+ diags.push({
205
+ range: value.range,
206
+ severity: 'error',
207
+ message: `${label} must be one of ${[...allowed].join(', ')}, got "${value.parsed}"`,
208
+ code: 'KIP_2001'
209
+ })
210
+ }
211
+
212
+ function checkUnitInterval(
213
+ field: string,
214
+ value: number,
215
+ range: Range,
216
+ diags: Diagnostic[]
217
+ ): void {
218
+ if (value >= 0 && value <= 1) return
219
+ diags.push({
220
+ range,
221
+ severity: 'error',
222
+ message: `${field} must be within [0, 1], got ${value}`,
223
+ code: 'KIP_2001'
224
+ })
225
+ }
226
+
227
+ // ---------------------------------------------------------------------------
228
+ // Handles
229
+ // ---------------------------------------------------------------------------
230
+
231
+ /**
232
+ * Handles are block-local and forward references are allowed, so binding is
233
+ * checked against the whole plan rather than in source order. A reference
234
+ * nothing binds is a typo the engine would only find at validation time.
235
+ */
236
+ function checkMutate(stmt: MutateStatement, diags: Diagnostic[]): void {
237
+ const bound = new Set<string>()
238
+ for (const clause of stmt.clauses) {
239
+ const handle = handleNameOf(clause)
240
+ if (handle) bound.add(handle)
241
+ }
242
+
243
+ const referenced: { name: string; range: Range }[] = []
244
+ for (const clause of stmt.clauses) collectHandleRefs(clause, referenced)
245
+
246
+ for (const ref of referenced) {
247
+ if (!bound.has(ref.name)) {
248
+ diags.push({
249
+ range: ref.range,
250
+ severity: 'error',
251
+ message: `?${ref.name} is not bound by any clause in this MUTATE block`,
252
+ code: 'KIP_2102'
253
+ })
254
+ }
255
+ }
256
+ }
257
+
258
+ function handleNameOf(clause: MutationClause): string | null {
259
+ switch (clause.kind) {
260
+ case 'CreateConceptStatement':
261
+ case 'UpsertConceptStatement':
262
+ case 'CreateEvidenceStatement':
263
+ case 'CreateAssertionStatement':
264
+ case 'CreateActivityStatement':
265
+ return clause.handle.name.slice(1)
266
+ case 'EnsurePropositionStatement':
267
+ case 'AssertStatement':
268
+ return clause.handle ? clause.handle.name.slice(1) : null
269
+ default:
270
+ return null
271
+ }
272
+ }
273
+
274
+ /** Collects `?handle` uses in value positions, where they must resolve. */
275
+ function collectHandleRefs(
276
+ clause: MutationClause,
277
+ out: { name: string; range: Range }[]
278
+ ): void {
279
+ const fromObject = (object: ObjectLiteral | undefined) => {
280
+ if (!object) return
281
+ for (const entry of object.entries) fromExpression(entry.value)
282
+ }
283
+ const fromExpression = (expr: Expression) => {
284
+ if (expr.kind === 'VariableRef') {
285
+ out.push({ name: expr.name.slice(1), range: expr.range })
286
+ } else if (expr.kind === 'ArrayLiteral') {
287
+ for (const element of expr.elements) fromExpression(element)
288
+ } else if (expr.kind === 'ObjectLiteral') {
289
+ for (const entry of expr.entries) fromExpression(entry.value)
290
+ } else if (expr.kind === 'FunctionCallExpr') {
291
+ for (const arg of expr.args) fromExpression(arg)
292
+ }
293
+ }
294
+
295
+ switch (clause.kind) {
296
+ case 'CreateConceptStatement':
297
+ case 'UpsertConceptStatement':
298
+ fromObject(clause.setFields?.assignments)
299
+ fromObject(clause.setAttributes?.assignments)
300
+ for (const facet of clause.setFacets) fromObject(facet.assignments)
301
+ for (const edge of clause.setStructural?.assignments ?? []) {
302
+ fromExpression(edge.value)
303
+ }
304
+ break
305
+
306
+ case 'CreateEvidenceStatement':
307
+ case 'CreateAssertionStatement':
308
+ case 'CreateActivityStatement':
309
+ fromObject(clause.setFields?.assignments)
310
+ for (const facet of clause.setFacets) fromObject(facet.assignments)
311
+ for (const edge of clause.setStructural?.assignments ?? []) {
312
+ fromExpression(edge.value)
313
+ }
314
+ break
315
+
316
+ case 'AssertStatement':
317
+ fromObject(clause.assignments)
318
+ if (clause.superseding?.kind === 'VariableRef') {
319
+ out.push({
320
+ name: clause.superseding.name.slice(1),
321
+ range: clause.superseding.range
322
+ })
323
+ }
324
+ break
325
+
326
+ case 'SupersedeAssertionStatement':
327
+ case 'CorrectEvidenceStatement':
328
+ for (const ref of [clause.target, clause.by]) {
329
+ if (ref.kind === 'VariableRef') {
330
+ out.push({ name: ref.name.slice(1), range: ref.range })
331
+ }
332
+ }
333
+ break
334
+ }
335
+ }
336
+
337
+ // ---------------------------------------------------------------------------
338
+ // Unbounded recall
339
+ // ---------------------------------------------------------------------------
340
+
341
+ function checkWhere(
342
+ where: WhereClause,
343
+ hasLimit: boolean,
344
+ diags: Diagnostic[]
345
+ ): void {
346
+ if (hasLimit) return
347
+ if (where.patterns.length === 0) return
348
+
349
+ // A pattern that constrains nothing enumerates the Space. That is a real
350
+ // query, but at scale it is almost always an omitted LIMIT.
351
+ if (where.patterns.every(isUnconstrained)) {
352
+ diags.push({
353
+ range: where.range,
354
+ severity: 'warning',
355
+ message:
356
+ 'this pattern constrains nothing and will scan the whole MemorySpace; add a LIMIT or a more specific match',
357
+ code: 'KIP_4002'
358
+ })
359
+ }
360
+ }
361
+
362
+ function isUnconstrained(pattern: WherePattern): boolean {
363
+ switch (pattern.kind) {
364
+ case 'ConceptPattern':
365
+ return isEmptyMatcher(pattern.matcher)
366
+ case 'AssertionPattern':
367
+ case 'EvidencePattern':
368
+ case 'ActivityPattern':
369
+ return isEmptyMatcher(pattern.matcher)
370
+ case 'PropositionPattern':
371
+ // `(id: ...)` names one Proposition, so it never scans.
372
+ if (pattern.tuple.id) return false
373
+ return (
374
+ !!pattern.tuple.subject &&
375
+ !!pattern.tuple.object &&
376
+ isOpenTerm(pattern.tuple.subject) &&
377
+ isOpenTerm(pattern.tuple.object)
378
+ )
379
+ default:
380
+ return false
381
+ }
382
+ }
383
+
384
+ function isEmptyMatcher(matcher: ObjectPattern): boolean {
385
+ return matcher.members.length === 0
386
+ }
387
+
388
+ function isOpenTerm(term: Expression | ObjectPattern): boolean {
389
+ if (term.kind === 'VariableRef') return true
390
+ if (term.kind === 'ObjectPattern') return term.members.length === 0
391
+ return false
392
+ }