@kernlang/test 3.4.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/dist/index.js ADDED
@@ -0,0 +1,3011 @@
1
+ import { generateCoreNode, parseDocumentWithDiagnostics, validateSchema, validateSemantics } from '@kernlang/core';
2
+ import { execFileSync } from 'child_process';
3
+ import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
4
+ import { dirname, join, relative, resolve } from 'path';
5
+ import { inspect, isDeepStrictEqual } from 'util';
6
+ import { createContext, Script } from 'vm';
7
+ const DISCOVERY_SKIP_DIRS = new Set([
8
+ '.git',
9
+ '.next',
10
+ '.turbo',
11
+ '.vercel',
12
+ 'coverage',
13
+ 'dist',
14
+ 'generated',
15
+ 'node_modules',
16
+ ]);
17
+ const NATIVE_TEST_PRESETS = {
18
+ apisafety: ['duplicateRoutes', 'emptyRoutes', 'unvalidatedRoutes', 'unguardedEffects', 'uncheckedRoutePathParams'],
19
+ coverage: ['untestedTransitions', 'untestedGuards'],
20
+ effects: ['unguardedEffects', 'sensitiveEffectsRequireAuth', 'effectWithoutCleanup', 'unrecoveredAsync'],
21
+ guard: ['invalidGuards', 'weakGuards', 'nonExhaustiveGuards'],
22
+ machine: ['deadStates', 'duplicateTransitions'],
23
+ mcpsafety: ['duplicateParams', 'invalidGuards', 'unguardedToolParams', 'missingPathGuards', 'ssrfRisks'],
24
+ strict: [
25
+ 'duplicateNames',
26
+ 'duplicateRoutes',
27
+ 'emptyRoutes',
28
+ 'duplicateTransitions',
29
+ 'deadStates',
30
+ 'deriveCycles',
31
+ 'codegenErrors',
32
+ 'invalidGuards',
33
+ 'nonExhaustiveGuards',
34
+ 'unvalidatedRoutes',
35
+ 'unguardedEffects',
36
+ 'weakGuards',
37
+ 'duplicateParams',
38
+ 'unguardedToolParams',
39
+ 'missingPathGuards',
40
+ 'ssrfRisks',
41
+ 'sensitiveEffectsRequireAuth',
42
+ 'uncheckedRoutePathParams',
43
+ 'effectWithoutCleanup',
44
+ 'unrecoveredAsync',
45
+ ],
46
+ };
47
+ const NATIVE_KERN_TEST_RULES = [
48
+ { ruleId: 'file:validates', description: 'The native test file itself can be read, parsed, and schema validated.' },
49
+ { ruleId: 'suite:hasassertions', description: 'Every native test suite contains at least one expect assertion.' },
50
+ {
51
+ ruleId: 'machine:reaches',
52
+ description: 'A machine can reach the requested state, optionally through an explicit transition path.',
53
+ },
54
+ {
55
+ ruleId: 'machine:transition',
56
+ description: 'A machine declares a named transition with optional from/to/guarded constraints.',
57
+ },
58
+ { ruleId: 'guard:exhaustive', description: 'A guard covers every variant of the referenced union type.' },
59
+ {
60
+ ruleId: 'kern:node',
61
+ description: 'A target KERN node exists and optionally matches child-count or prop-value constraints.',
62
+ },
63
+ {
64
+ ruleId: 'expr',
65
+ description: 'Evaluate a constrained runtime expression against target const/derive bindings, with optional equals/matches/throws comparators.',
66
+ },
67
+ {
68
+ ruleId: 'runtime:behavior',
69
+ description: 'Evaluate a constrained pure fn or derive assertion with scoped native test fixtures.',
70
+ },
71
+ { ruleId: 'expect:unsupported', description: 'The expect assertion shape is not supported by native kern test.' },
72
+ { ruleId: 'preset:unknown', description: 'The requested preset name is unknown.' },
73
+ { ruleId: 'no:schemaviolations', description: 'The target KERN file has no schema violations.' },
74
+ { ruleId: 'no:semanticviolations', description: 'The target KERN file has no semantic validation violations.' },
75
+ {
76
+ ruleId: 'no:codegenerrors',
77
+ description: 'The target KERN file can be passed through core code generation without generator errors.',
78
+ },
79
+ { ruleId: 'no:derivecycles', description: 'The target derive graph has no cycles.' },
80
+ { ruleId: 'no:deadstates', description: 'Machines have no unreachable states.', presets: ['machine'] },
81
+ {
82
+ ruleId: 'no:duplicatetransitions',
83
+ description: 'Machines do not declare duplicate transition names.',
84
+ presets: ['machine'],
85
+ },
86
+ {
87
+ ruleId: 'no:duplicatenames',
88
+ description: 'Sibling declarations do not reuse the same type/name pair.',
89
+ presets: ['strict'],
90
+ },
91
+ {
92
+ ruleId: 'no:duplicateroutes',
93
+ description: 'API/server routes do not duplicate method and path.',
94
+ presets: ['apiSafety', 'strict'],
95
+ },
96
+ {
97
+ ruleId: 'no:emptyroutes',
98
+ description: 'Routes declare executable behavior with handler/respond/derive/fmt/branch/each/collect/effect.',
99
+ presets: ['apiSafety', 'strict'],
100
+ },
101
+ {
102
+ ruleId: 'no:unvalidatedroutes',
103
+ description: 'Mutating routes have schema, validation, guard, or auth coverage.',
104
+ presets: ['apiSafety', 'strict'],
105
+ },
106
+ {
107
+ ruleId: 'no:uncheckedroutepathparams',
108
+ description: 'Route path params are declared, validated, or guarded.',
109
+ presets: ['apiSafety', 'strict'],
110
+ },
111
+ { ruleId: 'no:rawhandlers', description: 'Raw handler escapes are absent when a suite forbids them.' },
112
+ {
113
+ ruleId: 'no:invalidguards',
114
+ description: 'Guards reference valid params and have valid guard configuration.',
115
+ presets: ['guard', 'mcpSafety', 'strict'],
116
+ },
117
+ {
118
+ ruleId: 'no:weakguards',
119
+ description: 'Expression guards include an else branch, handler, or typed security kind.',
120
+ presets: ['guard', 'strict'],
121
+ },
122
+ {
123
+ ruleId: 'no:nonexhaustiveguards',
124
+ description: 'Variant guards that declare covered cases cover every variant of their union.',
125
+ presets: ['guard', 'strict'],
126
+ },
127
+ {
128
+ ruleId: 'no:duplicateparams',
129
+ description: 'Parameter containers do not declare duplicate params.',
130
+ presets: ['mcpSafety', 'strict'],
131
+ },
132
+ {
133
+ ruleId: 'no:unguardedtoolparams',
134
+ description: 'Required tool params have param-specific guard coverage.',
135
+ presets: ['mcpSafety', 'strict'],
136
+ },
137
+ {
138
+ ruleId: 'no:missingpathguards',
139
+ description: 'Path-like params have path containment guards.',
140
+ presets: ['mcpSafety', 'strict'],
141
+ },
142
+ {
143
+ ruleId: 'no:ssrfrisks',
144
+ description: 'URL-like params and network effects have URL/host allowlist coverage.',
145
+ presets: ['mcpSafety', 'strict'],
146
+ },
147
+ {
148
+ ruleId: 'no:unguardedeffects',
149
+ description: 'Detected effects have guard, auth, or validation coverage.',
150
+ presets: ['apiSafety', 'effects', 'strict'],
151
+ },
152
+ {
153
+ ruleId: 'no:sensitiveeffectsrequireauth',
154
+ description: 'Sensitive detected effects have auth coverage.',
155
+ presets: ['effects', 'strict'],
156
+ },
157
+ {
158
+ ruleId: 'no:effectwithoutcleanup',
159
+ description: 'Effect blocks that need cleanup define cleanup handlers.',
160
+ presets: ['effects', 'strict'],
161
+ },
162
+ {
163
+ ruleId: 'no:unrecoveredasync',
164
+ description: 'Async blocks with handlers define recovery behavior.',
165
+ presets: ['effects', 'strict'],
166
+ },
167
+ {
168
+ ruleId: 'no:untestedtransitions',
169
+ description: 'Machine transitions are covered by native reachability assertions.',
170
+ presets: ['coverage'],
171
+ },
172
+ {
173
+ ruleId: 'no:untestedguards',
174
+ description: 'Guards are covered by exhaustive or guard-preset assertions.',
175
+ presets: ['coverage'],
176
+ },
177
+ ];
178
+ export function listNativeKernTestRules() {
179
+ return NATIVE_KERN_TEST_RULES.map((rule) => ({
180
+ ...rule,
181
+ ...(rule.presets ? { presets: [...rule.presets] } : {}),
182
+ }));
183
+ }
184
+ export function explainNativeKernTestRule(ruleId) {
185
+ const normalized = ruleId.includes(':') ? ruleId.toLowerCase() : invariantRuleId(ruleId);
186
+ return listNativeKernTestRules().find((rule) => rule.ruleId === normalized);
187
+ }
188
+ function getProps(node) {
189
+ return node.props || {};
190
+ }
191
+ function str(v) {
192
+ return typeof v === 'string' ? v : '';
193
+ }
194
+ function getChildren(node, type) {
195
+ return (node.children || []).filter((child) => child.type === type);
196
+ }
197
+ function exprToString(value) {
198
+ if (typeof value === 'string')
199
+ return value;
200
+ if (value && typeof value === 'object' && '__expr' in value) {
201
+ const expr = value;
202
+ if (typeof expr.code === 'string')
203
+ return expr.code;
204
+ }
205
+ return '';
206
+ }
207
+ function isTruthy(value) {
208
+ return value === true || value === 'true';
209
+ }
210
+ function isJsStringLiteralSource(value) {
211
+ return /^'(?:\\[\s\S]|[^'\\])*'$/.test(value) || /^"(?:\\[\s\S]|[^"\\])*"$/.test(value);
212
+ }
213
+ function exprPropToRuntimeSource(node, propName) {
214
+ const props = getProps(node);
215
+ const value = props[propName];
216
+ if (value === undefined || value === '')
217
+ return '';
218
+ if (node.__quotedProps?.includes(propName)) {
219
+ const source = String(value);
220
+ return isJsStringLiteralSource(source) ? source : JSON.stringify(value);
221
+ }
222
+ return exprToString(value);
223
+ }
224
+ function runtimeExpectedSource(node, propName) {
225
+ const props = getProps(node);
226
+ const value = props[propName];
227
+ if (value === undefined)
228
+ return undefined;
229
+ if (value && typeof value === 'object' && '__expr' in value)
230
+ return exprToString(value);
231
+ const source = String(value);
232
+ if (node.__quotedProps?.includes(propName)) {
233
+ return isJsStringLiteralSource(source) ? source : JSON.stringify(source);
234
+ }
235
+ const trimmed = source.trim();
236
+ if (/^(?:true|false|null)$/.test(trimmed))
237
+ return trimmed;
238
+ if (/^-?(?:\d+|\d*\.\d+)(?:e[+-]?\d+)?$/i.test(trimmed))
239
+ return trimmed;
240
+ return JSON.stringify(source);
241
+ }
242
+ function runtimePatternValue(node, propName) {
243
+ const props = getProps(node);
244
+ const value = props[propName];
245
+ if (value === undefined)
246
+ return undefined;
247
+ const source = String(value);
248
+ if (node.__quotedProps?.includes(propName) && isJsStringLiteralSource(source)) {
249
+ try {
250
+ const script = new Script(`"use strict";\n(${source});`);
251
+ const evaluated = script.runInContext(createContext(runtimeContext()), {
252
+ timeout: RUNTIME_EXPR_TIMEOUT_MS,
253
+ });
254
+ return String(evaluated);
255
+ }
256
+ catch {
257
+ return source;
258
+ }
259
+ }
260
+ return source;
261
+ }
262
+ function escapeRegExp(value) {
263
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
264
+ }
265
+ function collectNodes(node, type, acc = []) {
266
+ if (!node)
267
+ return acc;
268
+ if (node.type === type)
269
+ acc.push(node);
270
+ for (const child of node.children || [])
271
+ collectNodes(child, type, acc);
272
+ return acc;
273
+ }
274
+ function parseList(value) {
275
+ const trimmed = value.trim().replace(/^\[/, '').replace(/\]$/, '');
276
+ if (!trimmed)
277
+ return [];
278
+ return trimmed
279
+ .split(',')
280
+ .map((part) => part.trim().replace(/^["']|["']$/g, ''))
281
+ .filter(Boolean);
282
+ }
283
+ function parseNameList(value) {
284
+ return parseList(value).flatMap((part) => part
285
+ .split('|')
286
+ .map((name) => name.trim())
287
+ .filter(Boolean));
288
+ }
289
+ function normalizeInvariant(value) {
290
+ return value.replace(/[-_\s]/g, '').toLowerCase();
291
+ }
292
+ function severityFromNode(node) {
293
+ const severity = str(getProps(node).severity).toLowerCase();
294
+ return severity === 'warn' || severity === 'warning' ? 'warn' : 'error';
295
+ }
296
+ function statusForEvaluation(passed, severity) {
297
+ if (passed)
298
+ return 'passed';
299
+ return severity === 'warn' ? 'warning' : 'failed';
300
+ }
301
+ function effectiveSeverity(requestedSeverity, evaluated) {
302
+ return evaluated.severity || requestedSeverity;
303
+ }
304
+ function isAssertionConfigurationFailure(message) {
305
+ if (!message)
306
+ return false;
307
+ return (message.startsWith('Unsupported native ') ||
308
+ message.startsWith('Runtime expr assertions ') ||
309
+ message.startsWith('Runtime expr assertion requires ') ||
310
+ message.startsWith('Runtime expr assertion cannot execute ') ||
311
+ message.startsWith('Runtime expr assertion has ') ||
312
+ message.startsWith('Runtime fn assertion ') ||
313
+ message.startsWith('Runtime derive assertion ') ||
314
+ message.startsWith('Runtime behavior assertion ') ||
315
+ message.startsWith('Node assertion requires ') ||
316
+ message.startsWith('Node assertion count ') ||
317
+ message === 'Unsupported native expect assertion.' ||
318
+ message.includes(' assertion requires ') ||
319
+ message.includes(' needs over=') ||
320
+ message.startsWith('Machine not found:') ||
321
+ message.startsWith('Guard not found:') ||
322
+ message.startsWith('Union not found') ||
323
+ message.startsWith('State not found in machine '));
324
+ }
325
+ function grepMatches(options, result) {
326
+ const grep = options?.grep;
327
+ if (!grep)
328
+ return true;
329
+ const haystack = [
330
+ result.suite,
331
+ result.caseName,
332
+ result.ruleId,
333
+ result.assertion,
334
+ result.message || '',
335
+ result.file || '',
336
+ ].join('\n');
337
+ if (grep instanceof RegExp) {
338
+ grep.lastIndex = 0;
339
+ return grep.test(haystack);
340
+ }
341
+ return haystack.toLowerCase().includes(grep.toLowerCase());
342
+ }
343
+ function invariantRuleId(value) {
344
+ return `no:${normalizeInvariant(value) || 'unknown'}`;
345
+ }
346
+ function presetRuleId(value) {
347
+ return `preset:${normalizeInvariant(value) || 'unknown'}`;
348
+ }
349
+ function nodeLabel(node) {
350
+ const props = getProps(node);
351
+ const name = str(props.name);
352
+ const method = str(props.method);
353
+ const path = str(props.path);
354
+ if (method || path)
355
+ return `${node.type} ${(method || 'GET').toUpperCase()} ${path || '<missing-path>'}`;
356
+ if (name)
357
+ return `${node.type} ${name}`;
358
+ return node.type;
359
+ }
360
+ function handlerText(node) {
361
+ return getChildren(node, 'handler')
362
+ .map((handler) => str(getProps(handler).code))
363
+ .filter(Boolean)
364
+ .join('\n');
365
+ }
366
+ function collectNamedHandlerBodies(root) {
367
+ const bodies = new Map();
368
+ for (const fn of collectNodes(root, 'fn')) {
369
+ const name = str(getProps(fn).name);
370
+ const code = handlerText(fn);
371
+ if (name && code)
372
+ bodies.set(name, code);
373
+ }
374
+ return bodies;
375
+ }
376
+ function reachableHandlerText(root, node) {
377
+ const helperBodies = collectNamedHandlerBodies(root);
378
+ const chunks = [];
379
+ const queue = [handlerText(node)];
380
+ const visited = new Set();
381
+ while (queue.length > 0) {
382
+ const current = queue.shift() || '';
383
+ if (!current)
384
+ continue;
385
+ chunks.push(current);
386
+ for (const [name, body] of helperBodies) {
387
+ if (visited.has(name))
388
+ continue;
389
+ if (!new RegExp(`\\b${escapeRegExp(name)}\\s*\\(`).test(current))
390
+ continue;
391
+ visited.add(name);
392
+ queue.push(body);
393
+ }
394
+ }
395
+ return chunks.join('\n');
396
+ }
397
+ function hasInlinePermissionGate(node) {
398
+ const code = handlerText(node);
399
+ if (!code)
400
+ return false;
401
+ const declaresPermissionCheck = /\b(?:function|const|let)\s+checkPermission\b/.test(code) ||
402
+ /\bcheckPermission\s*[:=]\s*(?:async\s*)?\(/.test(code);
403
+ if (!declaresPermissionCheck)
404
+ return false;
405
+ const returnsPermissionCheck = /\breturn\s*\{[\s\S]*\bcheckPermission\b[\s\S]*\}/.test(code);
406
+ const hasDecisionSignal = /\bPermissionDecision\b/.test(code) ||
407
+ /\bpermissionMode\b/.test(code) ||
408
+ /\bbehavior\s*:\s*['"](?:allow|ask|deny)['"]/.test(code);
409
+ return returnsPermissionCheck && hasDecisionSignal;
410
+ }
411
+ function hasGuardLikeChild(node) {
412
+ return ((node.children || []).some((child) => ['guard', 'auth', 'validate'].includes(child.type)) ||
413
+ hasInlinePermissionGate(node));
414
+ }
415
+ function isMultiSourceTransitionFalsePositive(violation, root) {
416
+ if (violation.rule !== 'machine-transition-from')
417
+ return false;
418
+ for (const machine of collectNodes(root, 'machine')) {
419
+ const stateNames = new Set(getChildren(machine, 'state').map((state) => str(getProps(state).name)));
420
+ for (const transition of getChildren(machine, 'transition')) {
421
+ if (transition.loc?.line !== violation.line)
422
+ continue;
423
+ const sources = parseNameList(str(getProps(transition).from));
424
+ if (sources.length > 1 && sources.every((source) => stateNames.has(source)))
425
+ return true;
426
+ }
427
+ }
428
+ return false;
429
+ }
430
+ function loadKernDocument(file) {
431
+ if (!existsSync(file)) {
432
+ return {
433
+ file,
434
+ diagnostics: [],
435
+ schemaViolations: [],
436
+ semanticViolations: [],
437
+ readError: `Not found: ${file}`,
438
+ };
439
+ }
440
+ const source = readFileSync(file, 'utf-8');
441
+ const parsed = parseDocumentWithDiagnostics(source);
442
+ const schemaViolations = validateSchema(parsed.root);
443
+ const semanticViolations = validateSemantics(parsed.root).filter((violation) => !isMultiSourceTransitionFalsePositive(violation, parsed.root));
444
+ return {
445
+ file,
446
+ root: parsed.root,
447
+ diagnostics: parsed.diagnostics,
448
+ schemaViolations,
449
+ semanticViolations,
450
+ };
451
+ }
452
+ function firstParseError(doc) {
453
+ return doc.diagnostics.find((diagnostic) => diagnostic.severity === 'error');
454
+ }
455
+ function targetBlockingMessage(doc) {
456
+ if (doc.readError)
457
+ return doc.readError;
458
+ const parseError = firstParseError(doc);
459
+ if (parseError)
460
+ return `Target has parse error at ${doc.file}:${parseError.line}:${parseError.col}: ${parseError.message}`;
461
+ const schemaViolation = doc.schemaViolations[0];
462
+ if (schemaViolation) {
463
+ return `Target has schema violation at ${doc.file}:${schemaViolation.line ?? 1}:${schemaViolation.col ?? 1}: ${schemaViolation.message}`;
464
+ }
465
+ return undefined;
466
+ }
467
+ function issueResult(file, message, issue) {
468
+ return {
469
+ suite: 'native test',
470
+ caseName: 'file validates',
471
+ ruleId: 'file:validates',
472
+ assertion: 'parse/schema validity',
473
+ severity: 'error',
474
+ status: 'failed',
475
+ message,
476
+ file,
477
+ line: issue?.line,
478
+ col: issue?.col,
479
+ };
480
+ }
481
+ function runtimeFixtureBinding(node) {
482
+ const props = getProps(node);
483
+ const name = str(props.name);
484
+ const expr = exprPropToRuntimeSource(node, 'value') || exprPropToRuntimeSource(node, 'expr');
485
+ if (!name || !expr)
486
+ return undefined;
487
+ return { name, expr, kind: 'fixture', line: node.loc?.line };
488
+ }
489
+ function runtimeFixtureBindings(node) {
490
+ return getChildren(node, 'fixture')
491
+ .map((fixture) => runtimeFixtureBinding(fixture))
492
+ .filter((fixture) => fixture !== undefined);
493
+ }
494
+ function collectAssertions(testNode) {
495
+ const suite = str(getProps(testNode).name) || 'unnamed test';
496
+ const assertions = [];
497
+ function pushExpectation(node, path, fixtures) {
498
+ assertions.push({
499
+ suite,
500
+ caseName: path.length > 0 ? path.join(' > ') : 'top-level',
501
+ node,
502
+ fixtures,
503
+ });
504
+ }
505
+ function visit(node, path, fixtures) {
506
+ const scopedFixtures = [...fixtures, ...runtimeFixtureBindings(node)];
507
+ if (node.type === 'expect') {
508
+ pushExpectation(node, path, scopedFixtures);
509
+ return;
510
+ }
511
+ if (node.type === 'it') {
512
+ const nextPath = [...path, str(getProps(node).name) || 'it'];
513
+ for (const child of node.children || []) {
514
+ if (child.type === 'expect')
515
+ pushExpectation(child, nextPath, scopedFixtures);
516
+ }
517
+ return;
518
+ }
519
+ if (node.type === 'describe') {
520
+ const nextPath = [...path, str(getProps(node).name) || 'describe'];
521
+ for (const child of node.children || [])
522
+ visit(child, nextPath, scopedFixtures);
523
+ return;
524
+ }
525
+ for (const child of node.children || [])
526
+ visit(child, path, scopedFixtures);
527
+ }
528
+ visit(testNode, [], []);
529
+ return assertions;
530
+ }
531
+ function assertionLabel(node) {
532
+ const props = getProps(node);
533
+ const preset = str(props.preset);
534
+ const nodeType = str(props.node);
535
+ const name = str(props.name);
536
+ const prop = str(props.prop);
537
+ const isValue = props.is === undefined ? '' : exprToString(props.is) || String(props.is);
538
+ const child = str(props.child);
539
+ const childName = str(props.childName);
540
+ const count = props.count === undefined ? '' : String(props.count);
541
+ const machine = str(props.machine);
542
+ const transition = str(props.transition);
543
+ const from = str(props.from);
544
+ const to = str(props.to);
545
+ const reaches = str(props.reaches);
546
+ const no = str(props.no);
547
+ const guard = str(props.guard);
548
+ const expr = exprToString(props.expr);
549
+ const fn = str(props.fn);
550
+ const derive = str(props.derive);
551
+ const args = exprToString(props.args);
552
+ const withValue = exprToString(props.with);
553
+ const equals = props.equals === undefined ? '' : exprToString(props.equals) || String(props.equals);
554
+ const matches = props.matches === undefined ? '' : String(props.matches);
555
+ const throws = props.throws === undefined ? '' : String(props.throws || 'true');
556
+ if (preset)
557
+ return `preset ${preset}`;
558
+ if (nodeType) {
559
+ const parts = [`node ${nodeType}`];
560
+ if (name)
561
+ parts.push(name);
562
+ if (child)
563
+ parts.push(`has ${child}${childName ? ` ${childName}` : ''}`);
564
+ if (prop)
565
+ parts.push(`prop ${prop}${isValue ? ` is ${isValue}` : ''}`);
566
+ if (count)
567
+ parts.push(`count ${count}`);
568
+ return parts.join(' ');
569
+ }
570
+ if (no)
571
+ return `${machine ? `machine ${machine} ` : ''}no ${no}`;
572
+ if (guard)
573
+ return `guard ${guard} exhaustive`;
574
+ if (machine && transition) {
575
+ return [`machine ${machine} transition ${transition}`, from ? `from ${from}` : '', to ? `to ${to}` : '']
576
+ .filter(Boolean)
577
+ .join(' ');
578
+ }
579
+ if (machine || reaches) {
580
+ return [`machine ${machine || '<missing>'}`, from ? `from ${from}` : '', `reaches ${reaches || '<missing>'}`]
581
+ .filter(Boolean)
582
+ .join(' ');
583
+ }
584
+ if (fn) {
585
+ const parts = [`fn ${fn}`];
586
+ if (args)
587
+ parts.push(`args ${args}`);
588
+ if (withValue)
589
+ parts.push(`with ${withValue}`);
590
+ if (equals)
591
+ parts.push(`equals ${equals}`);
592
+ if (matches)
593
+ parts.push(`matches ${matches}`);
594
+ if (throws)
595
+ parts.push(`throws ${throws}`);
596
+ return parts.join(' ');
597
+ }
598
+ if (derive) {
599
+ const parts = [`derive ${derive}`];
600
+ if (equals)
601
+ parts.push(`equals ${equals}`);
602
+ if (matches)
603
+ parts.push(`matches ${matches}`);
604
+ if (throws)
605
+ parts.push(`throws ${throws}`);
606
+ return parts.join(' ');
607
+ }
608
+ if (expr && equals)
609
+ return `expr ${expr} equals ${equals}`;
610
+ if (expr && matches)
611
+ return `expr ${expr} matches ${matches}`;
612
+ if (expr && throws)
613
+ return `expr ${expr} throws ${throws}`;
614
+ if (expr)
615
+ return `expr ${expr}`;
616
+ return 'expect';
617
+ }
618
+ function findDeriveCycles(root) {
619
+ const derives = collectNodes(root, 'derive')
620
+ .map((node) => ({
621
+ name: str(getProps(node).name),
622
+ expr: exprToString(getProps(node).expr),
623
+ }))
624
+ .filter((derive) => derive.name && derive.expr);
625
+ const names = derives.map((derive) => derive.name);
626
+ const graph = new Map();
627
+ for (const derive of derives) {
628
+ graph.set(derive.name, names.filter((name) => name !== derive.name && new RegExp(`\\b${escapeRegExp(name)}\\b`).test(derive.expr)));
629
+ if (new RegExp(`\\b${escapeRegExp(derive.name)}\\b`).test(derive.expr)) {
630
+ graph.get(derive.name).push(derive.name);
631
+ }
632
+ }
633
+ const cycles = [];
634
+ const visiting = new Set();
635
+ const visited = new Set();
636
+ const stack = [];
637
+ function dfs(name) {
638
+ if (visiting.has(name)) {
639
+ const idx = stack.indexOf(name);
640
+ cycles.push([...stack.slice(idx), name]);
641
+ return;
642
+ }
643
+ if (visited.has(name))
644
+ return;
645
+ visiting.add(name);
646
+ stack.push(name);
647
+ for (const dep of graph.get(name) || [])
648
+ dfs(dep);
649
+ stack.pop();
650
+ visiting.delete(name);
651
+ visited.add(name);
652
+ }
653
+ for (const name of names)
654
+ dfs(name);
655
+ return cycles;
656
+ }
657
+ function selectedMachines(root, machineName) {
658
+ const machines = collectNodes(root, 'machine');
659
+ return machineName ? machines.filter((machine) => str(getProps(machine).name) === machineName) : machines;
660
+ }
661
+ function findUnreachableStates(root, machineName) {
662
+ const failures = [];
663
+ const machines = selectedMachines(root, machineName);
664
+ if (machineName && machines.length === 0)
665
+ return [`Machine not found: ${machineName}`];
666
+ for (const machine of machines) {
667
+ const name = str(getProps(machine).name) || '<unnamed>';
668
+ const states = getChildren(machine, 'state').map((state) => ({
669
+ name: str(getProps(state).name),
670
+ initial: isTruthy(getProps(state).initial),
671
+ }));
672
+ const initialState = states.find((state) => state.initial)?.name || states[0]?.name;
673
+ if (!initialState)
674
+ continue;
675
+ const transitions = getChildren(machine, 'transition').map((transition) => ({
676
+ name: str(getProps(transition).name),
677
+ from: parseNameList(str(getProps(transition).from)),
678
+ to: str(getProps(transition).to),
679
+ }));
680
+ const visited = new Set([initialState]);
681
+ const queue = [initialState];
682
+ while (queue.length > 0) {
683
+ const current = queue.shift();
684
+ for (const transition of transitions.filter((candidate) => candidate.from.includes(current))) {
685
+ if (visited.has(transition.to))
686
+ continue;
687
+ visited.add(transition.to);
688
+ queue.push(transition.to);
689
+ }
690
+ }
691
+ const unreachable = states.map((state) => state.name).filter((state) => state && !visited.has(state));
692
+ if (unreachable.length > 0)
693
+ failures.push(`${name}: ${unreachable.join(', ')}`);
694
+ }
695
+ return failures;
696
+ }
697
+ function findDuplicateTransitions(root, machineName) {
698
+ const failures = [];
699
+ const machines = selectedMachines(root, machineName);
700
+ if (machineName && machines.length === 0)
701
+ return [`Machine not found: ${machineName}`];
702
+ for (const machine of machines) {
703
+ const name = str(getProps(machine).name) || '<unnamed>';
704
+ const seen = new Map();
705
+ for (const transition of getChildren(machine, 'transition')) {
706
+ const props = getProps(transition);
707
+ const transitionName = str(props.name) || '<unnamed>';
708
+ const sources = parseNameList(str(props.from));
709
+ for (const source of sources) {
710
+ const key = `${source}:${transitionName}`;
711
+ const previous = seen.get(key);
712
+ if (previous) {
713
+ failures.push(`${name}: transition '${transitionName}' is duplicated from '${source}' at line ${transition.loc?.line ?? '?'} (first at line ${previous.loc?.line ?? '?'})`);
714
+ }
715
+ else {
716
+ seen.set(key, transition);
717
+ }
718
+ }
719
+ }
720
+ }
721
+ return failures;
722
+ }
723
+ function findDuplicateRoutes(root) {
724
+ const seen = new Map();
725
+ const failures = [];
726
+ for (const route of collectNodes(root, 'route')) {
727
+ const props = getProps(route);
728
+ const method = (str(props.method) || 'get').toUpperCase();
729
+ const path = str(props.path);
730
+ if (!path)
731
+ continue;
732
+ const key = `${method} ${path}`;
733
+ const previous = seen.get(key);
734
+ if (previous) {
735
+ failures.push(`${key} at line ${route.loc?.line ?? '?'} (first at line ${previous.loc?.line ?? '?'})`);
736
+ }
737
+ else {
738
+ seen.set(key, route);
739
+ }
740
+ }
741
+ return failures;
742
+ }
743
+ function routeHasBehavior(route) {
744
+ return ['handler', 'respond', 'derive', 'fmt', 'branch', 'each', 'collect', 'effect'].some((childType) => getChildren(route, childType).length > 0);
745
+ }
746
+ function findEmptyRoutes(root) {
747
+ return collectNodes(root, 'route')
748
+ .filter((route) => !routeHasBehavior(route))
749
+ .map((route) => `${nodeLabel(route)} at line ${route.loc?.line ?? '?'} has no behavior node`);
750
+ }
751
+ function findDuplicateSiblingNames(root) {
752
+ const failures = [];
753
+ function visit(node) {
754
+ const seen = new Map();
755
+ for (const child of node.children || []) {
756
+ const name = str(getProps(child).name);
757
+ if (!name)
758
+ continue;
759
+ const key = `${child.type}:${name}`;
760
+ const previous = seen.get(key);
761
+ if (previous) {
762
+ failures.push(`${nodeLabel(child)} at line ${child.loc?.line ?? '?'} duplicates line ${previous.loc?.line ?? '?'}`);
763
+ }
764
+ else {
765
+ seen.set(key, child);
766
+ }
767
+ }
768
+ for (const child of node.children || [])
769
+ visit(child);
770
+ }
771
+ visit(root);
772
+ return failures;
773
+ }
774
+ function findWeakGuards(root) {
775
+ return collectNodes(root, 'guard')
776
+ .filter((guard) => {
777
+ const props = getProps(guard);
778
+ const hasExpr = 'expr' in props;
779
+ const hasSecurityKind = 'kind' in props || 'type' in props;
780
+ const hasElse = 'else' in props;
781
+ const hasHandler = getChildren(guard, 'handler').length > 0;
782
+ return hasExpr && !hasSecurityKind && !hasElse && !hasHandler;
783
+ })
784
+ .map((guard) => `${nodeLabel(guard)} at line ${guard.loc?.line ?? '?'} has expr but no else/handler`);
785
+ }
786
+ const EFFECT_PATTERNS = [
787
+ {
788
+ kind: 'shell',
789
+ label: 'shell command',
790
+ sensitive: true,
791
+ re: /\b(?:exec|execFile|execFileSync|execSync|spawn|spawnSync)\s*\(/,
792
+ },
793
+ {
794
+ kind: 'network',
795
+ label: 'network request',
796
+ sensitive: true,
797
+ re: /\b(?:fetch|axios|got|request)\s*\(|\bnew\s+WebSocket\s*\(/,
798
+ },
799
+ {
800
+ kind: 'fs-write',
801
+ label: 'filesystem write',
802
+ sensitive: true,
803
+ re: /\b(?:appendFile|appendFileSync|createWriteStream|mkdir|mkdirSync|rename|renameSync|rm|rmSync|rmdir|rmdirSync|unlink|unlinkSync|writeFile|writeFileSync)\s*\(/,
804
+ },
805
+ {
806
+ kind: 'fs-read',
807
+ label: 'filesystem read',
808
+ sensitive: true,
809
+ re: /\b(?:access|accessSync|createReadStream|existsSync|lstat|lstatSync|readFile|readFileSync|readdir|readdirSync|stat|statSync)\s*\(/,
810
+ },
811
+ {
812
+ kind: 'database',
813
+ label: 'database query',
814
+ sensitive: true,
815
+ re: /\b(?:client|collection|connection|database|db|knex|pool|prisma|repo|repository)\s*\.(?:create|delete|execute|findFirst|findMany|findUnique|insert|query|select|update|upsert)\s*\(|(?:^|[^\w.])(?:create|delete|execute|findFirst|findMany|findUnique|query|update|upsert)\s*\(|\bsql\s*`/,
816
+ },
817
+ {
818
+ kind: 'email',
819
+ label: 'email send',
820
+ sensitive: true,
821
+ re: /\b(?:mailer\.send|sendEmail|sendMail|transporter\.send)\s*\(/,
822
+ },
823
+ ];
824
+ function classifyEffect(code) {
825
+ const pattern = EFFECT_PATTERNS.find((candidate) => candidate.re.test(code));
826
+ if (!pattern)
827
+ return undefined;
828
+ return { kind: pattern.kind, label: pattern.label, sensitive: pattern.sensitive };
829
+ }
830
+ function findUnguardedEffects(root) {
831
+ const checkedTypes = new Set(['action', 'command', 'fn', 'job', 'middleware', 'route', 'tool']);
832
+ const failures = [];
833
+ function visit(node) {
834
+ if (checkedTypes.has(node.type)) {
835
+ const code = reachableHandlerText(root, node);
836
+ const effect = classifyEffect(code);
837
+ if (effect && !hasGuardLikeChild(node)) {
838
+ failures.push(`${nodeLabel(node)} at line ${node.loc?.line ?? '?'} performs ${effect.label} without guard/auth/validate/permission`);
839
+ }
840
+ }
841
+ for (const child of node.children || [])
842
+ visit(child);
843
+ }
844
+ visit(root);
845
+ return failures;
846
+ }
847
+ function findUnvalidatedMutatingRoutes(root) {
848
+ const mutatingMethods = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
849
+ return collectNodes(root, 'route')
850
+ .filter((route) => mutatingMethods.has((str(getProps(route).method) || 'get').toUpperCase()))
851
+ .filter((route) => !hasGuardLikeChild(route) && getChildren(route, 'schema').length === 0)
852
+ .map((route) => `${nodeLabel(route)} at line ${route.loc?.line ?? '?'} mutates without schema/validate/guard/auth`);
853
+ }
854
+ function findRawHandlerEscapes(root) {
855
+ return collectNodes(root, 'handler')
856
+ .filter((handler) => str(getProps(handler).code).trim().length > 0)
857
+ .map((handler) => `handler at line ${handler.loc?.line ?? '?'}`);
858
+ }
859
+ const PARAM_CONTAINER_TYPES = new Set(['tool', 'resource', 'prompt', 'fn', 'method', 'constructor', 'route']);
860
+ const BOUNDED_GUARD_CONTAINER_TYPES = new Set(['tool', 'route', 'resource', 'prompt']);
861
+ function parseLegacyParamNames(value) {
862
+ return value
863
+ .split(',')
864
+ .map((part) => part.trim().split('=')[0]?.trim() || '')
865
+ .map((part) => part
866
+ .replace(/[?].*$/, '')
867
+ .split(':')[0]
868
+ ?.trim() || '')
869
+ .filter(Boolean);
870
+ }
871
+ function declaredParamNames(node) {
872
+ const names = new Set();
873
+ for (const param of getChildren(node, 'param')) {
874
+ const name = str(getProps(param).name);
875
+ if (name)
876
+ names.add(name);
877
+ }
878
+ for (const paramsNode of getChildren(node, 'params')) {
879
+ const items = getProps(paramsNode).items;
880
+ if (Array.isArray(items)) {
881
+ for (const item of items) {
882
+ if (item && typeof item === 'object' && 'name' in item) {
883
+ const name = str(item.name);
884
+ if (name)
885
+ names.add(name);
886
+ }
887
+ }
888
+ }
889
+ }
890
+ for (const name of parseLegacyParamNames(str(getProps(node).params)))
891
+ names.add(name);
892
+ const path = str(getProps(node).path);
893
+ for (const match of path.matchAll(/:([A-Za-z_$][A-Za-z0-9_$]*)/g)) {
894
+ names.add(match[1]);
895
+ }
896
+ return names;
897
+ }
898
+ function numericProp(props, key) {
899
+ const value = props[key];
900
+ if (typeof value === 'number')
901
+ return Number.isFinite(value) ? value : undefined;
902
+ if (typeof value !== 'string' || !value.trim())
903
+ return undefined;
904
+ const parsed = Number(value);
905
+ return Number.isFinite(parsed) ? parsed : undefined;
906
+ }
907
+ function findInvalidGuards(root) {
908
+ const failures = [];
909
+ function visit(node, parent) {
910
+ if (node.type === 'guard') {
911
+ const props = getProps(node);
912
+ const kind = normalizeInvariant(str(props.kind) || str(props.type) || str(props.name));
913
+ const param = str(props.param) || str(props.target) || str(props.field);
914
+ const line = node.loc?.line ?? '?';
915
+ if (param && parent && PARAM_CONTAINER_TYPES.has(parent.type)) {
916
+ const params = declaredParamNames(parent);
917
+ if (!params.has(param)) {
918
+ failures.push(`${nodeLabel(node)} at line ${line} references unknown param '${param}' on ${nodeLabel(parent)}`);
919
+ }
920
+ }
921
+ if (parent &&
922
+ BOUNDED_GUARD_CONTAINER_TYPES.has(parent.type) &&
923
+ (kind === 'pathcontainment' || kind === 'pathcontainmentguard') &&
924
+ !str(props.allowlist) &&
925
+ !str(props.allow) &&
926
+ !str(props.root) &&
927
+ !str(props.roots)) {
928
+ failures.push(`${nodeLabel(node)} at line ${line} is path containment without allowlist/allow/root/roots`);
929
+ }
930
+ const min = numericProp(props, 'min');
931
+ const max = numericProp(props, 'max');
932
+ if (kind === 'validate' && min !== undefined && max !== undefined && min > max) {
933
+ failures.push(`${nodeLabel(node)} at line ${line} has min ${min} greater than max ${max}`);
934
+ }
935
+ const maxRequests = numericProp(props, 'maxRequests');
936
+ if (kind === 'ratelimit' && maxRequests !== undefined && maxRequests <= 0) {
937
+ failures.push(`${nodeLabel(node)} at line ${line} has maxRequests ${maxRequests}; expected > 0`);
938
+ }
939
+ const windowMs = numericProp(props, 'windowMs');
940
+ if (kind === 'ratelimit' && windowMs !== undefined && windowMs <= 0) {
941
+ failures.push(`${nodeLabel(node)} at line ${line} has windowMs ${windowMs}; expected > 0`);
942
+ }
943
+ }
944
+ for (const child of node.children || [])
945
+ visit(child, node);
946
+ }
947
+ visit(root);
948
+ return failures;
949
+ }
950
+ function declaredParamNameList(node) {
951
+ const names = [];
952
+ for (const param of getChildren(node, 'param')) {
953
+ const name = str(getProps(param).name);
954
+ if (name)
955
+ names.push(name);
956
+ }
957
+ for (const paramsNode of getChildren(node, 'params')) {
958
+ const items = getProps(paramsNode).items;
959
+ if (Array.isArray(items)) {
960
+ for (const item of items) {
961
+ if (item && typeof item === 'object' && 'name' in item) {
962
+ const name = str(item.name);
963
+ if (name)
964
+ names.push(name);
965
+ }
966
+ }
967
+ }
968
+ }
969
+ names.push(...parseLegacyParamNames(str(getProps(node).params)));
970
+ return names;
971
+ }
972
+ function guardKind(node) {
973
+ const props = getProps(node);
974
+ return normalizeInvariant(str(props.kind) || str(props.type) || str(props.name));
975
+ }
976
+ function guardParam(node) {
977
+ const props = getProps(node);
978
+ return str(props.param) || str(props.target) || str(props.field);
979
+ }
980
+ function guardTargetsParam(node, paramName) {
981
+ return guardParam(node) === paramName;
982
+ }
983
+ function paramNodeByName(container, paramName) {
984
+ return getChildren(container, 'param').find((param) => str(getProps(param).name) === paramName);
985
+ }
986
+ function paramSpecificGuards(container, paramName) {
987
+ const param = paramNodeByName(container, paramName);
988
+ return [
989
+ ...getChildren(container, 'guard').filter((guard) => guardTargetsParam(guard, paramName)),
990
+ ...(param ? getChildren(param, 'guard') : []),
991
+ ];
992
+ }
993
+ function hasParamSpecificGuard(container, paramName) {
994
+ return paramSpecificGuards(container, paramName).length > 0;
995
+ }
996
+ function isRequiredParam(node) {
997
+ return isTruthy(getProps(node).required);
998
+ }
999
+ function isPathLikeParam(node) {
1000
+ const props = getProps(node);
1001
+ const name = str(props.name).toLowerCase();
1002
+ const type = str(props.type).toLowerCase();
1003
+ return /(path|file|dir|directory|folder|root)/.test(name) || /(path|file|directory)/.test(type);
1004
+ }
1005
+ function isUrlLikeName(value) {
1006
+ return /(^|[_-])(url|uri|host|hostname|endpoint|baseurl|callback|webhook)([_-]|$)/i.test(value);
1007
+ }
1008
+ function hasPathContainmentGuard(container, paramName) {
1009
+ return paramSpecificGuards(container, paramName).some((guard) => {
1010
+ const kind = guardKind(guard);
1011
+ return kind === 'pathcontainment' || kind === 'pathcontainmentguard' || kind === 'path';
1012
+ });
1013
+ }
1014
+ function allChildGuards(node) {
1015
+ const guards = [];
1016
+ for (const child of node.children || []) {
1017
+ if (child.type === 'guard')
1018
+ guards.push(child);
1019
+ if (child.type === 'param')
1020
+ guards.push(...getChildren(child, 'guard'));
1021
+ }
1022
+ return guards;
1023
+ }
1024
+ function hasAuthLikeChild(node) {
1025
+ return (node.children || []).some((child) => {
1026
+ if (child.type === 'auth')
1027
+ return true;
1028
+ if (child.type === 'middleware') {
1029
+ const props = getProps(child);
1030
+ const names = `${str(props.name)} ${str(props.names)}`.toLowerCase();
1031
+ return /\bauth\b|oauth|session|jwt/.test(names);
1032
+ }
1033
+ if (child.type === 'guard') {
1034
+ const kind = guardKind(child);
1035
+ return kind === 'auth' || kind === 'oauth' || kind === 'session' || kind === 'jwt';
1036
+ }
1037
+ return false;
1038
+ });
1039
+ }
1040
+ function hasAuthorizationLikeGate(node) {
1041
+ return hasAuthLikeChild(node) || hasInlinePermissionGate(node);
1042
+ }
1043
+ function hasUrlAllowlistGuard(node, paramName) {
1044
+ return allChildGuards(node).some((guard) => {
1045
+ if (paramName && guardParam(guard) && !guardTargetsParam(guard, paramName))
1046
+ return false;
1047
+ const props = getProps(guard);
1048
+ const kind = guardKind(guard);
1049
+ const hasBoundary = str(props.allowlist) ||
1050
+ str(props.allow) ||
1051
+ str(props.host) ||
1052
+ str(props.hosts) ||
1053
+ str(props.domain) ||
1054
+ str(props.domains) ||
1055
+ str(props.pattern) ||
1056
+ str(props.regex);
1057
+ return (kind === 'urlallowlist' ||
1058
+ kind === 'hostallowlist' ||
1059
+ kind === 'domainallowlist' ||
1060
+ kind === 'ssrf' ||
1061
+ kind === 'allowlist' ||
1062
+ Boolean(hasBoundary));
1063
+ });
1064
+ }
1065
+ function findDuplicateParams(root) {
1066
+ const failures = [];
1067
+ for (const containerType of PARAM_CONTAINER_TYPES) {
1068
+ for (const container of collectNodes(root, containerType)) {
1069
+ const seen = new Set();
1070
+ for (const name of declaredParamNameList(container)) {
1071
+ if (seen.has(name)) {
1072
+ failures.push(`${nodeLabel(container)} duplicates param '${name}'`);
1073
+ }
1074
+ else {
1075
+ seen.add(name);
1076
+ }
1077
+ }
1078
+ }
1079
+ }
1080
+ return failures;
1081
+ }
1082
+ function findUnguardedRequiredToolParams(root) {
1083
+ const failures = [];
1084
+ for (const containerType of ['tool', 'resource', 'prompt']) {
1085
+ for (const container of collectNodes(root, containerType)) {
1086
+ for (const param of getChildren(container, 'param')) {
1087
+ const name = str(getProps(param).name);
1088
+ if (name && isRequiredParam(param) && !hasParamSpecificGuard(container, name)) {
1089
+ failures.push(`${nodeLabel(container)} requires param '${name}' without a param-specific guard`);
1090
+ }
1091
+ }
1092
+ }
1093
+ }
1094
+ return failures;
1095
+ }
1096
+ function findMissingPathGuards(root) {
1097
+ const failures = [];
1098
+ for (const containerType of ['tool', 'resource', 'prompt']) {
1099
+ for (const container of collectNodes(root, containerType)) {
1100
+ for (const param of getChildren(container, 'param')) {
1101
+ const name = str(getProps(param).name);
1102
+ if (name && isPathLikeParam(param) && !hasPathContainmentGuard(container, name)) {
1103
+ failures.push(`${nodeLabel(container)} path-like param '${name}' lacks pathContainment guard`);
1104
+ }
1105
+ }
1106
+ }
1107
+ }
1108
+ return failures;
1109
+ }
1110
+ function findSsrfRisks(root) {
1111
+ const failures = [];
1112
+ const checkedTypes = new Set(['action', 'command', 'fn', 'job', 'route', 'tool']);
1113
+ function visit(node) {
1114
+ if (checkedTypes.has(node.type)) {
1115
+ for (const param of getChildren(node, 'param')) {
1116
+ const name = str(getProps(param).name);
1117
+ if (name && isUrlLikeName(name) && !hasUrlAllowlistGuard(node, name)) {
1118
+ failures.push(`${nodeLabel(node)} URL-like param '${name}' lacks URL/host allowlist guard`);
1119
+ }
1120
+ }
1121
+ const code = reachableHandlerText(root, node);
1122
+ if (classifyEffect(code)?.kind === 'network' && !hasUrlAllowlistGuard(node)) {
1123
+ failures.push(`${nodeLabel(node)} performs network effect without URL/host allowlist guard`);
1124
+ }
1125
+ }
1126
+ for (const child of node.children || [])
1127
+ visit(child);
1128
+ }
1129
+ visit(root);
1130
+ return failures;
1131
+ }
1132
+ function findSensitiveEffectsWithoutAuth(root) {
1133
+ const checkedTypes = new Set(['action', 'command', 'fn', 'job', 'middleware', 'route', 'tool']);
1134
+ const failures = [];
1135
+ function visit(node) {
1136
+ if (checkedTypes.has(node.type)) {
1137
+ const effect = classifyEffect(reachableHandlerText(root, node));
1138
+ if (effect?.sensitive && !hasAuthorizationLikeGate(node)) {
1139
+ failures.push(`${nodeLabel(node)} performs ${effect.label} without auth/permission`);
1140
+ }
1141
+ }
1142
+ for (const child of node.children || [])
1143
+ visit(child);
1144
+ }
1145
+ visit(root);
1146
+ return failures;
1147
+ }
1148
+ function routePathParams(route) {
1149
+ const path = str(getProps(route).path);
1150
+ return [...path.matchAll(/:([A-Za-z_$][A-Za-z0-9_$]*)/g)].map((match) => match[1]);
1151
+ }
1152
+ function findUncheckedRoutePathParams(root) {
1153
+ const failures = [];
1154
+ for (const route of collectNodes(root, 'route')) {
1155
+ if (getChildren(route, 'validate').length > 0 || getChildren(route, 'schema').length > 0)
1156
+ continue;
1157
+ const declared = new Set(declaredParamNameList(route));
1158
+ for (const paramName of routePathParams(route)) {
1159
+ if (!declared.has(paramName) && !hasParamSpecificGuard(route, paramName)) {
1160
+ failures.push(`${nodeLabel(route)} path param '${paramName}' is not declared, validated, or guarded`);
1161
+ }
1162
+ }
1163
+ }
1164
+ return failures;
1165
+ }
1166
+ function findEffectsWithoutCleanup(root) {
1167
+ const needsCleanup = /\b(addEventListener|setInterval|setTimeout|subscribe|watch|fetch|AbortController|WebSocket)\b/;
1168
+ return collectNodes(root, 'effect')
1169
+ .filter((effect) => needsCleanup.test(reachableHandlerText(root, effect)) && getChildren(effect, 'cleanup').length === 0)
1170
+ .map((effect) => `${nodeLabel(effect)} at line ${effect.loc?.line ?? '?'} has side-effect handler without cleanup`);
1171
+ }
1172
+ function findUnrecoveredAsync(root) {
1173
+ return collectNodes(root, 'async')
1174
+ .filter((node) => handlerText(node).trim().length > 0 && getChildren(node, 'recover').length === 0)
1175
+ .map((node) => `${nodeLabel(node)} at line ${node.loc?.line ?? '?'} has async handler without recover`);
1176
+ }
1177
+ function assertionNoInvariant(node) {
1178
+ return normalizeInvariant(str(getProps(node).no));
1179
+ }
1180
+ function presetInvariantNames(node) {
1181
+ return (presetInvariants(node) || []).map(normalizeInvariant);
1182
+ }
1183
+ function assertionCoversAnyInvariant(node, invariants) {
1184
+ const no = assertionNoInvariant(node);
1185
+ if (no && invariants.has(no))
1186
+ return true;
1187
+ return presetInvariantNames(node).some((invariant) => invariants.has(invariant));
1188
+ }
1189
+ function syntheticTarget(root) {
1190
+ return { file: '<coverage>', root, diagnostics: [], schemaViolations: [], semanticViolations: [] };
1191
+ }
1192
+ function coveredTransitionsFromAssertion(root, assertion) {
1193
+ const props = getProps(assertion);
1194
+ const machineName = str(props.machine);
1195
+ if (!machineName || 'no' in props)
1196
+ return undefined;
1197
+ const transitionName = str(props.transition);
1198
+ if (transitionName) {
1199
+ const evaluated = evaluateMachineTransitionAssertion(assertion, syntheticTarget(root));
1200
+ return evaluated.passed ? { machineName, transitions: new Set([transitionName]) } : undefined;
1201
+ }
1202
+ const via = parseList(str(props.via));
1203
+ if (!('reaches' in props) || via.length === 0)
1204
+ return undefined;
1205
+ const evaluated = evaluateMachineReachability(assertion, syntheticTarget(root));
1206
+ return evaluated.passed ? { machineName, transitions: new Set(via) } : undefined;
1207
+ }
1208
+ function findUntestedTransitions(root, context, machineName) {
1209
+ const machines = selectedMachines(root, machineName);
1210
+ if (machineName && machines.length === 0)
1211
+ return [`Machine not found: ${machineName}`];
1212
+ const coveredByMachine = new Map();
1213
+ for (const assertion of context?.assertions || []) {
1214
+ const coverage = coveredTransitionsFromAssertion(root, assertion.node);
1215
+ if (!coverage || (machineName && coverage.machineName !== machineName))
1216
+ continue;
1217
+ const covered = coveredByMachine.get(coverage.machineName) || new Set();
1218
+ for (const transitionName of coverage.transitions)
1219
+ covered.add(transitionName);
1220
+ coveredByMachine.set(coverage.machineName, covered);
1221
+ }
1222
+ const failures = [];
1223
+ for (const machine of machines) {
1224
+ const name = str(getProps(machine).name) || '<unnamed>';
1225
+ const covered = coveredByMachine.get(name) || new Set();
1226
+ for (const transition of getChildren(machine, 'transition')) {
1227
+ const transitionName = str(getProps(transition).name);
1228
+ if (!transitionName || covered.has(transitionName))
1229
+ continue;
1230
+ failures.push(`${name}.${transitionName} at line ${transition.loc?.line ?? '?'}`);
1231
+ }
1232
+ }
1233
+ return failures;
1234
+ }
1235
+ function findUntestedGuards(root, context) {
1236
+ const guardCoverageInvariants = new Set([
1237
+ 'invalidguards',
1238
+ 'guardmisconfigurations',
1239
+ 'weakguards',
1240
+ 'nonexhaustiveguards',
1241
+ 'guardexhaustiveness',
1242
+ 'exhaustiveguards',
1243
+ ]);
1244
+ const assertions = context?.assertions || [];
1245
+ if (assertions.some((assertion) => assertionCoversAnyInvariant(assertion.node, guardCoverageInvariants)))
1246
+ return [];
1247
+ const explicitlyCovered = new Set(assertions.map((assertion) => str(getProps(assertion.node).guard)).filter(Boolean));
1248
+ return collectNodes(root, 'guard')
1249
+ .filter((guard) => {
1250
+ const name = str(getProps(guard).name);
1251
+ return !name || !explicitlyCovered.has(name);
1252
+ })
1253
+ .map((guard) => {
1254
+ const name = str(getProps(guard).name);
1255
+ return name
1256
+ ? `guard ${name} at line ${guard.loc?.line ?? '?'}`
1257
+ : `unnamed guard at line ${guard.loc?.line ?? '?'}`;
1258
+ });
1259
+ }
1260
+ function coverageMetric(total, uncovered) {
1261
+ const covered = Math.max(0, total - uncovered.length);
1262
+ return {
1263
+ total,
1264
+ covered,
1265
+ percent: total === 0 ? 100 : Math.round((covered / total) * 10000) / 100,
1266
+ uncovered,
1267
+ };
1268
+ }
1269
+ function combineCoverageMetrics(metrics) {
1270
+ const total = metrics.reduce((sum, metric) => sum + metric.total, 0);
1271
+ const uncovered = metrics.flatMap((metric) => metric.uncovered);
1272
+ return coverageMetric(total, uncovered);
1273
+ }
1274
+ function emptyCoverageSummary() {
1275
+ const empty = coverageMetric(0, []);
1276
+ return {
1277
+ total: 0,
1278
+ covered: 0,
1279
+ percent: 100,
1280
+ transitions: empty,
1281
+ guards: empty,
1282
+ targets: [],
1283
+ };
1284
+ }
1285
+ function combineCoverageSummaries(summaries) {
1286
+ const transitions = combineCoverageMetrics(summaries.map((summary) => summary.transitions));
1287
+ const guards = combineCoverageMetrics(summaries.map((summary) => summary.guards));
1288
+ const total = transitions.total + guards.total;
1289
+ const covered = transitions.covered + guards.covered;
1290
+ return {
1291
+ total,
1292
+ covered,
1293
+ percent: total === 0 ? 100 : Math.round((covered / total) * 10000) / 100,
1294
+ transitions,
1295
+ guards,
1296
+ targets: summaries.flatMap((summary) => summary.targets),
1297
+ };
1298
+ }
1299
+ function machineTransitionCoverage(root, assertions) {
1300
+ const machines = selectedMachines(root);
1301
+ const coveredByMachine = new Map();
1302
+ for (const assertion of assertions) {
1303
+ const coverage = coveredTransitionsFromAssertion(root, assertion.node);
1304
+ if (!coverage)
1305
+ continue;
1306
+ const covered = coveredByMachine.get(coverage.machineName) || new Set();
1307
+ for (const transitionName of coverage.transitions)
1308
+ covered.add(transitionName);
1309
+ coveredByMachine.set(coverage.machineName, covered);
1310
+ }
1311
+ let total = 0;
1312
+ const uncovered = [];
1313
+ for (const machine of machines) {
1314
+ const name = str(getProps(machine).name) || '<unnamed>';
1315
+ const covered = coveredByMachine.get(name) || new Set();
1316
+ for (const transition of getChildren(machine, 'transition')) {
1317
+ const transitionName = str(getProps(transition).name);
1318
+ if (!transitionName)
1319
+ continue;
1320
+ total += 1;
1321
+ if (!covered.has(transitionName)) {
1322
+ uncovered.push(`${name}.${transitionName} at line ${transition.loc?.line ?? '?'}`);
1323
+ }
1324
+ }
1325
+ }
1326
+ return coverageMetric(total, uncovered);
1327
+ }
1328
+ function guardCoverage(root, assertions) {
1329
+ const guards = collectNodes(root, 'guard');
1330
+ const guardCoverageInvariants = new Set([
1331
+ 'invalidguards',
1332
+ 'guardmisconfigurations',
1333
+ 'weakguards',
1334
+ 'nonexhaustiveguards',
1335
+ 'guardexhaustiveness',
1336
+ 'exhaustiveguards',
1337
+ ]);
1338
+ if (assertions.some((assertion) => assertionCoversAnyInvariant(assertion.node, guardCoverageInvariants))) {
1339
+ return coverageMetric(guards.length, []);
1340
+ }
1341
+ const explicitlyCovered = new Set(assertions.map((assertion) => str(getProps(assertion.node).guard)).filter(Boolean));
1342
+ const uncovered = guards
1343
+ .filter((guard) => {
1344
+ const name = str(getProps(guard).name);
1345
+ return !name || !explicitlyCovered.has(name);
1346
+ })
1347
+ .map((guard) => {
1348
+ const name = str(getProps(guard).name);
1349
+ return name
1350
+ ? `guard ${name} at line ${guard.loc?.line ?? '?'}`
1351
+ : `unnamed guard at line ${guard.loc?.line ?? '?'}`;
1352
+ });
1353
+ return coverageMetric(guards.length, uncovered);
1354
+ }
1355
+ function coverageForTarget(target, assertions) {
1356
+ if (!target.root) {
1357
+ return {
1358
+ file: target.file,
1359
+ transitions: coverageMetric(0, []),
1360
+ guards: coverageMetric(0, []),
1361
+ };
1362
+ }
1363
+ return {
1364
+ file: target.file,
1365
+ transitions: machineTransitionCoverage(target.root, assertions),
1366
+ guards: guardCoverage(target.root, assertions),
1367
+ };
1368
+ }
1369
+ function createCoverageSummary(targets) {
1370
+ const transitions = combineCoverageMetrics(targets.map((target) => target.transitions));
1371
+ const guards = combineCoverageMetrics(targets.map((target) => target.guards));
1372
+ const total = transitions.total + guards.total;
1373
+ const covered = transitions.covered + guards.covered;
1374
+ return {
1375
+ total,
1376
+ covered,
1377
+ percent: total === 0 ? 100 : Math.round((covered / total) * 10000) / 100,
1378
+ transitions,
1379
+ guards,
1380
+ targets,
1381
+ };
1382
+ }
1383
+ function codegenRoots(root) {
1384
+ return root.type === 'document' ? root.children || [] : [root];
1385
+ }
1386
+ function findCodegenErrors(root) {
1387
+ const failures = [];
1388
+ for (const node of codegenRoots(root)) {
1389
+ try {
1390
+ generateCoreNode(node);
1391
+ }
1392
+ catch (error) {
1393
+ failures.push(`${nodeLabel(node)} at line ${node.loc?.line ?? '?'}: ${error instanceof Error ? error.message : String(error)}`);
1394
+ }
1395
+ }
1396
+ return failures;
1397
+ }
1398
+ const RUNTIME_EXPR_TIMEOUT_MS = 100;
1399
+ const RUNTIME_ASYNC_PROCESS_TIMEOUT_MS = 1500;
1400
+ const RUNTIME_EXPR_UNSAFE_TOKEN = /\b(?:async|class|constructor|Date|delete|do|eval|fetch|for|Function|global|globalThis|import|new|process|prototype|require|setInterval|setTimeout|switch|this|throw|try|while|with|WebSocket|XMLHttpRequest|__proto__)\b/;
1401
+ const RUNTIME_FN_UNSAFE_TOKEN = /\b(?:class|constructor|Date|delete|do|eval|fetch|Function|global|globalThis|import|process|prototype|require|setInterval|setTimeout|switch|this|while|with|WebSocket|XMLHttpRequest|__proto__)\b/;
1402
+ function unsafeRuntimeExpressionReason(source, options = {}) {
1403
+ if (source.length > 2000)
1404
+ return 'expression is longer than 2000 characters';
1405
+ if (/[\r\n;]/.test(source))
1406
+ return 'multi-statement expressions are not supported';
1407
+ const unsafeToken = source.match(RUNTIME_EXPR_UNSAFE_TOKEN)?.[0];
1408
+ if (unsafeToken)
1409
+ return `unsupported token '${unsafeToken}'`;
1410
+ if (!options.allowAwait && /\bawait\b/.test(source))
1411
+ return "unsupported token 'await'";
1412
+ if (/(^|[^=!<>])=(?!=|>)/.test(source))
1413
+ return 'assignment is not supported';
1414
+ return undefined;
1415
+ }
1416
+ function unsafeRuntimeFunctionReason(source) {
1417
+ if (source.length > 5000)
1418
+ return 'function body is longer than 5000 characters';
1419
+ const unsafeToken = source.match(RUNTIME_FN_UNSAFE_TOKEN)?.[0];
1420
+ if (unsafeToken)
1421
+ return `unsupported token '${unsafeToken}'`;
1422
+ return undefined;
1423
+ }
1424
+ function isRuntimeBindingName(value) {
1425
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value);
1426
+ }
1427
+ function runtimeBindingSource(node) {
1428
+ if (node.type === 'const')
1429
+ return { expr: exprPropToRuntimeSource(node, 'value'), kind: 'expr' };
1430
+ if (node.type === 'derive' || node.type === 'let') {
1431
+ return { expr: exprPropToRuntimeSource(node, 'value') || exprPropToRuntimeSource(node, 'expr'), kind: 'expr' };
1432
+ }
1433
+ if (node.type === 'fn')
1434
+ return { expr: runtimeFunctionExpr(node), kind: 'fn' };
1435
+ return undefined;
1436
+ }
1437
+ function runtimeParamNames(node) {
1438
+ const names = [];
1439
+ for (const param of getChildren(node, 'param')) {
1440
+ const name = str(getProps(param).name);
1441
+ if (name)
1442
+ names.push(name);
1443
+ }
1444
+ if (names.length > 0)
1445
+ return names;
1446
+ return parseLegacyParamNames(str(getProps(node).params));
1447
+ }
1448
+ function runtimeFunctionExpr(node) {
1449
+ const code = handlerText(node);
1450
+ if (!code)
1451
+ return '';
1452
+ const params = runtimeParamNames(node);
1453
+ if (!params.every(isRuntimeBindingName))
1454
+ return '';
1455
+ const asyncKw = isTruthy(getProps(node).async) ? 'async ' : '';
1456
+ return `(${asyncKw}(${params.join(', ')}) => {\n${code.trim()}\n})`;
1457
+ }
1458
+ function collectRuntimeBindings(root) {
1459
+ const bindings = [];
1460
+ function visit(node) {
1461
+ if (node.type === 'const' || node.type === 'derive' || node.type === 'let' || node.type === 'fn') {
1462
+ const name = str(getProps(node).name);
1463
+ const binding = runtimeBindingSource(node);
1464
+ if (name && binding?.expr) {
1465
+ bindings.push({
1466
+ name,
1467
+ expr: binding.expr,
1468
+ kind: binding.kind,
1469
+ line: node.loc?.line,
1470
+ });
1471
+ }
1472
+ }
1473
+ for (const child of node.children || [])
1474
+ visit(child);
1475
+ }
1476
+ visit(root);
1477
+ return bindings;
1478
+ }
1479
+ function orderRuntimeBindings(bindings, entryExpr) {
1480
+ const byName = new Map();
1481
+ for (const binding of bindings) {
1482
+ if (!isRuntimeBindingName(binding.name)) {
1483
+ return { ordered: [], error: `invalid runtime binding name '${binding.name}' at line ${binding.line ?? '?'}` };
1484
+ }
1485
+ byName.set(binding.name, [...(byName.get(binding.name) || []), binding]);
1486
+ }
1487
+ const ordered = [];
1488
+ const visiting = new Set();
1489
+ const visited = new Set();
1490
+ const stack = [];
1491
+ function depsIn(source) {
1492
+ return [...byName.keys()].filter((name) => new RegExp(`\\b${escapeRegExp(name)}\\b`).test(source));
1493
+ }
1494
+ function bindingFor(name) {
1495
+ const candidates = byName.get(name) || [];
1496
+ if (candidates.length <= 1)
1497
+ return candidates[0];
1498
+ const [first, ...rest] = candidates;
1499
+ throw new Error(`duplicate runtime binding '${name}' at line ${rest[0].line ?? '?'} (first at line ${first.line ?? '?'})`);
1500
+ }
1501
+ function visit(name) {
1502
+ if (visited.has(name))
1503
+ return undefined;
1504
+ if (visiting.has(name)) {
1505
+ const start = stack.indexOf(name);
1506
+ return `runtime binding cycle: ${[...stack.slice(start), name].join(' -> ')}`;
1507
+ }
1508
+ let binding;
1509
+ try {
1510
+ binding = bindingFor(name);
1511
+ }
1512
+ catch (error) {
1513
+ return error instanceof Error ? error.message : String(error);
1514
+ }
1515
+ if (!binding)
1516
+ return undefined;
1517
+ visiting.add(name);
1518
+ stack.push(name);
1519
+ for (const dep of depsIn(binding.expr)) {
1520
+ const error = visit(dep);
1521
+ if (error)
1522
+ return error;
1523
+ }
1524
+ stack.pop();
1525
+ visiting.delete(name);
1526
+ visited.add(name);
1527
+ ordered.push(binding);
1528
+ return undefined;
1529
+ }
1530
+ for (const name of depsIn(entryExpr)) {
1531
+ const error = visit(name);
1532
+ if (error)
1533
+ return { ordered: [], error };
1534
+ }
1535
+ return { ordered };
1536
+ }
1537
+ function runtimeContext() {
1538
+ return {
1539
+ Array,
1540
+ Boolean,
1541
+ Error,
1542
+ JSON,
1543
+ Math,
1544
+ Number,
1545
+ Object,
1546
+ Promise,
1547
+ RangeError,
1548
+ ReferenceError,
1549
+ String,
1550
+ SyntaxError,
1551
+ TypeError,
1552
+ isFinite,
1553
+ isNaN,
1554
+ parseFloat,
1555
+ parseInt,
1556
+ };
1557
+ }
1558
+ function formatRuntimeValue(value) {
1559
+ return inspect(value, { breakLength: 80, depth: 4, sorted: true });
1560
+ }
1561
+ function runtimeValuesEqual(actual, expected) {
1562
+ if (Object.is(actual, expected))
1563
+ return true;
1564
+ if (isDeepStrictEqual(actual, expected))
1565
+ return true;
1566
+ try {
1567
+ const normalizedActual = JSON.parse(JSON.stringify(actual));
1568
+ const normalizedExpected = JSON.parse(JSON.stringify(expected));
1569
+ return isDeepStrictEqual(normalizedActual, normalizedExpected);
1570
+ }
1571
+ catch {
1572
+ return false;
1573
+ }
1574
+ }
1575
+ function formatThrownRuntimeError(error) {
1576
+ if (error instanceof Error)
1577
+ return `${error.name}: ${error.message}`;
1578
+ return String(error);
1579
+ }
1580
+ function runtimeFixtureSuffix(fixtures) {
1581
+ const names = fixtures.map((fixture) => fixture.name).filter(Boolean);
1582
+ return names.length > 0 ? `; fixtures: ${names.join(', ')}` : '';
1583
+ }
1584
+ function runtimeExpressionContext(expr, fixtures) {
1585
+ return `; expression: ${expr}${runtimeFixtureSuffix(fixtures)}`;
1586
+ }
1587
+ function runtimeBindingUnsafeReason(binding) {
1588
+ if (binding.kind === 'fn')
1589
+ return unsafeRuntimeFunctionReason(binding.expr);
1590
+ return unsafeRuntimeExpressionReason(binding.expr);
1591
+ }
1592
+ function thrownRuntimeErrorMatches(error, expected) {
1593
+ const normalized = expected.trim();
1594
+ if (!normalized || normalized === 'true')
1595
+ return true;
1596
+ if (error instanceof Error) {
1597
+ return error.name === normalized || error.constructor.name === normalized || error.message.includes(normalized);
1598
+ }
1599
+ return String(error).includes(normalized);
1600
+ }
1601
+ function buildRuntimeDeclarations(target, entryExprs, fixtures = []) {
1602
+ for (const entryExpr of entryExprs) {
1603
+ const problem = unsafeRuntimeExpressionReason(entryExpr, { allowAwait: true });
1604
+ if (problem) {
1605
+ return { message: `Runtime expr assertion cannot execute expression: ${problem}` };
1606
+ }
1607
+ }
1608
+ const bindings = orderRuntimeBindings([...collectRuntimeBindings(target.root), ...fixtures], entryExprs.join(' '));
1609
+ if (bindings.error) {
1610
+ return { message: `Runtime expr assertion cannot execute target bindings: ${bindings.error}` };
1611
+ }
1612
+ const declarations = [];
1613
+ for (const binding of bindings.ordered) {
1614
+ const bindingProblem = runtimeBindingUnsafeReason(binding);
1615
+ if (bindingProblem) {
1616
+ return {
1617
+ message: `Runtime expr assertion cannot execute target binding '${binding.name}': ${bindingProblem}`,
1618
+ };
1619
+ }
1620
+ declarations.push(`const ${binding.name} = (${binding.expr});`);
1621
+ }
1622
+ return { source: declarations.join('\n') };
1623
+ }
1624
+ function isThenable(value) {
1625
+ return (typeof value === 'object' &&
1626
+ value !== null &&
1627
+ 'then' in value &&
1628
+ typeof value.then === 'function');
1629
+ }
1630
+ function needsAsyncRuntime(declarations, expr) {
1631
+ return /\b(?:async|await|Promise)\b/.test(`${declarations}\n${expr}`);
1632
+ }
1633
+ function decodeRuntimeValue(encoded) {
1634
+ switch (encoded.type) {
1635
+ case 'undefined':
1636
+ return undefined;
1637
+ case 'number':
1638
+ if (encoded.value === 'NaN')
1639
+ return Number.NaN;
1640
+ return encoded.value === 'Infinity' ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
1641
+ case 'bigint':
1642
+ return BigInt(encoded.value);
1643
+ case 'unserializable':
1644
+ return encoded.value;
1645
+ case 'json':
1646
+ return encoded.value;
1647
+ }
1648
+ }
1649
+ function decodeRuntimeError(encoded) {
1650
+ const error = new Error(encoded?.message || 'Runtime evaluation failed');
1651
+ error.name = encoded?.name || 'Error';
1652
+ if (encoded?.stack)
1653
+ error.stack = encoded.stack;
1654
+ return error;
1655
+ }
1656
+ function asyncRuntimeChildSource() {
1657
+ return `
1658
+ const { readFileSync } = require('fs');
1659
+ const { createContext, Script } = require('vm');
1660
+
1661
+ function encodeRuntimeValue(value) {
1662
+ if (value === undefined) return { type: 'undefined' };
1663
+ if (typeof value === 'number' && Number.isNaN(value)) return { type: 'number', value: 'NaN' };
1664
+ if (value === Number.POSITIVE_INFINITY) return { type: 'number', value: 'Infinity' };
1665
+ if (value === Number.NEGATIVE_INFINITY) return { type: 'number', value: '-Infinity' };
1666
+ if (typeof value === 'bigint') return { type: 'bigint', value: value.toString() };
1667
+ try {
1668
+ JSON.stringify(value);
1669
+ return { type: 'json', value };
1670
+ } catch {
1671
+ return { type: 'unserializable', value: String(value) };
1672
+ }
1673
+ }
1674
+
1675
+ function encodeRuntimeError(error) {
1676
+ return {
1677
+ name: error && error.name ? String(error.name) : 'Error',
1678
+ message: error && error.message ? String(error.message) : String(error),
1679
+ stack: error && error.stack ? String(error.stack) : undefined,
1680
+ };
1681
+ }
1682
+
1683
+ function runtimeContext() {
1684
+ return {
1685
+ Array,
1686
+ Boolean,
1687
+ Error,
1688
+ JSON,
1689
+ Math,
1690
+ Number,
1691
+ Object,
1692
+ Promise,
1693
+ RangeError,
1694
+ ReferenceError,
1695
+ String,
1696
+ SyntaxError,
1697
+ TypeError,
1698
+ isFinite,
1699
+ isNaN,
1700
+ parseFloat,
1701
+ parseInt,
1702
+ };
1703
+ }
1704
+
1705
+ (async () => {
1706
+ const input = JSON.parse(readFileSync(0, 'utf-8'));
1707
+ try {
1708
+ const script = new Script('"use strict";\\n' + input.declarations + '\\n(async () => (' + input.expr + '))();', {
1709
+ filename: input.filename || 'native-kern-test:async',
1710
+ });
1711
+ const value = await script.runInContext(createContext(runtimeContext()), { timeout: input.timeout });
1712
+ process.stdout.write(JSON.stringify({ ok: true, value: encodeRuntimeValue(value) }));
1713
+ } catch (error) {
1714
+ process.stdout.write(JSON.stringify({ ok: false, error: encodeRuntimeError(error) }));
1715
+ }
1716
+ })().catch((error) => {
1717
+ process.stdout.write(JSON.stringify({ ok: false, error: encodeRuntimeError(error) }));
1718
+ });
1719
+ `;
1720
+ }
1721
+ function runRuntimeExpressionAsync(target, declarations, expr) {
1722
+ try {
1723
+ const output = execFileSync(process.execPath, ['-e', asyncRuntimeChildSource()], {
1724
+ input: JSON.stringify({
1725
+ declarations,
1726
+ expr,
1727
+ filename: `native-kern-test:${target.file}`,
1728
+ timeout: RUNTIME_EXPR_TIMEOUT_MS,
1729
+ }),
1730
+ encoding: 'utf-8',
1731
+ maxBuffer: 1024 * 1024,
1732
+ timeout: RUNTIME_ASYNC_PROCESS_TIMEOUT_MS,
1733
+ });
1734
+ const decoded = JSON.parse(output);
1735
+ return decoded.ok
1736
+ ? { ok: true, value: decodeRuntimeValue(decoded.value) }
1737
+ : { ok: false, error: decodeRuntimeError(decoded.error) };
1738
+ }
1739
+ catch (error) {
1740
+ const message = error instanceof Error ? error.message : String(error);
1741
+ return { ok: false, error: new Error(`Async runtime evaluation failed: ${message}`) };
1742
+ }
1743
+ }
1744
+ function runRuntimeExpressionSync(target, declarations, expr) {
1745
+ try {
1746
+ const script = new Script(`"use strict";\n${declarations}\n(${expr});`, {
1747
+ filename: `native-kern-test:${target.file}`,
1748
+ });
1749
+ return {
1750
+ ok: true,
1751
+ value: script.runInContext(createContext(runtimeContext()), {
1752
+ timeout: RUNTIME_EXPR_TIMEOUT_MS,
1753
+ }),
1754
+ };
1755
+ }
1756
+ catch (error) {
1757
+ return { ok: false, error };
1758
+ }
1759
+ }
1760
+ function runRuntimeExpression(target, declarations, expr) {
1761
+ if (needsAsyncRuntime(declarations, expr))
1762
+ return runRuntimeExpressionAsync(target, declarations, expr);
1763
+ const syncResult = runRuntimeExpressionSync(target, declarations, expr);
1764
+ if (syncResult.ok && isThenable(syncResult.value))
1765
+ return runRuntimeExpressionAsync(target, declarations, expr);
1766
+ return syncResult;
1767
+ }
1768
+ function evaluateRuntimeThrows(node, target, declarations, expr, fixtures, label = 'Runtime expr') {
1769
+ const props = getProps(node);
1770
+ const expectedRaw = props.throws === true || props.throws === '' ? 'true' : String(props.throws ?? 'true');
1771
+ const actual = runRuntimeExpression(target, declarations, expr);
1772
+ if (actual.ok) {
1773
+ return {
1774
+ passed: false,
1775
+ message: str(props.message) ||
1776
+ `${label} was expected to throw${expectedRaw && expectedRaw !== 'true' ? ` ${expectedRaw}` : ''}, but returned ${formatRuntimeValue(actual.value)}${runtimeExpressionContext(expr, fixtures)}`,
1777
+ };
1778
+ }
1779
+ if (!thrownRuntimeErrorMatches(actual.error, expectedRaw)) {
1780
+ return {
1781
+ passed: false,
1782
+ message: str(props.message) ||
1783
+ `${label} threw ${formatThrownRuntimeError(actual.error)}, expected ${expectedRaw}${runtimeExpressionContext(expr, fixtures)}`,
1784
+ };
1785
+ }
1786
+ return { passed: true };
1787
+ }
1788
+ function evaluateRuntimeSource(node, target, expr, fixtures = [], label = 'Runtime expr') {
1789
+ const blocking = targetBlockingMessage(target);
1790
+ if (blocking)
1791
+ return { passed: false, message: blocking };
1792
+ const props = getProps(node);
1793
+ const trimmedExpr = expr.trim();
1794
+ if (!trimmedExpr)
1795
+ return { passed: false, message: `${label} assertion requires an executable expression` };
1796
+ const expectedSource = runtimeExpectedSource(node, 'equals');
1797
+ const expressionSources = expectedSource ? [trimmedExpr, expectedSource] : [trimmedExpr];
1798
+ const declarations = buildRuntimeDeclarations(target, expressionSources, fixtures);
1799
+ if ('message' in declarations)
1800
+ return { passed: false, message: declarations.message };
1801
+ const declarationSource = declarations.source;
1802
+ if ('throws' in props) {
1803
+ return evaluateRuntimeThrows(node, target, declarationSource, trimmedExpr, fixtures, label);
1804
+ }
1805
+ const actual = runRuntimeExpression(target, declarationSource, trimmedExpr);
1806
+ if (!actual.ok) {
1807
+ return {
1808
+ passed: false,
1809
+ message: `${label} threw: ${actual.error instanceof Error ? actual.error.message : String(actual.error)}${runtimeExpressionContext(trimmedExpr, fixtures)}`,
1810
+ };
1811
+ }
1812
+ if (expectedSource !== undefined) {
1813
+ const expected = runRuntimeExpression(target, declarationSource, expectedSource);
1814
+ if (!expected.ok) {
1815
+ return {
1816
+ passed: false,
1817
+ message: `Runtime expr assertion cannot execute expected equals value: ${formatThrownRuntimeError(expected.error)}`,
1818
+ };
1819
+ }
1820
+ return runtimeValuesEqual(actual.value, expected.value)
1821
+ ? { passed: true }
1822
+ : {
1823
+ passed: false,
1824
+ message: str(props.message) ||
1825
+ `${label} expected ${formatRuntimeValue(expected.value)}, received ${formatRuntimeValue(actual.value)}${runtimeExpressionContext(trimmedExpr, fixtures)}`,
1826
+ };
1827
+ }
1828
+ if ('matches' in props) {
1829
+ const pattern = runtimePatternValue(node, 'matches') || '';
1830
+ try {
1831
+ const regex = new RegExp(pattern);
1832
+ return regex.test(String(actual.value))
1833
+ ? { passed: true }
1834
+ : {
1835
+ passed: false,
1836
+ message: str(props.message) ||
1837
+ `${label} value ${formatRuntimeValue(actual.value)} does not match /${pattern}/${runtimeExpressionContext(trimmedExpr, fixtures)}`,
1838
+ };
1839
+ }
1840
+ catch (error) {
1841
+ return {
1842
+ passed: false,
1843
+ message: `Runtime expr assertion has invalid matches regex: ${error instanceof Error ? error.message : String(error)}`,
1844
+ };
1845
+ }
1846
+ }
1847
+ return actual.value
1848
+ ? { passed: true }
1849
+ : {
1850
+ passed: false,
1851
+ message: str(props.message) || `${label} evaluated false${runtimeExpressionContext(trimmedExpr, fixtures)}`,
1852
+ };
1853
+ }
1854
+ function evaluateRuntimeExpression(node, target, fixtures = []) {
1855
+ const props = getProps(node);
1856
+ const expr = exprToString(props.expr).trim();
1857
+ if (!expr)
1858
+ return { passed: false, message: 'Runtime expr assertion requires expr={{...}}' };
1859
+ return evaluateRuntimeSource(node, target, expr, fixtures);
1860
+ }
1861
+ function targetHasNamedNode(target, type, name) {
1862
+ return collectNodes(target.root, type).some((node) => str(getProps(node).name) === name);
1863
+ }
1864
+ function runtimeCallExpression(node, fnName) {
1865
+ const props = getProps(node);
1866
+ const argsSource = exprToString(props.args).trim();
1867
+ const withSource = exprToString(props.with).trim();
1868
+ if (argsSource && withSource) {
1869
+ return { message: 'Runtime fn assertion cannot combine args={{...}} and with={{...}}' };
1870
+ }
1871
+ if (argsSource)
1872
+ return { expr: `${fnName}(...(${argsSource}))` };
1873
+ if (withSource)
1874
+ return { expr: `${fnName}(${withSource})` };
1875
+ return { expr: `${fnName}()` };
1876
+ }
1877
+ function evaluateRuntimeBehavior(node, target, fixtures = []) {
1878
+ const blocking = targetBlockingMessage(target);
1879
+ if (blocking)
1880
+ return { passed: false, message: blocking };
1881
+ const props = getProps(node);
1882
+ const fnName = str(props.fn);
1883
+ const deriveName = str(props.derive);
1884
+ if (fnName && deriveName)
1885
+ return { passed: false, message: 'Runtime behavior assertion cannot combine fn and derive' };
1886
+ if (fnName) {
1887
+ if (!targetHasNamedNode(target, 'fn', fnName)) {
1888
+ return { passed: false, message: `Runtime fn assertion target not found: ${fnName}` };
1889
+ }
1890
+ const call = runtimeCallExpression(node, fnName);
1891
+ if (call.message)
1892
+ return { passed: false, message: call.message };
1893
+ return evaluateRuntimeSource(node, target, call.expr || '', fixtures, `Runtime fn ${fnName}`);
1894
+ }
1895
+ if (deriveName) {
1896
+ if (!targetHasNamedNode(target, 'derive', deriveName)) {
1897
+ return { passed: false, message: `Runtime derive assertion target not found: ${deriveName}` };
1898
+ }
1899
+ return evaluateRuntimeSource(node, target, deriveName, fixtures, `Runtime derive ${deriveName}`);
1900
+ }
1901
+ return { passed: false, message: 'Runtime behavior assertion requires fn=<name> or derive=<name>' };
1902
+ }
1903
+ function nodeSearchText(node) {
1904
+ const props = getProps(node);
1905
+ const parts = [
1906
+ exprToString(props.expr),
1907
+ str(props.pattern),
1908
+ str(props.allow),
1909
+ str(props.allowlist),
1910
+ str(props.covers),
1911
+ str(props.kind),
1912
+ str(props.type),
1913
+ ];
1914
+ for (const child of node.children || []) {
1915
+ if (child.type === 'handler')
1916
+ parts.push(str(getProps(child).code));
1917
+ }
1918
+ return parts.filter(Boolean).join('\n');
1919
+ }
1920
+ function unionVariantNames(union) {
1921
+ return getChildren(union, 'variant')
1922
+ .map((variant) => str(getProps(variant).name) || str(getProps(variant).type))
1923
+ .filter(Boolean);
1924
+ }
1925
+ function isVariantGuardCandidate(guard) {
1926
+ const props = getProps(guard);
1927
+ const kind = guardKind(guard);
1928
+ return Boolean(str(props.over) || str(props.union) || str(props.covers) || kind === 'variant' || kind === 'exhaustive');
1929
+ }
1930
+ function resolveGuardUnion(root, guard, requestedUnion) {
1931
+ const unions = collectNodes(root, 'union');
1932
+ const guardProps = getProps(guard);
1933
+ const unionName = requestedUnion || str(guardProps.over) || str(guardProps.union);
1934
+ if (unionName)
1935
+ return unions.find((candidate) => str(getProps(candidate).name) === unionName);
1936
+ const covered = new Set(parseNameList(str(guardProps.covers)));
1937
+ if (covered.size > 0) {
1938
+ const candidates = unions.filter((union) => {
1939
+ const variants = new Set(unionVariantNames(union));
1940
+ return [...covered].every((variant) => variants.has(variant));
1941
+ });
1942
+ if (candidates.length === 1)
1943
+ return candidates[0];
1944
+ }
1945
+ return unions.length === 1 ? unions[0] : undefined;
1946
+ }
1947
+ function guardCoveredVariants(guard, assertion) {
1948
+ return new Set([
1949
+ ...(assertion ? parseNameList(str(getProps(assertion).covers)) : []),
1950
+ ...parseNameList(str(getProps(guard).covers)),
1951
+ ...parseNameList(str(getProps(guard).allow)),
1952
+ ]);
1953
+ }
1954
+ function missingGuardVariants(guard, union, assertion) {
1955
+ const explicitCoverage = guardCoveredVariants(guard, assertion);
1956
+ const searchable = nodeSearchText(guard);
1957
+ return unionVariantNames(union).filter((variant) => {
1958
+ if (explicitCoverage.has(variant))
1959
+ return false;
1960
+ return !new RegExp(`(?:^|[^A-Za-z0-9_$])${escapeRegExp(variant)}(?:$|[^A-Za-z0-9_$])`).test(searchable);
1961
+ });
1962
+ }
1963
+ function evaluateGuardExhaustiveness(node, target) {
1964
+ const blocking = targetBlockingMessage(target);
1965
+ if (blocking)
1966
+ return { passed: false, message: blocking };
1967
+ const props = getProps(node);
1968
+ const guardName = str(props.guard);
1969
+ if (!guardName)
1970
+ return { passed: false, message: 'Guard exhaustiveness assertion requires guard=<name>' };
1971
+ if (!isTruthy(props.exhaustive))
1972
+ return { passed: false, message: 'Guard exhaustiveness assertion requires exhaustive=true' };
1973
+ const guard = collectNodes(target.root, 'guard').find((candidate) => str(getProps(candidate).name) === guardName);
1974
+ if (!guard)
1975
+ return { passed: false, message: `Guard not found: ${guardName}` };
1976
+ const unionName = str(props.over) || str(props.union);
1977
+ const union = resolveGuardUnion(target.root, guard, unionName);
1978
+ if (!union) {
1979
+ return {
1980
+ passed: false,
1981
+ message: unionName
1982
+ ? `Union not found for guard exhaustiveness: ${unionName}`
1983
+ : 'Guard exhaustiveness needs over=<UnionName> when the target has zero or multiple unions',
1984
+ };
1985
+ }
1986
+ const variants = unionVariantNames(union);
1987
+ if (variants.length === 0)
1988
+ return { passed: false, message: `Union ${str(getProps(union).name)} has no variants` };
1989
+ const missing = missingGuardVariants(guard, union, node);
1990
+ return missing.length > 0
1991
+ ? {
1992
+ passed: false,
1993
+ message: `Guard ${guardName} is not exhaustive over ${str(getProps(union).name)}; missing variants: ${missing.join(', ')}`,
1994
+ }
1995
+ : { passed: true };
1996
+ }
1997
+ function findNonExhaustiveGuards(root) {
1998
+ const failures = [];
1999
+ for (const guard of collectNodes(root, 'guard')) {
2000
+ if (!isVariantGuardCandidate(guard))
2001
+ continue;
2002
+ const props = getProps(guard);
2003
+ const unionName = str(props.over) || str(props.union);
2004
+ const union = resolveGuardUnion(root, guard, unionName);
2005
+ const label = `${nodeLabel(guard)} at line ${guard.loc?.line ?? '?'}`;
2006
+ if (!union) {
2007
+ failures.push(unionName
2008
+ ? `${label} references unknown union ${unionName}`
2009
+ : `${label} cannot infer union; add over=<UnionName> or union=<UnionName>`);
2010
+ continue;
2011
+ }
2012
+ const variants = unionVariantNames(union);
2013
+ if (variants.length === 0) {
2014
+ failures.push(`${label} targets union ${str(getProps(union).name)} with no variants`);
2015
+ continue;
2016
+ }
2017
+ const missing = missingGuardVariants(guard, union);
2018
+ if (missing.length > 0) {
2019
+ failures.push(`${label} is not exhaustive over ${str(getProps(union).name)}; missing variants: ${missing.join(', ')}`);
2020
+ }
2021
+ }
2022
+ return failures;
2023
+ }
2024
+ function collectNodeMatches(root, type, matches = [], ancestors = []) {
2025
+ if (!root)
2026
+ return matches;
2027
+ if (root.type === type)
2028
+ matches.push({ node: root, ancestors });
2029
+ for (const child of root.children || [])
2030
+ collectNodeMatches(child, type, matches, [...ancestors, root]);
2031
+ return matches;
2032
+ }
2033
+ function ancestorMatches(ancestor, expected) {
2034
+ return ancestor.type === expected || str(getProps(ancestor).name) === expected || nodeLabel(ancestor) === expected;
2035
+ }
2036
+ function propComparableValue(value) {
2037
+ if (value === undefined)
2038
+ return '<missing>';
2039
+ const expr = exprToString(value);
2040
+ return expr || String(value);
2041
+ }
2042
+ function evaluateNodeAssertion(node, target) {
2043
+ const blocking = targetBlockingMessage(target);
2044
+ if (blocking)
2045
+ return { passed: false, message: blocking };
2046
+ const props = getProps(node);
2047
+ const type = str(props.node);
2048
+ if (!type)
2049
+ return { passed: false, message: 'Node assertion requires node=<type>' };
2050
+ const name = str(props.name);
2051
+ const within = str(props.within);
2052
+ const prop = str(props.prop);
2053
+ const expectedProp = props.is === undefined ? undefined : propComparableValue(props.is);
2054
+ const childType = str(props.child);
2055
+ const childName = str(props.childName);
2056
+ const expectedCount = props.count === undefined || props.count === '' ? undefined : Number(props.count);
2057
+ if (expectedCount !== undefined && (!Number.isInteger(expectedCount) || expectedCount < 0)) {
2058
+ return { passed: false, message: `Node assertion count must be a non-negative integer: ${String(props.count)}` };
2059
+ }
2060
+ const matches = collectNodeMatches(target.root, type)
2061
+ .filter((match) => !name || str(getProps(match.node).name) === name)
2062
+ .filter((match) => !within || match.ancestors.some((ancestor) => ancestorMatches(ancestor, within)));
2063
+ if (matches.length === 0) {
2064
+ return {
2065
+ passed: false,
2066
+ message: `KERN node not found: ${type}${name ? ` name=${name}` : ''}${within ? ` within=${within}` : ''}`,
2067
+ };
2068
+ }
2069
+ if (prop) {
2070
+ const propMatches = matches.filter((match) => {
2071
+ const actual = propComparableValue(getProps(match.node)[prop]);
2072
+ return expectedProp === undefined || actual === expectedProp;
2073
+ });
2074
+ if (propMatches.length === 0) {
2075
+ const actuals = matches.map((match) => propComparableValue(getProps(match.node)[prop]));
2076
+ return {
2077
+ passed: false,
2078
+ message: expectedProp === undefined
2079
+ ? `KERN node ${type}${name ? ` name=${name}` : ''} has no prop ${prop}`
2080
+ : `KERN node ${type}${name ? ` name=${name}` : ''} prop ${prop} expected ${expectedProp}, found ${actuals.join(', ')}`,
2081
+ };
2082
+ }
2083
+ }
2084
+ if (childType) {
2085
+ const childMatches = matches.flatMap((match) => getChildren(match.node, childType).filter((child) => !childName || str(getProps(child).name) === childName));
2086
+ if (expectedCount !== undefined) {
2087
+ return childMatches.length === expectedCount
2088
+ ? { passed: true }
2089
+ : {
2090
+ passed: false,
2091
+ message: `KERN node ${type}${name ? ` name=${name}` : ''} expected ${expectedCount} child ${childType}${childName ? ` name=${childName}` : ''}, found ${childMatches.length}`,
2092
+ };
2093
+ }
2094
+ return childMatches.length > 0
2095
+ ? { passed: true }
2096
+ : {
2097
+ passed: false,
2098
+ message: `KERN node ${type}${name ? ` name=${name}` : ''} missing child ${childType}${childName ? ` name=${childName}` : ''}`,
2099
+ };
2100
+ }
2101
+ if (expectedCount !== undefined) {
2102
+ return matches.length === expectedCount
2103
+ ? { passed: true }
2104
+ : { passed: false, message: `Expected ${expectedCount} KERN node ${type} matches, found ${matches.length}` };
2105
+ }
2106
+ return { passed: true };
2107
+ }
2108
+ function evaluateNoInvariant(node, target, context) {
2109
+ if (target.readError)
2110
+ return { passed: false, message: target.readError };
2111
+ const invariant = normalizeInvariant(str(getProps(node).no));
2112
+ const machineName = str(getProps(node).machine) || undefined;
2113
+ if (invariant === 'parseerrors') {
2114
+ const error = firstParseError(target);
2115
+ return error
2116
+ ? { passed: false, message: `Found parse error at ${target.file}:${error.line}:${error.col}: ${error.message}` }
2117
+ : { passed: true };
2118
+ }
2119
+ if (invariant === 'schemaviolations') {
2120
+ const violation = target.schemaViolations[0];
2121
+ return violation
2122
+ ? {
2123
+ passed: false,
2124
+ message: `Found schema violation at ${target.file}:${violation.line ?? 1}:${violation.col ?? 1}: ${violation.message}`,
2125
+ }
2126
+ : { passed: true };
2127
+ }
2128
+ if (invariant === 'semanticviolations') {
2129
+ const violation = target.semanticViolations[0];
2130
+ return violation
2131
+ ? {
2132
+ passed: false,
2133
+ message: `Found semantic violation at ${target.file}:${violation.line ?? 1}:${violation.col ?? 1}: ${violation.message}`,
2134
+ }
2135
+ : { passed: true };
2136
+ }
2137
+ if (invariant === 'codegenerrors' || invariant === 'codegenerationerrors' || invariant === 'compileerrors') {
2138
+ const blocking = targetBlockingMessage(target);
2139
+ if (blocking)
2140
+ return { passed: false, message: blocking };
2141
+ const errors = findCodegenErrors(target.root);
2142
+ return errors.length > 0
2143
+ ? { passed: false, message: `Found codegen errors: ${errors.join('; ')}` }
2144
+ : { passed: true };
2145
+ }
2146
+ if (invariant === 'cycles' || invariant === 'derivecycles') {
2147
+ const blocking = targetBlockingMessage(target);
2148
+ if (blocking)
2149
+ return { passed: false, message: blocking };
2150
+ const cycles = findDeriveCycles(target.root);
2151
+ return cycles.length > 0
2152
+ ? { passed: false, message: `Found derive cycle: ${cycles[0].join(' -> ')}` }
2153
+ : { passed: true };
2154
+ }
2155
+ if (invariant === 'deadstates' || invariant === 'unreachablestates') {
2156
+ const blocking = targetBlockingMessage(target);
2157
+ if (blocking)
2158
+ return { passed: false, message: blocking };
2159
+ const unreachable = findUnreachableStates(target.root, machineName);
2160
+ return unreachable.length > 0
2161
+ ? { passed: false, message: `Found unreachable machine states: ${unreachable.join('; ')}` }
2162
+ : { passed: true };
2163
+ }
2164
+ if (invariant === 'duplicatetransitions') {
2165
+ const blocking = targetBlockingMessage(target);
2166
+ if (blocking)
2167
+ return { passed: false, message: blocking };
2168
+ const duplicates = findDuplicateTransitions(target.root, machineName);
2169
+ return duplicates.length > 0
2170
+ ? { passed: false, message: `Found duplicate machine transitions: ${duplicates.join('; ')}` }
2171
+ : { passed: true };
2172
+ }
2173
+ if (invariant === 'duplicateroutes') {
2174
+ const blocking = targetBlockingMessage(target);
2175
+ if (blocking)
2176
+ return { passed: false, message: blocking };
2177
+ const duplicates = findDuplicateRoutes(target.root);
2178
+ return duplicates.length > 0
2179
+ ? { passed: false, message: `Found duplicate routes: ${duplicates.join('; ')}` }
2180
+ : { passed: true };
2181
+ }
2182
+ if (invariant === 'emptyroutes' || invariant === 'missingroutehandlers' || invariant === 'missingrouteresponses') {
2183
+ const blocking = targetBlockingMessage(target);
2184
+ if (blocking)
2185
+ return { passed: false, message: blocking };
2186
+ const emptyRoutes = findEmptyRoutes(target.root);
2187
+ return emptyRoutes.length > 0
2188
+ ? { passed: false, message: `Found empty routes: ${emptyRoutes.join('; ')}` }
2189
+ : { passed: true };
2190
+ }
2191
+ if (invariant === 'duplicatenames' || invariant === 'duplicatesiblingnames') {
2192
+ const blocking = targetBlockingMessage(target);
2193
+ if (blocking)
2194
+ return { passed: false, message: blocking };
2195
+ const duplicates = findDuplicateSiblingNames(target.root);
2196
+ return duplicates.length > 0
2197
+ ? { passed: false, message: `Found duplicate sibling names: ${duplicates.join('; ')}` }
2198
+ : { passed: true };
2199
+ }
2200
+ if (invariant === 'weakguards' || invariant === 'guardwithoutelse' || invariant === 'guardswithoutelse') {
2201
+ const blocking = targetBlockingMessage(target);
2202
+ if (blocking)
2203
+ return { passed: false, message: blocking };
2204
+ const weakGuards = findWeakGuards(target.root);
2205
+ return weakGuards.length > 0
2206
+ ? { passed: false, message: `Found weak guards: ${weakGuards.join('; ')}` }
2207
+ : { passed: true };
2208
+ }
2209
+ if (invariant === 'nonexhaustiveguards' || invariant === 'guardexhaustiveness' || invariant === 'exhaustiveguards') {
2210
+ const blocking = targetBlockingMessage(target);
2211
+ if (blocking)
2212
+ return { passed: false, message: blocking };
2213
+ const nonExhaustive = findNonExhaustiveGuards(target.root);
2214
+ return nonExhaustive.length > 0
2215
+ ? { passed: false, message: `Found non-exhaustive guards: ${nonExhaustive.join('; ')}` }
2216
+ : { passed: true };
2217
+ }
2218
+ if (invariant === 'unguardedeffects') {
2219
+ const blocking = targetBlockingMessage(target);
2220
+ if (blocking)
2221
+ return { passed: false, message: blocking };
2222
+ const unguarded = findUnguardedEffects(target.root);
2223
+ return unguarded.length > 0
2224
+ ? { passed: false, message: `Found unguarded effects: ${unguarded.join('; ')}` }
2225
+ : { passed: true };
2226
+ }
2227
+ if (invariant === 'unvalidatedroutes' || invariant === 'unguardedmutatingroutes') {
2228
+ const blocking = targetBlockingMessage(target);
2229
+ if (blocking)
2230
+ return { passed: false, message: blocking };
2231
+ const unvalidated = findUnvalidatedMutatingRoutes(target.root);
2232
+ return unvalidated.length > 0
2233
+ ? { passed: false, message: `Found unvalidated mutating routes: ${unvalidated.join('; ')}` }
2234
+ : { passed: true };
2235
+ }
2236
+ if (invariant === 'rawhandlers' || invariant === 'handlerescapes') {
2237
+ const blocking = targetBlockingMessage(target);
2238
+ if (blocking)
2239
+ return { passed: false, message: blocking };
2240
+ const handlers = findRawHandlerEscapes(target.root);
2241
+ return handlers.length > 0
2242
+ ? {
2243
+ passed: false,
2244
+ message: `Found raw handler escapes: ${handlers.slice(0, 10).join('; ')}${handlers.length > 10 ? `; +${handlers.length - 10} more` : ''}`,
2245
+ }
2246
+ : { passed: true };
2247
+ }
2248
+ if (invariant === 'invalidguards' || invariant === 'guardmisconfigurations') {
2249
+ const blocking = targetBlockingMessage(target);
2250
+ if (blocking)
2251
+ return { passed: false, message: blocking };
2252
+ const invalidGuards = findInvalidGuards(target.root);
2253
+ return invalidGuards.length > 0
2254
+ ? { passed: false, message: `Found invalid guards: ${invalidGuards.join('; ')}` }
2255
+ : { passed: true };
2256
+ }
2257
+ if (invariant === 'duplicateparams' || invariant === 'duplicateparameters') {
2258
+ const blocking = targetBlockingMessage(target);
2259
+ if (blocking)
2260
+ return { passed: false, message: blocking };
2261
+ const duplicates = findDuplicateParams(target.root);
2262
+ return duplicates.length > 0
2263
+ ? { passed: false, message: `Found duplicate params: ${duplicates.join('; ')}` }
2264
+ : { passed: true };
2265
+ }
2266
+ if (invariant === 'unguardedtoolparams' || invariant === 'unguardedrequiredparams') {
2267
+ const blocking = targetBlockingMessage(target);
2268
+ if (blocking)
2269
+ return { passed: false, message: blocking };
2270
+ const unguarded = findUnguardedRequiredToolParams(target.root);
2271
+ return unguarded.length > 0
2272
+ ? { passed: false, message: `Found unguarded required tool params: ${unguarded.join('; ')}` }
2273
+ : { passed: true };
2274
+ }
2275
+ if (invariant === 'missingpathguards' || invariant === 'pathparamguards') {
2276
+ const blocking = targetBlockingMessage(target);
2277
+ if (blocking)
2278
+ return { passed: false, message: blocking };
2279
+ const missing = findMissingPathGuards(target.root);
2280
+ return missing.length > 0
2281
+ ? { passed: false, message: `Found missing path guards: ${missing.join('; ')}` }
2282
+ : { passed: true };
2283
+ }
2284
+ if (invariant === 'ssrfrisks' || invariant === 'ssrf') {
2285
+ const blocking = targetBlockingMessage(target);
2286
+ if (blocking)
2287
+ return { passed: false, message: blocking };
2288
+ const risks = findSsrfRisks(target.root);
2289
+ return risks.length > 0 ? { passed: false, message: `Found SSRF risks: ${risks.join('; ')}` } : { passed: true };
2290
+ }
2291
+ if (invariant === 'sensitiveeffectsrequireauth' || invariant === 'missingeffectauth' || invariant === 'missingauth') {
2292
+ const blocking = targetBlockingMessage(target);
2293
+ if (blocking)
2294
+ return { passed: false, message: blocking };
2295
+ const missing = findSensitiveEffectsWithoutAuth(target.root);
2296
+ return missing.length > 0
2297
+ ? { passed: false, message: `Found sensitive effects without auth: ${missing.join('; ')}` }
2298
+ : { passed: true };
2299
+ }
2300
+ if (invariant === 'uncheckedroutepathparams' || invariant === 'routepathparams') {
2301
+ const blocking = targetBlockingMessage(target);
2302
+ if (blocking)
2303
+ return { passed: false, message: blocking };
2304
+ const unchecked = findUncheckedRoutePathParams(target.root);
2305
+ return unchecked.length > 0
2306
+ ? { passed: false, message: `Found unchecked route path params: ${unchecked.join('; ')}` }
2307
+ : { passed: true };
2308
+ }
2309
+ if (invariant === 'effectwithoutcleanup' || invariant === 'effectcleanup') {
2310
+ const blocking = targetBlockingMessage(target);
2311
+ if (blocking)
2312
+ return { passed: false, message: blocking };
2313
+ const effects = findEffectsWithoutCleanup(target.root);
2314
+ return effects.length > 0
2315
+ ? { passed: false, message: `Found effects without cleanup: ${effects.join('; ')}` }
2316
+ : { passed: true };
2317
+ }
2318
+ if (invariant === 'unrecoveredasync' || invariant === 'asyncrecover') {
2319
+ const blocking = targetBlockingMessage(target);
2320
+ if (blocking)
2321
+ return { passed: false, message: blocking };
2322
+ const asyncBlocks = findUnrecoveredAsync(target.root);
2323
+ return asyncBlocks.length > 0
2324
+ ? { passed: false, message: `Found unrecovered async blocks: ${asyncBlocks.join('; ')}` }
2325
+ : { passed: true };
2326
+ }
2327
+ if (invariant === 'untestedtransitions' || invariant === 'uncoveredtransitions') {
2328
+ const blocking = targetBlockingMessage(target);
2329
+ if (blocking)
2330
+ return { passed: false, message: blocking };
2331
+ const untested = findUntestedTransitions(target.root, context, machineName);
2332
+ return untested.length > 0
2333
+ ? { passed: false, message: `Found untested machine transitions: ${untested.join('; ')}` }
2334
+ : { passed: true };
2335
+ }
2336
+ if (invariant === 'untestedguards' || invariant === 'uncoveredguards') {
2337
+ const blocking = targetBlockingMessage(target);
2338
+ if (blocking)
2339
+ return { passed: false, message: blocking };
2340
+ const untested = findUntestedGuards(target.root, context);
2341
+ return untested.length > 0
2342
+ ? {
2343
+ passed: false,
2344
+ message: `Found untested guards: ${untested.join('; ')}. Add expect guard=<name> exhaustive=true or a guard-wide assertion such as expect preset=guard.`,
2345
+ }
2346
+ : { passed: true };
2347
+ }
2348
+ return { passed: false, message: `Unsupported native invariant: no=${str(getProps(node).no)}` };
2349
+ }
2350
+ function evaluateMachineReachability(node, target) {
2351
+ const blocking = targetBlockingMessage(target);
2352
+ if (blocking)
2353
+ return { passed: false, message: blocking };
2354
+ const props = getProps(node);
2355
+ const machineName = str(props.machine);
2356
+ const targetState = str(props.reaches);
2357
+ const fromState = str(props.from);
2358
+ const throughStates = parseNameList(str(props.through));
2359
+ const avoidedStates = new Set([...parseNameList(str(props.avoid)), ...parseNameList(str(props.avoids))]);
2360
+ const maxSteps = props.maxSteps === undefined || props.maxSteps === '' ? undefined : Number(props.maxSteps);
2361
+ if (!machineName) {
2362
+ return { passed: false, message: 'Machine reachability assertion requires machine=<name>' };
2363
+ }
2364
+ if (!targetState) {
2365
+ return { passed: false, message: 'Machine reachability assertion requires reaches=<state>' };
2366
+ }
2367
+ if (maxSteps !== undefined && (!Number.isInteger(maxSteps) || maxSteps < 0)) {
2368
+ return {
2369
+ passed: false,
2370
+ message: `Machine reachability maxSteps must be a non-negative integer: ${String(props.maxSteps)}`,
2371
+ };
2372
+ }
2373
+ const machine = collectNodes(target.root, 'machine').find((candidate) => str(getProps(candidate).name) === machineName);
2374
+ if (!machine)
2375
+ return { passed: false, message: `Machine not found: ${machineName}` };
2376
+ const states = getChildren(machine, 'state').map((state) => ({
2377
+ name: str(getProps(state).name),
2378
+ initial: isTruthy(getProps(state).initial),
2379
+ }));
2380
+ const transitions = getChildren(machine, 'transition').map((transition) => ({
2381
+ name: str(getProps(transition).name),
2382
+ from: parseNameList(str(getProps(transition).from)),
2383
+ to: str(getProps(transition).to),
2384
+ }));
2385
+ const initialState = states.find((state) => state.initial)?.name || states[0]?.name;
2386
+ if (!initialState)
2387
+ return { passed: false, message: `Machine ${machineName} has no states` };
2388
+ const startState = fromState || initialState;
2389
+ if (!states.some((state) => state.name === startState)) {
2390
+ return { passed: false, message: `State not found in machine ${machineName}: ${startState}` };
2391
+ }
2392
+ if (!states.some((state) => state.name === targetState)) {
2393
+ return { passed: false, message: `State not found in machine ${machineName}: ${targetState}` };
2394
+ }
2395
+ for (const through of throughStates) {
2396
+ if (!states.some((state) => state.name === through)) {
2397
+ return { passed: false, message: `State not found in machine ${machineName}: ${through}` };
2398
+ }
2399
+ }
2400
+ if (avoidedStates.has(startState)) {
2401
+ return { passed: false, message: `Path starts at avoided state ${startState} in machine ${machineName}` };
2402
+ }
2403
+ const via = parseList(str(props.via));
2404
+ if (via.length > 0) {
2405
+ let current = startState;
2406
+ const pathStates = [current];
2407
+ for (const transitionName of via) {
2408
+ const transition = transitions.find((candidate) => candidate.name === transitionName && candidate.from.includes(current));
2409
+ if (!transition) {
2410
+ return {
2411
+ passed: false,
2412
+ message: `Transition ${transitionName} is not reachable from state ${current} in machine ${machineName}`,
2413
+ };
2414
+ }
2415
+ current = transition.to;
2416
+ pathStates.push(current);
2417
+ if (avoidedStates.has(current)) {
2418
+ return {
2419
+ passed: false,
2420
+ message: `Path ${via.join(' -> ')} reaches avoided state ${current} in machine ${machineName}`,
2421
+ };
2422
+ }
2423
+ }
2424
+ if (maxSteps !== undefined && via.length > maxSteps) {
2425
+ return {
2426
+ passed: false,
2427
+ message: `Path ${via.join(' -> ')} uses ${via.length} transitions, above maxSteps=${maxSteps}`,
2428
+ };
2429
+ }
2430
+ const missingThrough = throughStates.filter((state) => !pathStates.includes(state));
2431
+ if (missingThrough.length > 0) {
2432
+ return {
2433
+ passed: false,
2434
+ message: `Path ${via.join(' -> ')} does not pass through required state(s): ${missingThrough.join(', ')}`,
2435
+ };
2436
+ }
2437
+ return current === targetState
2438
+ ? { passed: true, message: `Path ${via.join(' -> ')} reaches ${targetState}` }
2439
+ : { passed: false, message: `Path ${via.join(' -> ')} ended at ${current}, not ${targetState}` };
2440
+ }
2441
+ const defaultMaxDepth = Math.max(transitions.length + states.length, states.length);
2442
+ const maxDepth = maxSteps ?? defaultMaxDepth;
2443
+ const queue = [
2444
+ { state: startState, path: [], states: [startState] },
2445
+ ];
2446
+ const initialSatisfiedThrough = throughStates.filter((state) => state === startState).join(',');
2447
+ const visited = new Set([`${startState}:${initialSatisfiedThrough}`]);
2448
+ while (queue.length > 0) {
2449
+ const current = queue.shift();
2450
+ const missingThrough = throughStates.filter((state) => !current.states.includes(state));
2451
+ if (current.state === targetState && missingThrough.length === 0) {
2452
+ return {
2453
+ passed: true,
2454
+ message: current.path.length > 0
2455
+ ? `Path ${current.path.join(' -> ')} reaches ${targetState}`
2456
+ : 'Target is initial state',
2457
+ };
2458
+ }
2459
+ if (current.path.length >= maxDepth)
2460
+ continue;
2461
+ for (const transition of transitions.filter((candidate) => candidate.from.includes(current.state))) {
2462
+ if (avoidedStates.has(transition.to))
2463
+ continue;
2464
+ const nextStates = [...current.states, transition.to];
2465
+ const satisfiedThrough = throughStates.filter((state) => nextStates.includes(state)).join(',');
2466
+ const key = `${transition.to}:${satisfiedThrough}`;
2467
+ if (visited.has(key))
2468
+ continue;
2469
+ visited.add(key);
2470
+ queue.push({ state: transition.to, path: [...current.path, transition.name], states: nextStates });
2471
+ }
2472
+ }
2473
+ return {
2474
+ passed: false,
2475
+ message: `State ${targetState} is not reachable from ${startState} in machine ${machineName}`,
2476
+ };
2477
+ }
2478
+ function evaluateMachineTransitionAssertion(node, target) {
2479
+ const blocking = targetBlockingMessage(target);
2480
+ if (blocking)
2481
+ return { passed: false, message: blocking };
2482
+ const props = getProps(node);
2483
+ const machineName = str(props.machine);
2484
+ const transitionName = str(props.transition);
2485
+ if (!machineName)
2486
+ return { passed: false, message: 'Machine transition assertion requires machine=<name>' };
2487
+ if (!transitionName)
2488
+ return { passed: false, message: 'Machine transition assertion requires transition=<name>' };
2489
+ const machine = collectNodes(target.root, 'machine').find((candidate) => str(getProps(candidate).name) === machineName);
2490
+ if (!machine)
2491
+ return { passed: false, message: `Machine not found: ${machineName}` };
2492
+ const fromState = str(props.from);
2493
+ const toState = str(props.to);
2494
+ const guarded = props.guarded === undefined || props.guarded === '' ? undefined : isTruthy(props.guarded);
2495
+ const transitions = getChildren(machine, 'transition').filter((transition) => {
2496
+ const transitionProps = getProps(transition);
2497
+ if (str(transitionProps.name) !== transitionName)
2498
+ return false;
2499
+ if (fromState && !parseNameList(str(transitionProps.from)).includes(fromState))
2500
+ return false;
2501
+ if (toState && str(transitionProps.to) !== toState)
2502
+ return false;
2503
+ if (guarded !== undefined) {
2504
+ const hasGuard = transitionProps.guard !== undefined && transitionProps.guard !== '';
2505
+ if (hasGuard !== guarded)
2506
+ return false;
2507
+ }
2508
+ return true;
2509
+ });
2510
+ if (transitions.length > 0)
2511
+ return { passed: true };
2512
+ const declared = getChildren(machine, 'transition')
2513
+ .filter((transition) => str(getProps(transition).name) === transitionName)
2514
+ .map((transition) => {
2515
+ const transitionProps = getProps(transition);
2516
+ return `${str(transitionProps.name)} from=${str(transitionProps.from) || '<missing>'} to=${str(transitionProps.to) || '<missing>'}${transitionProps.guard !== undefined && transitionProps.guard !== '' ? ' guarded=true' : ' guarded=false'}`;
2517
+ });
2518
+ return {
2519
+ passed: false,
2520
+ message: declared.length > 0
2521
+ ? `Machine ${machineName} transition ${transitionName} did not match constraints from=${fromState || '<any>'} to=${toState || '<any>'}${guarded !== undefined ? ` guarded=${guarded}` : ''}; declared: ${declared.join('; ')}`
2522
+ : `Machine ${machineName} transition not found: ${transitionName}`,
2523
+ };
2524
+ }
2525
+ function nodeWithProps(node, props) {
2526
+ return { ...node, props };
2527
+ }
2528
+ function presetInvariants(node) {
2529
+ const preset = normalizeInvariant(str(getProps(node).preset));
2530
+ if (!preset)
2531
+ return undefined;
2532
+ return NATIVE_TEST_PRESETS[preset];
2533
+ }
2534
+ function evaluatePresetAssertion(node, target, context) {
2535
+ const preset = str(getProps(node).preset);
2536
+ const invariants = presetInvariants(node);
2537
+ if (!invariants) {
2538
+ return [
2539
+ {
2540
+ ruleId: presetRuleId(preset),
2541
+ assertion: `preset ${preset || '<missing>'}`,
2542
+ passed: false,
2543
+ severity: 'error',
2544
+ message: `Unsupported native preset: preset=${preset || '<missing>'}`,
2545
+ },
2546
+ ];
2547
+ }
2548
+ return invariants.map((invariant) => {
2549
+ const evaluated = evaluateNoInvariant(nodeWithProps(node, { ...getProps(node), no: invariant }), target, context);
2550
+ return {
2551
+ ruleId: invariantRuleId(invariant),
2552
+ assertion: `preset ${preset} / no ${invariant}`,
2553
+ passed: evaluated.passed,
2554
+ ...(isAssertionConfigurationFailure(evaluated.message) ? { severity: 'error' } : {}),
2555
+ ...(evaluated.message ? { message: evaluated.message } : {}),
2556
+ };
2557
+ });
2558
+ }
2559
+ function evaluateNativeAssertion(node, target, context) {
2560
+ const props = getProps(node);
2561
+ if ('preset' in props)
2562
+ return evaluatePresetAssertion(node, target, context);
2563
+ if ('node' in props) {
2564
+ const evaluated = evaluateNodeAssertion(node, target);
2565
+ return [
2566
+ {
2567
+ ruleId: 'kern:node',
2568
+ assertion: assertionLabel(node),
2569
+ passed: evaluated.passed,
2570
+ ...(isAssertionConfigurationFailure(evaluated.message) ? { severity: 'error' } : {}),
2571
+ ...(evaluated.message ? { message: evaluated.message } : {}),
2572
+ },
2573
+ ];
2574
+ }
2575
+ if ('no' in props) {
2576
+ const evaluated = evaluateNoInvariant(node, target, context);
2577
+ return [
2578
+ {
2579
+ ruleId: invariantRuleId(str(props.no)),
2580
+ assertion: assertionLabel(node),
2581
+ passed: evaluated.passed,
2582
+ ...(isAssertionConfigurationFailure(evaluated.message) ? { severity: 'error' } : {}),
2583
+ ...(evaluated.message ? { message: evaluated.message } : {}),
2584
+ },
2585
+ ];
2586
+ }
2587
+ if ('guard' in props) {
2588
+ const evaluated = evaluateGuardExhaustiveness(node, target);
2589
+ return [
2590
+ {
2591
+ ruleId: 'guard:exhaustive',
2592
+ assertion: assertionLabel(node),
2593
+ passed: evaluated.passed,
2594
+ ...(isAssertionConfigurationFailure(evaluated.message) ? { severity: 'error' } : {}),
2595
+ ...(evaluated.message ? { message: evaluated.message } : {}),
2596
+ },
2597
+ ];
2598
+ }
2599
+ if ('machine' in props && 'transition' in props) {
2600
+ const evaluated = evaluateMachineTransitionAssertion(node, target);
2601
+ return [
2602
+ {
2603
+ ruleId: 'machine:transition',
2604
+ assertion: assertionLabel(node),
2605
+ passed: evaluated.passed,
2606
+ ...(isAssertionConfigurationFailure(evaluated.message) ? { severity: 'error' } : {}),
2607
+ ...(evaluated.message ? { message: evaluated.message } : {}),
2608
+ },
2609
+ ];
2610
+ }
2611
+ if ('machine' in props || 'reaches' in props) {
2612
+ const evaluated = evaluateMachineReachability(node, target);
2613
+ return [
2614
+ {
2615
+ ruleId: 'machine:reaches',
2616
+ assertion: assertionLabel(node),
2617
+ passed: evaluated.passed,
2618
+ ...(isAssertionConfigurationFailure(evaluated.message) ? { severity: 'error' } : {}),
2619
+ ...(evaluated.message ? { message: evaluated.message } : {}),
2620
+ },
2621
+ ];
2622
+ }
2623
+ if ('fn' in props || 'derive' in props) {
2624
+ const evaluated = evaluateRuntimeBehavior(node, target, context?.fixtures || []);
2625
+ return [
2626
+ {
2627
+ ruleId: 'runtime:behavior',
2628
+ assertion: assertionLabel(node),
2629
+ passed: evaluated.passed,
2630
+ ...(isAssertionConfigurationFailure(evaluated.message) ? { severity: 'error' } : {}),
2631
+ ...(evaluated.message ? { message: evaluated.message } : {}),
2632
+ },
2633
+ ];
2634
+ }
2635
+ if ('expr' in props) {
2636
+ const evaluated = evaluateRuntimeExpression(node, target, context?.fixtures || []);
2637
+ return [
2638
+ {
2639
+ ruleId: 'expr',
2640
+ assertion: assertionLabel(node),
2641
+ passed: evaluated.passed,
2642
+ ...(isAssertionConfigurationFailure(evaluated.message) ? { severity: 'error' } : {}),
2643
+ ...(evaluated.message ? { message: evaluated.message } : {}),
2644
+ },
2645
+ ];
2646
+ }
2647
+ return [
2648
+ {
2649
+ ruleId: 'expect:unsupported',
2650
+ assertion: assertionLabel(node),
2651
+ passed: false,
2652
+ severity: 'error',
2653
+ message: 'Unsupported native expect assertion.',
2654
+ },
2655
+ ];
2656
+ }
2657
+ export function hasNativeKernTests(source) {
2658
+ return collectNodes(parseDocumentWithDiagnostics(source).root, 'test').length > 0;
2659
+ }
2660
+ function isKernFile(file) {
2661
+ return file.endsWith('.kern');
2662
+ }
2663
+ function hasNativeKernTestsInFile(file) {
2664
+ try {
2665
+ return hasNativeKernTests(readFileSync(file, 'utf-8'));
2666
+ }
2667
+ catch {
2668
+ return false;
2669
+ }
2670
+ }
2671
+ function discoverNativeKernTestFilesInDir(dir) {
2672
+ const files = [];
2673
+ function walk(current) {
2674
+ let entries;
2675
+ try {
2676
+ entries = readdirSync(current, { withFileTypes: true });
2677
+ }
2678
+ catch {
2679
+ return;
2680
+ }
2681
+ for (const entry of entries) {
2682
+ if (entry.isDirectory()) {
2683
+ if (!DISCOVERY_SKIP_DIRS.has(entry.name))
2684
+ walk(join(current, entry.name));
2685
+ continue;
2686
+ }
2687
+ if (!entry.isFile())
2688
+ continue;
2689
+ const file = join(current, entry.name);
2690
+ if (isKernFile(file) && hasNativeKernTestsInFile(file))
2691
+ files.push(resolve(file));
2692
+ }
2693
+ }
2694
+ walk(dir);
2695
+ return files.sort();
2696
+ }
2697
+ export function discoverNativeKernTestFiles(input) {
2698
+ const inputPath = resolve(input);
2699
+ if (!existsSync(inputPath))
2700
+ return [];
2701
+ const stat = statSync(inputPath);
2702
+ if (stat.isDirectory())
2703
+ return discoverNativeKernTestFilesInDir(inputPath);
2704
+ if (stat.isFile() && isKernFile(inputPath) && hasNativeKernTestsInFile(inputPath))
2705
+ return [inputPath];
2706
+ return [];
2707
+ }
2708
+ export function runNativeKernTests(file, options = {}) {
2709
+ const inputPath = resolve(file);
2710
+ const testDoc = loadKernDocument(inputPath);
2711
+ const results = [];
2712
+ const targetFiles = new Set();
2713
+ if (testDoc.readError) {
2714
+ results.push(issueResult(inputPath, testDoc.readError));
2715
+ return summarizeNativeTestRun(inputPath, targetFiles, results);
2716
+ }
2717
+ const parseError = firstParseError(testDoc);
2718
+ if (parseError) {
2719
+ results.push(issueResult(inputPath, parseError.message, parseError));
2720
+ return summarizeNativeTestRun(inputPath, targetFiles, results);
2721
+ }
2722
+ const schemaViolation = testDoc.schemaViolations[0];
2723
+ if (schemaViolation) {
2724
+ results.push(issueResult(inputPath, schemaViolation.message, schemaViolation));
2725
+ return summarizeNativeTestRun(inputPath, targetFiles, results);
2726
+ }
2727
+ const testNodes = collectNodes(testDoc.root, 'test');
2728
+ const targetCache = new Map([[inputPath, testDoc]]);
2729
+ const assertionsByTarget = new Map();
2730
+ const summarize = () => summarizeNativeTestRun(inputPath, targetFiles, results, createCoverageSummary([...targetFiles].sort().map((targetFile) => {
2731
+ const target = targetCache.get(targetFile);
2732
+ return coverageForTarget(target || {
2733
+ file: targetFile,
2734
+ diagnostics: [],
2735
+ schemaViolations: [],
2736
+ semanticViolations: [],
2737
+ readError: `Target not loaded: ${targetFile}`,
2738
+ }, assertionsByTarget.get(targetFile) || []);
2739
+ })));
2740
+ for (const testNode of testNodes) {
2741
+ const suite = str(getProps(testNode).name) || 'unnamed test';
2742
+ const targetProp = str(getProps(testNode).target);
2743
+ const targetPath = targetProp ? resolve(dirname(inputPath), targetProp) : inputPath;
2744
+ targetFiles.add(targetPath);
2745
+ let target = targetCache.get(targetPath);
2746
+ if (!target) {
2747
+ target = loadKernDocument(targetPath);
2748
+ targetCache.set(targetPath, target);
2749
+ }
2750
+ const assertions = collectAssertions(testNode);
2751
+ assertionsByTarget.set(targetPath, [...(assertionsByTarget.get(targetPath) || []), ...assertions]);
2752
+ if (assertions.length === 0) {
2753
+ results.push({
2754
+ suite,
2755
+ caseName: 'suite',
2756
+ ruleId: 'suite:hasassertions',
2757
+ assertion: 'has native expect assertions',
2758
+ severity: 'error',
2759
+ status: 'failed',
2760
+ message: 'No native expect assertions found. Add expect machine=..., expect no=deriveCycles, or expect no=schemaViolations.',
2761
+ file: inputPath,
2762
+ line: testNode.loc?.line,
2763
+ col: testNode.loc?.col,
2764
+ });
2765
+ continue;
2766
+ }
2767
+ for (const assertion of assertions) {
2768
+ const context = { assertions, fixtures: assertion.fixtures };
2769
+ const requestedSeverity = severityFromNode(assertion.node);
2770
+ for (const evaluated of evaluateNativeAssertion(assertion.node, target, context)) {
2771
+ const severity = effectiveSeverity(requestedSeverity, evaluated);
2772
+ const result = {
2773
+ suite: assertion.suite,
2774
+ caseName: assertion.caseName,
2775
+ ruleId: evaluated.ruleId,
2776
+ assertion: evaluated.assertion,
2777
+ severity,
2778
+ status: statusForEvaluation(evaluated.passed, severity),
2779
+ ...(evaluated.message ? { message: evaluated.message } : {}),
2780
+ file: inputPath,
2781
+ line: assertion.node.loc?.line,
2782
+ col: assertion.node.loc?.col,
2783
+ };
2784
+ if (!grepMatches(options, result))
2785
+ continue;
2786
+ results.push(result);
2787
+ if (options.bail && result.status === 'failed') {
2788
+ return summarize();
2789
+ }
2790
+ }
2791
+ }
2792
+ }
2793
+ return summarize();
2794
+ }
2795
+ export function runNativeKernTestRun(input, options = {}) {
2796
+ const inputPath = resolve(input);
2797
+ const files = [];
2798
+ for (const file of discoverNativeKernTestFiles(inputPath)) {
2799
+ const summary = runNativeKernTests(file, options);
2800
+ files.push(summary);
2801
+ if (options.bail && summary.failed > 0)
2802
+ break;
2803
+ }
2804
+ if (files.length === 0) {
2805
+ return {
2806
+ input: inputPath,
2807
+ testFiles: [],
2808
+ targetFiles: [],
2809
+ total: options.passWithNoTests ? 0 : 1,
2810
+ passed: 0,
2811
+ warnings: 0,
2812
+ failed: options.passWithNoTests ? 0 : 1,
2813
+ files: [],
2814
+ coverage: emptyCoverageSummary(),
2815
+ };
2816
+ }
2817
+ const targetFiles = new Set();
2818
+ for (const file of files) {
2819
+ for (const target of file.targetFiles)
2820
+ targetFiles.add(target);
2821
+ }
2822
+ return {
2823
+ input: inputPath,
2824
+ testFiles: files.map((file) => file.file),
2825
+ targetFiles: [...targetFiles].sort(),
2826
+ total: files.reduce((sum, file) => sum + file.total, 0),
2827
+ passed: files.reduce((sum, file) => sum + file.passed, 0),
2828
+ warnings: files.reduce((sum, file) => sum + file.warnings, 0),
2829
+ failed: files.reduce((sum, file) => sum + file.failed, 0),
2830
+ files,
2831
+ coverage: combineCoverageSummaries(files.map((file) => file.coverage)),
2832
+ };
2833
+ }
2834
+ function summarizeNativeTestRun(file, targetFiles, results, coverage = emptyCoverageSummary()) {
2835
+ const passed = results.filter((result) => result.status === 'passed').length;
2836
+ const warnings = results.filter((result) => result.status === 'warning').length;
2837
+ const failed = results.filter((result) => result.status === 'failed').length;
2838
+ return {
2839
+ file,
2840
+ targetFiles: [...targetFiles].sort(),
2841
+ total: results.length,
2842
+ passed,
2843
+ warnings,
2844
+ failed,
2845
+ results,
2846
+ coverage,
2847
+ };
2848
+ }
2849
+ function normalizeBaselineMessage(message) {
2850
+ return message
2851
+ .replace(/\bat line \d+\b/g, 'at line <line>')
2852
+ .replace(/:\d+:\d+/g, ':<line>:<col>')
2853
+ .replace(/\s+/g, ' ')
2854
+ .trim();
2855
+ }
2856
+ function warningDetailMessages(message) {
2857
+ if (!message)
2858
+ return [];
2859
+ const foundMatch = message.match(/^Found [^:]+:\s*(.+)$/);
2860
+ const body = foundMatch?.[1] || message;
2861
+ return body.split(/;\s+/).map(normalizeBaselineMessage).filter(Boolean);
2862
+ }
2863
+ function warningEntryKey(entry) {
2864
+ return (entry.signature ||
2865
+ JSON.stringify([
2866
+ entry.suite,
2867
+ entry.caseName,
2868
+ entry.ruleId,
2869
+ entry.assertion,
2870
+ entry.message ? normalizeBaselineMessage(entry.message) : '',
2871
+ ]));
2872
+ }
2873
+ function warningResultToBaselineEntries(result) {
2874
+ const details = warningDetailMessages(result.message);
2875
+ if (details.length === 0) {
2876
+ const signature = JSON.stringify([result.suite, result.caseName, result.ruleId, result.assertion, '']);
2877
+ return [
2878
+ {
2879
+ suite: result.suite,
2880
+ caseName: result.caseName,
2881
+ ruleId: result.ruleId,
2882
+ assertion: result.assertion,
2883
+ signature,
2884
+ },
2885
+ ];
2886
+ }
2887
+ return details.map((detail) => ({
2888
+ suite: result.suite,
2889
+ caseName: result.caseName,
2890
+ ruleId: result.ruleId,
2891
+ assertion: result.assertion,
2892
+ signature: JSON.stringify([result.suite, result.caseName, result.ruleId, result.assertion, detail]),
2893
+ message: detail,
2894
+ }));
2895
+ }
2896
+ export function createNativeKernTestBaseline(summary) {
2897
+ const results = 'results' in summary ? summary.results : summary.files.flatMap((fileSummary) => fileSummary.results);
2898
+ const warnings = results.filter((result) => result.status === 'warning').flatMap(warningResultToBaselineEntries);
2899
+ const seen = new Set();
2900
+ const uniqueWarnings = [];
2901
+ for (const warning of warnings) {
2902
+ const key = warningEntryKey(warning);
2903
+ if (seen.has(key))
2904
+ continue;
2905
+ seen.add(key);
2906
+ uniqueWarnings.push(warning);
2907
+ }
2908
+ uniqueWarnings.sort((a, b) => warningEntryKey(a).localeCompare(warningEntryKey(b)));
2909
+ return { version: 1, warnings: uniqueWarnings };
2910
+ }
2911
+ export function checkNativeKernTestBaseline(summary, baseline) {
2912
+ const actual = createNativeKernTestBaseline(summary).warnings;
2913
+ const expected = baseline.warnings || [];
2914
+ const expectedByKey = new Map(expected.map((entry) => [warningEntryKey(entry), entry]));
2915
+ const actualByKey = new Map(actual.map((entry) => [warningEntryKey(entry), entry]));
2916
+ const knownWarnings = actual.filter((entry) => expectedByKey.has(warningEntryKey(entry)));
2917
+ const newWarnings = actual.filter((entry) => !expectedByKey.has(warningEntryKey(entry)));
2918
+ const staleWarnings = expected.filter((entry) => !actualByKey.has(warningEntryKey(entry)));
2919
+ return {
2920
+ ok: newWarnings.length === 0 && staleWarnings.length === 0,
2921
+ knownWarnings,
2922
+ newWarnings,
2923
+ staleWarnings,
2924
+ };
2925
+ }
2926
+ function nativeCountsLine(summary) {
2927
+ return `${summary.passed} passed, ${summary.warnings} warnings, ${summary.failed} failed, ${summary.total} total`;
2928
+ }
2929
+ function coverageLine(name, metric) {
2930
+ return `${name}: ${metric.covered}/${metric.total} (${metric.percent}%)`;
2931
+ }
2932
+ export function formatNativeKernTestCoverage(coverage) {
2933
+ const lines = [
2934
+ `coverage ${coverage.covered}/${coverage.total} (${coverage.percent}%)`,
2935
+ coverageLine('transitions', coverage.transitions),
2936
+ coverageLine('guards', coverage.guards),
2937
+ ];
2938
+ const uncoveredTransitions = coverage.transitions.uncovered;
2939
+ const uncoveredGuards = coverage.guards.uncovered;
2940
+ if (uncoveredTransitions.length > 0) {
2941
+ lines.push('uncovered transitions:');
2942
+ for (const item of uncoveredTransitions)
2943
+ lines.push(` ${item}`);
2944
+ }
2945
+ if (uncoveredGuards.length > 0) {
2946
+ lines.push('uncovered guards:');
2947
+ for (const item of uncoveredGuards)
2948
+ lines.push(` ${item}`);
2949
+ }
2950
+ return `${lines.join('\n')}\n`;
2951
+ }
2952
+ function formatNativeKernTestResult(result, summaryFile) {
2953
+ const marker = result.status === 'passed' ? 'PASS' : result.status === 'warning' ? 'WARN' : 'FAIL';
2954
+ const loc = result.line
2955
+ ? ` (${relative(process.cwd(), result.file || summaryFile)}:${result.line}:${result.col ?? 1})`
2956
+ : '';
2957
+ const lines = [`${marker} ${result.suite} > ${result.caseName}: ${result.assertion} [${result.ruleId}]${loc}`];
2958
+ if (result.status !== 'passed' && result.message)
2959
+ lines.push(` ${result.message}`);
2960
+ return lines;
2961
+ }
2962
+ export function formatNativeKernTestSummary(summary, options = {}) {
2963
+ const lines = [
2964
+ options.format === 'compact'
2965
+ ? `kern test ${relative(process.cwd(), summary.file) || summary.file} - ${nativeCountsLine(summary)}`
2966
+ : `kern test ${relative(process.cwd(), summary.file) || summary.file}`,
2967
+ ];
2968
+ const results = options.format === 'compact' ? summary.results.filter((result) => result.status !== 'passed') : summary.results;
2969
+ for (const result of results) {
2970
+ lines.push(...formatNativeKernTestResult(result, summary.file));
2971
+ }
2972
+ if (options.format === 'compact' && results.length === 0)
2973
+ return `${lines.join('\n')}\n`;
2974
+ if (options.format !== 'compact')
2975
+ lines.push(nativeCountsLine(summary));
2976
+ return `${lines.join('\n')}\n`;
2977
+ }
2978
+ export function formatNativeKernTestRunSummary(summary, options = {}) {
2979
+ const lines = [
2980
+ options.format === 'compact'
2981
+ ? `kern test ${relative(process.cwd(), summary.input) || summary.input} - ${nativeCountsLine(summary)}`
2982
+ : `kern test ${relative(process.cwd(), summary.input) || summary.input}`,
2983
+ ];
2984
+ if (summary.files.length === 0) {
2985
+ lines.push('No native KERN test files found.');
2986
+ if (options.format !== 'compact')
2987
+ lines.push(nativeCountsLine(summary));
2988
+ return `${lines.join('\n')}\n`;
2989
+ }
2990
+ for (const fileSummary of summary.files) {
2991
+ if (options.format === 'compact') {
2992
+ const relFile = relative(process.cwd(), fileSummary.file) || fileSummary.file;
2993
+ if (fileSummary.failed > 0 || fileSummary.warnings > 0) {
2994
+ lines.push(`${relFile} - ${nativeCountsLine(fileSummary)}`);
2995
+ for (const result of fileSummary.results.filter((candidate) => candidate.status !== 'passed')) {
2996
+ lines.push(...formatNativeKernTestResult(result, fileSummary.file));
2997
+ }
2998
+ }
2999
+ }
3000
+ else {
3001
+ lines.push('');
3002
+ lines.push(formatNativeKernTestSummary(fileSummary).trimEnd());
3003
+ }
3004
+ }
3005
+ if (options.format !== 'compact') {
3006
+ lines.push('');
3007
+ lines.push(nativeCountsLine(summary));
3008
+ }
3009
+ return `${lines.join('\n')}\n`;
3010
+ }
3011
+ //# sourceMappingURL=index.js.map