@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.
- package/CHANGELOG.md +53 -0
- package/LICENSE +21 -0
- package/README.md +89 -64
- package/dist/ast.d.ts +511 -145
- package/dist/ast.d.ts.map +1 -1
- package/dist/diagnostics.d.ts +8 -2
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +32 -3
- package/dist/diagnostics.js.map +1 -1
- package/dist/exec-ast.d.ts +514 -149
- package/dist/exec-ast.d.ts.map +1 -1
- package/dist/exec-ast.js +8 -7
- package/dist/exec-ast.js.map +1 -1
- package/dist/formatter.d.ts.map +1 -1
- package/dist/formatter.js +870 -479
- package/dist/formatter.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/lexer.d.ts.map +1 -1
- package/dist/lexer.js +12 -30
- package/dist/lexer.js.map +1 -1
- package/dist/lower.d.ts +1 -2
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +1287 -598
- package/dist/lower.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +2410 -1300
- package/dist/parser.js.map +1 -1
- package/dist/semantics.d.ts +11 -7
- package/dist/semantics.d.ts.map +1 -1
- package/dist/semantics.js +295 -180
- package/dist/semantics.js.map +1 -1
- package/dist/token.d.ts +130 -40
- package/dist/token.d.ts.map +1 -1
- package/dist/token.js +264 -83
- package/dist/token.js.map +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/package.json +35 -5
- package/src/ast.ts +914 -0
- package/src/budget.ts +108 -0
- package/src/diagnostics.ts +182 -0
- package/src/errors.ts +42 -0
- package/src/exec-ast.ts +614 -0
- package/src/formatter.ts +1339 -0
- package/src/index.ts +226 -0
- package/src/lexer.ts +459 -0
- package/src/lower.ts +2011 -0
- package/src/parser.ts +3506 -0
- package/src/semantics.ts +392 -0
- package/src/token.ts +408 -0
- package/src/version.ts +13 -0
package/src/semantics.ts
ADDED
|
@@ -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
|
+
}
|