@json-schema-engine/compiler 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -0
  3. package/dist/emit.d.ts +52 -0
  4. package/dist/emit.d.ts.map +1 -0
  5. package/dist/emit.js +166 -0
  6. package/dist/emit.js.map +1 -0
  7. package/dist/explain.d.ts +28 -0
  8. package/dist/explain.d.ts.map +1 -0
  9. package/dist/explain.js +42 -0
  10. package/dist/explain.js.map +1 -0
  11. package/dist/index.d.ts +132 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +169 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/plan.d.ts +83 -0
  16. package/dist/plan.d.ts.map +1 -0
  17. package/dist/plan.js +385 -0
  18. package/dist/plan.js.map +1 -0
  19. package/dist/runtime-compile.d.ts +30 -0
  20. package/dist/runtime-compile.d.ts.map +1 -0
  21. package/dist/runtime-compile.js +29 -0
  22. package/dist/runtime-compile.js.map +1 -0
  23. package/dist/runtime.d.ts +157 -0
  24. package/dist/runtime.d.ts.map +1 -0
  25. package/dist/runtime.js +336 -0
  26. package/dist/runtime.js.map +1 -0
  27. package/dist/serialize/apply.d.ts +28 -0
  28. package/dist/serialize/apply.d.ts.map +1 -0
  29. package/dist/serialize/apply.js +211 -0
  30. package/dist/serialize/apply.js.map +1 -0
  31. package/dist/serialize/context.d.ts +143 -0
  32. package/dist/serialize/context.d.ts.map +1 -0
  33. package/dist/serialize/context.js +93 -0
  34. package/dist/serialize/context.js.map +1 -0
  35. package/dist/serialize/expressions.d.ts +7 -0
  36. package/dist/serialize/expressions.d.ts.map +1 -0
  37. package/dist/serialize/expressions.js +177 -0
  38. package/dist/serialize/expressions.js.map +1 -0
  39. package/dist/serialize/guards.d.ts +7 -0
  40. package/dist/serialize/guards.d.ts.map +1 -0
  41. package/dist/serialize/guards.js +19 -0
  42. package/dist/serialize/guards.js.map +1 -0
  43. package/dist/serialize/index.d.ts +7 -0
  44. package/dist/serialize/index.d.ts.map +1 -0
  45. package/dist/serialize/index.js +266 -0
  46. package/dist/serialize/index.js.map +1 -0
  47. package/dist/serialize/keywords.d.ts +32 -0
  48. package/dist/serialize/keywords.d.ts.map +1 -0
  49. package/dist/serialize/keywords.js +131 -0
  50. package/dist/serialize/keywords.js.map +1 -0
  51. package/dist/serialize/messages.d.ts +36 -0
  52. package/dist/serialize/messages.d.ts.map +1 -0
  53. package/dist/serialize/messages.js +100 -0
  54. package/dist/serialize/messages.js.map +1 -0
  55. package/dist/serialize/names.d.ts +47 -0
  56. package/dist/serialize/names.d.ts.map +1 -0
  57. package/dist/serialize/names.js +115 -0
  58. package/dist/serialize/names.js.map +1 -0
  59. package/dist/serialize/spans.d.ts +34 -0
  60. package/dist/serialize/spans.d.ts.map +1 -0
  61. package/dist/serialize/spans.js +61 -0
  62. package/dist/serialize/spans.js.map +1 -0
  63. package/dist/serialize/statements.d.ts +16 -0
  64. package/dist/serialize/statements.d.ts.map +1 -0
  65. package/dist/serialize/statements.js +396 -0
  66. package/dist/serialize/statements.js.map +1 -0
  67. package/dist/serialize/unit.d.ts +8 -0
  68. package/dist/serialize/unit.d.ts.map +1 -0
  69. package/dist/serialize/unit.js +107 -0
  70. package/dist/serialize/unit.js.map +1 -0
  71. package/dist/standalone.d.ts +24 -0
  72. package/dist/standalone.d.ts.map +1 -0
  73. package/dist/standalone.js +166 -0
  74. package/dist/standalone.js.map +1 -0
  75. package/package.json +38 -0
  76. package/src/emit.ts +196 -0
  77. package/src/explain.ts +67 -0
  78. package/src/index.ts +366 -0
  79. package/src/plan.ts +496 -0
  80. package/src/runtime-compile.ts +90 -0
  81. package/src/runtime.ts +599 -0
  82. package/src/serialize/apply.ts +251 -0
  83. package/src/serialize/context.ts +161 -0
  84. package/src/serialize/expressions.ts +202 -0
  85. package/src/serialize/guards.ts +20 -0
  86. package/src/serialize/index.ts +432 -0
  87. package/src/serialize/keywords.ts +144 -0
  88. package/src/serialize/messages.ts +120 -0
  89. package/src/serialize/names.ts +136 -0
  90. package/src/serialize/spans.ts +88 -0
  91. package/src/serialize/statements.ts +531 -0
  92. package/src/serialize/unit.ts +118 -0
  93. package/src/standalone.ts +187 -0
