@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.
- package/LICENSE +21 -0
- package/README.md +47 -0
- package/dist/emit.d.ts +52 -0
- package/dist/emit.d.ts.map +1 -0
- package/dist/emit.js +166 -0
- package/dist/emit.js.map +1 -0
- package/dist/explain.d.ts +28 -0
- package/dist/explain.d.ts.map +1 -0
- package/dist/explain.js +42 -0
- package/dist/explain.js.map +1 -0
- package/dist/index.d.ts +132 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +169 -0
- package/dist/index.js.map +1 -0
- package/dist/plan.d.ts +83 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/plan.js +385 -0
- package/dist/plan.js.map +1 -0
- package/dist/runtime-compile.d.ts +30 -0
- package/dist/runtime-compile.d.ts.map +1 -0
- package/dist/runtime-compile.js +29 -0
- package/dist/runtime-compile.js.map +1 -0
- package/dist/runtime.d.ts +157 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +336 -0
- package/dist/runtime.js.map +1 -0
- package/dist/serialize/apply.d.ts +28 -0
- package/dist/serialize/apply.d.ts.map +1 -0
- package/dist/serialize/apply.js +211 -0
- package/dist/serialize/apply.js.map +1 -0
- package/dist/serialize/context.d.ts +143 -0
- package/dist/serialize/context.d.ts.map +1 -0
- package/dist/serialize/context.js +93 -0
- package/dist/serialize/context.js.map +1 -0
- package/dist/serialize/expressions.d.ts +7 -0
- package/dist/serialize/expressions.d.ts.map +1 -0
- package/dist/serialize/expressions.js +177 -0
- package/dist/serialize/expressions.js.map +1 -0
- package/dist/serialize/guards.d.ts +7 -0
- package/dist/serialize/guards.d.ts.map +1 -0
- package/dist/serialize/guards.js +19 -0
- package/dist/serialize/guards.js.map +1 -0
- package/dist/serialize/index.d.ts +7 -0
- package/dist/serialize/index.d.ts.map +1 -0
- package/dist/serialize/index.js +266 -0
- package/dist/serialize/index.js.map +1 -0
- package/dist/serialize/keywords.d.ts +32 -0
- package/dist/serialize/keywords.d.ts.map +1 -0
- package/dist/serialize/keywords.js +131 -0
- package/dist/serialize/keywords.js.map +1 -0
- package/dist/serialize/messages.d.ts +36 -0
- package/dist/serialize/messages.d.ts.map +1 -0
- package/dist/serialize/messages.js +100 -0
- package/dist/serialize/messages.js.map +1 -0
- package/dist/serialize/names.d.ts +47 -0
- package/dist/serialize/names.d.ts.map +1 -0
- package/dist/serialize/names.js +115 -0
- package/dist/serialize/names.js.map +1 -0
- package/dist/serialize/spans.d.ts +34 -0
- package/dist/serialize/spans.d.ts.map +1 -0
- package/dist/serialize/spans.js +61 -0
- package/dist/serialize/spans.js.map +1 -0
- package/dist/serialize/statements.d.ts +16 -0
- package/dist/serialize/statements.d.ts.map +1 -0
- package/dist/serialize/statements.js +396 -0
- package/dist/serialize/statements.js.map +1 -0
- package/dist/serialize/unit.d.ts +8 -0
- package/dist/serialize/unit.d.ts.map +1 -0
- package/dist/serialize/unit.js +107 -0
- package/dist/serialize/unit.js.map +1 -0
- package/dist/standalone.d.ts +24 -0
- package/dist/standalone.d.ts.map +1 -0
- package/dist/standalone.js +166 -0
- package/dist/standalone.js.map +1 -0
- package/package.json +38 -0
- package/src/emit.ts +196 -0
- package/src/explain.ts +67 -0
- package/src/index.ts +366 -0
- package/src/plan.ts +496 -0
- package/src/runtime-compile.ts +90 -0
- package/src/runtime.ts +599 -0
- package/src/serialize/apply.ts +251 -0
- package/src/serialize/context.ts +161 -0
- package/src/serialize/expressions.ts +202 -0
- package/src/serialize/guards.ts +20 -0
- package/src/serialize/index.ts +432 -0
- package/src/serialize/keywords.ts +144 -0
- package/src/serialize/messages.ts +120 -0
- package/src/serialize/names.ts +136 -0
- package/src/serialize/spans.ts +88 -0
- package/src/serialize/statements.ts +531 -0
- package/src/serialize/unit.ts +118 -0
- 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
|
+
}
|