@ai-matrx/content-ir 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +27 -0
- package/dist/index.d.ts +1601 -0
- package/dist/index.js +4116 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4116 @@
|
|
|
1
|
+
import Ajv from 'ajv';
|
|
2
|
+
|
|
3
|
+
// core/ir-types.ts
|
|
4
|
+
var IR_VERSION = 1;
|
|
5
|
+
var IR_ENVELOPE_KEY = "__ir";
|
|
6
|
+
function irPathKey(path) {
|
|
7
|
+
return path.map((segment) => String(segment)).join(".");
|
|
8
|
+
}
|
|
9
|
+
function irPathsEqual(left, right) {
|
|
10
|
+
if (left.length !== right.length) return false;
|
|
11
|
+
for (let i = 0; i < left.length; i++) {
|
|
12
|
+
if (left[i] !== right[i]) return false;
|
|
13
|
+
}
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
function irPathIsUnderOrEqual(path, prefix) {
|
|
17
|
+
if (path.length < prefix.length) return false;
|
|
18
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
19
|
+
if (path[i] !== prefix[i]) return false;
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
function irPathLabel(path) {
|
|
24
|
+
if (path.length === 0) return "root";
|
|
25
|
+
const parts = [];
|
|
26
|
+
for (const segment of path) {
|
|
27
|
+
if (typeof segment === "number") {
|
|
28
|
+
parts[parts.length - 1] += `[${segment}]`;
|
|
29
|
+
} else {
|
|
30
|
+
parts.push(String(segment));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return parts.join(".");
|
|
34
|
+
}
|
|
35
|
+
function isEmptyResidue(residue) {
|
|
36
|
+
return (residue.extra === null || Object.keys(residue.extra).length === 0) && (residue.optionalMissing === null || residue.optionalMissing.length === 0) && (residue.notices === null || residue.notices.length === 0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// core/kind-schema.types.ts
|
|
40
|
+
var KIND_KEY = "__kind";
|
|
41
|
+
function readObjectKind(value) {
|
|
42
|
+
const kind = value[KIND_KEY];
|
|
43
|
+
return typeof kind === "string" ? kind : null;
|
|
44
|
+
}
|
|
45
|
+
function isScalarArrayType(type) {
|
|
46
|
+
return type === "string[]" || type === "number[]" || type === "boolean[]";
|
|
47
|
+
}
|
|
48
|
+
function scalarArrayItemType(type) {
|
|
49
|
+
if (type === "number[]") return "number";
|
|
50
|
+
if (type === "boolean[]") return "boolean";
|
|
51
|
+
return "string";
|
|
52
|
+
}
|
|
53
|
+
function isJsonAnyField(field) {
|
|
54
|
+
return field.type === "json" || field.type === "json[]";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// core/discriminator.ts
|
|
58
|
+
var JSON_DISCRIMINATOR = {
|
|
59
|
+
format: "json",
|
|
60
|
+
key: KIND_KEY
|
|
61
|
+
};
|
|
62
|
+
function xmlDiscriminator(tag) {
|
|
63
|
+
return { format: "xml", tag };
|
|
64
|
+
}
|
|
65
|
+
function fenceDiscriminator(language) {
|
|
66
|
+
return { format: "fence", language };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// core/ir-tree.ts
|
|
70
|
+
function isRecord(value) {
|
|
71
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
72
|
+
}
|
|
73
|
+
function mergeWithoutLoss(prior, incoming) {
|
|
74
|
+
if (!isRecord(prior)) return { value: incoming, rescued: [] };
|
|
75
|
+
if (!isRecord(incoming)) {
|
|
76
|
+
if (incoming !== null && incoming !== void 0) {
|
|
77
|
+
return { value: incoming, rescued: [] };
|
|
78
|
+
}
|
|
79
|
+
return { value: prior, rescued: Object.keys(prior) };
|
|
80
|
+
}
|
|
81
|
+
const rescued = Object.keys(prior).filter((key) => !(key in incoming));
|
|
82
|
+
if (rescued.length === 0) return { value: incoming, rescued };
|
|
83
|
+
return { value: { ...prior, ...incoming }, rescued };
|
|
84
|
+
}
|
|
85
|
+
var IrTree = class {
|
|
86
|
+
nodes = /* @__PURE__ */ new Map();
|
|
87
|
+
rawPaths = /* @__PURE__ */ new Map();
|
|
88
|
+
// pathKey → reason
|
|
89
|
+
dirty = /* @__PURE__ */ new Set();
|
|
90
|
+
/**
|
|
91
|
+
* KIND PRESERVATION (streaming db/cloud kinds): pathKey → identified kind
|
|
92
|
+
* for nodes whose kind is KNOWN (kind_identified / pending_schema) but that
|
|
93
|
+
* have no snapshot node yet because the schema is still cold-fetching.
|
|
94
|
+
* Without this, the envelope reports `kind: ""` for the whole pending
|
|
95
|
+
* window and the render seam can only show raw JSON.
|
|
96
|
+
*/
|
|
97
|
+
identifiedKinds = /* @__PURE__ */ new Map();
|
|
98
|
+
/** pathKeys currently waiting on a schema cold fetch. */
|
|
99
|
+
pendingSchemaPaths = /* @__PURE__ */ new Set();
|
|
100
|
+
/**
|
|
101
|
+
* Early top-level scalar fields (title, loading_message, …) captured for
|
|
102
|
+
* identified-but-schema-pending nodes — the loading-component fuel. Scalars
|
|
103
|
+
* only (no live parser references can escape through here). Superseded the
|
|
104
|
+
* moment a real snapshot node exists.
|
|
105
|
+
*/
|
|
106
|
+
earlyFields = /* @__PURE__ */ new Map();
|
|
107
|
+
/**
|
|
108
|
+
* pathKey → identified kind preserved through a SCHEMA-AVAILABILITY raw
|
|
109
|
+
* fallback (parser stamped `kind` on the raw_object event). Structural raws
|
|
110
|
+
* (missing __kind, duplicate key, validation failure) never land here.
|
|
111
|
+
*/
|
|
112
|
+
rawKinds = /* @__PURE__ */ new Map();
|
|
113
|
+
regionStatus = "streaming";
|
|
114
|
+
errorReason = null;
|
|
115
|
+
rootRawValue = null;
|
|
116
|
+
/** Notices for degrades that tried to erase already-published data. */
|
|
117
|
+
rescueNotices = [];
|
|
118
|
+
completedKind = "";
|
|
119
|
+
get status() {
|
|
120
|
+
return this.regionStatus;
|
|
121
|
+
}
|
|
122
|
+
applyEvent(event) {
|
|
123
|
+
switch (event.type) {
|
|
124
|
+
case "kind_identified": {
|
|
125
|
+
this.identifiedKinds.set(irPathKey(event.path), event.kind);
|
|
126
|
+
this.dirty.add(irPathKey(event.path));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
case "pending_schema": {
|
|
130
|
+
const pathKey = irPathKey(event.path);
|
|
131
|
+
this.identifiedKinds.set(pathKey, event.kind);
|
|
132
|
+
this.pendingSchemaPaths.add(pathKey);
|
|
133
|
+
this.dirty.add(pathKey);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
case "field": {
|
|
137
|
+
const parentKey = irPathKey(event.path.slice(0, -1));
|
|
138
|
+
if (this.nodes.has(parentKey)) return;
|
|
139
|
+
if (!this.identifiedKinds.has(parentKey)) return;
|
|
140
|
+
const v = event.value;
|
|
141
|
+
if (v !== null && typeof v === "object") return;
|
|
142
|
+
const bucket = this.earlyFields.get(parentKey) ?? {};
|
|
143
|
+
bucket[event.key] = v;
|
|
144
|
+
this.earlyFields.set(parentKey, bucket);
|
|
145
|
+
this.dirty.add(parentKey);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
case "block_snapshot": {
|
|
149
|
+
const pathKey = irPathKey(event.path);
|
|
150
|
+
this.pendingSchemaPaths.delete(pathKey);
|
|
151
|
+
this.earlyFields.delete(pathKey);
|
|
152
|
+
this.upsertNode(event.path, {
|
|
153
|
+
kind: event.kind,
|
|
154
|
+
value: event.value,
|
|
155
|
+
residue: event.residue,
|
|
156
|
+
complete: event.complete
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
case "raw_object": {
|
|
161
|
+
const pathKey = irPathKey(event.path);
|
|
162
|
+
this.pendingSchemaPaths.delete(pathKey);
|
|
163
|
+
this.earlyFields.delete(pathKey);
|
|
164
|
+
if (event.kind) this.rawKinds.set(pathKey, event.kind);
|
|
165
|
+
this.markRaw(event.path, event.reason, event.value);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
case "complete": {
|
|
169
|
+
this.regionStatus = this.errorReason ? "error" : "complete";
|
|
170
|
+
this.completedKind = event.kind;
|
|
171
|
+
if (this.rawPaths.has("") && isRecord(event.value)) {
|
|
172
|
+
const merged = mergeWithoutLoss(this.rootRawValue, event.value);
|
|
173
|
+
this.recordRescue("", "complete", merged.rescued);
|
|
174
|
+
if (isRecord(merged.value)) this.rootRawValue = merged.value;
|
|
175
|
+
}
|
|
176
|
+
this.dirty.add("");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
case "error": {
|
|
180
|
+
this.errorReason = event.reason;
|
|
181
|
+
this.regionStatus = "error";
|
|
182
|
+
this.dirty.add("");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
default:
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
getNode(pathKey) {
|
|
190
|
+
return this.nodes.get(pathKey) ?? null;
|
|
191
|
+
}
|
|
192
|
+
listNodes() {
|
|
193
|
+
return [...this.nodes.values()];
|
|
194
|
+
}
|
|
195
|
+
isRawPath(pathKey) {
|
|
196
|
+
return this.rawPaths.has(pathKey);
|
|
197
|
+
}
|
|
198
|
+
/** Dirty pathKeys since the last drain — the flush/notify unit. */
|
|
199
|
+
drainDirty() {
|
|
200
|
+
const drained = [...this.dirty];
|
|
201
|
+
this.dirty.clear();
|
|
202
|
+
return drained;
|
|
203
|
+
}
|
|
204
|
+
hasDirty() {
|
|
205
|
+
return this.dirty.size > 0;
|
|
206
|
+
}
|
|
207
|
+
// -------------------------------------------------------------------------
|
|
208
|
+
upsertNode(path, payload) {
|
|
209
|
+
const pathKey = irPathKey(path);
|
|
210
|
+
if (this.rawPaths.has(pathKey)) return;
|
|
211
|
+
const stabilized = this.stabilizeValue(
|
|
212
|
+
payload.value,
|
|
213
|
+
path,
|
|
214
|
+
pathKey
|
|
215
|
+
);
|
|
216
|
+
const prior = this.nodes.get(pathKey);
|
|
217
|
+
const node = {
|
|
218
|
+
kind: payload.kind,
|
|
219
|
+
kindState: payload.complete ? "resolved" : prior?.kindState ?? "resolved",
|
|
220
|
+
path,
|
|
221
|
+
pathKey,
|
|
222
|
+
value: stabilized,
|
|
223
|
+
residue: payload.residue,
|
|
224
|
+
complete: payload.complete,
|
|
225
|
+
version: (prior?.version ?? 0) + 1
|
|
226
|
+
};
|
|
227
|
+
this.nodes.set(pathKey, node);
|
|
228
|
+
this.dirty.add(pathKey);
|
|
229
|
+
this.propagateToAncestors(path, node.value);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Substitute kind-node children with their current tree values so sibling
|
|
233
|
+
* identities are stable; deep-copy everything else so no live parser
|
|
234
|
+
* reference ever escapes.
|
|
235
|
+
*/
|
|
236
|
+
stabilizeValue(value, path, applyingPathKey) {
|
|
237
|
+
if (Array.isArray(value)) {
|
|
238
|
+
return value.map(
|
|
239
|
+
(item, index) => this.stabilizeValue(item, [...path, index], applyingPathKey)
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
if (isRecord(value)) {
|
|
243
|
+
const pathKey = irPathKey(path);
|
|
244
|
+
if (pathKey !== applyingPathKey) {
|
|
245
|
+
const childNode = this.nodes.get(pathKey);
|
|
246
|
+
if (childNode) return childNode.value;
|
|
247
|
+
}
|
|
248
|
+
const out = {};
|
|
249
|
+
for (const [key, child] of Object.entries(value)) {
|
|
250
|
+
out[key] = this.stabilizeValue(child, [...path, key], applyingPathKey);
|
|
251
|
+
}
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
return value;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* COW spine rebuild: replace the child's slot in each ancestor kind-node's
|
|
258
|
+
* value, shallow-copying only the containers along the way. Siblings keep
|
|
259
|
+
* identity; every ancestor gets a new value identity + version bump.
|
|
260
|
+
*/
|
|
261
|
+
propagateToAncestors(childPath, childValue) {
|
|
262
|
+
let currentPath = childPath;
|
|
263
|
+
let currentValue = childValue;
|
|
264
|
+
while (currentPath.length > 0) {
|
|
265
|
+
const ancestor = this.findNearestAncestorNode(currentPath);
|
|
266
|
+
if (!ancestor) return;
|
|
267
|
+
const relative = currentPath.slice(ancestor.path.length);
|
|
268
|
+
const rebuilt = this.cloneAlong(ancestor.value, relative, currentValue);
|
|
269
|
+
if (rebuilt === ancestor.value) return;
|
|
270
|
+
const updated = {
|
|
271
|
+
...ancestor,
|
|
272
|
+
value: rebuilt,
|
|
273
|
+
version: ancestor.version + 1
|
|
274
|
+
};
|
|
275
|
+
this.nodes.set(ancestor.pathKey, updated);
|
|
276
|
+
this.dirty.add(ancestor.pathKey);
|
|
277
|
+
currentPath = ancestor.path;
|
|
278
|
+
currentValue = updated.value;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
findNearestAncestorNode(path) {
|
|
282
|
+
for (let len = path.length - 1; len >= 0; len--) {
|
|
283
|
+
const node = this.nodes.get(irPathKey(path.slice(0, len)));
|
|
284
|
+
if (node) return node;
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
cloneAlong(container, relative, leaf) {
|
|
289
|
+
if (relative.length === 0) return leaf;
|
|
290
|
+
const [head, ...rest] = relative;
|
|
291
|
+
if (Array.isArray(container)) {
|
|
292
|
+
const index = typeof head === "number" ? head : Number(head);
|
|
293
|
+
const copy = container.slice();
|
|
294
|
+
while (copy.length <= index) copy.push(void 0);
|
|
295
|
+
copy[index] = this.cloneAlong(container[index], rest, leaf);
|
|
296
|
+
return copy;
|
|
297
|
+
}
|
|
298
|
+
if (isRecord(container)) {
|
|
299
|
+
const key = String(head);
|
|
300
|
+
return {
|
|
301
|
+
...container,
|
|
302
|
+
[key]: this.cloneAlong(container[key], rest, leaf)
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const built = typeof head === "number" ? this.cloneAlong([], relative, leaf) : this.cloneAlong({}, relative, leaf);
|
|
306
|
+
return built;
|
|
307
|
+
}
|
|
308
|
+
markRaw(path, reason, value) {
|
|
309
|
+
const pathKey = irPathKey(path);
|
|
310
|
+
const prior = path.length === 0 ? this.nodes.get("")?.value ?? this.rootRawValue : this.nodes.get(pathKey)?.value;
|
|
311
|
+
const merged = mergeWithoutLoss(prior, value);
|
|
312
|
+
this.recordRescue(pathKey, reason, merged.rescued);
|
|
313
|
+
this.rawPaths.set(pathKey, reason);
|
|
314
|
+
this.nodes.delete(pathKey);
|
|
315
|
+
this.dirty.add(pathKey);
|
|
316
|
+
if (path.length === 0) {
|
|
317
|
+
if (isRecord(merged.value)) this.rootRawValue = merged.value;
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
this.propagateToAncestors(path, merged.value);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* A rescue means a degrade tried to erase data a user could already see —
|
|
324
|
+
* an upstream defect. It rides the envelope as a notice so it surfaces in the
|
|
325
|
+
* Error Inspector instead of being silently absorbed (`core/` is a pure
|
|
326
|
+
* kernel: no console, no capture — the notice IS the alarm).
|
|
327
|
+
*/
|
|
328
|
+
recordRescue(pathKey, reason, rescued) {
|
|
329
|
+
if (rescued.length === 0) return;
|
|
330
|
+
this.rescueNotices.push({
|
|
331
|
+
code: "degrade_data_rescued",
|
|
332
|
+
message: `degrade (${reason}) at path "${pathKey || "<root>"}" would have dropped: ${rescued.join(", ")}`
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Assemble the canonical envelope. ONE code path for stream + one-shot.
|
|
337
|
+
* Callers supply the fingerprint (one-shot hashes the source; live sessions
|
|
338
|
+
* keep an incremental hasher so no per-flush re-hash happens).
|
|
339
|
+
*/
|
|
340
|
+
buildEnvelope(fingerprint) {
|
|
341
|
+
const rootNode = this.nodes.get("");
|
|
342
|
+
const rootRawReason = this.rawPaths.get("") ?? null;
|
|
343
|
+
const notices = [];
|
|
344
|
+
if (this.errorReason) {
|
|
345
|
+
notices.push({ code: "parse_error", message: this.errorReason });
|
|
346
|
+
}
|
|
347
|
+
if (rootRawReason) {
|
|
348
|
+
notices.push({ code: "raw_fallback", message: rootRawReason });
|
|
349
|
+
}
|
|
350
|
+
notices.push(...this.rescueNotices);
|
|
351
|
+
const baseResidue = rootNode?.residue ?? null;
|
|
352
|
+
let residue = baseResidue;
|
|
353
|
+
if (notices.length > 0) {
|
|
354
|
+
residue = {
|
|
355
|
+
extra: baseResidue?.extra ?? null,
|
|
356
|
+
optionalMissing: baseResidue?.optionalMissing ?? null,
|
|
357
|
+
notices: [...baseResidue?.notices ?? [], ...notices]
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
const isRaw = rootRawReason !== null;
|
|
361
|
+
const identifiedKind = this.identifiedKinds.get("") ?? "";
|
|
362
|
+
const rootKind = isRaw ? this.rawKinds.get("") ?? "" : rootNode?.kind ?? (this.completedKind || identifiedKind);
|
|
363
|
+
const root = {
|
|
364
|
+
role: "structured",
|
|
365
|
+
kind: rootKind,
|
|
366
|
+
kindState: isRaw ? "raw" : rootNode ? rootNode.kindState : this.pendingSchemaPaths.has("") ? "pending_schema" : this.regionStatus === "streaming" ? identifiedKind ? "pending_schema" : "pending_kind" : "raw",
|
|
367
|
+
discriminator: JSON_DISCRIMINATOR,
|
|
368
|
+
path: [],
|
|
369
|
+
status: this.regionStatus,
|
|
370
|
+
value: rootNode?.value ?? this.rootRawValue ?? // Copy: the early-fields bucket keeps mutating as fields arrive; the
|
|
371
|
+
// envelope must be freezable (Redux dev-mode immutability).
|
|
372
|
+
(this.earlyFields.has("") ? { ...this.earlyFields.get("") } : {}),
|
|
373
|
+
residue
|
|
374
|
+
};
|
|
375
|
+
const nodeIndex = {};
|
|
376
|
+
for (const node of this.nodes.values()) {
|
|
377
|
+
if (node.pathKey === "") continue;
|
|
378
|
+
nodeIndex[node.pathKey] = {
|
|
379
|
+
kind: node.kind,
|
|
380
|
+
kindState: node.kindState,
|
|
381
|
+
status: node.complete ? "complete" : "streaming",
|
|
382
|
+
...node.residue ? { residue: node.residue } : {}
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
for (const [pathKey] of this.rawPaths) {
|
|
386
|
+
if (pathKey === "") continue;
|
|
387
|
+
nodeIndex[pathKey] = { kind: "", kindState: "raw", status: "complete" };
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
v: IR_VERSION,
|
|
391
|
+
engine: "fe-kind-parser",
|
|
392
|
+
fingerprint,
|
|
393
|
+
root,
|
|
394
|
+
...Object.keys(nodeIndex).length > 0 ? { nodeIndex } : {}
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// core/json-tokenizer.ts
|
|
400
|
+
var JsonStreamTokenizer = class {
|
|
401
|
+
constructor(onToken) {
|
|
402
|
+
this.onToken = onToken;
|
|
403
|
+
}
|
|
404
|
+
onToken;
|
|
405
|
+
mode = "normal";
|
|
406
|
+
pos = 0;
|
|
407
|
+
stringBuffer = "";
|
|
408
|
+
primitiveBuffer = "";
|
|
409
|
+
unicodeBuffer = "";
|
|
410
|
+
tokenStart = 0;
|
|
411
|
+
get position() {
|
|
412
|
+
return this.pos;
|
|
413
|
+
}
|
|
414
|
+
push(chunk) {
|
|
415
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
416
|
+
const ch = chunk[i];
|
|
417
|
+
const at = this.pos;
|
|
418
|
+
this.pos += 1;
|
|
419
|
+
if (ch === void 0) continue;
|
|
420
|
+
if (this.mode === "primitive") {
|
|
421
|
+
if (this.isDelimiter(ch)) {
|
|
422
|
+
this.emitPrimitive();
|
|
423
|
+
this.handleNormalChar(ch, at);
|
|
424
|
+
} else {
|
|
425
|
+
this.primitiveBuffer += ch;
|
|
426
|
+
}
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
if (this.mode === "string") {
|
|
430
|
+
if (ch === '"') {
|
|
431
|
+
this.onToken({
|
|
432
|
+
type: "string",
|
|
433
|
+
value: this.stringBuffer,
|
|
434
|
+
at: this.tokenStart
|
|
435
|
+
});
|
|
436
|
+
this.stringBuffer = "";
|
|
437
|
+
this.mode = "normal";
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (ch === "\\") {
|
|
441
|
+
this.mode = "escape";
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (ch === "\n" || ch === "\r") {
|
|
445
|
+
throw new Error(`Invalid unescaped newline in JSON string at ${at}`);
|
|
446
|
+
}
|
|
447
|
+
this.stringBuffer += ch;
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
if (this.mode === "escape") {
|
|
451
|
+
if (ch === "u") {
|
|
452
|
+
this.unicodeBuffer = "";
|
|
453
|
+
this.mode = "unicode";
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
const escaped = {
|
|
457
|
+
'"': '"',
|
|
458
|
+
"\\": "\\",
|
|
459
|
+
"/": "/",
|
|
460
|
+
b: "\b",
|
|
461
|
+
f: "\f",
|
|
462
|
+
n: "\n",
|
|
463
|
+
r: "\r",
|
|
464
|
+
t: " "
|
|
465
|
+
};
|
|
466
|
+
if (!(ch in escaped)) {
|
|
467
|
+
throw new Error(`Invalid JSON escape sequence at ${at}`);
|
|
468
|
+
}
|
|
469
|
+
this.stringBuffer += escaped[ch];
|
|
470
|
+
this.mode = "string";
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (this.mode === "unicode") {
|
|
474
|
+
if (!/[0-9a-fA-F]/.test(ch)) {
|
|
475
|
+
throw new Error(`Invalid unicode escape at ${at}`);
|
|
476
|
+
}
|
|
477
|
+
this.unicodeBuffer += ch;
|
|
478
|
+
if (this.unicodeBuffer.length === 4) {
|
|
479
|
+
this.stringBuffer += String.fromCharCode(
|
|
480
|
+
parseInt(this.unicodeBuffer, 16)
|
|
481
|
+
);
|
|
482
|
+
this.unicodeBuffer = "";
|
|
483
|
+
this.mode = "string";
|
|
484
|
+
}
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
this.handleNormalChar(ch, at);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
end() {
|
|
491
|
+
if (this.mode === "primitive") {
|
|
492
|
+
this.emitPrimitive();
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
if (this.mode !== "normal") {
|
|
496
|
+
throw new Error(
|
|
497
|
+
`Stream ended while parsing JSON ${this.mode} at ${this.pos}`
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
handleNormalChar(ch, at) {
|
|
502
|
+
if (/\s/.test(ch)) return;
|
|
503
|
+
if (ch === "{" || ch === "}" || ch === "[" || ch === "]" || ch === ":" || ch === ",") {
|
|
504
|
+
this.onToken({ type: "punct", value: ch, at });
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (ch === '"') {
|
|
508
|
+
this.mode = "string";
|
|
509
|
+
this.stringBuffer = "";
|
|
510
|
+
this.tokenStart = at;
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (/[-0-9tfn]/.test(ch)) {
|
|
514
|
+
this.mode = "primitive";
|
|
515
|
+
this.primitiveBuffer = ch;
|
|
516
|
+
this.tokenStart = at;
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
throw new Error(`Unexpected character "${ch}" at ${at}`);
|
|
520
|
+
}
|
|
521
|
+
emitPrimitive() {
|
|
522
|
+
const raw = this.primitiveBuffer;
|
|
523
|
+
const at = this.tokenStart;
|
|
524
|
+
this.primitiveBuffer = "";
|
|
525
|
+
this.mode = "normal";
|
|
526
|
+
let value;
|
|
527
|
+
try {
|
|
528
|
+
value = JSON.parse(raw);
|
|
529
|
+
} catch {
|
|
530
|
+
throw new Error(`Invalid JSON primitive "${raw}" at ${at}`);
|
|
531
|
+
}
|
|
532
|
+
if (typeof value === "number") {
|
|
533
|
+
this.onToken({ type: "number", value, at });
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if (typeof value === "boolean") {
|
|
537
|
+
this.onToken({ type: "boolean", value, at });
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (value === null) {
|
|
541
|
+
this.onToken({ type: "null", value, at });
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
throw new Error(`Unsupported JSON primitive "${raw}" at ${at}`);
|
|
545
|
+
}
|
|
546
|
+
isDelimiter(ch) {
|
|
547
|
+
return /\s/.test(ch) || ch === "{" || ch === "}" || ch === "[" || ch === "]" || ch === ":" || ch === ",";
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
// core/kind-snapshot.ts
|
|
552
|
+
function emptyValueForFieldSchema(field) {
|
|
553
|
+
if (field.nullable) return null;
|
|
554
|
+
switch (field.type) {
|
|
555
|
+
case "string":
|
|
556
|
+
return "";
|
|
557
|
+
case "number":
|
|
558
|
+
return 0;
|
|
559
|
+
case "boolean":
|
|
560
|
+
return false;
|
|
561
|
+
case "json":
|
|
562
|
+
return null;
|
|
563
|
+
case "string[]":
|
|
564
|
+
case "number[]":
|
|
565
|
+
case "boolean[]":
|
|
566
|
+
case "json[]":
|
|
567
|
+
case "array":
|
|
568
|
+
return [];
|
|
569
|
+
case "object":
|
|
570
|
+
case "inline_object":
|
|
571
|
+
case "record":
|
|
572
|
+
return {};
|
|
573
|
+
case "enum":
|
|
574
|
+
return "";
|
|
575
|
+
case "union":
|
|
576
|
+
if (field.scalars.includes("string")) return "";
|
|
577
|
+
if (field.scalars.includes("number")) return 0;
|
|
578
|
+
if (field.scalars.includes("boolean")) return false;
|
|
579
|
+
return {};
|
|
580
|
+
default:
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
function buildCompliantKindSnapshot(schema, partial) {
|
|
585
|
+
const value = {
|
|
586
|
+
[KIND_KEY]: schema.kind
|
|
587
|
+
};
|
|
588
|
+
const optionalMissing = [];
|
|
589
|
+
for (const [fieldName, fieldSchema] of Object.entries(schema.fields)) {
|
|
590
|
+
if (fieldName in partial && partial[fieldName] !== void 0) {
|
|
591
|
+
value[fieldName] = partial[fieldName];
|
|
592
|
+
} else if (fieldSchema.required) {
|
|
593
|
+
value[fieldName] = emptyValueForFieldSchema(fieldSchema);
|
|
594
|
+
} else {
|
|
595
|
+
optionalMissing.push(fieldName);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
let extra = null;
|
|
599
|
+
for (const [fieldName, fieldValue] of Object.entries(partial)) {
|
|
600
|
+
if (fieldName === KIND_KEY) continue;
|
|
601
|
+
if (fieldName in schema.fields) continue;
|
|
602
|
+
if (extra === null) extra = {};
|
|
603
|
+
extra[fieldName] = fieldValue;
|
|
604
|
+
}
|
|
605
|
+
const residue = {
|
|
606
|
+
extra,
|
|
607
|
+
optionalMissing: optionalMissing.length > 0 ? optionalMissing : null,
|
|
608
|
+
notices: null
|
|
609
|
+
};
|
|
610
|
+
return { value, residue: isEmptyResidue(residue) ? null : residue };
|
|
611
|
+
}
|
|
612
|
+
function mergeResidueIntoValue(value, residue) {
|
|
613
|
+
if (!residue?.extra) return value;
|
|
614
|
+
return { ...value, ...residue.extra };
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// core/kind-parser.ts
|
|
618
|
+
var jsonRootKeyLookup = null;
|
|
619
|
+
function setJsonRootKeyLookup(lookup) {
|
|
620
|
+
jsonRootKeyLookup = lookup;
|
|
621
|
+
}
|
|
622
|
+
function isSchemaResolver(source) {
|
|
623
|
+
return typeof source.get === "function";
|
|
624
|
+
}
|
|
625
|
+
function safeCopy(value) {
|
|
626
|
+
try {
|
|
627
|
+
return structuredClone(value);
|
|
628
|
+
} catch {
|
|
629
|
+
return value;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
var KindStreamParser = class {
|
|
633
|
+
constructor(options) {
|
|
634
|
+
this.options = options;
|
|
635
|
+
this.resolver = isSchemaResolver(options.schemas) ? options.schemas : {
|
|
636
|
+
get: (kind) => options.schemas[kind]
|
|
637
|
+
};
|
|
638
|
+
this.tokenizer = new JsonStreamTokenizer(
|
|
639
|
+
(token) => this.handleToken(token)
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
options;
|
|
643
|
+
resolver;
|
|
644
|
+
stack = [];
|
|
645
|
+
objectKinds = /* @__PURE__ */ new Map();
|
|
646
|
+
inlineSchemas = /* @__PURE__ */ new Map();
|
|
647
|
+
recordSchemas = /* @__PURE__ */ new Map();
|
|
648
|
+
rawObjectPaths = /* @__PURE__ */ new Set();
|
|
649
|
+
/**
|
|
650
|
+
* Subtrees whose value domain is "any JSON" by schema (`json` / `json[]`
|
|
651
|
+
* fields, `record` with `values:"json"` members). OPAQUE by contract: no
|
|
652
|
+
* kind identification, no pending_kind, no raw_object degradation — unknown
|
|
653
|
+
* structure here is the declared shape, not a failure. Propagates to every
|
|
654
|
+
* descendant compound.
|
|
655
|
+
*/
|
|
656
|
+
opaquePaths = /* @__PURE__ */ new Set();
|
|
657
|
+
deferredFields = /* @__PURE__ */ new Map();
|
|
658
|
+
awaitingKindPaths = /* @__PURE__ */ new Set();
|
|
659
|
+
/** Paths whose kind came from parent-schema prediction, unconfirmed so far. */
|
|
660
|
+
speculativeKinds = /* @__PURE__ */ new Set();
|
|
661
|
+
/**
|
|
662
|
+
* Kind named by the root object's FIRST key through the `json_root_key`
|
|
663
|
+
* surface registry — a candidate, adopted only at root finalize.
|
|
664
|
+
*/
|
|
665
|
+
rootSurfaceKind = null;
|
|
666
|
+
/** kind → paths (by key) waiting for the resolver's cold fetch. */
|
|
667
|
+
pendingSchemaPaths = /* @__PURE__ */ new Map();
|
|
668
|
+
/** Pending-schema paths whose object already closed. */
|
|
669
|
+
closedPendingPaths = /* @__PURE__ */ new Set();
|
|
670
|
+
/** Schemas delivered via notifySchemaArrived (overlay over the resolver). */
|
|
671
|
+
arrivedSchemas = /* @__PURE__ */ new Map();
|
|
672
|
+
tokenizer;
|
|
673
|
+
root;
|
|
674
|
+
rootKind = "";
|
|
675
|
+
rootDone = false;
|
|
676
|
+
failed = false;
|
|
677
|
+
push(chunk) {
|
|
678
|
+
if (this.failed || this.rootDone) return;
|
|
679
|
+
try {
|
|
680
|
+
this.tokenizer.push(chunk);
|
|
681
|
+
} catch (error) {
|
|
682
|
+
this.fail(
|
|
683
|
+
error instanceof Error ? error.message : String(error),
|
|
684
|
+
this.tokenizer.position
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
end() {
|
|
689
|
+
if (this.failed) return;
|
|
690
|
+
try {
|
|
691
|
+
this.tokenizer.end();
|
|
692
|
+
} catch (error) {
|
|
693
|
+
this.fail(
|
|
694
|
+
error instanceof Error ? error.message : String(error),
|
|
695
|
+
this.tokenizer.position
|
|
696
|
+
);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
this.resolvePendingSchemasAsRaw();
|
|
700
|
+
if (!this.rootDone) {
|
|
701
|
+
this.fail(
|
|
702
|
+
"Stream ended before the root JSON object was complete.",
|
|
703
|
+
this.tokenizer.position
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
resolvePendingSchemasAsRaw() {
|
|
708
|
+
const at = this.tokenizer.position;
|
|
709
|
+
for (const [kind, paths] of [...this.pendingSchemaPaths]) {
|
|
710
|
+
this.pendingSchemaPaths.delete(kind);
|
|
711
|
+
for (const [pathKey, path] of paths) {
|
|
712
|
+
this.closedPendingPaths.delete(pathKey);
|
|
713
|
+
if (this.rawObjectPaths.has(pathKey)) continue;
|
|
714
|
+
const value = this.getLiveObjectValue(path) ?? this.getFinalizedObjectValue(path) ?? {};
|
|
715
|
+
this.emitRawObject(
|
|
716
|
+
path,
|
|
717
|
+
safeCopy(value),
|
|
718
|
+
`No block schema registered for "${kind}".`,
|
|
719
|
+
at,
|
|
720
|
+
kind
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
/** True once the root value has closed — the region is fully consumed. */
|
|
726
|
+
get isComplete() {
|
|
727
|
+
return this.rootDone;
|
|
728
|
+
}
|
|
729
|
+
get hasFailed() {
|
|
730
|
+
return this.failed;
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Upgrade-in-place: the registry's cold fetch answered. Pending nodes for
|
|
734
|
+
* this kind validate and complete (closed nodes retroactively); a null
|
|
735
|
+
* schema (fetch miss) drops them to raw. Safe to call after end().
|
|
736
|
+
*/
|
|
737
|
+
notifySchemaArrived(kind, schema) {
|
|
738
|
+
const waiting = this.pendingSchemaPaths.get(kind);
|
|
739
|
+
if (!waiting) return;
|
|
740
|
+
this.pendingSchemaPaths.delete(kind);
|
|
741
|
+
if (schema) {
|
|
742
|
+
this.arrivedSchemas.set(kind, schema);
|
|
743
|
+
}
|
|
744
|
+
const at = this.tokenizer.position;
|
|
745
|
+
for (const [pathKey, path] of waiting) {
|
|
746
|
+
if (this.rawObjectPaths.has(pathKey)) continue;
|
|
747
|
+
const value = this.getLiveObjectValue(path) ?? this.getFinalizedObjectValue(path);
|
|
748
|
+
if (!schema) {
|
|
749
|
+
this.emitRawObject(
|
|
750
|
+
path,
|
|
751
|
+
safeCopy(value ?? {}),
|
|
752
|
+
`No block schema registered for "${kind}".`,
|
|
753
|
+
at,
|
|
754
|
+
kind
|
|
755
|
+
);
|
|
756
|
+
this.closedPendingPaths.delete(pathKey);
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
if (this.closedPendingPaths.has(pathKey)) {
|
|
760
|
+
this.closedPendingPaths.delete(pathKey);
|
|
761
|
+
if (value) {
|
|
762
|
+
this.finalizeTypedObject(path, value, at);
|
|
763
|
+
}
|
|
764
|
+
} else {
|
|
765
|
+
this.emitBlockSnapshotForObject(path, at);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
handleToken(token) {
|
|
770
|
+
if (this.failed) return;
|
|
771
|
+
if (this.rootDone) {
|
|
772
|
+
this.fail("Unexpected token after complete JSON object.", token.at);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
if (token.type === "punct") {
|
|
776
|
+
this.handlePunctuation(token);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
if (token.type === "string") {
|
|
780
|
+
this.handleString(token);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
this.beginScalar(token.value, token.at);
|
|
784
|
+
}
|
|
785
|
+
handleString(token) {
|
|
786
|
+
const frame = this.currentFrame();
|
|
787
|
+
if (frame?.kind === "object" && (frame.expecting === "keyOrEnd" || frame.expecting === "key")) {
|
|
788
|
+
this.acceptObjectKey(frame, token.value, token.at);
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
this.beginScalar(token.value, token.at);
|
|
792
|
+
}
|
|
793
|
+
handlePunctuation(token) {
|
|
794
|
+
switch (token.value) {
|
|
795
|
+
case "{":
|
|
796
|
+
this.beginCompound("object", token.at);
|
|
797
|
+
return;
|
|
798
|
+
case "[":
|
|
799
|
+
this.beginCompound("array", token.at);
|
|
800
|
+
return;
|
|
801
|
+
case "}":
|
|
802
|
+
this.closeCompound("object", token.at);
|
|
803
|
+
return;
|
|
804
|
+
case "]":
|
|
805
|
+
this.closeCompound("array", token.at);
|
|
806
|
+
return;
|
|
807
|
+
case ":":
|
|
808
|
+
this.acceptColon(token.at);
|
|
809
|
+
return;
|
|
810
|
+
case ",":
|
|
811
|
+
this.acceptComma(token.at);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
beginCompound(kind, at) {
|
|
816
|
+
const value = kind === "object" ? {} : [];
|
|
817
|
+
const container = this.currentFrame();
|
|
818
|
+
const path = this.placeValue(value, kind, at, false);
|
|
819
|
+
if (!path || this.failed) return;
|
|
820
|
+
const opaque = container !== void 0 && this.opaquePaths.has(this.pathKey(container.path)) || this.isJsonAnyPlacement(path);
|
|
821
|
+
if (opaque) this.opaquePaths.add(this.pathKey(path));
|
|
822
|
+
if (kind === "object") {
|
|
823
|
+
this.emit({ type: "object_start", path, at });
|
|
824
|
+
if (opaque) {
|
|
825
|
+
this.stack.push({
|
|
826
|
+
kind: "object",
|
|
827
|
+
path,
|
|
828
|
+
value,
|
|
829
|
+
expecting: "keyOrEnd",
|
|
830
|
+
keyCount: 0
|
|
831
|
+
});
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
this.registerObjectContext(path);
|
|
835
|
+
const pathKey = this.pathKey(path);
|
|
836
|
+
this.stack.push({
|
|
837
|
+
kind: "object",
|
|
838
|
+
path,
|
|
839
|
+
value,
|
|
840
|
+
expecting: "keyOrEnd",
|
|
841
|
+
keyCount: 0
|
|
842
|
+
});
|
|
843
|
+
if (this.inlineSchemas.has(pathKey) || this.recordSchemas.has(pathKey)) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
const speculated = this.resolveSpeculativeKind(path);
|
|
847
|
+
if (speculated) {
|
|
848
|
+
this.objectKinds.set(pathKey, speculated);
|
|
849
|
+
this.speculativeKinds.add(pathKey);
|
|
850
|
+
if (path.length === 0) {
|
|
851
|
+
this.rootKind = speculated;
|
|
852
|
+
}
|
|
853
|
+
this.emit({
|
|
854
|
+
type: "kind_identified",
|
|
855
|
+
kind: speculated,
|
|
856
|
+
path,
|
|
857
|
+
speculative: true,
|
|
858
|
+
at
|
|
859
|
+
});
|
|
860
|
+
this.emitBlockSnapshotForObject(path, at);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
this.awaitingKindPaths.add(pathKey);
|
|
864
|
+
this.emit({ type: "pending_kind", path, at });
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
const parentField = this.parentFieldName(path);
|
|
868
|
+
if (parentField) {
|
|
869
|
+
this.emit({ type: "array_start", path, field: parentField, at });
|
|
870
|
+
}
|
|
871
|
+
this.stack.push({
|
|
872
|
+
kind: "array",
|
|
873
|
+
path,
|
|
874
|
+
value,
|
|
875
|
+
expecting: "valueOrEnd",
|
|
876
|
+
nextIndex: 0
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* THE JSON-ROOT-KEY SURFACE — `content_ir.kind_surface` rows of type
|
|
881
|
+
* `json_root_key`, live at last (they were inert phantom rows until
|
|
882
|
+
* 2026-08-20).
|
|
883
|
+
*
|
|
884
|
+
* A legacy payload such as `{"quiz_title": ...}` carries no `__kind`, but
|
|
885
|
+
* the ONE surface registry knows exactly which kind that root key names —
|
|
886
|
+
* the same lookup the SERVER performs before adapting the payload
|
|
887
|
+
* (`aidream .../processing/blocks/envelope.py`). Consulting it HERE, in the
|
|
888
|
+
* shared parser core, is what makes one place decide it: both hosts — the
|
|
889
|
+
* one-shot `normalizeJsonRegion` (DB reload / reconcile) and the live
|
|
890
|
+
* `openParseSession` (streaming) — build their parser through
|
|
891
|
+
* `createKindStreamParser`, so neither passes an option and neither can
|
|
892
|
+
* drift from the other.
|
|
893
|
+
*
|
|
894
|
+
* Recorded at the first root key; ADOPTED only when the root object closes
|
|
895
|
+
* (`completeTypedObject`). That is the surface registry's complete-only
|
|
896
|
+
* convergence law, and every json_root_key row is `streaming:false` — these
|
|
897
|
+
* legacy shapes are recognised by their whole payload, so speculating
|
|
898
|
+
* mid-stream would flash a kind component over an object that may never
|
|
899
|
+
* satisfy the schema. An explicit `expectedRootKind` (an agent's declared
|
|
900
|
+
* output schema) is stronger context and always wins; an actual `__kind`
|
|
901
|
+
* still wins over both.
|
|
902
|
+
*/
|
|
903
|
+
noteRootSurfaceKind(key) {
|
|
904
|
+
if (key === KIND_KEY) return;
|
|
905
|
+
if (this.options.expectedRootKind) return;
|
|
906
|
+
if (this.rootKind || this.objectKinds.has(this.pathKey([]))) return;
|
|
907
|
+
const kind = this.resolver.kindForJsonRootKey?.(key) ?? jsonRootKeyLookup?.(key);
|
|
908
|
+
if (!kind) return;
|
|
909
|
+
this.rootSurfaceKind = kind;
|
|
910
|
+
if (!this.lookupSchema(kind)) {
|
|
911
|
+
this.resolver.request?.(kind);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Prediction from the parent schema: object field → declared kind; array
|
|
916
|
+
* item → sole itemKind; root → expectedRootKind. Only when the schema is
|
|
917
|
+
* actually resolvable (a prediction we can't validate against is not a
|
|
918
|
+
* commitment worth making).
|
|
919
|
+
*/
|
|
920
|
+
resolveSpeculativeKind(path) {
|
|
921
|
+
if (path.length === 0) {
|
|
922
|
+
const expected = this.options.expectedRootKind;
|
|
923
|
+
return expected && this.lookupObjectSchema(expected) ? expected : null;
|
|
924
|
+
}
|
|
925
|
+
const last = path[path.length - 1];
|
|
926
|
+
if (typeof last === "string") {
|
|
927
|
+
const fieldSchema2 = this.resolveParentFieldSchema(path);
|
|
928
|
+
if (fieldSchema2?.type === "object" && this.lookupObjectSchema(fieldSchema2.kind)) {
|
|
929
|
+
return fieldSchema2.kind;
|
|
930
|
+
}
|
|
931
|
+
return null;
|
|
932
|
+
}
|
|
933
|
+
const fieldName = this.parentFieldName(path);
|
|
934
|
+
if (!fieldName) return null;
|
|
935
|
+
const ownerPath = path.slice(0, -2);
|
|
936
|
+
const ownerKind = this.getObjectKindForPath(ownerPath);
|
|
937
|
+
if (!ownerKind) return null;
|
|
938
|
+
const fieldSchema = this.lookupSchema(ownerKind)?.fields[fieldName];
|
|
939
|
+
const soleItemKind = fieldSchema?.type === "array" && fieldSchema.itemKinds.length === 1 ? fieldSchema.itemKinds[0] : void 0;
|
|
940
|
+
if (soleItemKind !== void 0 && this.lookupObjectSchema(soleItemKind)) {
|
|
941
|
+
return soleItemKind;
|
|
942
|
+
}
|
|
943
|
+
return null;
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* A schema usable for OBJECT speculation/snapshots — root-form kinds
|
|
947
|
+
* (non-object data-only shapes) are never a valid object commitment.
|
|
948
|
+
*/
|
|
949
|
+
lookupObjectSchema(kind) {
|
|
950
|
+
const schema = this.lookupSchema(kind);
|
|
951
|
+
return schema && !schema.root ? schema : void 0;
|
|
952
|
+
}
|
|
953
|
+
beginScalar(value, at) {
|
|
954
|
+
this.placeValue(value, "scalar", at, true);
|
|
955
|
+
}
|
|
956
|
+
placeValue(value, valueKind, at, finalizedImmediately) {
|
|
957
|
+
if (this.root === void 0) {
|
|
958
|
+
if (valueKind !== "object") {
|
|
959
|
+
this.fail("Root value must be a JSON object.", at);
|
|
960
|
+
return null;
|
|
961
|
+
}
|
|
962
|
+
this.root = value;
|
|
963
|
+
return [];
|
|
964
|
+
}
|
|
965
|
+
const parent = this.currentFrame();
|
|
966
|
+
if (!parent) {
|
|
967
|
+
this.fail("Unexpected value after root object.", at);
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
let path;
|
|
971
|
+
let fieldKey;
|
|
972
|
+
if (parent.kind === "object") {
|
|
973
|
+
if (parent.expecting !== "value") {
|
|
974
|
+
this.fail(
|
|
975
|
+
`Unexpected value inside object. Expected ${parent.expecting}.`,
|
|
976
|
+
at
|
|
977
|
+
);
|
|
978
|
+
return null;
|
|
979
|
+
}
|
|
980
|
+
if (parent.currentKey === void 0) {
|
|
981
|
+
this.fail("Internal parser error: missing object key.", at);
|
|
982
|
+
return null;
|
|
983
|
+
}
|
|
984
|
+
fieldKey = parent.currentKey;
|
|
985
|
+
path = [...parent.path, fieldKey];
|
|
986
|
+
const placementError = this.validateFieldPlacement(
|
|
987
|
+
parent,
|
|
988
|
+
fieldKey,
|
|
989
|
+
valueKind,
|
|
990
|
+
value
|
|
991
|
+
);
|
|
992
|
+
if (placementError) {
|
|
993
|
+
this.markNodeRaw(parent.path, parent.value, placementError, at);
|
|
994
|
+
}
|
|
995
|
+
parent.value[fieldKey] = value;
|
|
996
|
+
parent.currentKey = void 0;
|
|
997
|
+
parent.expecting = "commaOrEnd";
|
|
998
|
+
} else {
|
|
999
|
+
if (parent.expecting !== "valueOrEnd" && parent.expecting !== "value") {
|
|
1000
|
+
this.fail(
|
|
1001
|
+
`Unexpected value inside array. Expected ${parent.expecting}.`,
|
|
1002
|
+
at
|
|
1003
|
+
);
|
|
1004
|
+
return null;
|
|
1005
|
+
}
|
|
1006
|
+
const index = parent.nextIndex;
|
|
1007
|
+
path = [...parent.path, index];
|
|
1008
|
+
fieldKey = this.parentFieldName(path);
|
|
1009
|
+
parent.value.push(value);
|
|
1010
|
+
parent.nextIndex += 1;
|
|
1011
|
+
parent.expecting = "commaOrEnd";
|
|
1012
|
+
}
|
|
1013
|
+
if (finalizedImmediately) {
|
|
1014
|
+
this.onValueFinalized(path, value, at);
|
|
1015
|
+
}
|
|
1016
|
+
return path;
|
|
1017
|
+
}
|
|
1018
|
+
acceptObjectKey(frame, key, at) {
|
|
1019
|
+
if (frame.expecting !== "keyOrEnd" && frame.expecting !== "key") {
|
|
1020
|
+
this.fail(`Unexpected object key "${key}".`, at);
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
if (Object.prototype.hasOwnProperty.call(frame.value, key)) {
|
|
1024
|
+
this.markNodeRaw(
|
|
1025
|
+
frame.path,
|
|
1026
|
+
frame.value,
|
|
1027
|
+
`Duplicate key "${key}".`,
|
|
1028
|
+
at
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
if (frame.path.length === 0 && frame.keyCount === 0) {
|
|
1032
|
+
this.noteRootSurfaceKind(key);
|
|
1033
|
+
}
|
|
1034
|
+
frame.currentKey = key;
|
|
1035
|
+
frame.keyCount += 1;
|
|
1036
|
+
frame.expecting = "colon";
|
|
1037
|
+
}
|
|
1038
|
+
acceptColon(at) {
|
|
1039
|
+
const frame = this.currentFrame();
|
|
1040
|
+
if (!frame || frame.kind !== "object" || frame.expecting !== "colon") {
|
|
1041
|
+
this.fail("Unexpected colon.", at);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
frame.expecting = "value";
|
|
1045
|
+
}
|
|
1046
|
+
acceptComma(at) {
|
|
1047
|
+
const frame = this.currentFrame();
|
|
1048
|
+
if (!frame || frame.expecting !== "commaOrEnd") {
|
|
1049
|
+
this.fail("Unexpected comma.", at);
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
if (frame.kind === "object") {
|
|
1053
|
+
frame.expecting = "key";
|
|
1054
|
+
} else {
|
|
1055
|
+
frame.expecting = "value";
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
closeCompound(kind, at) {
|
|
1059
|
+
const frame = this.currentFrame();
|
|
1060
|
+
if (!frame || frame.kind !== kind) {
|
|
1061
|
+
this.fail(
|
|
1062
|
+
`Unexpected closing ${kind === "object" ? "brace" : "bracket"}.`,
|
|
1063
|
+
at
|
|
1064
|
+
);
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
if (frame.kind === "object") {
|
|
1068
|
+
if (frame.expecting !== "keyOrEnd" && frame.expecting !== "commaOrEnd") {
|
|
1069
|
+
this.fail(`Object closed too early. Expected ${frame.expecting}.`, at);
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
} else if (frame.expecting !== "valueOrEnd" && frame.expecting !== "commaOrEnd") {
|
|
1073
|
+
this.fail(`Array closed too early. Expected ${frame.expecting}.`, at);
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
this.stack.pop();
|
|
1077
|
+
if (this.stack.length === 0) {
|
|
1078
|
+
this.rootDone = true;
|
|
1079
|
+
}
|
|
1080
|
+
this.onValueFinalized(frame.path, frame.value, at);
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* True when a value placed at `path` sits directly under a json-any
|
|
1084
|
+
* placement: a `json`/`json[]` FIELD, or a member of a `record` whose
|
|
1085
|
+
* values are `"json"`. (Deeper descendants inherit via `opaquePaths`.)
|
|
1086
|
+
*/
|
|
1087
|
+
isJsonAnyPlacement(path) {
|
|
1088
|
+
const fieldSchema = this.resolveParentFieldSchema(path);
|
|
1089
|
+
if (fieldSchema && isJsonAnyField(fieldSchema)) return true;
|
|
1090
|
+
const last = path[path.length - 1];
|
|
1091
|
+
if (typeof last === "string") {
|
|
1092
|
+
const parentKey = this.pathKey(path.slice(0, -1));
|
|
1093
|
+
if (this.recordSchemas.get(parentKey) === "json") return true;
|
|
1094
|
+
}
|
|
1095
|
+
return false;
|
|
1096
|
+
}
|
|
1097
|
+
onValueFinalized(path, value, at) {
|
|
1098
|
+
if (this.failed) return;
|
|
1099
|
+
const pathKey = this.pathKey(path);
|
|
1100
|
+
const parentIsOpaque = path.length > 0 && this.opaquePaths.has(this.pathKey(path.slice(0, -1)));
|
|
1101
|
+
if (this.opaquePaths.has(pathKey)) {
|
|
1102
|
+
if (!parentIsOpaque) this.emitFieldIfReady(path, value, at);
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
if (parentIsOpaque) return;
|
|
1106
|
+
if (this.isKindFieldPath(path)) {
|
|
1107
|
+
if (typeof value !== "string") {
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
this.onKindDiscriminatorArrived(path.slice(0, -1), value, at);
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
1114
|
+
const objectValue = value;
|
|
1115
|
+
const inlineFields = this.inlineSchemas.get(pathKey);
|
|
1116
|
+
if (inlineFields) {
|
|
1117
|
+
const inlineError = this.validateObjectAgainstFields(
|
|
1118
|
+
objectValue,
|
|
1119
|
+
inlineFields
|
|
1120
|
+
);
|
|
1121
|
+
if (inlineError.error) {
|
|
1122
|
+
this.emitRawObject(path, objectValue, inlineError.error, at);
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
this.inlineSchemas.delete(pathKey);
|
|
1126
|
+
this.emitSchemaNotices(path, "inline_object", inlineError, at);
|
|
1127
|
+
this.emit({
|
|
1128
|
+
type: "object_complete",
|
|
1129
|
+
kind: "inline_object",
|
|
1130
|
+
path,
|
|
1131
|
+
value: objectValue,
|
|
1132
|
+
at
|
|
1133
|
+
});
|
|
1134
|
+
} else {
|
|
1135
|
+
const recordValueType = this.recordSchemas.get(pathKey);
|
|
1136
|
+
if (recordValueType) {
|
|
1137
|
+
const recordError = this.validateRecordObject(
|
|
1138
|
+
objectValue,
|
|
1139
|
+
recordValueType
|
|
1140
|
+
);
|
|
1141
|
+
if (recordError) {
|
|
1142
|
+
this.emitRawObject(path, objectValue, recordError, at);
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
this.recordSchemas.delete(pathKey);
|
|
1146
|
+
this.emit({
|
|
1147
|
+
type: "object_complete",
|
|
1148
|
+
kind: "record",
|
|
1149
|
+
path,
|
|
1150
|
+
value: objectValue,
|
|
1151
|
+
at
|
|
1152
|
+
});
|
|
1153
|
+
} else {
|
|
1154
|
+
this.completeTypedObject(path, objectValue, at);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
this.emitFieldIfReady(path, value, at);
|
|
1159
|
+
if (path.length === 0) {
|
|
1160
|
+
this.completeRoot(value, at);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
/** __kind arrived for an object — confirm speculation, identify, or backtrack. */
|
|
1164
|
+
onKindDiscriminatorArrived(objectPath, kind, at) {
|
|
1165
|
+
const objectPathKey = this.pathKey(objectPath);
|
|
1166
|
+
if (this.rawObjectPaths.has(objectPathKey)) return;
|
|
1167
|
+
const prior = this.objectKinds.get(objectPathKey);
|
|
1168
|
+
const wasSpeculative = this.speculativeKinds.has(objectPathKey);
|
|
1169
|
+
if (prior !== void 0 && wasSpeculative) {
|
|
1170
|
+
this.speculativeKinds.delete(objectPathKey);
|
|
1171
|
+
if (prior === kind) {
|
|
1172
|
+
this.emitBlockSnapshotForObject(objectPath, at);
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
if (this.lookupSchema(kind) && this.validateArrayItemKind(objectPath, kind) === null && this.speculativeRetagAllowed(objectPath, kind)) {
|
|
1176
|
+
this.objectKinds.set(objectPathKey, kind);
|
|
1177
|
+
if (objectPath.length === 0) {
|
|
1178
|
+
this.rootKind = kind;
|
|
1179
|
+
}
|
|
1180
|
+
this.emit({ type: "kind_identified", kind, path: objectPath, at });
|
|
1181
|
+
this.emitBlockSnapshotForObject(objectPath, at);
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
const live = this.getLiveObjectValue(objectPath);
|
|
1185
|
+
this.objectKinds.delete(objectPathKey);
|
|
1186
|
+
if (objectPath.length === 0) {
|
|
1187
|
+
this.rootKind = "";
|
|
1188
|
+
}
|
|
1189
|
+
this.emitRawObject(
|
|
1190
|
+
objectPath,
|
|
1191
|
+
safeCopy(live ?? {}),
|
|
1192
|
+
`Speculated kind "${prior}" contradicted by ${KIND_KEY} "${kind}".`,
|
|
1193
|
+
at
|
|
1194
|
+
);
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
this.objectKinds.set(objectPathKey, kind);
|
|
1198
|
+
this.emit({
|
|
1199
|
+
type: "kind_identified",
|
|
1200
|
+
kind,
|
|
1201
|
+
path: objectPath,
|
|
1202
|
+
at
|
|
1203
|
+
});
|
|
1204
|
+
this.clearKindWait(objectPath, "identified", at, kind);
|
|
1205
|
+
if (objectPath.length === 0) {
|
|
1206
|
+
this.rootKind = kind;
|
|
1207
|
+
}
|
|
1208
|
+
if (!this.lookupSchema(kind) && this.resolver.request) {
|
|
1209
|
+
this.addPendingSchemaPath(kind, objectPath);
|
|
1210
|
+
this.emit({ type: "pending_schema", kind, path: objectPath, at });
|
|
1211
|
+
this.resolver.request(kind);
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
this.flushDeferredFields(objectPath, kind, at);
|
|
1215
|
+
this.emitBlockSnapshotForObject(objectPath, at);
|
|
1216
|
+
}
|
|
1217
|
+
/** A contradicted speculation may re-tag only where the new kind is legal. */
|
|
1218
|
+
speculativeRetagAllowed(path, kind) {
|
|
1219
|
+
if (path.length === 0) {
|
|
1220
|
+
return true;
|
|
1221
|
+
}
|
|
1222
|
+
const last = path[path.length - 1];
|
|
1223
|
+
if (typeof last === "number") {
|
|
1224
|
+
const fieldName = this.parentFieldName(path);
|
|
1225
|
+
const ownerKind = this.getObjectKindForPath(path.slice(0, -2));
|
|
1226
|
+
if (!fieldName || !ownerKind) return false;
|
|
1227
|
+
const fieldSchema = this.lookupSchema(ownerKind)?.fields[fieldName];
|
|
1228
|
+
return fieldSchema?.type === "array" && fieldSchema.itemKinds.includes(kind);
|
|
1229
|
+
}
|
|
1230
|
+
return false;
|
|
1231
|
+
}
|
|
1232
|
+
addPendingSchemaPath(kind, path) {
|
|
1233
|
+
const pathKey = this.pathKey(path);
|
|
1234
|
+
const existing = this.pendingSchemaPaths.get(kind) ?? /* @__PURE__ */ new Map();
|
|
1235
|
+
existing.set(pathKey, path);
|
|
1236
|
+
this.pendingSchemaPaths.set(kind, existing);
|
|
1237
|
+
}
|
|
1238
|
+
completeTypedObject(path, objectValue, at) {
|
|
1239
|
+
const pathKey = this.pathKey(path);
|
|
1240
|
+
if (this.rawObjectPaths.has(pathKey)) return;
|
|
1241
|
+
const declaredKind = readObjectKind(objectValue);
|
|
1242
|
+
const committedKind = this.objectKinds.get(pathKey);
|
|
1243
|
+
if (committedKind && this.pendingSchemaPaths.get(committedKind)?.has(pathKey)) {
|
|
1244
|
+
this.closedPendingPaths.add(pathKey);
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
if (!declaredKind && committedKind && this.speculativeKinds.has(pathKey)) {
|
|
1248
|
+
this.speculativeKinds.delete(pathKey);
|
|
1249
|
+
this.finalizeSpeculatedObject(path, objectValue, committedKind, at);
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
if (path.length === 0 && !declaredKind && !committedKind && this.rootSurfaceKind) {
|
|
1253
|
+
const surfaceKind = this.rootSurfaceKind;
|
|
1254
|
+
this.rootSurfaceKind = null;
|
|
1255
|
+
this.objectKinds.set(pathKey, surfaceKind);
|
|
1256
|
+
this.rootKind = surfaceKind;
|
|
1257
|
+
this.emit({
|
|
1258
|
+
type: "kind_identified",
|
|
1259
|
+
kind: surfaceKind,
|
|
1260
|
+
path,
|
|
1261
|
+
speculative: true,
|
|
1262
|
+
at
|
|
1263
|
+
});
|
|
1264
|
+
this.clearKindWait(path, "identified", at, surfaceKind);
|
|
1265
|
+
this.finalizeSpeculatedObject(path, objectValue, surfaceKind, at);
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
if (!declaredKind) {
|
|
1269
|
+
this.emitRawObject(
|
|
1270
|
+
path,
|
|
1271
|
+
objectValue,
|
|
1272
|
+
`Object is missing "${KIND_KEY}".`,
|
|
1273
|
+
at
|
|
1274
|
+
);
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
this.speculativeKinds.delete(pathKey);
|
|
1278
|
+
this.finalizeTypedObject(path, objectValue, at);
|
|
1279
|
+
}
|
|
1280
|
+
/** Validate + complete an object whose value carries __kind. */
|
|
1281
|
+
finalizeTypedObject(path, objectValue, at) {
|
|
1282
|
+
const pathKey = this.pathKey(path);
|
|
1283
|
+
const kind = readObjectKind(objectValue);
|
|
1284
|
+
if (!kind) {
|
|
1285
|
+
this.emitRawObject(
|
|
1286
|
+
path,
|
|
1287
|
+
objectValue,
|
|
1288
|
+
`Object is missing "${KIND_KEY}".`,
|
|
1289
|
+
at
|
|
1290
|
+
);
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
const schema = this.lookupSchema(kind);
|
|
1294
|
+
if (!schema) {
|
|
1295
|
+
this.emitRawObject(
|
|
1296
|
+
path,
|
|
1297
|
+
objectValue,
|
|
1298
|
+
`No block schema registered for "${kind}".`,
|
|
1299
|
+
at,
|
|
1300
|
+
kind
|
|
1301
|
+
);
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
const arrayItemError = this.validateArrayItemKind(path, kind);
|
|
1305
|
+
if (arrayItemError) {
|
|
1306
|
+
this.emitRawObject(path, objectValue, arrayItemError, at);
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
const outcome = this.validateObjectAgainstSchema(objectValue, schema);
|
|
1310
|
+
if (outcome.error) {
|
|
1311
|
+
this.emitRawObject(path, objectValue, outcome.error, at);
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
this.objectKinds.set(pathKey, kind);
|
|
1315
|
+
this.emitSchemaNotices(path, kind, outcome, at);
|
|
1316
|
+
this.emitBlockSnapshotForObject(path, at, true);
|
|
1317
|
+
this.emit({
|
|
1318
|
+
type: "object_complete",
|
|
1319
|
+
kind,
|
|
1320
|
+
path,
|
|
1321
|
+
value: objectValue,
|
|
1322
|
+
at
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
/** Validate + complete an object typed purely by parent prediction. */
|
|
1326
|
+
finalizeSpeculatedObject(path, objectValue, kind, at) {
|
|
1327
|
+
const schema = this.lookupSchema(kind);
|
|
1328
|
+
if (!schema) {
|
|
1329
|
+
this.emitRawObject(
|
|
1330
|
+
path,
|
|
1331
|
+
objectValue,
|
|
1332
|
+
`No block schema registered for "${kind}".`,
|
|
1333
|
+
at,
|
|
1334
|
+
kind
|
|
1335
|
+
);
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
const outcome = this.validateObjectAgainstSchema(
|
|
1339
|
+
{ ...objectValue, [KIND_KEY]: kind },
|
|
1340
|
+
schema
|
|
1341
|
+
);
|
|
1342
|
+
if (outcome.error) {
|
|
1343
|
+
this.emitRawObject(path, objectValue, outcome.error, at);
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
this.emitSchemaNotices(path, kind, outcome, at);
|
|
1347
|
+
this.emitBlockSnapshotForObject(path, at, true);
|
|
1348
|
+
this.emit({
|
|
1349
|
+
type: "object_complete",
|
|
1350
|
+
kind,
|
|
1351
|
+
path,
|
|
1352
|
+
value: objectValue,
|
|
1353
|
+
at
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
validateArrayItemKind(path, kind) {
|
|
1357
|
+
const last = path[path.length - 1];
|
|
1358
|
+
if (typeof last !== "number") return null;
|
|
1359
|
+
const fieldName = this.parentFieldName(path);
|
|
1360
|
+
if (!fieldName) return null;
|
|
1361
|
+
const ownerObjectPath = path.slice(0, -2);
|
|
1362
|
+
const parentKind = this.getObjectKindForPath(ownerObjectPath);
|
|
1363
|
+
if (!parentKind) return null;
|
|
1364
|
+
const fieldSchema = this.lookupSchema(parentKind)?.fields[fieldName];
|
|
1365
|
+
if (!fieldSchema || fieldSchema.type !== "array") return null;
|
|
1366
|
+
if (!fieldSchema.itemKinds.includes(kind)) {
|
|
1367
|
+
return `Kind "${kind}" is not allowed in "${fieldName}" on "${parentKind}" (expected one of: ${fieldSchema.itemKinds.join(", ")}).`;
|
|
1368
|
+
}
|
|
1369
|
+
return null;
|
|
1370
|
+
}
|
|
1371
|
+
completeRoot(rootObject, at) {
|
|
1372
|
+
const pathKey = this.pathKey([]);
|
|
1373
|
+
if (this.rawObjectPaths.has(pathKey)) {
|
|
1374
|
+
this.emit({
|
|
1375
|
+
type: "complete",
|
|
1376
|
+
kind: readObjectKind(rootObject) ?? "",
|
|
1377
|
+
value: rootObject,
|
|
1378
|
+
at
|
|
1379
|
+
});
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
const committedKind = this.objectKinds.get(pathKey);
|
|
1383
|
+
if (committedKind && this.pendingSchemaPaths.get(committedKind)?.has(pathKey)) {
|
|
1384
|
+
this.emit({
|
|
1385
|
+
type: "complete",
|
|
1386
|
+
kind: committedKind,
|
|
1387
|
+
value: rootObject,
|
|
1388
|
+
at
|
|
1389
|
+
});
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
this.emit({
|
|
1393
|
+
type: "complete",
|
|
1394
|
+
kind: this.objectKinds.get(pathKey) ?? readObjectKind(rootObject) ?? "",
|
|
1395
|
+
value: rootObject,
|
|
1396
|
+
at
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
/** Mark a node raw (node-scoped failure) without killing the stream. */
|
|
1400
|
+
markNodeRaw(path, liveValue, reason, at) {
|
|
1401
|
+
const pathKey = this.pathKey(path);
|
|
1402
|
+
if (this.rawObjectPaths.has(pathKey)) return;
|
|
1403
|
+
this.speculativeKinds.delete(pathKey);
|
|
1404
|
+
this.emitRawObject(path, safeCopy(liveValue), reason, at);
|
|
1405
|
+
}
|
|
1406
|
+
emitRawObject(path, value, reason, at, identifiedKind) {
|
|
1407
|
+
const pathKey = this.pathKey(path);
|
|
1408
|
+
if (this.rawObjectPaths.has(pathKey)) return;
|
|
1409
|
+
this.rawObjectPaths.add(pathKey);
|
|
1410
|
+
this.clearKindWait(path, "raw_fallback", at, void 0, reason);
|
|
1411
|
+
this.emit({
|
|
1412
|
+
type: "raw_object",
|
|
1413
|
+
path,
|
|
1414
|
+
value,
|
|
1415
|
+
reason,
|
|
1416
|
+
...identifiedKind !== void 0 && { kind: identifiedKind },
|
|
1417
|
+
at
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
clearKindWait(path, outcome, at, kind, reason) {
|
|
1421
|
+
const pathKey = this.pathKey(path);
|
|
1422
|
+
if (!this.awaitingKindPaths.has(pathKey)) return;
|
|
1423
|
+
this.awaitingKindPaths.delete(pathKey);
|
|
1424
|
+
this.emit({
|
|
1425
|
+
type: "kind_wait_end",
|
|
1426
|
+
path,
|
|
1427
|
+
outcome,
|
|
1428
|
+
...kind !== void 0 && { kind },
|
|
1429
|
+
...reason !== void 0 && { reason },
|
|
1430
|
+
at
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
emitFieldIfReady(path, value, at) {
|
|
1434
|
+
const fieldKey = this.fieldKeyFromPath(path);
|
|
1435
|
+
if (!fieldKey || fieldKey === KIND_KEY) return;
|
|
1436
|
+
const parentPath = path.slice(0, -1);
|
|
1437
|
+
const objectKind = this.getDirectObjectKind(parentPath);
|
|
1438
|
+
if (!objectKind) {
|
|
1439
|
+
const parentPathKey = this.pathKey(parentPath);
|
|
1440
|
+
const deferred = this.deferredFields.get(parentPathKey) ?? [];
|
|
1441
|
+
deferred.push({ key: fieldKey, value, at });
|
|
1442
|
+
this.deferredFields.set(parentPathKey, deferred);
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
const schemaResolved = this.lookupSchema(objectKind) !== void 0;
|
|
1446
|
+
if (schemaResolved && !this.isAllowedSchemaField(parentPath, fieldKey, objectKind)) {
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
this.emit({
|
|
1450
|
+
type: "field",
|
|
1451
|
+
kind: objectKind,
|
|
1452
|
+
path,
|
|
1453
|
+
key: fieldKey,
|
|
1454
|
+
value,
|
|
1455
|
+
at
|
|
1456
|
+
});
|
|
1457
|
+
this.emitBlockSnapshotForObject(parentPath, at);
|
|
1458
|
+
}
|
|
1459
|
+
flushDeferredFields(objectPath, kind, at) {
|
|
1460
|
+
const pathKey = this.pathKey(objectPath);
|
|
1461
|
+
const deferred = this.deferredFields.get(pathKey);
|
|
1462
|
+
if (!deferred) return;
|
|
1463
|
+
for (const entry of deferred) {
|
|
1464
|
+
if (entry.key === KIND_KEY) continue;
|
|
1465
|
+
if (!this.isAllowedSchemaField(objectPath, entry.key, kind)) continue;
|
|
1466
|
+
this.emit({
|
|
1467
|
+
type: "field",
|
|
1468
|
+
kind,
|
|
1469
|
+
path: [...objectPath, entry.key],
|
|
1470
|
+
key: entry.key,
|
|
1471
|
+
value: entry.value,
|
|
1472
|
+
at: entry.at
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
this.deferredFields.delete(pathKey);
|
|
1476
|
+
this.emitBlockSnapshotForObject(objectPath, at);
|
|
1477
|
+
}
|
|
1478
|
+
getLiveObjectValue(path) {
|
|
1479
|
+
const pathKey = this.pathKey(path);
|
|
1480
|
+
for (let i = this.stack.length - 1; i >= 0; i--) {
|
|
1481
|
+
const frame = this.stack[i];
|
|
1482
|
+
if (!frame) continue;
|
|
1483
|
+
if (frame.kind === "object" && this.pathKey(frame.path) === pathKey) {
|
|
1484
|
+
return frame.value;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
return null;
|
|
1488
|
+
}
|
|
1489
|
+
emitBlockSnapshotForObject(objectPath, at, complete = false) {
|
|
1490
|
+
const pathKey = this.pathKey(objectPath);
|
|
1491
|
+
if (this.rawObjectPaths.has(pathKey)) return;
|
|
1492
|
+
const kind = this.getDirectObjectKind(objectPath);
|
|
1493
|
+
if (!kind) return;
|
|
1494
|
+
const schema = this.lookupObjectSchema(kind);
|
|
1495
|
+
if (!schema) return;
|
|
1496
|
+
const partial = complete ? this.getFinalizedObjectValue(objectPath) : this.getLiveObjectValue(objectPath);
|
|
1497
|
+
if (!partial) return;
|
|
1498
|
+
const { value, residue } = buildCompliantKindSnapshot(schema, partial);
|
|
1499
|
+
this.emit({
|
|
1500
|
+
type: "block_snapshot",
|
|
1501
|
+
kind,
|
|
1502
|
+
path: objectPath,
|
|
1503
|
+
value,
|
|
1504
|
+
residue,
|
|
1505
|
+
complete,
|
|
1506
|
+
at
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* On `complete` snapshots (and post-close schema upgrades) the frame has
|
|
1511
|
+
* already been popped — the finalized value lives in the root tree.
|
|
1512
|
+
*/
|
|
1513
|
+
getFinalizedObjectValue(path) {
|
|
1514
|
+
const live = this.getLiveObjectValue(path);
|
|
1515
|
+
if (live) return live;
|
|
1516
|
+
if (path.length === 0) {
|
|
1517
|
+
return typeof this.root === "object" && this.root !== null && !Array.isArray(this.root) ? this.root : null;
|
|
1518
|
+
}
|
|
1519
|
+
let cursor = this.root;
|
|
1520
|
+
for (const segment of path) {
|
|
1521
|
+
if (cursor === null || typeof cursor !== "object") return null;
|
|
1522
|
+
cursor = cursor[segment];
|
|
1523
|
+
}
|
|
1524
|
+
return typeof cursor === "object" && cursor !== null && !Array.isArray(cursor) ? cursor : null;
|
|
1525
|
+
}
|
|
1526
|
+
validateFieldPlacement(parent, fieldKey, valueKind, value) {
|
|
1527
|
+
const parentPathKey = this.pathKey(parent.path);
|
|
1528
|
+
const inlineFields = this.inlineSchemas.get(parentPathKey);
|
|
1529
|
+
if (inlineFields) {
|
|
1530
|
+
const fieldSchema = inlineFields[fieldKey];
|
|
1531
|
+
if (!fieldSchema) {
|
|
1532
|
+
return null;
|
|
1533
|
+
}
|
|
1534
|
+
return this.validateValueAgainstField(
|
|
1535
|
+
fieldSchema,
|
|
1536
|
+
valueKind,
|
|
1537
|
+
value,
|
|
1538
|
+
fieldKey
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
const recordValueType = this.recordSchemas.get(parentPathKey);
|
|
1542
|
+
if (recordValueType) {
|
|
1543
|
+
if (recordValueType === "json") return null;
|
|
1544
|
+
if (valueKind !== "scalar") {
|
|
1545
|
+
return `Record field "${fieldKey}" must be a scalar.`;
|
|
1546
|
+
}
|
|
1547
|
+
return this.validateRecordScalar(recordValueType, value, fieldKey);
|
|
1548
|
+
}
|
|
1549
|
+
return null;
|
|
1550
|
+
}
|
|
1551
|
+
emitSchemaNotices(path, kind, outcome, at) {
|
|
1552
|
+
for (const field of outcome.optionalMissing) {
|
|
1553
|
+
this.emit({
|
|
1554
|
+
type: "optional_field_missing",
|
|
1555
|
+
kind,
|
|
1556
|
+
path,
|
|
1557
|
+
field,
|
|
1558
|
+
at
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
for (const field of outcome.extraFields) {
|
|
1562
|
+
this.emit({
|
|
1563
|
+
type: "extra_field",
|
|
1564
|
+
kind,
|
|
1565
|
+
path,
|
|
1566
|
+
field,
|
|
1567
|
+
at
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
validateObjectAgainstSchema(objectValue, schema) {
|
|
1572
|
+
const optionalMissing = [];
|
|
1573
|
+
const extraFields = [];
|
|
1574
|
+
const emptyOutcome = { optionalMissing, extraFields };
|
|
1575
|
+
if (schema.root) {
|
|
1576
|
+
return {
|
|
1577
|
+
error: `Kind "${schema.kind}" has a non-object root form and cannot be a "${KIND_KEY}" object.`,
|
|
1578
|
+
...emptyOutcome
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
const kind = readObjectKind(objectValue);
|
|
1582
|
+
if (kind !== schema.kind) {
|
|
1583
|
+
return {
|
|
1584
|
+
error: `Object "${KIND_KEY}" is "${kind ?? "missing"}", expected "${schema.kind}".`,
|
|
1585
|
+
...emptyOutcome
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
for (const [fieldName, fieldSchema] of Object.entries(schema.fields)) {
|
|
1589
|
+
if (fieldSchema.required) {
|
|
1590
|
+
if (!(fieldName in objectValue)) {
|
|
1591
|
+
return {
|
|
1592
|
+
error: `Kind "${schema.kind}" is missing required field "${fieldName}".`,
|
|
1593
|
+
...emptyOutcome
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
continue;
|
|
1597
|
+
}
|
|
1598
|
+
if (!(fieldName in objectValue)) {
|
|
1599
|
+
optionalMissing.push(fieldName);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
for (const [fieldName, fieldValue] of Object.entries(objectValue)) {
|
|
1603
|
+
if (fieldName === KIND_KEY) continue;
|
|
1604
|
+
const fieldSchema = schema.fields[fieldName];
|
|
1605
|
+
if (!fieldSchema) {
|
|
1606
|
+
extraFields.push(fieldName);
|
|
1607
|
+
continue;
|
|
1608
|
+
}
|
|
1609
|
+
const error = this.validateFinalFieldValue(
|
|
1610
|
+
fieldSchema,
|
|
1611
|
+
fieldValue,
|
|
1612
|
+
fieldName,
|
|
1613
|
+
schema.kind
|
|
1614
|
+
);
|
|
1615
|
+
if (error) {
|
|
1616
|
+
return { error, optionalMissing, extraFields };
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
return { error: null, optionalMissing, extraFields };
|
|
1620
|
+
}
|
|
1621
|
+
validateFinalFieldValue(fieldSchema, value, fieldName, objectKind) {
|
|
1622
|
+
if (fieldSchema.type === "json") return null;
|
|
1623
|
+
if (fieldSchema.type === "json[]") {
|
|
1624
|
+
if (value === null) {
|
|
1625
|
+
return fieldSchema.nullable ? null : `Field "${fieldName}" on kind "${objectKind}" cannot be null.`;
|
|
1626
|
+
}
|
|
1627
|
+
if (!Array.isArray(value)) {
|
|
1628
|
+
return `Field "${fieldName}" on kind "${objectKind}" must be an array.`;
|
|
1629
|
+
}
|
|
1630
|
+
return null;
|
|
1631
|
+
}
|
|
1632
|
+
if (isScalarArrayType(fieldSchema.type)) {
|
|
1633
|
+
if (!Array.isArray(value)) {
|
|
1634
|
+
return `Field "${fieldName}" on kind "${objectKind}" must be an array.`;
|
|
1635
|
+
}
|
|
1636
|
+
const itemType = scalarArrayItemType(fieldSchema.type);
|
|
1637
|
+
if (!value.every((item) => typeof item === itemType)) {
|
|
1638
|
+
return `Field "${fieldName}" on kind "${objectKind}" must be an array of ${itemType}s.`;
|
|
1639
|
+
}
|
|
1640
|
+
if (fieldSchema.type === "string[]" && fieldSchema.values !== void 0 && !fieldSchema.open) {
|
|
1641
|
+
const allowed = fieldSchema.values;
|
|
1642
|
+
const bad = value.find(
|
|
1643
|
+
(item) => typeof item === "string" && !allowed.includes(item)
|
|
1644
|
+
);
|
|
1645
|
+
if (bad !== void 0) {
|
|
1646
|
+
return `Field "${fieldName}" on kind "${objectKind}" items must be one of: ${allowed.join(", ")}.`;
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
return null;
|
|
1650
|
+
}
|
|
1651
|
+
if (fieldSchema.type === "array") {
|
|
1652
|
+
if (!Array.isArray(value)) {
|
|
1653
|
+
return `Field "${fieldName}" on kind "${objectKind}" must be an array.`;
|
|
1654
|
+
}
|
|
1655
|
+
return null;
|
|
1656
|
+
}
|
|
1657
|
+
if (fieldSchema.type === "object") {
|
|
1658
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1659
|
+
return `Field "${fieldName}" on kind "${objectKind}" must be an object.`;
|
|
1660
|
+
}
|
|
1661
|
+
const nestedKind = readObjectKind(value);
|
|
1662
|
+
if (nestedKind !== fieldSchema.kind) {
|
|
1663
|
+
return `Field "${fieldName}" on kind "${objectKind}" must be kind "${fieldSchema.kind}".`;
|
|
1664
|
+
}
|
|
1665
|
+
const nestedSchema = this.lookupSchema(fieldSchema.kind);
|
|
1666
|
+
if (!nestedSchema) {
|
|
1667
|
+
return `Unknown nested kind "${fieldSchema.kind}" on field "${fieldName}".`;
|
|
1668
|
+
}
|
|
1669
|
+
return this.validateObjectAgainstSchema(
|
|
1670
|
+
value,
|
|
1671
|
+
nestedSchema
|
|
1672
|
+
).error;
|
|
1673
|
+
}
|
|
1674
|
+
if (fieldSchema.type === "inline_object") {
|
|
1675
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1676
|
+
return `Field "${fieldName}" on kind "${objectKind}" must be an inline object.`;
|
|
1677
|
+
}
|
|
1678
|
+
return this.validateObjectAgainstFields(
|
|
1679
|
+
value,
|
|
1680
|
+
fieldSchema.fields
|
|
1681
|
+
).error;
|
|
1682
|
+
}
|
|
1683
|
+
if (fieldSchema.type === "record") {
|
|
1684
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1685
|
+
return `Field "${fieldName}" on kind "${objectKind}" must be a record object.`;
|
|
1686
|
+
}
|
|
1687
|
+
return this.validateRecordObject(
|
|
1688
|
+
value,
|
|
1689
|
+
fieldSchema.values
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
if (fieldSchema.type === "union") {
|
|
1693
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
1694
|
+
const kinds = fieldSchema.kinds ?? [];
|
|
1695
|
+
if (kinds.length === 0) {
|
|
1696
|
+
return `Field "${fieldName}" on kind "${objectKind}" must be ${fieldSchema.scalars.join(" | ")}.`;
|
|
1697
|
+
}
|
|
1698
|
+
const memberKind = readObjectKind(value);
|
|
1699
|
+
if (!memberKind || !kinds.includes(memberKind)) {
|
|
1700
|
+
return `Field "${fieldName}" on kind "${objectKind}" must be one of kinds: ${kinds.join(", ")}.`;
|
|
1701
|
+
}
|
|
1702
|
+
const memberSchema = this.lookupSchema(memberKind);
|
|
1703
|
+
if (!memberSchema) {
|
|
1704
|
+
return `Unknown union member kind "${memberKind}" on field "${fieldName}".`;
|
|
1705
|
+
}
|
|
1706
|
+
return this.validateObjectAgainstSchema(
|
|
1707
|
+
value,
|
|
1708
|
+
memberSchema
|
|
1709
|
+
).error;
|
|
1710
|
+
}
|
|
1711
|
+
return this.validateScalarField(fieldSchema, value, fieldName);
|
|
1712
|
+
}
|
|
1713
|
+
if (fieldSchema.type === "string" || fieldSchema.type === "number" || fieldSchema.type === "boolean" || fieldSchema.type === "enum") {
|
|
1714
|
+
return this.validateScalarField(fieldSchema, value, fieldName);
|
|
1715
|
+
}
|
|
1716
|
+
return `Unsupported field schema for "${fieldName}".`;
|
|
1717
|
+
}
|
|
1718
|
+
validateValueAgainstField(fieldSchema, valueKind, value, fieldName) {
|
|
1719
|
+
if (fieldSchema.type === "json") {
|
|
1720
|
+
return null;
|
|
1721
|
+
}
|
|
1722
|
+
if (fieldSchema.type === "array" || fieldSchema.type === "json[]") {
|
|
1723
|
+
return valueKind === "array" ? null : `Field "${fieldName}" must be an array.`;
|
|
1724
|
+
}
|
|
1725
|
+
if (fieldSchema.type === "object" || fieldSchema.type === "inline_object") {
|
|
1726
|
+
return valueKind === "object" ? null : `Field "${fieldName}" must be an object.`;
|
|
1727
|
+
}
|
|
1728
|
+
if (fieldSchema.type === "record") {
|
|
1729
|
+
return valueKind === "object" ? null : `Field "${fieldName}" must be a record object.`;
|
|
1730
|
+
}
|
|
1731
|
+
if (isScalarArrayType(fieldSchema.type)) {
|
|
1732
|
+
return valueKind === "array" ? null : `Field "${fieldName}" must be an array.`;
|
|
1733
|
+
}
|
|
1734
|
+
if (fieldSchema.type === "union" && (fieldSchema.kinds?.length ?? 0) > 0) {
|
|
1735
|
+
if (valueKind === "object") return null;
|
|
1736
|
+
}
|
|
1737
|
+
if (valueKind !== "scalar") {
|
|
1738
|
+
return `Field "${fieldName}" must be a scalar.`;
|
|
1739
|
+
}
|
|
1740
|
+
return this.validateScalarField(fieldSchema, value, fieldName);
|
|
1741
|
+
}
|
|
1742
|
+
validateScalarField(fieldSchema, value, fieldName) {
|
|
1743
|
+
if (value === null) {
|
|
1744
|
+
return fieldSchema.nullable ? null : `Field "${fieldName}" cannot be null.`;
|
|
1745
|
+
}
|
|
1746
|
+
if (fieldSchema.type === "enum") {
|
|
1747
|
+
if (typeof value !== "string") {
|
|
1748
|
+
return `Field "${fieldName}" must be a string.`;
|
|
1749
|
+
}
|
|
1750
|
+
if (!fieldSchema.open && !fieldSchema.values.includes(value)) {
|
|
1751
|
+
return `Field "${fieldName}" must be one of: ${fieldSchema.values.join(", ")}.`;
|
|
1752
|
+
}
|
|
1753
|
+
return null;
|
|
1754
|
+
}
|
|
1755
|
+
if (fieldSchema.type === "union") {
|
|
1756
|
+
const valueType = typeof value;
|
|
1757
|
+
if (valueType !== "string" && valueType !== "number" && valueType !== "boolean") {
|
|
1758
|
+
return `Field "${fieldName}" must be ${fieldSchema.scalars.join(" | ")}.`;
|
|
1759
|
+
}
|
|
1760
|
+
if (!fieldSchema.scalars.includes(
|
|
1761
|
+
valueType
|
|
1762
|
+
)) {
|
|
1763
|
+
return `Field "${fieldName}" must be ${fieldSchema.scalars.join(" | ")}.`;
|
|
1764
|
+
}
|
|
1765
|
+
return null;
|
|
1766
|
+
}
|
|
1767
|
+
if (fieldSchema.type === "string" || fieldSchema.type === "number" || fieldSchema.type === "boolean") {
|
|
1768
|
+
if (typeof value !== fieldSchema.type) {
|
|
1769
|
+
return `Field "${fieldName}" must be ${fieldSchema.type}.`;
|
|
1770
|
+
}
|
|
1771
|
+
if (fieldSchema.type === "number" && typeof value === "number") {
|
|
1772
|
+
if (fieldSchema.min !== void 0 && value < fieldSchema.min) {
|
|
1773
|
+
return `Field "${fieldName}" must be >= ${fieldSchema.min}.`;
|
|
1774
|
+
}
|
|
1775
|
+
if (fieldSchema.max !== void 0 && value > fieldSchema.max) {
|
|
1776
|
+
return `Field "${fieldName}" must be <= ${fieldSchema.max}.`;
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
return null;
|
|
1780
|
+
}
|
|
1781
|
+
return `Field "${fieldName}" is not a scalar.`;
|
|
1782
|
+
}
|
|
1783
|
+
validateRecordScalar(valueType, value, fieldName) {
|
|
1784
|
+
if (valueType === "json") return null;
|
|
1785
|
+
if (typeof value !== valueType) {
|
|
1786
|
+
return `Record field "${fieldName}" must be ${valueType}.`;
|
|
1787
|
+
}
|
|
1788
|
+
return null;
|
|
1789
|
+
}
|
|
1790
|
+
validateRecordObject(objectValue, valueType) {
|
|
1791
|
+
if (valueType === "json") return null;
|
|
1792
|
+
for (const [key, entry] of Object.entries(objectValue)) {
|
|
1793
|
+
if (typeof entry !== valueType) {
|
|
1794
|
+
return `Record key "${key}" must be ${valueType}.`;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
return null;
|
|
1798
|
+
}
|
|
1799
|
+
validateObjectAgainstFields(objectValue, fields) {
|
|
1800
|
+
const optionalMissing = [];
|
|
1801
|
+
const extraFields = [];
|
|
1802
|
+
const emptyOutcome = { optionalMissing, extraFields };
|
|
1803
|
+
for (const [fieldName, fieldSchema] of Object.entries(fields)) {
|
|
1804
|
+
if (fieldSchema.required) {
|
|
1805
|
+
if (!(fieldName in objectValue)) {
|
|
1806
|
+
return {
|
|
1807
|
+
error: `Inline object is missing required field "${fieldName}".`,
|
|
1808
|
+
...emptyOutcome
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
continue;
|
|
1812
|
+
}
|
|
1813
|
+
if (!(fieldName in objectValue)) {
|
|
1814
|
+
optionalMissing.push(fieldName);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
for (const [fieldName, fieldValue] of Object.entries(objectValue)) {
|
|
1818
|
+
const fieldSchema = fields[fieldName];
|
|
1819
|
+
if (!fieldSchema) {
|
|
1820
|
+
extraFields.push(fieldName);
|
|
1821
|
+
continue;
|
|
1822
|
+
}
|
|
1823
|
+
const error = this.validateFinalFieldValue(
|
|
1824
|
+
fieldSchema,
|
|
1825
|
+
fieldValue,
|
|
1826
|
+
fieldName,
|
|
1827
|
+
"inline_object"
|
|
1828
|
+
);
|
|
1829
|
+
if (error) {
|
|
1830
|
+
return { error, optionalMissing, extraFields };
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
return { error: null, optionalMissing, extraFields };
|
|
1834
|
+
}
|
|
1835
|
+
registerObjectContext(path) {
|
|
1836
|
+
const fieldSchema = this.resolveParentFieldSchema(path);
|
|
1837
|
+
if (!fieldSchema) return;
|
|
1838
|
+
const pathKey = this.pathKey(path);
|
|
1839
|
+
if (fieldSchema.type === "inline_object") {
|
|
1840
|
+
this.inlineSchemas.set(pathKey, fieldSchema.fields);
|
|
1841
|
+
}
|
|
1842
|
+
if (fieldSchema.type === "record") {
|
|
1843
|
+
this.recordSchemas.set(pathKey, fieldSchema.values);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
resolveParentFieldSchema(path) {
|
|
1847
|
+
const fieldKey = this.fieldKeyFromPath(path);
|
|
1848
|
+
if (!fieldKey) return void 0;
|
|
1849
|
+
const parentPath = path.slice(0, -1);
|
|
1850
|
+
const parentKind = this.getObjectKindForPath(parentPath);
|
|
1851
|
+
if (!parentKind) return void 0;
|
|
1852
|
+
return this.lookupSchema(parentKind)?.fields[fieldKey];
|
|
1853
|
+
}
|
|
1854
|
+
getObjectKindForPath(path) {
|
|
1855
|
+
return this.objectKinds.get(this.pathKey(path)) ?? (path.length === 0 ? this.rootKind || null : null);
|
|
1856
|
+
}
|
|
1857
|
+
getDirectObjectKind(objectPath) {
|
|
1858
|
+
if (objectPath.length === 0) {
|
|
1859
|
+
return this.rootKind || null;
|
|
1860
|
+
}
|
|
1861
|
+
return this.objectKinds.get(this.pathKey(objectPath)) ?? null;
|
|
1862
|
+
}
|
|
1863
|
+
isAllowedSchemaField(objectPath, fieldKey, objectKind) {
|
|
1864
|
+
const pathKey = this.pathKey(objectPath);
|
|
1865
|
+
const inlineFields = this.inlineSchemas.get(pathKey);
|
|
1866
|
+
if (inlineFields) {
|
|
1867
|
+
return fieldKey in inlineFields;
|
|
1868
|
+
}
|
|
1869
|
+
if (this.recordSchemas.has(pathKey)) {
|
|
1870
|
+
return true;
|
|
1871
|
+
}
|
|
1872
|
+
const schema = this.lookupSchema(objectKind);
|
|
1873
|
+
return !!schema?.fields[fieldKey];
|
|
1874
|
+
}
|
|
1875
|
+
parentFieldName(path) {
|
|
1876
|
+
if (path.length === 0) return void 0;
|
|
1877
|
+
const parent = path[path.length - 2];
|
|
1878
|
+
return typeof parent === "string" ? parent : void 0;
|
|
1879
|
+
}
|
|
1880
|
+
fieldKeyFromPath(path) {
|
|
1881
|
+
const last = path[path.length - 1];
|
|
1882
|
+
return typeof last === "string" ? last : null;
|
|
1883
|
+
}
|
|
1884
|
+
isKindFieldPath(path) {
|
|
1885
|
+
return path[path.length - 1] === KIND_KEY;
|
|
1886
|
+
}
|
|
1887
|
+
pathKey(path) {
|
|
1888
|
+
return path.map((segment) => String(segment)).join(".");
|
|
1889
|
+
}
|
|
1890
|
+
lookupSchema(kind) {
|
|
1891
|
+
return this.resolver.get(kind) ?? this.arrivedSchemas.get(kind);
|
|
1892
|
+
}
|
|
1893
|
+
fail(reason, at) {
|
|
1894
|
+
if (this.failed) return;
|
|
1895
|
+
this.failed = true;
|
|
1896
|
+
this.emit({ type: "error", reason, at });
|
|
1897
|
+
}
|
|
1898
|
+
emit(event) {
|
|
1899
|
+
this.options.onEvent(event);
|
|
1900
|
+
}
|
|
1901
|
+
currentFrame() {
|
|
1902
|
+
return this.stack[this.stack.length - 1];
|
|
1903
|
+
}
|
|
1904
|
+
};
|
|
1905
|
+
function createKindStreamParser(options) {
|
|
1906
|
+
return new KindStreamParser(options);
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
// core/fingerprint.ts
|
|
1910
|
+
var SEED_A = 2166136261;
|
|
1911
|
+
var SEED_B = 16777619;
|
|
1912
|
+
function fnv1aStep(hash, input) {
|
|
1913
|
+
let h = hash >>> 0;
|
|
1914
|
+
for (let i = 0; i < input.length; i++) {
|
|
1915
|
+
h ^= input.charCodeAt(i);
|
|
1916
|
+
h = h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)) >>> 0;
|
|
1917
|
+
}
|
|
1918
|
+
return h >>> 0;
|
|
1919
|
+
}
|
|
1920
|
+
function createFingerprinter() {
|
|
1921
|
+
let a = SEED_A;
|
|
1922
|
+
let b = SEED_B;
|
|
1923
|
+
let length = 0;
|
|
1924
|
+
return {
|
|
1925
|
+
push(chunk) {
|
|
1926
|
+
a = fnv1aStep(a, chunk);
|
|
1927
|
+
b = fnv1aStep(b, chunk);
|
|
1928
|
+
length += chunk.length;
|
|
1929
|
+
},
|
|
1930
|
+
current() {
|
|
1931
|
+
return `${length.toString(36)}-${a.toString(36)}${b.toString(36)}`;
|
|
1932
|
+
}
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
1935
|
+
function fingerprintText(source) {
|
|
1936
|
+
const hasher = createFingerprinter();
|
|
1937
|
+
hasher.push(source);
|
|
1938
|
+
return hasher.current();
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
// core/normalize.ts
|
|
1942
|
+
function isCanonicalBlockIR(value) {
|
|
1943
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1944
|
+
const candidate = value;
|
|
1945
|
+
return candidate.v === IR_VERSION && typeof candidate.fingerprint === "string" && typeof candidate.engine === "string" && typeof candidate.root === "object" && candidate.root !== null && candidate.root.role === "structured";
|
|
1946
|
+
}
|
|
1947
|
+
function reuseEnvelopeIfCurrent(source, candidate) {
|
|
1948
|
+
if (!isCanonicalBlockIR(candidate)) return null;
|
|
1949
|
+
return candidate.fingerprint === fingerprintText(source) ? candidate : null;
|
|
1950
|
+
}
|
|
1951
|
+
function envelopeFromCompleteValue(value, kind, options) {
|
|
1952
|
+
return {
|
|
1953
|
+
v: IR_VERSION,
|
|
1954
|
+
engine: "fe-kind-parser",
|
|
1955
|
+
fingerprint: fingerprintText(JSON.stringify(value)),
|
|
1956
|
+
root: {
|
|
1957
|
+
role: "structured",
|
|
1958
|
+
kind,
|
|
1959
|
+
kindState: "resolved",
|
|
1960
|
+
discriminator: options?.discriminator ?? { format: "json", key: KIND_KEY },
|
|
1961
|
+
path: [],
|
|
1962
|
+
status: "complete",
|
|
1963
|
+
value,
|
|
1964
|
+
residue: null
|
|
1965
|
+
}
|
|
1966
|
+
};
|
|
1967
|
+
}
|
|
1968
|
+
function normalizeJsonRegion(source, options) {
|
|
1969
|
+
const reused = reuseEnvelopeIfCurrent(source, options.existing);
|
|
1970
|
+
if (reused) return reused;
|
|
1971
|
+
const tree = new IrTree();
|
|
1972
|
+
const parser = createKindStreamParser({
|
|
1973
|
+
schemas: options.schemas,
|
|
1974
|
+
...options.expectedRootKind !== void 0 && {
|
|
1975
|
+
expectedRootKind: options.expectedRootKind
|
|
1976
|
+
},
|
|
1977
|
+
onEvent(event) {
|
|
1978
|
+
tree.applyEvent(event);
|
|
1979
|
+
}
|
|
1980
|
+
});
|
|
1981
|
+
parser.push(source);
|
|
1982
|
+
parser.end();
|
|
1983
|
+
return tree.buildEnvelope(fingerprintText(source));
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
// core/envelope-cache.ts
|
|
1987
|
+
var IR_ENVELOPE_CACHE_VERSION = 1;
|
|
1988
|
+
function isRecord2(value) {
|
|
1989
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1990
|
+
}
|
|
1991
|
+
function isIrEnvelopeCache(value) {
|
|
1992
|
+
if (!isRecord2(value)) return false;
|
|
1993
|
+
if (value.v !== IR_ENVELOPE_CACHE_VERSION) return false;
|
|
1994
|
+
if (!isRecord2(value.blocks)) return false;
|
|
1995
|
+
const entries = Object.entries(value.blocks);
|
|
1996
|
+
if (entries.length === 0) return false;
|
|
1997
|
+
for (const [fingerprint, envelope] of entries) {
|
|
1998
|
+
if (!isCanonicalBlockIR(envelope)) return false;
|
|
1999
|
+
if (envelope.fingerprint !== fingerprint) return false;
|
|
2000
|
+
if (envelope.root.status !== "complete") return false;
|
|
2001
|
+
}
|
|
2002
|
+
return true;
|
|
2003
|
+
}
|
|
2004
|
+
function envelopeCacheFromEnvelopes(envelopes) {
|
|
2005
|
+
let blocks = null;
|
|
2006
|
+
for (const envelope of envelopes) {
|
|
2007
|
+
if (!isCanonicalBlockIR(envelope)) continue;
|
|
2008
|
+
if (envelope.root.status !== "complete") continue;
|
|
2009
|
+
(blocks ??= {})[envelope.fingerprint] = envelope;
|
|
2010
|
+
}
|
|
2011
|
+
return blocks ? { v: IR_ENVELOPE_CACHE_VERSION, blocks } : null;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
// core/envelope-read.ts
|
|
2015
|
+
function readEnvelope(metadata) {
|
|
2016
|
+
const candidate = metadata?.[IR_ENVELOPE_KEY];
|
|
2017
|
+
return isCanonicalBlockIR(candidate) ? candidate : null;
|
|
2018
|
+
}
|
|
2019
|
+
function classifyInboundEnvelopeMetadata(metadata) {
|
|
2020
|
+
if (!metadata || !(IR_ENVELOPE_KEY in metadata)) {
|
|
2021
|
+
return { outcome: "absent", metadata: metadata ?? void 0 };
|
|
2022
|
+
}
|
|
2023
|
+
const candidate = metadata[IR_ENVELOPE_KEY];
|
|
2024
|
+
if (isCanonicalBlockIR(candidate)) {
|
|
2025
|
+
return { outcome: "valid", metadata, envelope: candidate };
|
|
2026
|
+
}
|
|
2027
|
+
const engine = typeof candidate === "object" && candidate !== null && typeof candidate.engine === "string" ? candidate.engine : "unknown";
|
|
2028
|
+
const { [IR_ENVELOPE_KEY]: _dropped, ...rest } = metadata;
|
|
2029
|
+
return { outcome: "malformed", metadata: rest, engine, raw: candidate };
|
|
2030
|
+
}
|
|
2031
|
+
function sanitizeInboundEnvelopeMetadata(metadata, context, hooks = {}) {
|
|
2032
|
+
const verdict = classifyInboundEnvelopeMetadata(metadata);
|
|
2033
|
+
if (verdict.outcome === "valid") {
|
|
2034
|
+
hooks.seedEnvelope?.(verdict.envelope);
|
|
2035
|
+
return verdict.metadata;
|
|
2036
|
+
}
|
|
2037
|
+
if (verdict.outcome === "malformed") {
|
|
2038
|
+
hooks.reportMalformed?.({
|
|
2039
|
+
blockId: context.blockId,
|
|
2040
|
+
engine: verdict.engine,
|
|
2041
|
+
raw: verdict.raw
|
|
2042
|
+
});
|
|
2043
|
+
return verdict.metadata;
|
|
2044
|
+
}
|
|
2045
|
+
return verdict.metadata;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
// core/envelope-value.ts
|
|
2049
|
+
function isRecord3(value) {
|
|
2050
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2051
|
+
}
|
|
2052
|
+
function reconstructRegionValue(envelope) {
|
|
2053
|
+
const cloned = structuredClone(envelope.root.value);
|
|
2054
|
+
const applyExtras = (target, extras) => {
|
|
2055
|
+
if (!extras) return;
|
|
2056
|
+
for (const [key, value] of Object.entries(extras)) {
|
|
2057
|
+
target[key] = structuredClone(value);
|
|
2058
|
+
}
|
|
2059
|
+
};
|
|
2060
|
+
applyExtras(cloned, envelope.root.residue?.extra);
|
|
2061
|
+
for (const [pathKey, meta] of Object.entries(envelope.nodeIndex ?? {})) {
|
|
2062
|
+
const residue = meta.residue;
|
|
2063
|
+
if (!residue || isEmptyResidue(residue) || !residue.extra) continue;
|
|
2064
|
+
const segments = pathKey.split(".");
|
|
2065
|
+
let cursor = cloned;
|
|
2066
|
+
for (const segment of segments) {
|
|
2067
|
+
if (Array.isArray(cursor)) {
|
|
2068
|
+
cursor = cursor[Number(segment)];
|
|
2069
|
+
} else if (isRecord3(cursor)) {
|
|
2070
|
+
cursor = cursor[segment];
|
|
2071
|
+
} else {
|
|
2072
|
+
cursor = void 0;
|
|
2073
|
+
break;
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
if (isRecord3(cursor)) {
|
|
2077
|
+
applyExtras(cursor, residue.extra);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
return cloned;
|
|
2081
|
+
}
|
|
2082
|
+
function stripKindDeep(value) {
|
|
2083
|
+
if (Array.isArray(value)) {
|
|
2084
|
+
return value.map(stripKindDeep);
|
|
2085
|
+
}
|
|
2086
|
+
if (isRecord3(value)) {
|
|
2087
|
+
const out = {};
|
|
2088
|
+
for (const [key, child] of Object.entries(value)) {
|
|
2089
|
+
if (key === KIND_KEY) continue;
|
|
2090
|
+
out[key] = stripKindDeep(child);
|
|
2091
|
+
}
|
|
2092
|
+
return out;
|
|
2093
|
+
}
|
|
2094
|
+
return value;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
// utils/text-case-converter.ts
|
|
2098
|
+
var DEFAULT_WORD_REPLACEMENTS = {
|
|
2099
|
+
// Acronyms & initialisms
|
|
2100
|
+
"api": "API",
|
|
2101
|
+
"apis": "APIs",
|
|
2102
|
+
"ui": "UI",
|
|
2103
|
+
"ux": "UX",
|
|
2104
|
+
"id": "ID",
|
|
2105
|
+
"ids": "IDs",
|
|
2106
|
+
"qr": "QR",
|
|
2107
|
+
"ssr": "SSR",
|
|
2108
|
+
"csr": "CSR",
|
|
2109
|
+
"ssg": "SSG",
|
|
2110
|
+
"isr": "ISR",
|
|
2111
|
+
"spa": "SPA",
|
|
2112
|
+
"pwa": "PWA",
|
|
2113
|
+
"sdk": "SDK",
|
|
2114
|
+
"sdks": "SDKs",
|
|
2115
|
+
"cli": "CLI",
|
|
2116
|
+
"tty": "TTY",
|
|
2117
|
+
"repl": "REPL",
|
|
2118
|
+
"ci": "CI",
|
|
2119
|
+
"cd": "CD",
|
|
2120
|
+
"cpu": "CPU",
|
|
2121
|
+
"cpus": "CPUs",
|
|
2122
|
+
"gpu": "GPU",
|
|
2123
|
+
"gpus": "GPUs",
|
|
2124
|
+
"ram": "RAM",
|
|
2125
|
+
"rom": "ROM",
|
|
2126
|
+
"ssd": "SSD",
|
|
2127
|
+
"ssds": "SSDs",
|
|
2128
|
+
"hdd": "HDD",
|
|
2129
|
+
"hdds": "HDDs",
|
|
2130
|
+
"kpi": "KPI",
|
|
2131
|
+
"kpis": "KPIs",
|
|
2132
|
+
"sla": "SLA",
|
|
2133
|
+
"slas": "SLAs",
|
|
2134
|
+
"slo": "SLO",
|
|
2135
|
+
"slos": "SLOs",
|
|
2136
|
+
"sli": "SLI",
|
|
2137
|
+
"slis": "SLIs",
|
|
2138
|
+
"dom": "DOM",
|
|
2139
|
+
// Web, formats, protocols
|
|
2140
|
+
"url": "URL",
|
|
2141
|
+
"urls": "URLs",
|
|
2142
|
+
"uri": "URI",
|
|
2143
|
+
"uris": "URIs",
|
|
2144
|
+
"http": "HTTP",
|
|
2145
|
+
"https": "HTTPS",
|
|
2146
|
+
"html": "HTML",
|
|
2147
|
+
"css": "CSS",
|
|
2148
|
+
"json": "JSON",
|
|
2149
|
+
"yaml": "YAML",
|
|
2150
|
+
"yml": "YML",
|
|
2151
|
+
"toml": "TOML",
|
|
2152
|
+
"csv": "CSV",
|
|
2153
|
+
"pdf": "PDF",
|
|
2154
|
+
"tsv": "TSV",
|
|
2155
|
+
"jpg": "JPG",
|
|
2156
|
+
"jpeg": "JPEG",
|
|
2157
|
+
"png": "PNG",
|
|
2158
|
+
"gif": "GIF",
|
|
2159
|
+
"webp": "WebP",
|
|
2160
|
+
"heic": "HEIC",
|
|
2161
|
+
"heif": "HEIF",
|
|
2162
|
+
"bmp": "BMP",
|
|
2163
|
+
"tiff": "TIFF",
|
|
2164
|
+
"ico": "ICO",
|
|
2165
|
+
"xml": "XML",
|
|
2166
|
+
"sql": "SQL",
|
|
2167
|
+
"db": "DB",
|
|
2168
|
+
"dbs": "DBs",
|
|
2169
|
+
"nosql": "NoSQL",
|
|
2170
|
+
"graphql": "GraphQL",
|
|
2171
|
+
"grpc": "gRPC",
|
|
2172
|
+
"rest": "REST",
|
|
2173
|
+
"restful": "RESTful",
|
|
2174
|
+
"websocket": "WebSocket",
|
|
2175
|
+
"websockets": "WebSockets",
|
|
2176
|
+
"webrtc": "WebRTC",
|
|
2177
|
+
// Networking
|
|
2178
|
+
"ip": "IP",
|
|
2179
|
+
"ipv4": "IPv4",
|
|
2180
|
+
"ipv6": "IPv6",
|
|
2181
|
+
"dns": "DNS",
|
|
2182
|
+
"dhcp": "DHCP",
|
|
2183
|
+
"nat": "NAT",
|
|
2184
|
+
"tcp": "TCP",
|
|
2185
|
+
"udp": "UDP",
|
|
2186
|
+
"icmp": "ICMP",
|
|
2187
|
+
"ttl": "TTL",
|
|
2188
|
+
"lan": "LAN",
|
|
2189
|
+
"wan": "WAN",
|
|
2190
|
+
"vlan": "VLAN",
|
|
2191
|
+
"cdn": "CDN",
|
|
2192
|
+
"ftp": "FTP",
|
|
2193
|
+
"ssh": "SSH",
|
|
2194
|
+
"tls": "TLS",
|
|
2195
|
+
"ssl": "SSL",
|
|
2196
|
+
// Security & crypto
|
|
2197
|
+
"jwt": "JWT",
|
|
2198
|
+
"jws": "JWS",
|
|
2199
|
+
"jwe": "JWE",
|
|
2200
|
+
"hmac": "HMAC",
|
|
2201
|
+
"rsa": "RSA",
|
|
2202
|
+
"ecdsa": "ECDSA",
|
|
2203
|
+
"aes": "AES",
|
|
2204
|
+
"pbkdf2": "PBKDF2",
|
|
2205
|
+
"argon2": "Argon2",
|
|
2206
|
+
"scrypt": "scrypt",
|
|
2207
|
+
"totp": "TOTP",
|
|
2208
|
+
"hotp": "HOTP",
|
|
2209
|
+
"mfa": "MFA",
|
|
2210
|
+
"2fa": "2FA",
|
|
2211
|
+
"csrf": "CSRF",
|
|
2212
|
+
"xss": "XSS",
|
|
2213
|
+
"ssrf": "SSRF",
|
|
2214
|
+
"rce": "RCE",
|
|
2215
|
+
"dos": "DoS",
|
|
2216
|
+
"ddos": "DDoS",
|
|
2217
|
+
"mitm": "MITM",
|
|
2218
|
+
"csp": "CSP",
|
|
2219
|
+
"cors": "CORS",
|
|
2220
|
+
"pii": "PII",
|
|
2221
|
+
"phi": "PHI",
|
|
2222
|
+
"gdpr": "GDPR",
|
|
2223
|
+
"ccpa": "CCPA",
|
|
2224
|
+
"hipaa": "HIPAA",
|
|
2225
|
+
"rfc": "RFC",
|
|
2226
|
+
// Platforms, langs, tools (single-token)
|
|
2227
|
+
"javascript": "JavaScript",
|
|
2228
|
+
"typescript": "TypeScript",
|
|
2229
|
+
"jsx": "JSX",
|
|
2230
|
+
"tsx": "TSX",
|
|
2231
|
+
"node": "Node",
|
|
2232
|
+
// (used when tokenized alone)
|
|
2233
|
+
"deno": "Deno",
|
|
2234
|
+
"bun": "Bun",
|
|
2235
|
+
"react": "React",
|
|
2236
|
+
"nextjs": "Next.js",
|
|
2237
|
+
// if your tokenizer drops dots, keep this
|
|
2238
|
+
"nodejs": "Node.js",
|
|
2239
|
+
"postgresql": "PostgreSQL",
|
|
2240
|
+
"postgres": "Postgres",
|
|
2241
|
+
"mysql": "MySQL",
|
|
2242
|
+
"sqlite": "SQLite",
|
|
2243
|
+
"redis": "Redis",
|
|
2244
|
+
"supabase": "Supabase",
|
|
2245
|
+
"docker": "Docker",
|
|
2246
|
+
"kubernetes": "Kubernetes",
|
|
2247
|
+
"k8s": "Kubernetes",
|
|
2248
|
+
"helm": "Helm",
|
|
2249
|
+
"npm": "npm",
|
|
2250
|
+
"pnpm": "pnpm",
|
|
2251
|
+
"yarn": "Yarn",
|
|
2252
|
+
"eslint": "ESLint",
|
|
2253
|
+
"prettier": "Prettier",
|
|
2254
|
+
"vite": "Vite",
|
|
2255
|
+
"webpack": "Webpack",
|
|
2256
|
+
"babel": "Babel",
|
|
2257
|
+
// OS & vendors
|
|
2258
|
+
"macos": "macOS",
|
|
2259
|
+
"ios": "iOS",
|
|
2260
|
+
"ipados": "iPadOS",
|
|
2261
|
+
"watchos": "watchOS",
|
|
2262
|
+
"tvos": "tvOS",
|
|
2263
|
+
"windows": "Windows",
|
|
2264
|
+
"linux": "Linux",
|
|
2265
|
+
"ubuntu": "Ubuntu",
|
|
2266
|
+
"github": "GitHub",
|
|
2267
|
+
"gitlab": "GitLab",
|
|
2268
|
+
"bitbucket": "Bitbucket",
|
|
2269
|
+
// Data & analytics
|
|
2270
|
+
"etl": "ETL",
|
|
2271
|
+
"elt": "ELT",
|
|
2272
|
+
"olap": "OLAP",
|
|
2273
|
+
"oltp": "OLTP",
|
|
2274
|
+
"bi": "BI",
|
|
2275
|
+
// Time & locales
|
|
2276
|
+
"utc": "UTC",
|
|
2277
|
+
"gmt": "GMT",
|
|
2278
|
+
"pst": "PST",
|
|
2279
|
+
"pdt": "PDT",
|
|
2280
|
+
"pt": "PT",
|
|
2281
|
+
// Common “small words” to keep lowercase (unless first/last word)
|
|
2282
|
+
"or": "or",
|
|
2283
|
+
"and": "and",
|
|
2284
|
+
"the": "the",
|
|
2285
|
+
"of": "of",
|
|
2286
|
+
"in": "in",
|
|
2287
|
+
"to": "to",
|
|
2288
|
+
"with": "with",
|
|
2289
|
+
"as": "as",
|
|
2290
|
+
"by": "by",
|
|
2291
|
+
"for": "for",
|
|
2292
|
+
"on": "on",
|
|
2293
|
+
"at": "at",
|
|
2294
|
+
"up": "up",
|
|
2295
|
+
"a": "a",
|
|
2296
|
+
"an": "an",
|
|
2297
|
+
"is": "is",
|
|
2298
|
+
"are": "are",
|
|
2299
|
+
"was": "was",
|
|
2300
|
+
"were": "were",
|
|
2301
|
+
"be": "be",
|
|
2302
|
+
"but": "but",
|
|
2303
|
+
"nor": "nor",
|
|
2304
|
+
"so": "so",
|
|
2305
|
+
"yet": "yet",
|
|
2306
|
+
"per": "per",
|
|
2307
|
+
"via": "via",
|
|
2308
|
+
// Latin abbreviations (tokenized as words in some pipelines)
|
|
2309
|
+
"eg": "e.g.",
|
|
2310
|
+
"ie": "i.e.",
|
|
2311
|
+
"etc": "etc.",
|
|
2312
|
+
"aka": "aka",
|
|
2313
|
+
"vs": "vs.",
|
|
2314
|
+
"v": "v.",
|
|
2315
|
+
// Client abbreviations
|
|
2316
|
+
"CIC": "CIC",
|
|
2317
|
+
"AGR": "AGR",
|
|
2318
|
+
"AGER": "AGER",
|
|
2319
|
+
"DD": "DD",
|
|
2320
|
+
"TS": "TS",
|
|
2321
|
+
"TM": "TM",
|
|
2322
|
+
"arman": "Arman"
|
|
2323
|
+
};
|
|
2324
|
+
var DEFAULT_OPTIONS = {
|
|
2325
|
+
textCase: "title",
|
|
2326
|
+
wordReplacements: DEFAULT_WORD_REPLACEMENTS,
|
|
2327
|
+
trim: true
|
|
2328
|
+
};
|
|
2329
|
+
function formatText(text, options = {}) {
|
|
2330
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
2331
|
+
if (!text) return "";
|
|
2332
|
+
let normalized = text.replace(/_/g, " ").replace(/-/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").replace(/\s+/g, " ");
|
|
2333
|
+
if (opts.trim) {
|
|
2334
|
+
normalized = normalized.trim();
|
|
2335
|
+
}
|
|
2336
|
+
let caseTransformed = normalized;
|
|
2337
|
+
switch (opts.textCase) {
|
|
2338
|
+
case "title":
|
|
2339
|
+
caseTransformed = normalized.replace(
|
|
2340
|
+
/\w\S*/g,
|
|
2341
|
+
(word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
|
|
2342
|
+
);
|
|
2343
|
+
break;
|
|
2344
|
+
case "sentence":
|
|
2345
|
+
if (normalized.length > 0) {
|
|
2346
|
+
caseTransformed = normalized.charAt(0).toUpperCase() + normalized.slice(1).toLowerCase();
|
|
2347
|
+
}
|
|
2348
|
+
break;
|
|
2349
|
+
case "lower":
|
|
2350
|
+
caseTransformed = normalized.toLowerCase();
|
|
2351
|
+
break;
|
|
2352
|
+
case "upper":
|
|
2353
|
+
caseTransformed = normalized.toUpperCase();
|
|
2354
|
+
break;
|
|
2355
|
+
}
|
|
2356
|
+
let result = caseTransformed;
|
|
2357
|
+
if (opts.wordReplacements) {
|
|
2358
|
+
Object.entries(opts.wordReplacements).forEach(([key, value]) => {
|
|
2359
|
+
const regex = new RegExp(`\\b${key}\\b`, "gi");
|
|
2360
|
+
result = result.replace(regex, value);
|
|
2361
|
+
});
|
|
2362
|
+
}
|
|
2363
|
+
return result;
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
// core/schema-structure.ts
|
|
2367
|
+
function formatBlockLabel(key) {
|
|
2368
|
+
return formatText(key, { textCase: "title" });
|
|
2369
|
+
}
|
|
2370
|
+
function schemaStructureDepth(schema, allSchemas, visiting = /* @__PURE__ */ new Set()) {
|
|
2371
|
+
if (visiting.has(schema.kind)) return 0;
|
|
2372
|
+
visiting.add(schema.kind);
|
|
2373
|
+
let max = 0;
|
|
2374
|
+
for (const field of Object.values(schema.fields)) {
|
|
2375
|
+
max = Math.max(max, fieldStructureDepth(field, allSchemas, visiting));
|
|
2376
|
+
}
|
|
2377
|
+
visiting.delete(schema.kind);
|
|
2378
|
+
return max;
|
|
2379
|
+
}
|
|
2380
|
+
function fieldStructureDepth(field, allSchemas, visiting) {
|
|
2381
|
+
switch (field.type) {
|
|
2382
|
+
case "array": {
|
|
2383
|
+
let itemMax = 0;
|
|
2384
|
+
for (const itemKind of field.itemKinds) {
|
|
2385
|
+
const itemSchema = allSchemas[itemKind];
|
|
2386
|
+
itemMax = Math.max(
|
|
2387
|
+
itemMax,
|
|
2388
|
+
itemSchema ? 1 + schemaStructureDepth(itemSchema, allSchemas, visiting) : 1
|
|
2389
|
+
);
|
|
2390
|
+
}
|
|
2391
|
+
return itemMax;
|
|
2392
|
+
}
|
|
2393
|
+
case "object": {
|
|
2394
|
+
const ref = allSchemas[field.kind];
|
|
2395
|
+
return ref ? 1 + schemaStructureDepth(ref, allSchemas, visiting) : 1;
|
|
2396
|
+
}
|
|
2397
|
+
case "inline_object": {
|
|
2398
|
+
let nested = 0;
|
|
2399
|
+
for (const child of Object.values(field.fields)) {
|
|
2400
|
+
nested = Math.max(
|
|
2401
|
+
nested,
|
|
2402
|
+
fieldStructureDepth(child, allSchemas, visiting)
|
|
2403
|
+
);
|
|
2404
|
+
}
|
|
2405
|
+
return nested > 0 ? 1 + nested : 1;
|
|
2406
|
+
}
|
|
2407
|
+
default:
|
|
2408
|
+
return 0;
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
function schemaLayoutMode(schema, allSchemas) {
|
|
2412
|
+
const depth = schemaStructureDepth(schema, allSchemas);
|
|
2413
|
+
if (depth <= 0) return "flat";
|
|
2414
|
+
if (depth === 1) return "grid";
|
|
2415
|
+
return "nested";
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
// session/parse-session.ts
|
|
2419
|
+
var ParseSession = class {
|
|
2420
|
+
identity;
|
|
2421
|
+
parser;
|
|
2422
|
+
tree = new IrTree();
|
|
2423
|
+
listeners = /* @__PURE__ */ new Map();
|
|
2424
|
+
anyListeners = /* @__PURE__ */ new Set();
|
|
2425
|
+
unsubscribeSchemaArrivals;
|
|
2426
|
+
fingerprinter = createFingerprinter();
|
|
2427
|
+
ended = false;
|
|
2428
|
+
constructor(options) {
|
|
2429
|
+
this.identity = options.identity;
|
|
2430
|
+
this.parser = createKindStreamParser({
|
|
2431
|
+
schemas: options.schemas,
|
|
2432
|
+
...options.expectedRootKind !== void 0 && {
|
|
2433
|
+
expectedRootKind: options.expectedRootKind
|
|
2434
|
+
},
|
|
2435
|
+
onEvent: (event) => {
|
|
2436
|
+
this.tree.applyEvent(event);
|
|
2437
|
+
options.onEvent?.(event);
|
|
2438
|
+
}
|
|
2439
|
+
});
|
|
2440
|
+
this.unsubscribeSchemaArrivals = options.onSchemaArrived?.((kind, schema) => {
|
|
2441
|
+
this.parser.notifySchemaArrived(kind, schema);
|
|
2442
|
+
this.flushNotify();
|
|
2443
|
+
}) ?? null;
|
|
2444
|
+
}
|
|
2445
|
+
/** The single writer's input. Does NOT notify — host flushes on its cadence. */
|
|
2446
|
+
write(chunk) {
|
|
2447
|
+
this.fingerprinter.push(chunk);
|
|
2448
|
+
this.parser.push(chunk);
|
|
2449
|
+
}
|
|
2450
|
+
end() {
|
|
2451
|
+
if (this.ended) return;
|
|
2452
|
+
this.ended = true;
|
|
2453
|
+
this.parser.end();
|
|
2454
|
+
}
|
|
2455
|
+
get isEnded() {
|
|
2456
|
+
return this.ended;
|
|
2457
|
+
}
|
|
2458
|
+
get status() {
|
|
2459
|
+
return this.tree.status;
|
|
2460
|
+
}
|
|
2461
|
+
getNode(pathKey) {
|
|
2462
|
+
return this.tree.getNode(pathKey);
|
|
2463
|
+
}
|
|
2464
|
+
listNodes() {
|
|
2465
|
+
return this.tree.listNodes();
|
|
2466
|
+
}
|
|
2467
|
+
isRawPath(pathKey) {
|
|
2468
|
+
return this.tree.isRawPath(pathKey);
|
|
2469
|
+
}
|
|
2470
|
+
subscribe(pathKey, listener) {
|
|
2471
|
+
const set = this.listeners.get(pathKey) ?? /* @__PURE__ */ new Set();
|
|
2472
|
+
set.add(listener);
|
|
2473
|
+
this.listeners.set(pathKey, set);
|
|
2474
|
+
return () => {
|
|
2475
|
+
set.delete(listener);
|
|
2476
|
+
if (set.size === 0) this.listeners.delete(pathKey);
|
|
2477
|
+
};
|
|
2478
|
+
}
|
|
2479
|
+
/** Structural subscription: fires when ANY node changes (mount lists). */
|
|
2480
|
+
subscribeAny(listener) {
|
|
2481
|
+
this.anyListeners.add(listener);
|
|
2482
|
+
return () => {
|
|
2483
|
+
this.anyListeners.delete(listener);
|
|
2484
|
+
};
|
|
2485
|
+
}
|
|
2486
|
+
/** Publish dirty paths to their readers. Host-cadence, coalesced. */
|
|
2487
|
+
flushNotify() {
|
|
2488
|
+
if (!this.tree.hasDirty()) return;
|
|
2489
|
+
const dirty = this.tree.drainDirty();
|
|
2490
|
+
for (const pathKey of dirty) {
|
|
2491
|
+
const set = this.listeners.get(pathKey);
|
|
2492
|
+
if (!set) continue;
|
|
2493
|
+
for (const listener of set) listener();
|
|
2494
|
+
}
|
|
2495
|
+
if (this.anyListeners.size > 0) {
|
|
2496
|
+
for (const listener of this.anyListeners) listener();
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
buildEnvelope() {
|
|
2500
|
+
return this.tree.buildEnvelope(this.fingerprinter.current());
|
|
2501
|
+
}
|
|
2502
|
+
dispose() {
|
|
2503
|
+
this.unsubscribeSchemaArrivals?.();
|
|
2504
|
+
this.listeners.clear();
|
|
2505
|
+
this.anyListeners.clear();
|
|
2506
|
+
}
|
|
2507
|
+
};
|
|
2508
|
+
|
|
2509
|
+
// session/session-manager.ts
|
|
2510
|
+
var sessions = /* @__PURE__ */ new Map();
|
|
2511
|
+
function openParseSession(options) {
|
|
2512
|
+
const existing = sessions.get(options.identity);
|
|
2513
|
+
if (existing && !existing.isEnded) {
|
|
2514
|
+
throw new Error(
|
|
2515
|
+
`content-ir: a writer is already open for identity "${options.identity}". One writer per stream identity \u2014 everyone else reads.`
|
|
2516
|
+
);
|
|
2517
|
+
}
|
|
2518
|
+
existing?.dispose();
|
|
2519
|
+
const session = new ParseSession(options);
|
|
2520
|
+
sessions.set(options.identity, session);
|
|
2521
|
+
return session;
|
|
2522
|
+
}
|
|
2523
|
+
function getParseSession(identity) {
|
|
2524
|
+
return sessions.get(identity) ?? null;
|
|
2525
|
+
}
|
|
2526
|
+
function disposeParseSession(identity) {
|
|
2527
|
+
const session = sessions.get(identity);
|
|
2528
|
+
if (!session) return;
|
|
2529
|
+
session.dispose();
|
|
2530
|
+
sessions.delete(identity);
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
// registry/kind-storage-transform.ts
|
|
2534
|
+
var ROOT_STORAGE_NAME = "__root";
|
|
2535
|
+
var KindStorageError = class extends Error {
|
|
2536
|
+
constructor(message) {
|
|
2537
|
+
super(message);
|
|
2538
|
+
this.name = "KindStorageError";
|
|
2539
|
+
}
|
|
2540
|
+
};
|
|
2541
|
+
var PATH_SEP = ".";
|
|
2542
|
+
function base(field) {
|
|
2543
|
+
const out = {
|
|
2544
|
+
name: ""
|
|
2545
|
+
};
|
|
2546
|
+
if (field.required) out.required = true;
|
|
2547
|
+
if (field.nullable) out.nullable = true;
|
|
2548
|
+
if (field.description !== void 0) out.description = field.description;
|
|
2549
|
+
if (field.default !== void 0) out.default = field.default;
|
|
2550
|
+
return out;
|
|
2551
|
+
}
|
|
2552
|
+
function storeField(name, field, path, edges) {
|
|
2553
|
+
const b = { ...base(field), name };
|
|
2554
|
+
switch (field.type) {
|
|
2555
|
+
case "string":
|
|
2556
|
+
case "boolean":
|
|
2557
|
+
case "number[]":
|
|
2558
|
+
case "boolean[]":
|
|
2559
|
+
case "json":
|
|
2560
|
+
case "json[]":
|
|
2561
|
+
return { ...b, type: field.type };
|
|
2562
|
+
case "number":
|
|
2563
|
+
return {
|
|
2564
|
+
...b,
|
|
2565
|
+
type: "number",
|
|
2566
|
+
...field.min !== void 0 ? { min: field.min } : {},
|
|
2567
|
+
...field.max !== void 0 ? { max: field.max } : {},
|
|
2568
|
+
...field.step !== void 0 ? { step: field.step } : {}
|
|
2569
|
+
};
|
|
2570
|
+
case "string[]":
|
|
2571
|
+
return {
|
|
2572
|
+
...b,
|
|
2573
|
+
type: "string[]",
|
|
2574
|
+
...field.values !== void 0 ? { values: [...field.values] } : {},
|
|
2575
|
+
...field.open ? { open: true } : {}
|
|
2576
|
+
};
|
|
2577
|
+
case "record":
|
|
2578
|
+
return { ...b, type: "record", values: field.values };
|
|
2579
|
+
case "enum":
|
|
2580
|
+
return {
|
|
2581
|
+
...b,
|
|
2582
|
+
type: "enum",
|
|
2583
|
+
values: [...field.values],
|
|
2584
|
+
...field.open ? { open: true } : {}
|
|
2585
|
+
};
|
|
2586
|
+
case "union": {
|
|
2587
|
+
if (field.kinds && field.kinds.length > 0) {
|
|
2588
|
+
field.kinds.forEach((childKind, position) => {
|
|
2589
|
+
edges.push({ fieldPath: path, childKind, position });
|
|
2590
|
+
});
|
|
2591
|
+
return { ...b, type: "union", scalars: [...field.scalars], hasKinds: true };
|
|
2592
|
+
}
|
|
2593
|
+
return { ...b, type: "union", scalars: [...field.scalars] };
|
|
2594
|
+
}
|
|
2595
|
+
case "object":
|
|
2596
|
+
edges.push({ fieldPath: path, childKind: field.kind, position: null });
|
|
2597
|
+
return { ...b, type: "object" };
|
|
2598
|
+
case "array":
|
|
2599
|
+
field.itemKinds.forEach((childKind, position) => {
|
|
2600
|
+
edges.push({ fieldPath: path, childKind, position });
|
|
2601
|
+
});
|
|
2602
|
+
return { ...b, type: "array" };
|
|
2603
|
+
case "inline_object": {
|
|
2604
|
+
const fields = [];
|
|
2605
|
+
for (const [childName, childField] of Object.entries(field.fields)) {
|
|
2606
|
+
fields.push(
|
|
2607
|
+
storeField(
|
|
2608
|
+
childName,
|
|
2609
|
+
childField,
|
|
2610
|
+
`${path}${PATH_SEP}${childName}`,
|
|
2611
|
+
edges
|
|
2612
|
+
)
|
|
2613
|
+
);
|
|
2614
|
+
}
|
|
2615
|
+
return field.open ? { ...b, type: "inline_object", fields, open: true } : { ...b, type: "inline_object", fields };
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
function kindSchemaToStorage(schema) {
|
|
2620
|
+
const data = [];
|
|
2621
|
+
const edges = [];
|
|
2622
|
+
if (schema.root) {
|
|
2623
|
+
if (Object.keys(schema.fields).length > 0) {
|
|
2624
|
+
throw new KindStorageError(
|
|
2625
|
+
`kind "${schema.kind}": root form and a non-empty fields map are mutually exclusive.`
|
|
2626
|
+
);
|
|
2627
|
+
}
|
|
2628
|
+
data.push(storeField(ROOT_STORAGE_NAME, schema.root, ROOT_STORAGE_NAME, edges));
|
|
2629
|
+
return { data, edges };
|
|
2630
|
+
}
|
|
2631
|
+
for (const [name, field] of Object.entries(schema.fields)) {
|
|
2632
|
+
if (name === ROOT_STORAGE_NAME) {
|
|
2633
|
+
throw new KindStorageError(
|
|
2634
|
+
`kind "${schema.kind}": field name "${ROOT_STORAGE_NAME}" is reserved for the non-object root form.`
|
|
2635
|
+
);
|
|
2636
|
+
}
|
|
2637
|
+
data.push(storeField(name, field, name, edges));
|
|
2638
|
+
}
|
|
2639
|
+
return { data, edges };
|
|
2640
|
+
}
|
|
2641
|
+
function indexEdges(edges) {
|
|
2642
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
2643
|
+
for (const edge of edges) {
|
|
2644
|
+
const list = byPath.get(edge.fieldPath);
|
|
2645
|
+
if (list) list.push(edge);
|
|
2646
|
+
else byPath.set(edge.fieldPath, [edge]);
|
|
2647
|
+
}
|
|
2648
|
+
return byPath;
|
|
2649
|
+
}
|
|
2650
|
+
function restoreField(element, path, byPath) {
|
|
2651
|
+
const b = {};
|
|
2652
|
+
if (element.required) b.required = true;
|
|
2653
|
+
if (element.nullable) b.nullable = true;
|
|
2654
|
+
if (element.description !== void 0) b.description = element.description;
|
|
2655
|
+
if (element.default !== void 0) b.default = element.default;
|
|
2656
|
+
switch (element.type) {
|
|
2657
|
+
case "string":
|
|
2658
|
+
case "boolean":
|
|
2659
|
+
case "number[]":
|
|
2660
|
+
case "boolean[]":
|
|
2661
|
+
case "json":
|
|
2662
|
+
case "json[]":
|
|
2663
|
+
return { ...b, type: element.type };
|
|
2664
|
+
case "number":
|
|
2665
|
+
return {
|
|
2666
|
+
...b,
|
|
2667
|
+
type: "number",
|
|
2668
|
+
...element.min !== void 0 ? { min: element.min } : {},
|
|
2669
|
+
...element.max !== void 0 ? { max: element.max } : {},
|
|
2670
|
+
...element.step !== void 0 ? { step: element.step } : {}
|
|
2671
|
+
};
|
|
2672
|
+
case "string[]":
|
|
2673
|
+
return {
|
|
2674
|
+
...b,
|
|
2675
|
+
type: "string[]",
|
|
2676
|
+
...element.values !== void 0 ? { values: [...element.values] } : {},
|
|
2677
|
+
...element.open ? { open: true } : {}
|
|
2678
|
+
};
|
|
2679
|
+
case "record":
|
|
2680
|
+
return { ...b, type: "record", values: element.values };
|
|
2681
|
+
case "enum":
|
|
2682
|
+
return {
|
|
2683
|
+
...b,
|
|
2684
|
+
type: "enum",
|
|
2685
|
+
values: [...element.values],
|
|
2686
|
+
...element.open ? { open: true } : {}
|
|
2687
|
+
};
|
|
2688
|
+
case "union": {
|
|
2689
|
+
const list = byPath.get(path) ?? [];
|
|
2690
|
+
if (element.hasKinds) {
|
|
2691
|
+
if (list.length === 0) {
|
|
2692
|
+
throw new KindStorageError(
|
|
2693
|
+
`union field "${path}" declares kind members (hasKinds) but has no edges.`
|
|
2694
|
+
);
|
|
2695
|
+
}
|
|
2696
|
+
const kinds = [...list].sort((a, z) => (a.position ?? 0) - (z.position ?? 0)).map((e) => e.childKind);
|
|
2697
|
+
return { ...b, type: "union", scalars: [...element.scalars], kinds };
|
|
2698
|
+
}
|
|
2699
|
+
if (list.length > 0) {
|
|
2700
|
+
throw new KindStorageError(
|
|
2701
|
+
`union field "${path}" has ${list.length} edge(s) but does not declare hasKinds.`
|
|
2702
|
+
);
|
|
2703
|
+
}
|
|
2704
|
+
return { ...b, type: "union", scalars: [...element.scalars] };
|
|
2705
|
+
}
|
|
2706
|
+
case "object": {
|
|
2707
|
+
const list = byPath.get(path) ?? [];
|
|
2708
|
+
if (list.length !== 1) {
|
|
2709
|
+
throw new KindStorageError(
|
|
2710
|
+
`object field "${path}" must have exactly one edge, found ${list.length}.`
|
|
2711
|
+
);
|
|
2712
|
+
}
|
|
2713
|
+
const [edge] = list;
|
|
2714
|
+
if (!edge) {
|
|
2715
|
+
throw new KindStorageError(
|
|
2716
|
+
`object field "${path}" must have exactly one edge, found 0.`
|
|
2717
|
+
);
|
|
2718
|
+
}
|
|
2719
|
+
return { ...b, type: "object", kind: edge.childKind };
|
|
2720
|
+
}
|
|
2721
|
+
case "array": {
|
|
2722
|
+
const list = byPath.get(path) ?? [];
|
|
2723
|
+
if (list.length === 0) {
|
|
2724
|
+
throw new KindStorageError(
|
|
2725
|
+
`array field "${path}" must have at least one edge.`
|
|
2726
|
+
);
|
|
2727
|
+
}
|
|
2728
|
+
const itemKinds = [...list].sort((a, z) => (a.position ?? 0) - (z.position ?? 0)).map((e) => e.childKind);
|
|
2729
|
+
return { ...b, type: "array", itemKinds };
|
|
2730
|
+
}
|
|
2731
|
+
case "inline_object": {
|
|
2732
|
+
const fields = {};
|
|
2733
|
+
for (const child of element.fields) {
|
|
2734
|
+
fields[child.name] = restoreField(
|
|
2735
|
+
child,
|
|
2736
|
+
`${path}${PATH_SEP}${child.name}`,
|
|
2737
|
+
byPath
|
|
2738
|
+
);
|
|
2739
|
+
}
|
|
2740
|
+
return element.open ? { ...b, type: "inline_object", fields, open: true } : { ...b, type: "inline_object", fields };
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
function storageToKindSchema(kind, shape) {
|
|
2745
|
+
const byPath = indexEdges(shape.edges);
|
|
2746
|
+
const [first] = shape.data;
|
|
2747
|
+
if (first && first.name === ROOT_STORAGE_NAME) {
|
|
2748
|
+
if (shape.data.length !== 1) {
|
|
2749
|
+
throw new KindStorageError(
|
|
2750
|
+
`kind "${kind}": a "${ROOT_STORAGE_NAME}" element must be the only data element (found ${shape.data.length}).`
|
|
2751
|
+
);
|
|
2752
|
+
}
|
|
2753
|
+
return {
|
|
2754
|
+
kind,
|
|
2755
|
+
fields: {},
|
|
2756
|
+
root: restoreField(first, ROOT_STORAGE_NAME, byPath)
|
|
2757
|
+
};
|
|
2758
|
+
}
|
|
2759
|
+
const fields = {};
|
|
2760
|
+
for (const element of shape.data) {
|
|
2761
|
+
if (element.name === ROOT_STORAGE_NAME) {
|
|
2762
|
+
throw new KindStorageError(
|
|
2763
|
+
`kind "${kind}": "${ROOT_STORAGE_NAME}" must be the first and only data element.`
|
|
2764
|
+
);
|
|
2765
|
+
}
|
|
2766
|
+
fields[element.name] = restoreField(element, element.name, byPath);
|
|
2767
|
+
}
|
|
2768
|
+
return { kind, fields };
|
|
2769
|
+
}
|
|
2770
|
+
var ajv = new Ajv({ allErrors: true, strict: false });
|
|
2771
|
+
function stripKind(value) {
|
|
2772
|
+
if (Array.isArray(value)) return value.map(stripKind);
|
|
2773
|
+
if (value && typeof value === "object") {
|
|
2774
|
+
const out = {};
|
|
2775
|
+
for (const [k, v] of Object.entries(value)) {
|
|
2776
|
+
if (k === KIND_KEY) continue;
|
|
2777
|
+
out[k] = stripKind(v);
|
|
2778
|
+
}
|
|
2779
|
+
return out;
|
|
2780
|
+
}
|
|
2781
|
+
return value;
|
|
2782
|
+
}
|
|
2783
|
+
function validateStructuralLeg(sample, emittedJsonSchema) {
|
|
2784
|
+
let validate;
|
|
2785
|
+
try {
|
|
2786
|
+
validate = ajv.compile(emittedJsonSchema);
|
|
2787
|
+
} catch (err) {
|
|
2788
|
+
return {
|
|
2789
|
+
ok: false,
|
|
2790
|
+
detail: `emitted_json_schema failed to compile: ${err instanceof Error ? err.message : String(err)}`
|
|
2791
|
+
};
|
|
2792
|
+
}
|
|
2793
|
+
const ok = validate(stripKind(sample));
|
|
2794
|
+
if (ok) return { ok: true };
|
|
2795
|
+
const errors = (validate.errors ?? []).map((e) => `${e.instancePath || "(root)"} ${e.message ?? ""}`.trim()).slice(0, 8);
|
|
2796
|
+
return { ok: false, detail: `sample failed schema: ${errors.join("; ")}` };
|
|
2797
|
+
}
|
|
2798
|
+
var SERVER_DATA_ANNOTATION_KEYS = /* @__PURE__ */ new Set(["language"]);
|
|
2799
|
+
var SUBSTANCE_DEPTH_LIMIT = 8;
|
|
2800
|
+
function isSubstantiveValue(value, depth = 0) {
|
|
2801
|
+
if (value === null || value === void 0) return false;
|
|
2802
|
+
if (typeof value === "string") return value.trim().length > 0;
|
|
2803
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
2804
|
+
if (typeof value === "boolean") return true;
|
|
2805
|
+
if (depth >= SUBSTANCE_DEPTH_LIMIT) return true;
|
|
2806
|
+
if (Array.isArray(value)) {
|
|
2807
|
+
return value.some((entry) => isSubstantiveValue(entry, depth + 1));
|
|
2808
|
+
}
|
|
2809
|
+
if (typeof value === "object") {
|
|
2810
|
+
return Object.values(value).some(
|
|
2811
|
+
(entry) => isSubstantiveValue(entry, depth + 1)
|
|
2812
|
+
);
|
|
2813
|
+
}
|
|
2814
|
+
return false;
|
|
2815
|
+
}
|
|
2816
|
+
function describeUnrenderableBridgeOutput(serverData) {
|
|
2817
|
+
if (serverData === void 0 || serverData === null) {
|
|
2818
|
+
return "bridge returned no serverData (undefined)";
|
|
2819
|
+
}
|
|
2820
|
+
if (typeof serverData !== "object") {
|
|
2821
|
+
return `bridge returned a non-object serverData (${typeof serverData})`;
|
|
2822
|
+
}
|
|
2823
|
+
if (Array.isArray(serverData)) {
|
|
2824
|
+
return "bridge returned an array, not a serverData record";
|
|
2825
|
+
}
|
|
2826
|
+
const record = serverData;
|
|
2827
|
+
const keys = Object.keys(record);
|
|
2828
|
+
if (keys.length === 0) return "bridge returned an empty object ({})";
|
|
2829
|
+
const contentKeys = keys.filter((key) => !SERVER_DATA_ANNOTATION_KEYS.has(key));
|
|
2830
|
+
if (contentKeys.length === 0) {
|
|
2831
|
+
return `bridge returned only annotation keys [${keys.join(", ")}] \u2014 that is the raw code-region annotation, not kind data`;
|
|
2832
|
+
}
|
|
2833
|
+
if (!contentKeys.some((key) => isSubstantiveValue(record[key]))) {
|
|
2834
|
+
return `bridge returned serverData whose every content value is empty (keys: ${contentKeys.join(", ")}) \u2014 structurally present, semantically empty`;
|
|
2835
|
+
}
|
|
2836
|
+
return null;
|
|
2837
|
+
}
|
|
2838
|
+
function validateRender(kind, sample, definition, resolvedComponent, dataOnly) {
|
|
2839
|
+
if (dataOnly) {
|
|
2840
|
+
return {
|
|
2841
|
+
ok: true,
|
|
2842
|
+
detail: "data-only contract kind \u2014 render leg is structurally inapplicable (n/a)"
|
|
2843
|
+
};
|
|
2844
|
+
}
|
|
2845
|
+
if (!definition && !resolvedComponent?.isActive) {
|
|
2846
|
+
return {
|
|
2847
|
+
ok: false,
|
|
2848
|
+
detail: `kind "${kind}" has no component (not in the compiled registry, and no active role='output' kind_component row) \u2014 nothing to render`
|
|
2849
|
+
};
|
|
2850
|
+
}
|
|
2851
|
+
if (definition && !definition.legacyBlockType && !definition.component && !definition.toLegacyServerData && !resolvedComponent?.isActive) {
|
|
2852
|
+
return {
|
|
2853
|
+
ok: false,
|
|
2854
|
+
detail: `kind "${kind}" has no component (no compiled legacyBlockType/component facet, and no active role='output' kind_component row) \u2014 nothing to render`
|
|
2855
|
+
};
|
|
2856
|
+
}
|
|
2857
|
+
if (definition?.toLegacyServerData) {
|
|
2858
|
+
let serverData;
|
|
2859
|
+
try {
|
|
2860
|
+
serverData = definition.toLegacyServerData(
|
|
2861
|
+
envelopeFromCompleteValue(sample, kind)
|
|
2862
|
+
);
|
|
2863
|
+
} catch (err) {
|
|
2864
|
+
return {
|
|
2865
|
+
ok: false,
|
|
2866
|
+
detail: `toLegacyServerData threw: ${err instanceof Error ? err.message : String(err)}`
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
const problem = describeUnrenderableBridgeOutput(serverData);
|
|
2870
|
+
if (problem) {
|
|
2871
|
+
return {
|
|
2872
|
+
ok: false,
|
|
2873
|
+
detail: `${problem} (the "No ${kind} available" failure)`
|
|
2874
|
+
};
|
|
2875
|
+
}
|
|
2876
|
+
return { ok: true };
|
|
2877
|
+
}
|
|
2878
|
+
const satisfier = definition?.legacyBlockType ? `compiled component "${definition.legacyBlockType}"` : definition?.component ? "compiled component facet" : resolvedComponent ? `resolved ${resolvedComponent.source} component "${resolvedComponent.componentKey}"` : "component";
|
|
2879
|
+
return {
|
|
2880
|
+
ok: true,
|
|
2881
|
+
detail: `bridgeless kind \u2014 ${satisfier} parses content itself; full DOM render check deferred to an RTL harness`
|
|
2882
|
+
};
|
|
2883
|
+
}
|
|
2884
|
+
function runKindDualGate(input) {
|
|
2885
|
+
const structural = validateStructuralLeg(input.sample, input.emittedJsonSchema);
|
|
2886
|
+
const render = validateRender(
|
|
2887
|
+
input.kind,
|
|
2888
|
+
input.sample,
|
|
2889
|
+
input.definition,
|
|
2890
|
+
input.resolvedComponent,
|
|
2891
|
+
input.dataOnly
|
|
2892
|
+
);
|
|
2893
|
+
return { isActive: structural.ok && render.ok, structural, render };
|
|
2894
|
+
}
|
|
2895
|
+
function describeDualGateFailure(kind, result) {
|
|
2896
|
+
if (result.isActive) return "";
|
|
2897
|
+
const parts = [];
|
|
2898
|
+
if (!result.structural.ok) {
|
|
2899
|
+
parts.push(`structural(Pydantic): ${result.structural.detail ?? "failed"}`);
|
|
2900
|
+
}
|
|
2901
|
+
if (!result.render.ok) {
|
|
2902
|
+
parts.push(`render(UI): ${result.render.detail ?? "failed"}`);
|
|
2903
|
+
}
|
|
2904
|
+
return `kind "${kind}" failed the dual gate \u2014 ${parts.join(" | ")}`;
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
// convert/openai-schema-converter.ts
|
|
2908
|
+
function requiredNullableFlags(required, nullable) {
|
|
2909
|
+
return {
|
|
2910
|
+
...required ? { required: true } : {},
|
|
2911
|
+
...nullable ? { nullable: true } : {}
|
|
2912
|
+
};
|
|
2913
|
+
}
|
|
2914
|
+
var METADATA_KEYS = /* @__PURE__ */ new Set([
|
|
2915
|
+
"description",
|
|
2916
|
+
"title",
|
|
2917
|
+
"default",
|
|
2918
|
+
"examples",
|
|
2919
|
+
"format",
|
|
2920
|
+
"minimum",
|
|
2921
|
+
"maximum",
|
|
2922
|
+
"multipleOf",
|
|
2923
|
+
"exclusiveMinimum",
|
|
2924
|
+
"exclusiveMaximum",
|
|
2925
|
+
"minLength",
|
|
2926
|
+
"maxLength",
|
|
2927
|
+
"minItems",
|
|
2928
|
+
"maxItems",
|
|
2929
|
+
"pattern",
|
|
2930
|
+
"const",
|
|
2931
|
+
"$schema",
|
|
2932
|
+
"$id",
|
|
2933
|
+
"deprecated",
|
|
2934
|
+
"readOnly",
|
|
2935
|
+
"writeOnly"
|
|
2936
|
+
]);
|
|
2937
|
+
function isRecord4(value) {
|
|
2938
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2939
|
+
}
|
|
2940
|
+
function deepClone(value) {
|
|
2941
|
+
return JSON.parse(JSON.stringify(value));
|
|
2942
|
+
}
|
|
2943
|
+
function fieldSchemaSummary(field) {
|
|
2944
|
+
if (field.type === "enum") {
|
|
2945
|
+
return `enum(${field.values.join("|")}${field.open ? "|+any" : ""})${field.required ? "*" : ""}`;
|
|
2946
|
+
}
|
|
2947
|
+
if (field.type === "array") {
|
|
2948
|
+
return `array<${field.itemKinds.join("|")}>${field.required ? "*" : ""}`;
|
|
2949
|
+
}
|
|
2950
|
+
if (field.type === "inline_object") {
|
|
2951
|
+
return `inline{${Object.keys(field.fields).join(",")}}${field.required ? "*" : ""}`;
|
|
2952
|
+
}
|
|
2953
|
+
if (field.type === "object") {
|
|
2954
|
+
return `object:${field.kind}${field.required ? "*" : ""}`;
|
|
2955
|
+
}
|
|
2956
|
+
if (field.type === "union") {
|
|
2957
|
+
const members = [...field.scalars, ...field.kinds ?? []];
|
|
2958
|
+
return `union(${members.join("|")})${field.required ? "*" : ""}`;
|
|
2959
|
+
}
|
|
2960
|
+
if (field.type === "record") {
|
|
2961
|
+
return `record(${field.values})${field.required ? "*" : ""}`;
|
|
2962
|
+
}
|
|
2963
|
+
return `${field.type}${field.required ? "*" : ""}${field.nullable ? "?" : ""}`;
|
|
2964
|
+
}
|
|
2965
|
+
function resolvePrimaryType(node) {
|
|
2966
|
+
const raw = node.type;
|
|
2967
|
+
if (typeof raw === "string") {
|
|
2968
|
+
return { type: raw === "integer" ? "number" : raw, nullable: false };
|
|
2969
|
+
}
|
|
2970
|
+
if (Array.isArray(raw)) {
|
|
2971
|
+
const types = raw.filter((t) => typeof t === "string");
|
|
2972
|
+
const nullable = types.includes("null");
|
|
2973
|
+
const primary = types.find((t) => t !== "null") ?? (nullable && types.length === 1 ? "null" : null);
|
|
2974
|
+
if (primary === "integer") {
|
|
2975
|
+
return { type: "number", nullable };
|
|
2976
|
+
}
|
|
2977
|
+
return { type: primary ?? null, nullable };
|
|
2978
|
+
}
|
|
2979
|
+
if (node.enum) return { type: "string", nullable: false };
|
|
2980
|
+
if (node.properties) return { type: "object", nullable: false };
|
|
2981
|
+
if (node.items) return { type: "array", nullable: false };
|
|
2982
|
+
return { type: null, nullable: false };
|
|
2983
|
+
}
|
|
2984
|
+
function carriedMetadataKeys(field) {
|
|
2985
|
+
if (field === null) return /* @__PURE__ */ new Set();
|
|
2986
|
+
const carried = /* @__PURE__ */ new Set(["description", "default"]);
|
|
2987
|
+
if (field.type === "number") {
|
|
2988
|
+
carried.add("minimum");
|
|
2989
|
+
carried.add("maximum");
|
|
2990
|
+
carried.add("multipleOf");
|
|
2991
|
+
}
|
|
2992
|
+
return carried;
|
|
2993
|
+
}
|
|
2994
|
+
function collectDropped(node, path, carriedField = null) {
|
|
2995
|
+
const carried = carriedMetadataKeys(carriedField);
|
|
2996
|
+
const dropped = {};
|
|
2997
|
+
for (const [key, value] of Object.entries(node)) {
|
|
2998
|
+
if (METADATA_KEYS.has(key) && !carried.has(key)) {
|
|
2999
|
+
dropped[key] = value;
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
if (Object.keys(dropped).length > 0) {
|
|
3003
|
+
return [{ path, dropped }];
|
|
3004
|
+
}
|
|
3005
|
+
return [];
|
|
3006
|
+
}
|
|
3007
|
+
function attachNodeMetadata(node, field) {
|
|
3008
|
+
let out = field;
|
|
3009
|
+
if (typeof node.description === "string" && out.description === void 0) {
|
|
3010
|
+
out = { ...out, description: node.description };
|
|
3011
|
+
}
|
|
3012
|
+
if (node.default !== void 0 && out.default === void 0) {
|
|
3013
|
+
out = { ...out, default: node.default };
|
|
3014
|
+
}
|
|
3015
|
+
if (out.type === "number") {
|
|
3016
|
+
const bounds = {};
|
|
3017
|
+
if (typeof node.minimum === "number" && out.min === void 0) {
|
|
3018
|
+
bounds.min = node.minimum;
|
|
3019
|
+
}
|
|
3020
|
+
if (typeof node.maximum === "number" && out.max === void 0) {
|
|
3021
|
+
bounds.max = node.maximum;
|
|
3022
|
+
}
|
|
3023
|
+
if (typeof node.multipleOf === "number" && out.step === void 0) {
|
|
3024
|
+
bounds.step = node.multipleOf;
|
|
3025
|
+
}
|
|
3026
|
+
if (Object.keys(bounds).length > 0) out = { ...out, ...bounds };
|
|
3027
|
+
}
|
|
3028
|
+
return out;
|
|
3029
|
+
}
|
|
3030
|
+
function synthesizeItemKindSlug(schemaName, fieldName) {
|
|
3031
|
+
const base2 = fieldName.endsWith("ies") ? `${fieldName.slice(0, -3)}y` : fieldName.endsWith("s") ? fieldName.slice(0, -1) : fieldName;
|
|
3032
|
+
return `${schemaName}_${base2}`;
|
|
3033
|
+
}
|
|
3034
|
+
function makeKindJsonSchemaProperty(kindSlug, strict) {
|
|
3035
|
+
const base2 = {
|
|
3036
|
+
type: "string",
|
|
3037
|
+
description: "Block discriminator for render pipeline."
|
|
3038
|
+
};
|
|
3039
|
+
if (strict) {
|
|
3040
|
+
return { ...base2, const: kindSlug };
|
|
3041
|
+
}
|
|
3042
|
+
return { ...base2, enum: [kindSlug] };
|
|
3043
|
+
}
|
|
3044
|
+
function injectKindIntoObjectSchema(objectSchema, kindSlug, strict) {
|
|
3045
|
+
const clone = deepClone(objectSchema);
|
|
3046
|
+
const properties = isRecord4(clone.properties) ? { ...clone.properties } : {};
|
|
3047
|
+
properties[KIND_KEY] = makeKindJsonSchemaProperty(kindSlug, strict);
|
|
3048
|
+
const required = Array.isArray(clone.required) ? clone.required.filter((k) => typeof k === "string") : [];
|
|
3049
|
+
const nextRequired = required.includes(KIND_KEY) ? required : [KIND_KEY, ...required];
|
|
3050
|
+
return {
|
|
3051
|
+
...clone,
|
|
3052
|
+
properties,
|
|
3053
|
+
required: nextRequired,
|
|
3054
|
+
...strict ? { additionalProperties: false } : {}
|
|
3055
|
+
};
|
|
3056
|
+
}
|
|
3057
|
+
function injectKindsIntoRootSchema(rootSchema, rootKindSlug, arrayBindings, strict) {
|
|
3058
|
+
let updated = injectKindIntoObjectSchema(rootSchema, rootKindSlug, strict);
|
|
3059
|
+
if (!isRecord4(updated.properties)) {
|
|
3060
|
+
return updated;
|
|
3061
|
+
}
|
|
3062
|
+
const properties = { ...updated.properties };
|
|
3063
|
+
for (const binding of arrayBindings) {
|
|
3064
|
+
const fieldNode = properties[binding.arrayField];
|
|
3065
|
+
if (!isRecord4(fieldNode)) continue;
|
|
3066
|
+
const items = fieldNode.items;
|
|
3067
|
+
const itemNode = Array.isArray(items) ? items[0] : items;
|
|
3068
|
+
if (!isRecord4(itemNode)) continue;
|
|
3069
|
+
const updatedItem = injectKindIntoObjectSchema(
|
|
3070
|
+
itemNode,
|
|
3071
|
+
binding.itemKindSlug,
|
|
3072
|
+
strict
|
|
3073
|
+
);
|
|
3074
|
+
properties[binding.arrayField] = {
|
|
3075
|
+
...fieldNode,
|
|
3076
|
+
items: updatedItem
|
|
3077
|
+
};
|
|
3078
|
+
}
|
|
3079
|
+
return { ...updated, properties };
|
|
3080
|
+
}
|
|
3081
|
+
function buildAgentSchemaWithRenderBlockSupport(input, rootKindSlug, arrayBindings, strict) {
|
|
3082
|
+
if (!isRecord4(input)) return null;
|
|
3083
|
+
const normalized = normalizeAiSchemaInput(input);
|
|
3084
|
+
if (!normalized.rootSchema) return null;
|
|
3085
|
+
if (normalized.rootSchema.type !== "object" && !isRecord4(normalized.rootSchema.properties)) {
|
|
3086
|
+
return deepClone(input);
|
|
3087
|
+
}
|
|
3088
|
+
const updatedRoot = injectKindsIntoRootSchema(
|
|
3089
|
+
normalized.rootSchema,
|
|
3090
|
+
rootKindSlug,
|
|
3091
|
+
arrayBindings,
|
|
3092
|
+
strict
|
|
3093
|
+
);
|
|
3094
|
+
if (isRecord4(input.schema) && typeof input.name === "string") {
|
|
3095
|
+
return {
|
|
3096
|
+
...deepClone(input),
|
|
3097
|
+
schema: updatedRoot
|
|
3098
|
+
};
|
|
3099
|
+
}
|
|
3100
|
+
if (isRecord4(input.json_schema)) {
|
|
3101
|
+
const inner = buildAgentSchemaWithRenderBlockSupport(
|
|
3102
|
+
input.json_schema,
|
|
3103
|
+
rootKindSlug,
|
|
3104
|
+
arrayBindings,
|
|
3105
|
+
strict
|
|
3106
|
+
);
|
|
3107
|
+
return inner ? { ...deepClone(input), json_schema: inner } : null;
|
|
3108
|
+
}
|
|
3109
|
+
if (typeof input.name === "string") {
|
|
3110
|
+
return {
|
|
3111
|
+
...deepClone(input),
|
|
3112
|
+
schema: updatedRoot
|
|
3113
|
+
};
|
|
3114
|
+
}
|
|
3115
|
+
return updatedRoot;
|
|
3116
|
+
}
|
|
3117
|
+
function isAnyValueSchema(node) {
|
|
3118
|
+
return node.type === void 0 && node.enum === void 0 && node.const === void 0 && node.properties === void 0 && node.items === void 0 && node.additionalProperties === void 0 && node.anyOf === void 0 && node.oneOf === void 0 && node.allOf === void 0 && node.$ref === void 0;
|
|
3119
|
+
}
|
|
3120
|
+
function registerDeclaredKindDraft(declaredKind, objectNode, path, ctx) {
|
|
3121
|
+
if (ctx.blockSchemas.some((draft) => draft.slug === declaredKind)) return;
|
|
3122
|
+
if (!isRecord4(objectNode.properties)) return;
|
|
3123
|
+
const memberRequired = new Set(
|
|
3124
|
+
Array.isArray(objectNode.required) ? objectNode.required.filter((k) => typeof k === "string") : []
|
|
3125
|
+
);
|
|
3126
|
+
const fields = {};
|
|
3127
|
+
for (const [propName, propNode] of Object.entries(objectNode.properties)) {
|
|
3128
|
+
if (!isRecord4(propNode)) continue;
|
|
3129
|
+
if (propName === KIND_KEY) continue;
|
|
3130
|
+
const converted = convertProperty(
|
|
3131
|
+
propName,
|
|
3132
|
+
propNode,
|
|
3133
|
+
memberRequired.has(propName),
|
|
3134
|
+
`${path}.${propName}`,
|
|
3135
|
+
ctx
|
|
3136
|
+
);
|
|
3137
|
+
if (converted) fields[propName] = converted;
|
|
3138
|
+
}
|
|
3139
|
+
ctx.blockSchemas.push({
|
|
3140
|
+
slug: declaredKind,
|
|
3141
|
+
label: formatBlockLabel(declaredKind),
|
|
3142
|
+
fields
|
|
3143
|
+
});
|
|
3144
|
+
}
|
|
3145
|
+
function convertProperty(fieldName, node, required, path, ctx) {
|
|
3146
|
+
const core = convertPropertyCore(fieldName, node, required, path, ctx);
|
|
3147
|
+
const field = core === null ? null : attachNodeMetadata(node, core);
|
|
3148
|
+
ctx.droppedMetadata.push(...collectDropped(node, path, field));
|
|
3149
|
+
return field;
|
|
3150
|
+
}
|
|
3151
|
+
function convertPropertyCore(fieldName, node, required, path, ctx) {
|
|
3152
|
+
if (typeof node.$ref === "string") {
|
|
3153
|
+
ctx.problems.push({
|
|
3154
|
+
severity: "error",
|
|
3155
|
+
path,
|
|
3156
|
+
message: `$ref is not supported ("${node.$ref}"). Inline the schema manually.`
|
|
3157
|
+
});
|
|
3158
|
+
return null;
|
|
3159
|
+
}
|
|
3160
|
+
if (Array.isArray(node.anyOf) || Array.isArray(node.oneOf)) {
|
|
3161
|
+
const variants = node.anyOf ?? node.oneOf;
|
|
3162
|
+
const scalarTypes = /* @__PURE__ */ new Set();
|
|
3163
|
+
const enumVariantSets = [];
|
|
3164
|
+
const memberKinds = [];
|
|
3165
|
+
const anonymousObjects = [];
|
|
3166
|
+
let sawNull = false;
|
|
3167
|
+
let unsupported = false;
|
|
3168
|
+
for (const variant of variants) {
|
|
3169
|
+
if (!isRecord4(variant)) {
|
|
3170
|
+
unsupported = true;
|
|
3171
|
+
continue;
|
|
3172
|
+
}
|
|
3173
|
+
if (typeof variant.$ref === "string") {
|
|
3174
|
+
ctx.problems.push({
|
|
3175
|
+
severity: "error",
|
|
3176
|
+
path,
|
|
3177
|
+
message: `$ref inside anyOf/oneOf is not supported ("${variant.$ref}"). Inline the schema manually.`
|
|
3178
|
+
});
|
|
3179
|
+
unsupported = true;
|
|
3180
|
+
continue;
|
|
3181
|
+
}
|
|
3182
|
+
const { type: type2, nullable: nullable2 } = resolvePrimaryType(variant);
|
|
3183
|
+
if (nullable2) sawNull = true;
|
|
3184
|
+
if (type2 === "null") {
|
|
3185
|
+
sawNull = true;
|
|
3186
|
+
continue;
|
|
3187
|
+
}
|
|
3188
|
+
if (type2 === "string" && Array.isArray(variant.enum)) {
|
|
3189
|
+
enumVariantSets.push(
|
|
3190
|
+
variant.enum.filter((v) => typeof v === "string")
|
|
3191
|
+
);
|
|
3192
|
+
continue;
|
|
3193
|
+
}
|
|
3194
|
+
if (type2 === "string" || type2 === "number" || type2 === "boolean") {
|
|
3195
|
+
scalarTypes.add(type2);
|
|
3196
|
+
continue;
|
|
3197
|
+
}
|
|
3198
|
+
if (type2 === "object" && isRecord4(variant.properties)) {
|
|
3199
|
+
const declared = readBlockKindFromProperties(variant.properties);
|
|
3200
|
+
if (declared) {
|
|
3201
|
+
memberKinds.push(declared);
|
|
3202
|
+
registerDeclaredKindDraft(declared, variant, `${path}<${declared}>`, ctx);
|
|
3203
|
+
} else {
|
|
3204
|
+
anonymousObjects.push(variant);
|
|
3205
|
+
}
|
|
3206
|
+
continue;
|
|
3207
|
+
}
|
|
3208
|
+
unsupported = true;
|
|
3209
|
+
}
|
|
3210
|
+
if (unsupported) {
|
|
3211
|
+
ctx.problems.push({
|
|
3212
|
+
severity: "error",
|
|
3213
|
+
path,
|
|
3214
|
+
message: "anyOf/oneOf with unsupported variants cannot be converted automatically."
|
|
3215
|
+
});
|
|
3216
|
+
return null;
|
|
3217
|
+
}
|
|
3218
|
+
if (enumVariantSets.length > 0 && anonymousObjects.length === 0 && memberKinds.length === 0 && [...scalarTypes].every((s) => s === "string")) {
|
|
3219
|
+
const values = [...new Set(enumVariantSets.flat())];
|
|
3220
|
+
const open = scalarTypes.has("string");
|
|
3221
|
+
return {
|
|
3222
|
+
...requiredNullableFlags(required, sawNull),
|
|
3223
|
+
type: "enum",
|
|
3224
|
+
values,
|
|
3225
|
+
...open ? { open: true } : {}
|
|
3226
|
+
};
|
|
3227
|
+
}
|
|
3228
|
+
if (enumVariantSets.length > 0) {
|
|
3229
|
+
ctx.problems.push({
|
|
3230
|
+
severity: "warning",
|
|
3231
|
+
path,
|
|
3232
|
+
message: "anyOf/oneOf mixes an enum variant with non-string members \u2014 enum widened to string within the union."
|
|
3233
|
+
});
|
|
3234
|
+
scalarTypes.add("string");
|
|
3235
|
+
}
|
|
3236
|
+
if (anonymousObjects.length === 1 && scalarTypes.size === 0 && memberKinds.length === 0) {
|
|
3237
|
+
const [sole] = anonymousObjects;
|
|
3238
|
+
const converted = sole ? convertProperty(fieldName, sole, required, path, ctx) : null;
|
|
3239
|
+
if (!converted) return null;
|
|
3240
|
+
return sawNull ? { ...converted, nullable: true } : converted;
|
|
3241
|
+
}
|
|
3242
|
+
if (anonymousObjects.length > 0) {
|
|
3243
|
+
ctx.problems.push({
|
|
3244
|
+
severity: "error",
|
|
3245
|
+
path,
|
|
3246
|
+
message: "anyOf/oneOf mixing anonymous objects with other variants cannot be converted \u2014 declare __kind on each object variant."
|
|
3247
|
+
});
|
|
3248
|
+
return null;
|
|
3249
|
+
}
|
|
3250
|
+
if (scalarTypes.size === 1 && memberKinds.length === 0) {
|
|
3251
|
+
const [scalar] = [...scalarTypes];
|
|
3252
|
+
if (scalar) {
|
|
3253
|
+
return { ...requiredNullableFlags(required, sawNull), type: scalar };
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
if (scalarTypes.size > 0 || memberKinds.length > 0) {
|
|
3257
|
+
ctx.problems.push({
|
|
3258
|
+
severity: "warning",
|
|
3259
|
+
path,
|
|
3260
|
+
message: `Converted anyOf/oneOf to union of: ${[
|
|
3261
|
+
...scalarTypes,
|
|
3262
|
+
...memberKinds
|
|
3263
|
+
].join(", ")}.`
|
|
3264
|
+
});
|
|
3265
|
+
return {
|
|
3266
|
+
...requiredNullableFlags(required, sawNull),
|
|
3267
|
+
type: "union",
|
|
3268
|
+
scalars: [...scalarTypes],
|
|
3269
|
+
...memberKinds.length > 0 ? { kinds: memberKinds } : {}
|
|
3270
|
+
};
|
|
3271
|
+
}
|
|
3272
|
+
ctx.problems.push({
|
|
3273
|
+
severity: "warning",
|
|
3274
|
+
path,
|
|
3275
|
+
message: "anyOf/oneOf carried only null variants \u2014 widened to json (any value)."
|
|
3276
|
+
});
|
|
3277
|
+
return { ...requiredNullableFlags(required), type: "json" };
|
|
3278
|
+
}
|
|
3279
|
+
if (Array.isArray(node.allOf)) {
|
|
3280
|
+
ctx.problems.push({
|
|
3281
|
+
severity: "warning",
|
|
3282
|
+
path,
|
|
3283
|
+
message: "allOf merged using first object branch only."
|
|
3284
|
+
});
|
|
3285
|
+
const objectBranch = node.allOf.find(
|
|
3286
|
+
(b) => b.properties
|
|
3287
|
+
);
|
|
3288
|
+
if (objectBranch) {
|
|
3289
|
+
return convertProperty(fieldName, objectBranch, required, path, ctx);
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
if (isAnyValueSchema(node)) {
|
|
3293
|
+
ctx.problems.push({
|
|
3294
|
+
severity: "info",
|
|
3295
|
+
path,
|
|
3296
|
+
message: `Typeless schema at "${path || "(root)"}" \u2014 any JSON value (json).`
|
|
3297
|
+
});
|
|
3298
|
+
return { ...requiredNullableFlags(required), type: "json" };
|
|
3299
|
+
}
|
|
3300
|
+
const { type, nullable } = resolvePrimaryType(node);
|
|
3301
|
+
if (type === "string") {
|
|
3302
|
+
if (Array.isArray(node.enum)) {
|
|
3303
|
+
const values = node.enum.filter(
|
|
3304
|
+
(v) => typeof v === "string"
|
|
3305
|
+
);
|
|
3306
|
+
if (values.length !== node.enum.length) {
|
|
3307
|
+
ctx.problems.push({
|
|
3308
|
+
severity: "warning",
|
|
3309
|
+
path,
|
|
3310
|
+
message: "Enum contains non-string values; non-strings dropped."
|
|
3311
|
+
});
|
|
3312
|
+
}
|
|
3313
|
+
return {
|
|
3314
|
+
...requiredNullableFlags(required, nullable),
|
|
3315
|
+
type: "enum",
|
|
3316
|
+
values
|
|
3317
|
+
};
|
|
3318
|
+
}
|
|
3319
|
+
return {
|
|
3320
|
+
...requiredNullableFlags(required, nullable),
|
|
3321
|
+
type: "string"
|
|
3322
|
+
};
|
|
3323
|
+
}
|
|
3324
|
+
if (type === "number") {
|
|
3325
|
+
return {
|
|
3326
|
+
...requiredNullableFlags(required, nullable),
|
|
3327
|
+
type: "number"
|
|
3328
|
+
};
|
|
3329
|
+
}
|
|
3330
|
+
if (type === "boolean") {
|
|
3331
|
+
return {
|
|
3332
|
+
...requiredNullableFlags(required, nullable),
|
|
3333
|
+
type: "boolean"
|
|
3334
|
+
};
|
|
3335
|
+
}
|
|
3336
|
+
if (type === "array") {
|
|
3337
|
+
const items = node.items;
|
|
3338
|
+
if (!items) {
|
|
3339
|
+
ctx.problems.push({
|
|
3340
|
+
severity: "error",
|
|
3341
|
+
path,
|
|
3342
|
+
message: "Array field is missing items schema."
|
|
3343
|
+
});
|
|
3344
|
+
return null;
|
|
3345
|
+
}
|
|
3346
|
+
const itemNode = Array.isArray(items) ? items[0] : items;
|
|
3347
|
+
if (!isRecord4(itemNode)) {
|
|
3348
|
+
ctx.problems.push({
|
|
3349
|
+
severity: "error",
|
|
3350
|
+
path,
|
|
3351
|
+
message: "Array items schema must be an object."
|
|
3352
|
+
});
|
|
3353
|
+
return null;
|
|
3354
|
+
}
|
|
3355
|
+
if (isAnyValueSchema(itemNode)) {
|
|
3356
|
+
return {
|
|
3357
|
+
...requiredNullableFlags(required, nullable),
|
|
3358
|
+
type: "json[]"
|
|
3359
|
+
};
|
|
3360
|
+
}
|
|
3361
|
+
const itemVariants = Array.isArray(itemNode.anyOf) ? itemNode.anyOf : Array.isArray(itemNode.oneOf) ? itemNode.oneOf : null;
|
|
3362
|
+
if (itemVariants) {
|
|
3363
|
+
const itemEnumSets = [];
|
|
3364
|
+
let itemPlainString = false;
|
|
3365
|
+
let itemsAllString = true;
|
|
3366
|
+
for (const variant of itemVariants) {
|
|
3367
|
+
if (!isRecord4(variant) || resolvePrimaryType(variant).type !== "string") {
|
|
3368
|
+
itemsAllString = false;
|
|
3369
|
+
break;
|
|
3370
|
+
}
|
|
3371
|
+
if (Array.isArray(variant.enum)) {
|
|
3372
|
+
itemEnumSets.push(
|
|
3373
|
+
variant.enum.filter((v) => typeof v === "string")
|
|
3374
|
+
);
|
|
3375
|
+
} else {
|
|
3376
|
+
itemPlainString = true;
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
if (itemsAllString && itemEnumSets.length > 0) {
|
|
3380
|
+
return {
|
|
3381
|
+
...requiredNullableFlags(required, nullable),
|
|
3382
|
+
type: "string[]",
|
|
3383
|
+
values: [...new Set(itemEnumSets.flat())],
|
|
3384
|
+
...itemPlainString ? { open: true } : {}
|
|
3385
|
+
};
|
|
3386
|
+
}
|
|
3387
|
+
const itemKinds = [];
|
|
3388
|
+
for (const variant of itemVariants) {
|
|
3389
|
+
if (!isRecord4(variant) || !isRecord4(variant.properties)) {
|
|
3390
|
+
ctx.problems.push({
|
|
3391
|
+
severity: "error",
|
|
3392
|
+
path: `${path}[]`,
|
|
3393
|
+
message: "Array items anyOf/oneOf variants must be inline objects declaring __kind."
|
|
3394
|
+
});
|
|
3395
|
+
return null;
|
|
3396
|
+
}
|
|
3397
|
+
const declared = readBlockKindFromProperties(variant.properties);
|
|
3398
|
+
if (!declared) {
|
|
3399
|
+
ctx.problems.push({
|
|
3400
|
+
severity: "error",
|
|
3401
|
+
path: `${path}[]`,
|
|
3402
|
+
message: "Array items anyOf/oneOf variant is missing a __kind const/enum declaration."
|
|
3403
|
+
});
|
|
3404
|
+
return null;
|
|
3405
|
+
}
|
|
3406
|
+
itemKinds.push(declared);
|
|
3407
|
+
registerDeclaredKindDraft(declared, variant, `${path}[]<${declared}>`, ctx);
|
|
3408
|
+
ctx.arrayBindings.push({ arrayField: fieldName, itemKindSlug: declared });
|
|
3409
|
+
}
|
|
3410
|
+
return {
|
|
3411
|
+
...requiredNullableFlags(required, nullable),
|
|
3412
|
+
type: "array",
|
|
3413
|
+
itemKinds
|
|
3414
|
+
};
|
|
3415
|
+
}
|
|
3416
|
+
const itemType = resolvePrimaryType(itemNode).type;
|
|
3417
|
+
if (itemType === "string") {
|
|
3418
|
+
if (Array.isArray(itemNode.enum)) {
|
|
3419
|
+
return {
|
|
3420
|
+
...requiredNullableFlags(required, nullable),
|
|
3421
|
+
type: "string[]",
|
|
3422
|
+
values: itemNode.enum.filter(
|
|
3423
|
+
(v) => typeof v === "string"
|
|
3424
|
+
)
|
|
3425
|
+
};
|
|
3426
|
+
}
|
|
3427
|
+
return {
|
|
3428
|
+
...requiredNullableFlags(required, nullable),
|
|
3429
|
+
type: "string[]"
|
|
3430
|
+
};
|
|
3431
|
+
}
|
|
3432
|
+
if (itemType === "number") {
|
|
3433
|
+
return {
|
|
3434
|
+
...requiredNullableFlags(required, nullable),
|
|
3435
|
+
type: "number[]"
|
|
3436
|
+
};
|
|
3437
|
+
}
|
|
3438
|
+
if (itemType === "boolean") {
|
|
3439
|
+
return {
|
|
3440
|
+
...requiredNullableFlags(required, nullable),
|
|
3441
|
+
type: "boolean[]"
|
|
3442
|
+
};
|
|
3443
|
+
}
|
|
3444
|
+
if (itemType === "object" && isRecord4(itemNode.properties)) {
|
|
3445
|
+
const declaredItemKind = readBlockKindFromProperties(itemNode.properties);
|
|
3446
|
+
const itemKindSlug = declaredItemKind ?? synthesizeItemKindSlug(ctx.schemaName, fieldName);
|
|
3447
|
+
const itemRequired = new Set(
|
|
3448
|
+
Array.isArray(itemNode.required) ? itemNode.required.filter((k) => typeof k === "string") : []
|
|
3449
|
+
);
|
|
3450
|
+
const itemFields = {};
|
|
3451
|
+
for (const [propName, propNode] of Object.entries(itemNode.properties)) {
|
|
3452
|
+
if (!isRecord4(propNode)) continue;
|
|
3453
|
+
if (propName === KIND_KEY) continue;
|
|
3454
|
+
const converted = convertProperty(
|
|
3455
|
+
propName,
|
|
3456
|
+
propNode,
|
|
3457
|
+
itemRequired.has(propName),
|
|
3458
|
+
`${path}[].${propName}`,
|
|
3459
|
+
ctx
|
|
3460
|
+
);
|
|
3461
|
+
if (converted) {
|
|
3462
|
+
itemFields[propName] = converted;
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
const alreadyHasBlockKind = propNameIsBlockKind(itemNode.properties) || itemRequired.has(KIND_KEY);
|
|
3466
|
+
ctx.arrayBindings.push({
|
|
3467
|
+
arrayField: fieldName,
|
|
3468
|
+
itemKindSlug
|
|
3469
|
+
});
|
|
3470
|
+
if (!ctx.blockSchemas.some((draft) => draft.slug === itemKindSlug)) {
|
|
3471
|
+
ctx.blockSchemas.push({
|
|
3472
|
+
slug: itemKindSlug,
|
|
3473
|
+
label: declaredItemKind ? formatBlockLabel(itemKindSlug) : formatBlockLabel(`${ctx.schemaName}_${fieldName}_item`),
|
|
3474
|
+
fields: itemFields
|
|
3475
|
+
});
|
|
3476
|
+
}
|
|
3477
|
+
if (!alreadyHasBlockKind) {
|
|
3478
|
+
ctx.problems.push({
|
|
3479
|
+
severity: "warning",
|
|
3480
|
+
path: `${path}[]`,
|
|
3481
|
+
message: `Array "${fieldName}" items need __kind:"${itemKindSlug}" at runtime (OPTION 1: server injects, OPTION 2: use agent schema with __kind).`
|
|
3482
|
+
});
|
|
3483
|
+
}
|
|
3484
|
+
return {
|
|
3485
|
+
...requiredNullableFlags(required, nullable),
|
|
3486
|
+
type: "array",
|
|
3487
|
+
itemKinds: [itemKindSlug]
|
|
3488
|
+
};
|
|
3489
|
+
}
|
|
3490
|
+
ctx.problems.push({
|
|
3491
|
+
severity: "error",
|
|
3492
|
+
path,
|
|
3493
|
+
message: `Unsupported array items type "${itemType ?? "unknown"}".`
|
|
3494
|
+
});
|
|
3495
|
+
return null;
|
|
3496
|
+
}
|
|
3497
|
+
if (type === "object") {
|
|
3498
|
+
const ap = node.additionalProperties;
|
|
3499
|
+
const apIsOpen = ap === true || isRecord4(ap) && isAnyValueSchema(ap);
|
|
3500
|
+
if (isRecord4(node.properties)) {
|
|
3501
|
+
const nestedRequired = new Set(
|
|
3502
|
+
Array.isArray(node.required) ? node.required.filter((k) => typeof k === "string") : []
|
|
3503
|
+
);
|
|
3504
|
+
const nestedFields = {};
|
|
3505
|
+
for (const [propName, propNode] of Object.entries(node.properties)) {
|
|
3506
|
+
if (!isRecord4(propNode)) continue;
|
|
3507
|
+
if (propName === KIND_KEY) continue;
|
|
3508
|
+
const converted = convertProperty(
|
|
3509
|
+
propName,
|
|
3510
|
+
propNode,
|
|
3511
|
+
nestedRequired.has(propName),
|
|
3512
|
+
`${path}.${propName}`,
|
|
3513
|
+
ctx
|
|
3514
|
+
);
|
|
3515
|
+
if (converted) {
|
|
3516
|
+
nestedFields[propName] = converted;
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
const referencedKind = readBlockKindFromProperties(node.properties);
|
|
3520
|
+
if (referencedKind) {
|
|
3521
|
+
return {
|
|
3522
|
+
...requiredNullableFlags(required, nullable),
|
|
3523
|
+
type: "object",
|
|
3524
|
+
kind: referencedKind
|
|
3525
|
+
};
|
|
3526
|
+
}
|
|
3527
|
+
let open = apIsOpen;
|
|
3528
|
+
if (!apIsOpen && isRecord4(ap)) {
|
|
3529
|
+
ctx.problems.push({
|
|
3530
|
+
severity: "warning",
|
|
3531
|
+
path,
|
|
3532
|
+
message: "Typed additionalProperties alongside declared properties is not representable \u2014 treated as an OPEN object (extra values unconstrained)."
|
|
3533
|
+
});
|
|
3534
|
+
open = true;
|
|
3535
|
+
} else if (ap === false) {
|
|
3536
|
+
ctx.droppedMetadata.push({
|
|
3537
|
+
path,
|
|
3538
|
+
dropped: { additionalProperties: false }
|
|
3539
|
+
});
|
|
3540
|
+
}
|
|
3541
|
+
return {
|
|
3542
|
+
...requiredNullableFlags(required, nullable),
|
|
3543
|
+
type: "inline_object",
|
|
3544
|
+
fields: nestedFields,
|
|
3545
|
+
...open ? { open: true } : {}
|
|
3546
|
+
};
|
|
3547
|
+
}
|
|
3548
|
+
if (apIsOpen || ap === void 0) {
|
|
3549
|
+
if (ap === void 0) {
|
|
3550
|
+
ctx.problems.push({
|
|
3551
|
+
severity: "info",
|
|
3552
|
+
path,
|
|
3553
|
+
message: `Bare object schema at "${path || "(root)"}" \u2014 record of any JSON values.`
|
|
3554
|
+
});
|
|
3555
|
+
}
|
|
3556
|
+
return {
|
|
3557
|
+
...requiredNullableFlags(required, nullable),
|
|
3558
|
+
type: "record",
|
|
3559
|
+
values: "json"
|
|
3560
|
+
};
|
|
3561
|
+
}
|
|
3562
|
+
if (isRecord4(ap)) {
|
|
3563
|
+
const apType = resolvePrimaryType(ap).type;
|
|
3564
|
+
if (apType === "string" || apType === "number" || apType === "boolean") {
|
|
3565
|
+
return {
|
|
3566
|
+
...requiredNullableFlags(required, nullable),
|
|
3567
|
+
type: "record",
|
|
3568
|
+
values: apType
|
|
3569
|
+
};
|
|
3570
|
+
}
|
|
3571
|
+
ctx.problems.push({
|
|
3572
|
+
severity: "warning",
|
|
3573
|
+
path,
|
|
3574
|
+
message: `Record value schema at "${path}" is not scalar \u2014 widened to record of any JSON values.`
|
|
3575
|
+
});
|
|
3576
|
+
return {
|
|
3577
|
+
...requiredNullableFlags(required, nullable),
|
|
3578
|
+
type: "record",
|
|
3579
|
+
values: "json"
|
|
3580
|
+
};
|
|
3581
|
+
}
|
|
3582
|
+
return {
|
|
3583
|
+
...requiredNullableFlags(required, nullable),
|
|
3584
|
+
type: "inline_object",
|
|
3585
|
+
fields: {}
|
|
3586
|
+
};
|
|
3587
|
+
}
|
|
3588
|
+
ctx.problems.push({
|
|
3589
|
+
severity: "error",
|
|
3590
|
+
path,
|
|
3591
|
+
message: `Unsupported or missing JSON Schema type at "${path}".`
|
|
3592
|
+
});
|
|
3593
|
+
return null;
|
|
3594
|
+
}
|
|
3595
|
+
function propNameIsBlockKind(properties) {
|
|
3596
|
+
return KIND_KEY in properties;
|
|
3597
|
+
}
|
|
3598
|
+
function readBlockKindFromProperties(properties) {
|
|
3599
|
+
const kindProp = properties[KIND_KEY];
|
|
3600
|
+
if (!isRecord4(kindProp)) return null;
|
|
3601
|
+
if (typeof kindProp.const === "string") return kindProp.const;
|
|
3602
|
+
if (Array.isArray(kindProp.enum) && typeof kindProp.enum[0] === "string") {
|
|
3603
|
+
return kindProp.enum[0];
|
|
3604
|
+
}
|
|
3605
|
+
return null;
|
|
3606
|
+
}
|
|
3607
|
+
function normalizeAiSchemaInput(input) {
|
|
3608
|
+
const parseErrors = [];
|
|
3609
|
+
if (!isRecord4(input)) {
|
|
3610
|
+
return {
|
|
3611
|
+
name: null,
|
|
3612
|
+
strict: null,
|
|
3613
|
+
rootSchema: null,
|
|
3614
|
+
parseErrors: ["Input must be a JSON object."]
|
|
3615
|
+
};
|
|
3616
|
+
}
|
|
3617
|
+
if (isRecord4(input.json_schema)) {
|
|
3618
|
+
return normalizeAiSchemaInput(input.json_schema);
|
|
3619
|
+
}
|
|
3620
|
+
if (isRecord4(input.schema) && typeof input.name === "string") {
|
|
3621
|
+
return {
|
|
3622
|
+
name: input.name,
|
|
3623
|
+
strict: typeof input.strict === "boolean" ? input.strict : null,
|
|
3624
|
+
rootSchema: input.schema,
|
|
3625
|
+
parseErrors: []
|
|
3626
|
+
};
|
|
3627
|
+
}
|
|
3628
|
+
if (typeof input.type === "string" || Array.isArray(input.type) || input.properties || input.items) {
|
|
3629
|
+
const name = typeof input.name === "string" ? input.name : typeof input.title === "string" ? input.title : null;
|
|
3630
|
+
return {
|
|
3631
|
+
name,
|
|
3632
|
+
strict: typeof input.strict === "boolean" ? input.strict : null,
|
|
3633
|
+
rootSchema: input,
|
|
3634
|
+
parseErrors: []
|
|
3635
|
+
};
|
|
3636
|
+
}
|
|
3637
|
+
parseErrors.push(
|
|
3638
|
+
"Expected OpenAI output_schema shape { name, schema } or a root JSON Schema object."
|
|
3639
|
+
);
|
|
3640
|
+
return { name: null, strict: null, rootSchema: null, parseErrors };
|
|
3641
|
+
}
|
|
3642
|
+
function convertAiSchemaToBlockFields(schemaName, rootSchema, strict) {
|
|
3643
|
+
const ctx = {
|
|
3644
|
+
schemaName,
|
|
3645
|
+
problems: [],
|
|
3646
|
+
droppedMetadata: [],
|
|
3647
|
+
blockSchemas: [],
|
|
3648
|
+
arrayBindings: []
|
|
3649
|
+
};
|
|
3650
|
+
ctx.droppedMetadata.push(...collectDropped(rootSchema, ""));
|
|
3651
|
+
const rootAp = rootSchema.additionalProperties;
|
|
3652
|
+
const rootIsOpen = rootAp === true || isRecord4(rootAp) && isAnyValueSchema(rootAp);
|
|
3653
|
+
const properties = rootSchema.properties;
|
|
3654
|
+
if (!isRecord4(properties) || rootIsOpen) {
|
|
3655
|
+
const rootField = convertProperty("$root", rootSchema, false, "$root", ctx);
|
|
3656
|
+
if (!rootField) {
|
|
3657
|
+
ctx.problems.push({
|
|
3658
|
+
severity: "error",
|
|
3659
|
+
path: "",
|
|
3660
|
+
message: "Root schema could not be converted (see problems above) \u2014 expected an object with properties, a scalar/array/json root, or an open object."
|
|
3661
|
+
});
|
|
3662
|
+
return emptyConversionResult(schemaName, strict, ctx);
|
|
3663
|
+
}
|
|
3664
|
+
ctx.blockSchemas.unshift({
|
|
3665
|
+
slug: schemaName,
|
|
3666
|
+
label: formatBlockLabel(schemaName),
|
|
3667
|
+
fields: {},
|
|
3668
|
+
root: rootField
|
|
3669
|
+
});
|
|
3670
|
+
return {
|
|
3671
|
+
schemaName,
|
|
3672
|
+
strict,
|
|
3673
|
+
blockSchemas: ctx.blockSchemas,
|
|
3674
|
+
problems: ctx.problems,
|
|
3675
|
+
droppedMetadata: ctx.droppedMetadata
|
|
3676
|
+
};
|
|
3677
|
+
}
|
|
3678
|
+
const required = new Set(
|
|
3679
|
+
Array.isArray(rootSchema.required) ? rootSchema.required.filter((k) => typeof k === "string") : []
|
|
3680
|
+
);
|
|
3681
|
+
const convertedFields = {};
|
|
3682
|
+
for (const [fieldName, fieldNode] of Object.entries(properties)) {
|
|
3683
|
+
if (!isRecord4(fieldNode)) continue;
|
|
3684
|
+
if (fieldName === KIND_KEY) continue;
|
|
3685
|
+
const converted = convertProperty(
|
|
3686
|
+
fieldName,
|
|
3687
|
+
fieldNode,
|
|
3688
|
+
required.has(fieldName),
|
|
3689
|
+
fieldName,
|
|
3690
|
+
ctx
|
|
3691
|
+
);
|
|
3692
|
+
if (converted) {
|
|
3693
|
+
convertedFields[fieldName] = converted;
|
|
3694
|
+
}
|
|
3695
|
+
}
|
|
3696
|
+
const rootHasKind = propNameIsBlockKind(properties) || required.has(KIND_KEY);
|
|
3697
|
+
if (!rootHasKind) {
|
|
3698
|
+
ctx.problems.push({
|
|
3699
|
+
severity: "warning",
|
|
3700
|
+
path: "",
|
|
3701
|
+
message: `Root object needs __kind:"${schemaName}" at runtime (OPTION 1: server injects, OPTION 2: use agent schema with __kind).`
|
|
3702
|
+
});
|
|
3703
|
+
}
|
|
3704
|
+
const rootDraft = {
|
|
3705
|
+
slug: schemaName,
|
|
3706
|
+
label: formatBlockLabel(schemaName),
|
|
3707
|
+
fields: convertedFields
|
|
3708
|
+
};
|
|
3709
|
+
ctx.blockSchemas.unshift(rootDraft);
|
|
3710
|
+
if (typeof rootSchema.additionalProperties === "boolean") {
|
|
3711
|
+
ctx.droppedMetadata.push({
|
|
3712
|
+
path: "",
|
|
3713
|
+
dropped: { additionalProperties: rootSchema.additionalProperties }
|
|
3714
|
+
});
|
|
3715
|
+
}
|
|
3716
|
+
if (strict) {
|
|
3717
|
+
ctx.droppedMetadata.push({
|
|
3718
|
+
path: "",
|
|
3719
|
+
dropped: { strict }
|
|
3720
|
+
});
|
|
3721
|
+
}
|
|
3722
|
+
return {
|
|
3723
|
+
schemaName,
|
|
3724
|
+
strict,
|
|
3725
|
+
blockSchemas: ctx.blockSchemas,
|
|
3726
|
+
problems: ctx.problems,
|
|
3727
|
+
droppedMetadata: ctx.droppedMetadata
|
|
3728
|
+
};
|
|
3729
|
+
}
|
|
3730
|
+
function emptyConversionResult(schemaName, strict, ctx) {
|
|
3731
|
+
return {
|
|
3732
|
+
schemaName,
|
|
3733
|
+
strict,
|
|
3734
|
+
blockSchemas: ctx.blockSchemas,
|
|
3735
|
+
problems: ctx.problems,
|
|
3736
|
+
droppedMetadata: ctx.droppedMetadata
|
|
3737
|
+
};
|
|
3738
|
+
}
|
|
3739
|
+
function compareFieldSchemas(aiField, blockField, fieldName) {
|
|
3740
|
+
const aiPresent = aiField !== void 0;
|
|
3741
|
+
const blockPresent = blockField !== void 0;
|
|
3742
|
+
const aiSummary = aiField ? fieldSchemaSummary(aiField) : null;
|
|
3743
|
+
const blockSummary = blockField ? fieldSchemaSummary(blockField) : null;
|
|
3744
|
+
if (aiPresent && blockPresent) {
|
|
3745
|
+
if (aiSummary === blockSummary) {
|
|
3746
|
+
return {
|
|
3747
|
+
field: fieldName,
|
|
3748
|
+
aiPresent,
|
|
3749
|
+
blockPresent,
|
|
3750
|
+
aiSummary,
|
|
3751
|
+
blockSummary,
|
|
3752
|
+
status: "match"
|
|
3753
|
+
};
|
|
3754
|
+
}
|
|
3755
|
+
const aiRicher = aiField?.type === "enum" && blockField?.type === "string" || aiField?.type === "inline_object" && blockField?.type !== "inline_object";
|
|
3756
|
+
const blockRicher = blockField?.type === "enum" && aiField?.type === "string" || blockField?.required && !aiField?.required;
|
|
3757
|
+
return {
|
|
3758
|
+
field: fieldName,
|
|
3759
|
+
aiPresent,
|
|
3760
|
+
blockPresent,
|
|
3761
|
+
aiSummary,
|
|
3762
|
+
blockSummary,
|
|
3763
|
+
status: aiRicher ? "ai_richer" : blockRicher ? "block_richer" : "type_mismatch",
|
|
3764
|
+
...aiSummary !== blockSummary && {
|
|
3765
|
+
detail: `${aiSummary} vs ${blockSummary}`
|
|
3766
|
+
}
|
|
3767
|
+
};
|
|
3768
|
+
}
|
|
3769
|
+
if (aiPresent) {
|
|
3770
|
+
return {
|
|
3771
|
+
field: fieldName,
|
|
3772
|
+
aiPresent,
|
|
3773
|
+
blockPresent,
|
|
3774
|
+
aiSummary,
|
|
3775
|
+
blockSummary,
|
|
3776
|
+
status: "ai_only"
|
|
3777
|
+
};
|
|
3778
|
+
}
|
|
3779
|
+
return {
|
|
3780
|
+
field: fieldName,
|
|
3781
|
+
aiPresent,
|
|
3782
|
+
blockPresent,
|
|
3783
|
+
aiSummary,
|
|
3784
|
+
blockSummary,
|
|
3785
|
+
status: "block_only"
|
|
3786
|
+
};
|
|
3787
|
+
}
|
|
3788
|
+
function compareWithExistingKindSchema(convertedFields, existing) {
|
|
3789
|
+
if (!existing) return [];
|
|
3790
|
+
const allFields = /* @__PURE__ */ new Set([
|
|
3791
|
+
...Object.keys(convertedFields),
|
|
3792
|
+
...Object.keys(existing.fields)
|
|
3793
|
+
]);
|
|
3794
|
+
return [...allFields].sort().map(
|
|
3795
|
+
(field) => compareFieldSchemas(
|
|
3796
|
+
convertedFields[field],
|
|
3797
|
+
existing.fields[field],
|
|
3798
|
+
field
|
|
3799
|
+
)
|
|
3800
|
+
);
|
|
3801
|
+
}
|
|
3802
|
+
function runSchemaConversion(input, existingSchemas) {
|
|
3803
|
+
const normalized = normalizeAiSchemaInput(input);
|
|
3804
|
+
if (!normalized.rootSchema || !normalized.name) {
|
|
3805
|
+
return {
|
|
3806
|
+
schemaName: normalized.name,
|
|
3807
|
+
strict: normalized.strict,
|
|
3808
|
+
blockSchemas: [],
|
|
3809
|
+
agentSchemaWithKinds: null,
|
|
3810
|
+
problems: [],
|
|
3811
|
+
droppedMetadata: [],
|
|
3812
|
+
comparisons: [],
|
|
3813
|
+
parseErrors: normalized.parseErrors.length ? normalized.parseErrors : ["Schema name and root schema are required."]
|
|
3814
|
+
};
|
|
3815
|
+
}
|
|
3816
|
+
const strict = normalized.strict ?? false;
|
|
3817
|
+
const core = convertAiSchemaToBlockFields(
|
|
3818
|
+
normalized.name,
|
|
3819
|
+
normalized.rootSchema,
|
|
3820
|
+
strict
|
|
3821
|
+
);
|
|
3822
|
+
const existing = existingSchemas[normalized.name] ?? null;
|
|
3823
|
+
const rootDraft = core.blockSchemas[0];
|
|
3824
|
+
const comparisons = rootDraft ? compareWithExistingKindSchema(rootDraft.fields, existing) : [];
|
|
3825
|
+
const problems = [...core.problems];
|
|
3826
|
+
if (!existing) {
|
|
3827
|
+
problems.push({
|
|
3828
|
+
severity: "info",
|
|
3829
|
+
path: "",
|
|
3830
|
+
message: `No existing block schema with slug "${normalized.name}".`
|
|
3831
|
+
});
|
|
3832
|
+
}
|
|
3833
|
+
const agentSchemaWithKinds = buildAgentSchemaWithRenderBlockSupport(
|
|
3834
|
+
input,
|
|
3835
|
+
normalized.name,
|
|
3836
|
+
collectArrayBindingsFromDrafts(core.blockSchemas, normalized.name),
|
|
3837
|
+
strict
|
|
3838
|
+
);
|
|
3839
|
+
return {
|
|
3840
|
+
...core,
|
|
3841
|
+
agentSchemaWithKinds,
|
|
3842
|
+
comparisons,
|
|
3843
|
+
problems,
|
|
3844
|
+
parseErrors: []
|
|
3845
|
+
};
|
|
3846
|
+
}
|
|
3847
|
+
function collectArrayBindingsFromDrafts(drafts, rootSlug) {
|
|
3848
|
+
const root = drafts.find((d) => d.slug === rootSlug);
|
|
3849
|
+
if (!root) return [];
|
|
3850
|
+
const bindings = [];
|
|
3851
|
+
for (const [fieldName, field] of Object.entries(root.fields)) {
|
|
3852
|
+
if (field.type !== "array") continue;
|
|
3853
|
+
for (const itemKind of field.itemKinds) {
|
|
3854
|
+
bindings.push({ arrayField: fieldName, itemKindSlug: itemKind });
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
return bindings;
|
|
3858
|
+
}
|
|
3859
|
+
function validateBlockSchemaSavePlan(blockSchemas, existingSlugs, hasConversionErrors) {
|
|
3860
|
+
const errors = [];
|
|
3861
|
+
const normalizedExisting = new Set(
|
|
3862
|
+
existingSlugs.map((s) => s.trim().toLowerCase())
|
|
3863
|
+
);
|
|
3864
|
+
const batchSlugs = /* @__PURE__ */ new Set();
|
|
3865
|
+
const itemKindRefs = [];
|
|
3866
|
+
for (const draft of blockSchemas) {
|
|
3867
|
+
const slug = draft.slug.trim();
|
|
3868
|
+
const normalized = slug.toLowerCase();
|
|
3869
|
+
if (!slug) {
|
|
3870
|
+
errors.push("Every block schema must have a non-empty slug.");
|
|
3871
|
+
continue;
|
|
3872
|
+
}
|
|
3873
|
+
if (batchSlugs.has(normalized)) {
|
|
3874
|
+
errors.push(`Duplicate slug in conversion batch: "${slug}".`);
|
|
3875
|
+
}
|
|
3876
|
+
batchSlugs.add(normalized);
|
|
3877
|
+
}
|
|
3878
|
+
const entries = blockSchemas.map((draft) => {
|
|
3879
|
+
const existsInDb = normalizedExisting.has(draft.slug.trim().toLowerCase());
|
|
3880
|
+
return {
|
|
3881
|
+
draft,
|
|
3882
|
+
existsInDb,
|
|
3883
|
+
willSave: !existsInDb
|
|
3884
|
+
};
|
|
3885
|
+
});
|
|
3886
|
+
for (const draft of blockSchemas) {
|
|
3887
|
+
for (const [fieldName, field] of Object.entries(draft.fields)) {
|
|
3888
|
+
if (field.type !== "array") continue;
|
|
3889
|
+
for (const itemKind of field.itemKinds) {
|
|
3890
|
+
const inBatch = batchSlugs.has(itemKind.trim().toLowerCase());
|
|
3891
|
+
const inDb = normalizedExisting.has(itemKind.trim().toLowerCase());
|
|
3892
|
+
const satisfied = inBatch || inDb;
|
|
3893
|
+
itemKindRefs.push({
|
|
3894
|
+
parentSlug: draft.slug,
|
|
3895
|
+
field: fieldName,
|
|
3896
|
+
itemKind,
|
|
3897
|
+
satisfied,
|
|
3898
|
+
source: inBatch ? "batch" : inDb ? "database" : "missing"
|
|
3899
|
+
});
|
|
3900
|
+
if (!satisfied) {
|
|
3901
|
+
errors.push(
|
|
3902
|
+
`"${draft.slug}".${fieldName} references itemKind "${itemKind}" \u2014 not in this batch and not in DB.`
|
|
3903
|
+
);
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
const newCount = entries.filter((e) => e.willSave).length;
|
|
3909
|
+
if (newCount === 0 && blockSchemas.length > 0) {
|
|
3910
|
+
errors.push("All block schema slugs already exist in the database.");
|
|
3911
|
+
}
|
|
3912
|
+
if (hasConversionErrors) {
|
|
3913
|
+
errors.push("Resolve conversion errors before saving.");
|
|
3914
|
+
}
|
|
3915
|
+
return {
|
|
3916
|
+
entries,
|
|
3917
|
+
itemKindRefs,
|
|
3918
|
+
newCount,
|
|
3919
|
+
canSave: errors.length === 0 && newCount > 0,
|
|
3920
|
+
errors
|
|
3921
|
+
};
|
|
3922
|
+
}
|
|
3923
|
+
function fieldsToDbPayload(fields) {
|
|
3924
|
+
return JSON.parse(JSON.stringify(fields));
|
|
3925
|
+
}
|
|
3926
|
+
function isDuplicateBlockSlug(slug, entries) {
|
|
3927
|
+
const normalized = slug.trim().toLowerCase();
|
|
3928
|
+
return entries.some((e) => e.slug.trim().toLowerCase() === normalized);
|
|
3929
|
+
}
|
|
3930
|
+
|
|
3931
|
+
// convert/kind-to-json-schema.ts
|
|
3932
|
+
function collectReferencedKinds(fields) {
|
|
3933
|
+
const out = [];
|
|
3934
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3935
|
+
const add = (kind) => {
|
|
3936
|
+
if (!seen.has(kind)) {
|
|
3937
|
+
seen.add(kind);
|
|
3938
|
+
out.push(kind);
|
|
3939
|
+
}
|
|
3940
|
+
};
|
|
3941
|
+
const visit = (field) => {
|
|
3942
|
+
if (field.type === "object") {
|
|
3943
|
+
add(field.kind);
|
|
3944
|
+
} else if (field.type === "array") {
|
|
3945
|
+
field.itemKinds.forEach(add);
|
|
3946
|
+
} else if (field.type === "union") {
|
|
3947
|
+
(field.kinds ?? []).forEach(add);
|
|
3948
|
+
} else if (field.type === "inline_object") {
|
|
3949
|
+
Object.values(field.fields).forEach(visit);
|
|
3950
|
+
}
|
|
3951
|
+
};
|
|
3952
|
+
Object.values(fields).forEach(visit);
|
|
3953
|
+
return out;
|
|
3954
|
+
}
|
|
3955
|
+
function collectSchemaReferencedKinds(schema) {
|
|
3956
|
+
if (!schema.root) return collectReferencedKinds(schema.fields);
|
|
3957
|
+
return collectReferencedKinds({ [ROOT_REF_FIELD]: schema.root });
|
|
3958
|
+
}
|
|
3959
|
+
var ROOT_REF_FIELD = "__root";
|
|
3960
|
+
function kindSchemaToJsonSchema(kind, resolve, options = {}) {
|
|
3961
|
+
const strict = options.strict ?? false;
|
|
3962
|
+
const injectKind = options.injectKind ?? true;
|
|
3963
|
+
const rootSchema = resolve(kind);
|
|
3964
|
+
if (!rootSchema) return null;
|
|
3965
|
+
const visited = /* @__PURE__ */ new Set([kind]);
|
|
3966
|
+
const defsOrder = [];
|
|
3967
|
+
const resolvedDefs = /* @__PURE__ */ new Map();
|
|
3968
|
+
const unresolved = [];
|
|
3969
|
+
const queue = collectSchemaReferencedKinds(rootSchema);
|
|
3970
|
+
while (queue.length > 0) {
|
|
3971
|
+
const next = queue.shift();
|
|
3972
|
+
if (visited.has(next)) continue;
|
|
3973
|
+
visited.add(next);
|
|
3974
|
+
defsOrder.push(next);
|
|
3975
|
+
const schema = resolve(next);
|
|
3976
|
+
if (!schema) {
|
|
3977
|
+
unresolved.push(next);
|
|
3978
|
+
continue;
|
|
3979
|
+
}
|
|
3980
|
+
resolvedDefs.set(next, schema);
|
|
3981
|
+
queue.push(...collectSchemaReferencedKinds(schema));
|
|
3982
|
+
}
|
|
3983
|
+
const refFor = (slug) => slug === kind ? { $ref: "#" } : { $ref: `#/$defs/${slug}` };
|
|
3984
|
+
const withNull = (type, nullable) => nullable ? [type, "null"] : type;
|
|
3985
|
+
function fieldToJsonSchema(field) {
|
|
3986
|
+
const node = fieldToJsonSchemaCore(field);
|
|
3987
|
+
if (field.description !== void 0) node.description = field.description;
|
|
3988
|
+
if (field.default !== void 0) node.default = field.default;
|
|
3989
|
+
return node;
|
|
3990
|
+
}
|
|
3991
|
+
function fieldToJsonSchemaCore(field) {
|
|
3992
|
+
switch (field.type) {
|
|
3993
|
+
case "string":
|
|
3994
|
+
case "boolean":
|
|
3995
|
+
return { type: withNull(field.type, field.nullable) };
|
|
3996
|
+
case "number":
|
|
3997
|
+
return {
|
|
3998
|
+
type: withNull("number", field.nullable),
|
|
3999
|
+
...!strict && field.min !== void 0 ? { minimum: field.min } : {},
|
|
4000
|
+
...!strict && field.max !== void 0 ? { maximum: field.max } : {},
|
|
4001
|
+
...!strict && field.step !== void 0 ? { multipleOf: field.step } : {}
|
|
4002
|
+
};
|
|
4003
|
+
case "json":
|
|
4004
|
+
return {};
|
|
4005
|
+
case "string[]": {
|
|
4006
|
+
const items = field.values === void 0 ? { type: "string" } : field.open ? {
|
|
4007
|
+
anyOf: [
|
|
4008
|
+
{ type: "string", enum: [...field.values] },
|
|
4009
|
+
{ type: "string" }
|
|
4010
|
+
]
|
|
4011
|
+
} : { type: "string", enum: [...field.values] };
|
|
4012
|
+
return { type: withNull("array", field.nullable), items };
|
|
4013
|
+
}
|
|
4014
|
+
case "number[]":
|
|
4015
|
+
case "boolean[]":
|
|
4016
|
+
return {
|
|
4017
|
+
type: withNull("array", field.nullable),
|
|
4018
|
+
items: { type: scalarArrayItemType(field.type) }
|
|
4019
|
+
};
|
|
4020
|
+
case "json[]":
|
|
4021
|
+
return {
|
|
4022
|
+
type: withNull("array", field.nullable),
|
|
4023
|
+
items: {}
|
|
4024
|
+
};
|
|
4025
|
+
case "enum": {
|
|
4026
|
+
if (!field.open) {
|
|
4027
|
+
return {
|
|
4028
|
+
type: withNull("string", field.nullable),
|
|
4029
|
+
enum: [...field.values]
|
|
4030
|
+
};
|
|
4031
|
+
}
|
|
4032
|
+
const variants = [
|
|
4033
|
+
{ type: "string", enum: [...field.values] },
|
|
4034
|
+
{ type: "string" }
|
|
4035
|
+
];
|
|
4036
|
+
if (field.nullable) variants.push({ type: "null" });
|
|
4037
|
+
return { anyOf: variants };
|
|
4038
|
+
}
|
|
4039
|
+
case "union": {
|
|
4040
|
+
const variants = field.scalars.map((scalar) => ({
|
|
4041
|
+
type: scalar
|
|
4042
|
+
}));
|
|
4043
|
+
for (const memberKind of field.kinds ?? []) {
|
|
4044
|
+
variants.push(refFor(memberKind));
|
|
4045
|
+
}
|
|
4046
|
+
if (field.nullable) variants.push({ type: "null" });
|
|
4047
|
+
return { anyOf: variants };
|
|
4048
|
+
}
|
|
4049
|
+
case "record":
|
|
4050
|
+
return {
|
|
4051
|
+
type: withNull("object", field.nullable),
|
|
4052
|
+
additionalProperties: field.values === "json" ? true : { type: field.values }
|
|
4053
|
+
};
|
|
4054
|
+
case "inline_object": {
|
|
4055
|
+
const properties = {};
|
|
4056
|
+
const required = [];
|
|
4057
|
+
for (const [name, child] of Object.entries(field.fields)) {
|
|
4058
|
+
properties[name] = fieldToJsonSchema(child);
|
|
4059
|
+
if (child.required) required.push(name);
|
|
4060
|
+
}
|
|
4061
|
+
return {
|
|
4062
|
+
type: withNull("object", field.nullable),
|
|
4063
|
+
properties,
|
|
4064
|
+
required,
|
|
4065
|
+
...field.open ? { additionalProperties: true } : strict ? { additionalProperties: false } : {}
|
|
4066
|
+
};
|
|
4067
|
+
}
|
|
4068
|
+
case "object":
|
|
4069
|
+
return field.nullable ? { anyOf: [refFor(field.kind), { type: "null" }] } : refFor(field.kind);
|
|
4070
|
+
case "array": {
|
|
4071
|
+
const [soleItemKind] = field.itemKinds;
|
|
4072
|
+
const items = field.itemKinds.length === 1 && soleItemKind !== void 0 ? refFor(soleItemKind) : { anyOf: field.itemKinds.map(refFor) };
|
|
4073
|
+
return { type: withNull("array", field.nullable), items };
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
4077
|
+
function kindObjectSchema(schema) {
|
|
4078
|
+
if (schema.root) {
|
|
4079
|
+
return fieldToJsonSchema(schema.root);
|
|
4080
|
+
}
|
|
4081
|
+
const properties = {};
|
|
4082
|
+
const required = [];
|
|
4083
|
+
for (const [name, field] of Object.entries(schema.fields)) {
|
|
4084
|
+
properties[name] = fieldToJsonSchema(field);
|
|
4085
|
+
if (field.required) required.push(name);
|
|
4086
|
+
}
|
|
4087
|
+
const node = {
|
|
4088
|
+
type: "object",
|
|
4089
|
+
properties,
|
|
4090
|
+
required,
|
|
4091
|
+
...strict ? { additionalProperties: false } : {}
|
|
4092
|
+
};
|
|
4093
|
+
return injectKind ? injectKindIntoObjectSchema(node, schema.kind, strict) : node;
|
|
4094
|
+
}
|
|
4095
|
+
function unresolvedStub(slug) {
|
|
4096
|
+
const node = {
|
|
4097
|
+
type: "object",
|
|
4098
|
+
description: `Unresolved kind "${slug}" \u2014 schema not available at export time.`
|
|
4099
|
+
};
|
|
4100
|
+
return injectKind ? injectKindIntoObjectSchema(node, slug, false) : node;
|
|
4101
|
+
}
|
|
4102
|
+
const rootNode = kindObjectSchema(rootSchema);
|
|
4103
|
+
if (defsOrder.length === 0) {
|
|
4104
|
+
return { name: kind, schema: rootNode, strict, unresolved };
|
|
4105
|
+
}
|
|
4106
|
+
const defs = {};
|
|
4107
|
+
for (const slug of defsOrder) {
|
|
4108
|
+
const schema = resolvedDefs.get(slug);
|
|
4109
|
+
defs[slug] = schema ? kindObjectSchema(schema) : unresolvedStub(slug);
|
|
4110
|
+
}
|
|
4111
|
+
return { name: kind, schema: { ...rootNode, $defs: defs }, strict, unresolved };
|
|
4112
|
+
}
|
|
4113
|
+
|
|
4114
|
+
export { IR_ENVELOPE_CACHE_VERSION, IR_ENVELOPE_KEY, IR_VERSION, IrTree, JSON_DISCRIMINATOR, JsonStreamTokenizer, KIND_KEY, KindStorageError, KindStreamParser, ParseSession, ROOT_STORAGE_NAME, buildAgentSchemaWithRenderBlockSupport, buildCompliantKindSnapshot, classifyInboundEnvelopeMetadata, collectReferencedKinds, collectSchemaReferencedKinds, compareWithExistingKindSchema, convertAiSchemaToBlockFields, createFingerprinter, createKindStreamParser, describeDualGateFailure, disposeParseSession, emptyValueForFieldSchema, envelopeCacheFromEnvelopes, envelopeFromCompleteValue, fenceDiscriminator, fieldsToDbPayload, fingerprintText, formatBlockLabel, getParseSession, injectKindIntoObjectSchema, irPathIsUnderOrEqual, irPathKey, irPathLabel, irPathsEqual, isCanonicalBlockIR, isDuplicateBlockSlug, isEmptyResidue, isIrEnvelopeCache, isJsonAnyField, isScalarArrayType, kindSchemaToJsonSchema, kindSchemaToStorage, mergeResidueIntoValue, normalizeAiSchemaInput, normalizeJsonRegion, openParseSession, readEnvelope, readObjectKind, reconstructRegionValue, reuseEnvelopeIfCurrent, runKindDualGate, runSchemaConversion, sanitizeInboundEnvelopeMetadata, scalarArrayItemType, schemaLayoutMode, schemaStructureDepth, setJsonRootKeyLookup, storageToKindSchema, stripKindDeep, validateBlockSchemaSavePlan, validateStructuralLeg, xmlDiscriminator };
|
|
4115
|
+
//# sourceMappingURL=index.js.map
|
|
4116
|
+
//# sourceMappingURL=index.js.map
|