package/src/plan.ts ADDED
@@ -0,0 +1,496 @@
1
+ // Compilation planner (M6.2): classifies every reachable schema node as a
2
+ // static (compilable) or interpreted (trampoline) unit, using ONLY public
3
+ // core APIs and analyze() facts — never keyword names (D1). Conservative by
4
+ // design: anything uncertain falls back to the interpreter, which is always
5
+ // correct.
6
+
7
+ import {
8
+ UnresolvableRefError,
9
+ type Engine,
10
+ type JsonValue,
11
+ type SchemaRef,
12
+ type StaticFacts,
13
+ type SubschemaApplication,
14
+ } from "@json-schema-engine/core";
15
+
16
+ /** Why a unit is interpreted rather than compiled. */
17
+ export type FallbackCause =
18
+ | "dynamic" // $dynamicRef-class keyword present
19
+ | "unlowerable" // a keyword without lower(), or unevaluated* without static coverage
20
+ | "cycle" // participates in a possible in-place cycle
21
+ | "nonSchema"; // ref-into-data; the interpreter's D19 backstop reports it
22
+
23
+ /** Static evaluated-name coverage for one schema object (slice: own-node trio only). */
24
+ export interface StaticNameCoverage {
25
+ names: readonly string[];
26
+ patterns: readonly string[];
27
+ coversAllNames: boolean;
28
+ prefixCount: number;
29
+ coversAllIndexes: boolean;
30
+ }
31
+
32
+ /** One application edge out of a unit, resolved at plan time. */
33
+ export interface PlannedApplication {
34
+ keyword: string;
35
+ app: SubschemaApplication;
36
+ targetKey: string;
37
+ }
38
+
39
+ export interface PlannedUnit {
40
+ key: string; // `${baseUri}#${pointer}` — canonical unit identity
41
+ ref: SchemaRef;
42
+ kind: "static" | "interpreted";
43
+ cause?: FallbackCause;
44
+ /** static units: resolved outgoing application edges (keyword order preserved) */
45
+ edges: PlannedApplication[];
46
+ /** static coverage for this schema object, when computable (slice rule) */
47
+ coverage: StaticNameCoverage | null;
48
+ /**
49
+ * Consumer compiled with RUNTIME evaluated-set tracking instead of the
50
+ * interpreter (COMPILED-CONSUMERS.md phase B): a flag-mode consumer whose
51
+ * evaluated coverage is dynamic (static licensing failed), or ANY list-mode
52
+ * consumer (list plans never static-license — see PlanOptions.output).
53
+ * `coverage` stays null; the unit's body threads a runtime coverage channel
54
+ * through its in-place closure (its {@link inRegion} members) and its
55
+ * consumer keywords read it. Never set together with {@link inRegion}
56
+ * (nested tracked consumers island).
57
+ */
58
+ tracking?: boolean;
59
+ /**
60
+ * A static unit in some tracked unit's in-place coverage region: reachable
61
+ * from a tracked (or region) unit via in-place edges. Emitted with a second
62
+ * calling convention (a trailing coverage channel) so its producers'
63
+ * post-success coverage flows to the consumer (COMPILED-CONSUMERS.md phase B).
64
+ */
65
+ inRegion?: boolean;
66
+ /** true when any apply path from this unit can reach an interpreted unit */
67
+ reachesInterpreted: boolean;
68
+ /** number of planned edges targeting this unit (D9 inline licensing) */
69
+ useCount: number;
70
+ }
71
+
72
+ export interface CompilationPlan {
73
+ rootKey: string;
74
+ units: Map<string, PlannedUnit>;
75
+ /** every regex source any static unit tests (pattern + coverage patterns) */
76
+ patterns: string[];
77
+ /** every format name any static unit's asserting `format` tests */
78
+ formats: string[];
79
+ /** interpreted units in stable order; index = target-table slot */
80
+ targets: PlannedUnit[];
81
+ }
82
+
83
+ const unitKey = (ref: SchemaRef): string => `${ref.baseUri}#${ref.pointer}`;
84
+
85
+ const isObj = (v: JsonValue): v is Record<string, JsonValue> =>
86
+ typeof v === "object" && v !== null && !Array.isArray(v);
87
+
88
+ /**
89
+ * Build the compilation plan for one registered root schema. The walk mirrors
90
+ * the registration walk's position logic by construction: descent uses the
91
+ * same analyze() facts and the same registry.child pointer navigation.
92
+ */
93
+ /** Options for {@link buildPlan}. */
94
+ export interface PlanOptions {
95
+ /**
96
+ * The artifact's output mode. Static-coverage licensing for unevaluated*
97
+ * consumers is sound for "flag" only: coverage models the parent-SUCCESS
98
+ * path, but when a contributor (e.g. prefixItems inside an allOf branch)
99
+ * FAILS, the interpreter drops its annotations and unevaluated* reports
100
+ * additional errors. That difference is verdict-invisible — the failing
101
+ * contributor already fails the parent — but list output must reproduce
102
+ * the interpreter's error units exactly. So "list" plans NEVER static-
103
+ * license a consumer: every consumer is compiled with runtime coverage
104
+ * tracking (`tracking`), which reproduces the drop-on-failure behavior by
105
+ * construction (a failed application's channel span truncates, so the sweep
106
+ * covers less). "flag" keeps the static-coverage fast path.
107
+ */
108
+ output?: "flag" | "list";
109
+ }
110
+
111
+ export function buildPlan(
112
+ engine: Engine,
113
+ schemaUri: string,
114
+ options: PlanOptions = {},
115
+ ): CompilationPlan {
116
+ const registry = engine.registry;
117
+ const units = new Map<string, PlannedUnit>();
118
+ const patterns = new Set<string>();
119
+ const formats = new Set<string>();
120
+
121
+ const rootRef = registry.rootRef(schemaUri);
122
+
123
+ // inPlaceChain: units connected to the current edge via consecutive
124
+ // in-place applications — a back-edge into this chain is an in-place
125
+ // cycle (same-cursor re-entry, InfiniteLoopError class) and its target
126
+ // must stay interpreted. A descending edge resets the chain.
127
+ const plan = (
128
+ ref: SchemaRef,
129
+ inPlaceChain: readonly string[],
130
+ ): PlannedUnit => {
131
+ const key = unitKey(ref);
132
+ const existing = units.get(key);
133
+ if (existing) return existing;
134
+
135
+ const unit: PlannedUnit = {
136
+ key,
137
+ ref,
138
+ kind: "static",
139
+ edges: [],
140
+ coverage: null,
141
+ reachesInterpreted: false,
142
+ useCount: 0,
143
+ };
144
+ units.set(key, unit);
145
+
146
+ const node = ref.node;
147
+ if (typeof node === "boolean") return unit; // trivially static
148
+
149
+ if (!isObj(node)) {
150
+ unit.kind = "interpreted";
151
+ unit.cause = "nonSchema"; // interpreter D19 backstop reports it lazily
152
+ return unit;
153
+ }
154
+
155
+ // No dialect allowlist: keyword facts are the compiler's whole window
156
+ // into semantics (D1), so a dialect compiles exactly when every present
157
+ // keyword lowers — the per-keyword check below. That is what lets a
158
+ // dialect PACKAGE (draft-04, future OAS dialects) become compilable
159
+ // purely by shipping lower() on its behaviors; the only dialect-LEVEL
160
+ // semantic the planner must mirror is refIgnoresSiblings, handled
161
+ // generically here.
162
+ const dialect = registry.dialectFor(ref.baseUri);
163
+
164
+ // draft-07/06 (D18, engine.ts:397): a $ref makes every sibling keyword
165
+ // act as if absent — plan/lower ONLY $ref, exactly as applySchemaAtDepth
166
+ // skips every non-$ref entry when refOnly. Without this, a draft-07
167
+ // $ref-with-siblings unit would compile both the ref AND the siblings,
168
+ // a real divergence from the interpreter (unreachable before this
169
+ // milestone, since every legacy unit was interpreted regardless).
170
+ const refOnly = dialect.refIgnoresSiblings && Object.hasOwn(node, "$ref");
171
+
172
+ // Gather per-keyword facts; classify the node.
173
+ interface KeywordPlan {
174
+ name: string;
175
+ facts: StaticFacts;
176
+ }
177
+ const present: KeywordPlan[] = [];
178
+ let consumerPresent = false;
179
+ for (const entry of dialect.ordered) {
180
+ if (refOnly && entry.name !== "$ref") continue;
181
+ if (!Object.hasOwn(node, entry.name)) continue;
182
+ const behavior = entry.behavior;
183
+ const value = node[entry.name]!;
184
+ const facts = behavior.analyze?.(value, { schema: node }) ?? {};
185
+ if (facts.dynamicScopeSensitive) {
186
+ unit.kind = "interpreted";
187
+ unit.cause = "dynamic";
188
+ return unit;
189
+ }
190
+ if (typeof behavior.lower !== "function") {
191
+ unit.kind = "interpreted";
192
+ unit.cause = "unlowerable";
193
+ return unit;
194
+ }
195
+ // A coverage consumer (unevaluated*) declares `consumes` AND an
196
+ // evaluated-coverage fact; a keyword consuming other dependency data
197
+ // (then/else reading if's outcome) needs no channel.
198
+ if (
199
+ (facts.consumes?.length ?? 0) > 0 &&
200
+ (facts.evaluatesNames !== undefined ||
201
+ facts.evaluatesIndexes !== undefined)
202
+ ) {
203
+ consumerPresent = true;
204
+ }
205
+ for (const rx of facts.regexes ?? []) patterns.add(rx);
206
+ for (const fmt of facts.formats ?? []) formats.add(fmt);
207
+ present.push({ name: entry.name, facts });
208
+ }
209
+ // Unknown keywords: annotations, elided in flag mode.
210
+
211
+ // Consumer licensing (D9a): a consumer lowers only when its coverage
212
+ // kind is statically known — from this object's own contributors plus,
213
+ // transitively, unconditional asserting in-place applications (allOf
214
+ // conjuncts, $ref chains). Anything runtime-conditional (anyOf/oneOf/
215
+ // if/dependentSchemas branches, if's condition, dynamic references,
216
+ // cycles) makes that coverage kind dynamic and the node interpreted.
217
+ if (consumerPresent) {
218
+ if (options.output === "list") {
219
+ // See PlanOptions.output: list mode NEVER static-licenses. Static
220
+ // coverage models only the parent-SUCCESS path (the M6.6 finding) —
221
+ // a FAILING contributor's dropped annotations make the interpreter
222
+ // emit additional unevaluated* errors, verdict-invisible but list-
223
+ // visible. Runtime tracking reproduces that by construction: the
224
+ // failed application's channel span truncates and the consumer sweep
225
+ // then covers less. So every list consumer is tracked; the unit stays
226
+ // static and CONTINUES to edge resolution (its tracked body needs
227
+ // planned in-place edges to thread the channel), and the region
228
+ // fixpoint below threads its in-place closure.
229
+ unit.tracking = true;
230
+ } else {
231
+ // Flag mode: static-coverage licensing is sound (verdict-only), so a
232
+ // consumer whose coverage kind is statically known keeps the fast path.
233
+ const halves = coverageHalves(registry, ref, new Set(), true);
234
+ const needsNames = present.some(
235
+ (k) => (k.facts.consumes?.length ?? 0) > 0 && k.facts.evaluatesNames,
236
+ );
237
+ const needsIndexes = present.some(
238
+ (k) =>
239
+ (k.facts.consumes?.length ?? 0) > 0 && k.facts.evaluatesIndexes,
240
+ );
241
+ if (
242
+ (needsNames && halves.name === null) ||
243
+ (needsIndexes && halves.index === null)
244
+ ) {
245
+ // Static licensing failed. FLAG mode compiles the consumer anyway,
246
+ // with runtime evaluated-set tracking (coverage stays null); the
247
+ // unit stays static and CONTINUES to edge resolution / child
248
+ // planning like any static unit — its tracked body needs planned
249
+ // in-place edges to thread the coverage channel through. The region
250
+ // membership of those edges is computed after the walk (below).
251
+ unit.tracking = true;
252
+ } else {
253
+ unit.coverage = {
254
+ names: halves.name ? [...halves.name.names] : [],
255
+ patterns: halves.name ? halves.name.patterns : [],
256
+ coversAllNames: halves.name?.all ?? false,
257
+ prefixCount: halves.index?.prefix ?? 0,
258
+ coversAllIndexes: halves.index?.all ?? false,
259
+ };
260
+ for (const pat of unit.coverage.patterns) patterns.add(pat);
261
+ }
262
+ }
263
+ }
264
+
265
+ // Resolve application edges; plan children.
266
+ for (const { name, facts } of present) {
267
+ for (const app of facts.applications ?? []) {
268
+ let target: SchemaRef;
269
+ try {
270
+ if (app.ref !== undefined) {
271
+ target = registry.resolveRef(app.ref, ref.baseUri);
272
+ } else if (app.sibling !== undefined) {
273
+ target = registry.child(ref, [app.sibling, ...app.path]);
274
+ } else {
275
+ target = registry.child(ref, [name, ...app.path]);
276
+ }
277
+ } catch (err) {
278
+ if (err instanceof UnresolvableRefError) {
279
+ // Lazy-failure parity: the interpreter throws only when the
280
+ // reference is actually followed, so the whole node falls back.
281
+ unit.kind = "interpreted";
282
+ unit.cause = "unlowerable";
283
+ unit.edges = [];
284
+ return unit;
285
+ }
286
+ throw err;
287
+ }
288
+ const targetKey = unitKey(target);
289
+ if (app.mode === "inPlace") {
290
+ if (inPlaceChain.includes(targetKey) || targetKey === key) {
291
+ // In-place cycle: same-cursor re-entry. The interpreter's
292
+ // seen-set gives exact InfiniteLoopError parity.
293
+ const t =
294
+ units.get(targetKey) ?? plan(target, [...inPlaceChain, key]);
295
+ t.kind = "interpreted";
296
+ t.cause = "cycle";
297
+ t.edges = [];
298
+ unit.edges.push({ keyword: name, app, targetKey });
299
+ continue;
300
+ }
301
+ plan(target, [...inPlaceChain, key]);
302
+ } else {
303
+ plan(target, []);
304
+ }
305
+ unit.edges.push({ keyword: name, app, targetKey });
306
+ }
307
+ }
308
+ return unit;
309
+ };
310
+
311
+ const root = plan(rootRef, []);
312
+
313
+ // Coverage-region fixpoint (COMPILED-CONSUMERS.md phase B). A tracked
314
+ // consumer's coverage comes from its own producers AND, recursively, its
315
+ // asserting in-place applications; each static unit in that in-place closure
316
+ // must report its post-success coverage through the runtime channel, so it
317
+ // gets the second (channel-threaded) calling convention (inRegion). Seeded
318
+ // by tracked units, propagated through in-place edges of static units.
319
+ //
320
+ // Exception — a nested tracked consumer reached inside the closure: threading
321
+ // one unit's channel through another's consumer is a v1 non-goal, so it is
322
+ // re-classified interpreted and becomes an ISLAND. The parent then trampolines
323
+ // it (fragCov) and folds its harvested root coverage — no nested channel.
324
+ // Islands and already-interpreted targets reached by region edges stay
325
+ // interpreted. A consumer-bearing STATIC-coverage unit stays a normal region
326
+ // member (its produces flow like any producer's).
327
+ const regionStack = [...units.values()]
328
+ .filter((u) => u.tracking)
329
+ .map((u) => u.key);
330
+ while (regionStack.length > 0) {
331
+ const u = units.get(regionStack.pop()!)!;
332
+ if (u.kind !== "static") continue; // a seed islanded by another seed
333
+ for (const edge of u.edges) {
334
+ if (edge.app.mode !== "inPlace") continue;
335
+ const m = units.get(edge.targetKey)!;
336
+ if (m.kind !== "static") continue; // island target: stays interpreted
337
+ // Boolean subschemas contribute no coverage and fold to a literal at the
338
+ // call site — never a channel-threaded member.
339
+ if (typeof m.ref.node === "boolean") continue;
340
+ if (m.tracking) {
341
+ // Nested tracked consumer: island it (v1 nested-channel simplification).
342
+ m.kind = "interpreted";
343
+ m.cause = "unlowerable";
344
+ m.edges = [];
345
+ delete m.tracking;
346
+ continue;
347
+ }
348
+ if (!m.inRegion) {
349
+ m.inRegion = true;
350
+ regionStack.push(m.key);
351
+ }
352
+ }
353
+ }
354
+
355
+ // reachesInterpreted fixpoint over the edge graph.
356
+ let changed = true;
357
+ while (changed) {
358
+ changed = false;
359
+ for (const unit of units.values()) {
360
+ if (unit.kind === "interpreted" || unit.reachesInterpreted) continue;
361
+ for (const edge of unit.edges) {
362
+ const t = units.get(edge.targetKey)!;
363
+ if (t.kind === "interpreted" || t.reachesInterpreted) {
364
+ unit.reachesInterpreted = true;
365
+ changed = true;
366
+ break;
367
+ }
368
+ }
369
+ }
370
+ }
371
+
372
+ for (const unit of units.values()) {
373
+ if (unit.kind !== "static") continue;
374
+ for (const edge of unit.edges) units.get(edge.targetKey)!.useCount++;
375
+ }
376
+
377
+ const targets = [...units.values()].filter((u) => u.kind === "interpreted");
378
+ return {
379
+ rootKey: root.key,
380
+ units,
381
+ patterns: [...patterns],
382
+ formats: [...formats],
383
+ targets,
384
+ };
385
+ }
386
+
387
+ /** Per-kind static coverage: null = dynamic (statically unknowable). */
388
+ interface CoverageHalves {
389
+ name: { names: Set<string>; patterns: string[]; all: boolean } | null;
390
+ index: { prefix: number; all: boolean } | null;
391
+ }
392
+
393
+ /**
394
+ * The evaluated-coverage a schema node contributes at its own cursor (D9a),
395
+ * including — transitively — unconditional asserting in-place applications.
396
+ * `excludeConsumers` is true only for the licensing node itself: a
397
+ * consumer's own coverage fact describes the state AFTER it runs. Inside
398
+ * transitive targets, consumer facts count (post-success contribution).
399
+ * Facts come from analyze() and are valid regardless of which tier
400
+ * evaluates the target, so contribution is independent of compilability.
401
+ */
402
+ function coverageHalves(
403
+ registry: import("@json-schema-engine/core").SchemaRegistry,
404
+ ref: SchemaRef,
405
+ visiting: Set<string>,
406
+ excludeConsumers: boolean,
407
+ ): CoverageHalves {
408
+ const key = unitKey(ref);
409
+ if (visiting.has(key)) return { name: null, index: null }; // cycle
410
+ visiting.add(key);
411
+ try {
412
+ const node = ref.node;
413
+ if (typeof node === "boolean") {
414
+ // Contributes nothing; `false` fails the parent, making coverage moot.
415
+ return {
416
+ name: { names: new Set(), patterns: [], all: false },
417
+ index: { prefix: 0, all: false },
418
+ };
419
+ }
420
+ if (!isObj(node)) return { name: null, index: null };
421
+ // Coverage comes from analyze() facts alone, dialect-agnostic (D1) —
422
+ // the keyword-author contract is that evaluates* facts are complete.
423
+ const dialect = registry.dialectFor(ref.baseUri);
424
+ // Same $ref-only reading as buildPlan's refOnly (engine.ts:397): a
425
+ // draft-07/06 sibling contributes nothing when $ref is present.
426
+ const refOnly = dialect.refIgnoresSiblings && Object.hasOwn(node, "$ref");
427
+
428
+ const acc: CoverageHalves = {
429
+ name: { names: new Set(), patterns: [], all: false },
430
+ index: { prefix: 0, all: false },
431
+ };
432
+ const fold = (h: CoverageHalves): void => {
433
+ if (acc.name && h.name) {
434
+ for (const n of h.name.names) acc.name.names.add(n);
435
+ acc.name.patterns.push(...h.name.patterns);
436
+ acc.name.all ||= h.name.all;
437
+ } else acc.name = null;
438
+ if (acc.index && h.index) {
439
+ acc.index.prefix = Math.max(acc.index.prefix, h.index.prefix);
440
+ acc.index.all ||= h.index.all;
441
+ } else acc.index = null;
442
+ };
443
+
444
+ for (const entry of dialect.ordered) {
445
+ if (refOnly && entry.name !== "$ref") continue;
446
+ if (!Object.hasOwn(node, entry.name)) continue;
447
+ const value = node[entry.name]!;
448
+ const facts = entry.behavior.analyze?.(value, { schema: node }) ?? {};
449
+ if (facts.dynamicScopeSensitive) return { name: null, index: null };
450
+ const isConsumer = (facts.consumes?.length ?? 0) > 0;
451
+ if (!(excludeConsumers && isConsumer)) {
452
+ if (acc.name && facts.evaluatesNames) {
453
+ const c = facts.evaluatesNames;
454
+ if (c.kind === "dynamic") acc.name = null;
455
+ else if (c.kind === "all") acc.name.all = true;
456
+ else if (c.kind === "names")
457
+ for (const n of c.names) acc.name.names.add(n);
458
+ else acc.name.patterns.push(...c.patterns);
459
+ }
460
+ if (acc.index && facts.evaluatesIndexes) {
461
+ const c = facts.evaluatesIndexes;
462
+ if (c.kind === "dynamic") acc.index = null;
463
+ else if (c.kind === "prefix")
464
+ acc.index.prefix = Math.max(acc.index.prefix, c.count);
465
+ // allFrom's start is bounded by this node's own prefix
466
+ // contribution (items starts after sibling prefixItems), so the
467
+ // per-node union covers everything.
468
+ else acc.index.all = true;
469
+ }
470
+ }
471
+ for (const app of facts.applications ?? []) {
472
+ if (app.mode !== "inPlace") continue; // child cursors: no contribution here
473
+ if (app.inverted) continue; // never survives the parent-success path
474
+ if (app.conditional || !app.asserts) {
475
+ // Runtime-conditional contribution (branching, or if's condition
476
+ // merging only on its own success): statically unknowable.
477
+ return { name: null, index: null };
478
+ }
479
+ let target: SchemaRef;
480
+ try {
481
+ target =
482
+ app.ref !== undefined
483
+ ? registry.resolveRef(app.ref, ref.baseUri)
484
+ : registry.child(ref, [app.sibling ?? entry.name, ...app.path]);
485
+ } catch {
486
+ return { name: null, index: null };
487
+ }
488
+ fold(coverageHalves(registry, target, visiting, false));
489
+ }
490
+ if (!acc.name && !acc.index) return acc; // both dynamic already
491
+ }
492
+ return acc;
493
+ } finally {
494
+ visiting.delete(key);
495
+ }
496
+ }
@@ -0,0 +1,90 @@
1
+ // The ONLY module that materializes code at runtime (D10 runtime mode).
2
+ // Confining `new Function` here keeps the CSP story auditable: everything
3
+ // else, including the interpreter, is greppably free of code generation.
4
+ // Standalone source emission (M6.5) bypasses this module entirely.
5
+
6
+ import type {
7
+ AnnotationUnit,
8
+ JsonValue,
9
+ SchemaRef,
10
+ } from "@json-schema-engine/core";
11
+ import type { Runtime, TraceState } from "./runtime.js";
12
+
13
+ /** A compiled flag-mode validator. */
14
+ export type CompiledValidate = (instance: JsonValue) => boolean;
15
+
16
+ /** A compiled list-mode evaluator (interpreter-exact flat error units). */
17
+ export type CompiledEvaluateList<E> = (instance: JsonValue) => {
18
+ valid: boolean;
19
+ errors: E[];
20
+ };
21
+
22
+ /**
23
+ * A compiled annotation-mode evaluator: flat error units plus the raw
24
+ * (list-filtered) annotation units. The artifact wrapper applies the `keep`
25
+ * predicate and the valid-only presence rule over these.
26
+ */
27
+ export type CompiledEvaluateListAnn<E> = (instance: JsonValue) => {
28
+ valid: boolean;
29
+ errors: E[];
30
+ annotations: AnnotationUnit[];
31
+ };
32
+
33
+ /** Instantiate list-mode artifact source (same closure contract as {@link instantiate}). */
34
+ export function instantiateList<E>(
35
+ source: string,
36
+ runtime: Runtime,
37
+ targets: readonly SchemaRef[],
38
+ ): CompiledEvaluateList<E> {
39
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
40
+ const factory = new Function("R", "T", source) as (
41
+ R: Runtime,
42
+ T: readonly SchemaRef[],
43
+ ) => CompiledEvaluateList<E>;
44
+ return factory(runtime, targets);
45
+ }
46
+
47
+ /** Instantiate annotation-mode artifact source (same closure contract as {@link instantiate}). */
48
+ export function instantiateListAnn<E>(
49
+ source: string,
50
+ runtime: Runtime,
51
+ targets: readonly SchemaRef[],
52
+ ): CompiledEvaluateListAnn<E> {
53
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
54
+ const factory = new Function("R", "T", source) as (
55
+ R: Runtime,
56
+ T: readonly SchemaRef[],
57
+ ) => CompiledEvaluateListAnn<E>;
58
+ return factory(runtime, targets);
59
+ }
60
+
61
+ /** A compiled trace-mode evaluator: the filled per-evaluation {@link TraceState}. */
62
+ export type CompiledEvaluateTrace = (instance: JsonValue) => TraceState;
63
+
64
+ /** Instantiate trace-mode artifact source (same closure contract as {@link instantiate}). */
65
+ export function instantiateTrace(
66
+ source: string,
67
+ runtime: Runtime,
68
+ targets: readonly SchemaRef[],
69
+ ): CompiledEvaluateTrace {
70
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
71
+ const factory = new Function("R", "T", source) as (
72
+ R: Runtime,
73
+ T: readonly SchemaRef[],
74
+ ) => CompiledEvaluateTrace;
75
+ return factory(runtime, targets);
76
+ }
77
+
78
+ /** Instantiate artifact source against its runtime closure and target table. */
79
+ export function instantiate(
80
+ source: string,
81
+ runtime: Runtime,
82
+ targets: readonly SchemaRef[],
83
+ ): CompiledValidate {
84
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
85
+ const factory = new Function("R", "T", source) as (
86
+ R: Runtime,
87
+ T: readonly SchemaRef[],
88
+ ) => CompiledValidate;
89
+ return factory(runtime, targets);
90
+ }