@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.d.ts
ADDED
|
@@ -0,0 +1,1601 @@
|
|
|
1
|
+
import { ComponentType } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The canonical IR contract for structured content.
|
|
5
|
+
*
|
|
6
|
+
* Every source (live agent stream, DB reload, Python-preprocessed results,
|
|
7
|
+
* notes, any future surface) is normalized into these shapes exactly once.
|
|
8
|
+
* Anything already carrying this IR passes through downstream layers by
|
|
9
|
+
* reference — see `core/normalize.ts` for the idempotence law.
|
|
10
|
+
*
|
|
11
|
+
* This file is pure types + path helpers. No React, no Redux, no IO.
|
|
12
|
+
*/
|
|
13
|
+
/** Path of a value inside a parsed region (object keys + array indices). */
|
|
14
|
+
type IrPath = Array<string | number>;
|
|
15
|
+
/** How a node's kind was (or wasn't) established. */
|
|
16
|
+
type IrKindState = "resolved" | "speculative" | "pending_kind" | "pending_schema" | "raw";
|
|
17
|
+
/**
|
|
18
|
+
* System-level zero-data-loss channel. Unknown keys are NEVER merged into a
|
|
19
|
+
* node's `value` (they'd be indistinguishable from schema fields); they are
|
|
20
|
+
* carried here verbatim — Protobuf unknown-fields discipline. Distinct from
|
|
21
|
+
* any domain-level `additionalDetails` field, which is an ordinary schema
|
|
22
|
+
* field inside `value`.
|
|
23
|
+
*/
|
|
24
|
+
interface IrResidue {
|
|
25
|
+
/** Keys present in the source object but absent from the kind schema. */
|
|
26
|
+
extra: Record<string, unknown> | null;
|
|
27
|
+
/** Optional schema fields the source never provided. */
|
|
28
|
+
optionalMissing: string[] | null;
|
|
29
|
+
/** Structured warnings attached during parsing (speculation backtracks, truncation, …). */
|
|
30
|
+
notices: Array<{
|
|
31
|
+
code: string;
|
|
32
|
+
message: string;
|
|
33
|
+
at?: number;
|
|
34
|
+
}> | null;
|
|
35
|
+
}
|
|
36
|
+
/** Which wire syntax carried the kind discriminator for a node. */
|
|
37
|
+
type IrDiscriminator = {
|
|
38
|
+
format: "json";
|
|
39
|
+
key: "__kind";
|
|
40
|
+
} | {
|
|
41
|
+
format: "xml";
|
|
42
|
+
tag: string;
|
|
43
|
+
} | {
|
|
44
|
+
format: "fence";
|
|
45
|
+
language: string;
|
|
46
|
+
};
|
|
47
|
+
/** A schema-shaped structured node. Children live inside `value` (and carry their own metadata via `nodeIndex`). */
|
|
48
|
+
interface IrStructuredNode {
|
|
49
|
+
role: "structured";
|
|
50
|
+
/** Canonical kind slug. Empty string while pending. */
|
|
51
|
+
kind: string;
|
|
52
|
+
kindState: IrKindState;
|
|
53
|
+
discriminator: IrDiscriminator;
|
|
54
|
+
/** Region-relative path ([] = region root). */
|
|
55
|
+
path: IrPath;
|
|
56
|
+
status: "streaming" | "complete" | "error";
|
|
57
|
+
/** Compliant snapshot: schema fields + __kind ONLY. Unknown keys → residue. */
|
|
58
|
+
value: Record<string, unknown>;
|
|
59
|
+
residue: IrResidue | null;
|
|
60
|
+
}
|
|
61
|
+
/** Envelope version. Bump = migration point for persisted envelopes. */
|
|
62
|
+
declare const IR_VERSION: 1;
|
|
63
|
+
/** Reserved key carrying the envelope inside RenderBlockPayload.data. */
|
|
64
|
+
declare const IR_ENVELOPE_KEY: "__ir";
|
|
65
|
+
/**
|
|
66
|
+
* One parsed region's canonical form. Serializable (plain JSON), carried on
|
|
67
|
+
* render blocks (`data.__ir`), persisted on artifacts and message metadata,
|
|
68
|
+
* and — with `engine: "py-block-detector"` — accepted pre-built from Python.
|
|
69
|
+
*/
|
|
70
|
+
interface CanonicalBlockIR {
|
|
71
|
+
v: typeof IR_VERSION;
|
|
72
|
+
/** Provenance: which implementation produced this envelope. */
|
|
73
|
+
engine: "fe-kind-parser" | "py-block-detector";
|
|
74
|
+
/** Stable hash of the region source text — the idempotence / cache key. */
|
|
75
|
+
fingerprint: string;
|
|
76
|
+
root: IrStructuredNode;
|
|
77
|
+
/**
|
|
78
|
+
* pathKey → node metadata for per-path readers (child kinds under root).
|
|
79
|
+
* Carries each child node's residue — child snapshot values inside
|
|
80
|
+
* `root.value` hold schema fields only, so WITHOUT this the envelope would
|
|
81
|
+
* silently drop nested unknown keys (zero-data-loss violation).
|
|
82
|
+
*/
|
|
83
|
+
nodeIndex?: Record<string, Pick<IrStructuredNode, "kind" | "kindState" | "status"> & {
|
|
84
|
+
residue?: IrResidue | null;
|
|
85
|
+
}>;
|
|
86
|
+
}
|
|
87
|
+
/** Normalizer output segment: prose stays raw text; structured regions carry IR. */
|
|
88
|
+
type CanonicalSegment = {
|
|
89
|
+
role: "text";
|
|
90
|
+
content: string;
|
|
91
|
+
} | {
|
|
92
|
+
role: "block";
|
|
93
|
+
blockType: string;
|
|
94
|
+
content: string;
|
|
95
|
+
ir: CanonicalBlockIR | null;
|
|
96
|
+
};
|
|
97
|
+
interface CanonicalContent {
|
|
98
|
+
v: typeof IR_VERSION;
|
|
99
|
+
segments: CanonicalSegment[];
|
|
100
|
+
}
|
|
101
|
+
declare function irPathKey(path: IrPath): string;
|
|
102
|
+
declare function irPathsEqual(left: IrPath, right: IrPath): boolean;
|
|
103
|
+
declare function irPathIsUnderOrEqual(path: IrPath, prefix: IrPath): boolean;
|
|
104
|
+
/** Human label for a path ("root", "cards[2].front"). */
|
|
105
|
+
declare function irPathLabel(path: IrPath): string;
|
|
106
|
+
/** True when every residue channel is empty — normalize to null instead. */
|
|
107
|
+
declare function isEmptyResidue(residue: IrResidue): boolean;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* KindSchema — the data-defined field model for a registered kind.
|
|
111
|
+
*
|
|
112
|
+
* `__kind` (KIND_KEY) is the carried discriminator: it is NOT part of a
|
|
113
|
+
* kind's field map; the parser enforces it via `KindSchema.kind` and stamps
|
|
114
|
+
* it onto every compliant snapshot.
|
|
115
|
+
*
|
|
116
|
+
* Moved from app/(dev)/demos/json-block-detector/kind-schemas.ts.
|
|
117
|
+
*
|
|
118
|
+
* 2026-07-15 expressivity extension (A2) — four constructs the Python-owned
|
|
119
|
+
* pydantic schemas need that the v1 vocabulary could not express:
|
|
120
|
+
* - `{type:"json"}` / `{type:"json[]"}` — any JSON value / array of any
|
|
121
|
+
* JSON values (pydantic bare `Any` fields, `items: {}` arrays, `{}`
|
|
122
|
+
* schemas). A `json` value is implicitly nullable — `null` IS a JSON
|
|
123
|
+
* value — so `nullable` is meaningless (and ignored) on it.
|
|
124
|
+
* - `record` values widened to `"json"` (pydantic `dict[str, Any]` /
|
|
125
|
+
* `additionalProperties: true`).
|
|
126
|
+
* - `union` may now carry `kinds` (object unions — anyOf over kind refs,
|
|
127
|
+
* optionally mixed with scalars). Refs externalize to `kind_edge` rows
|
|
128
|
+
* exactly like `array.itemKinds`.
|
|
129
|
+
* - `KindSchema.root` — a NON-OBJECT root form: the kind's VALUE is the
|
|
130
|
+
* root field itself (scalar / array / json / open object), not a `__kind`
|
|
131
|
+
* object with fields. Root-form kinds are data-only: the streaming
|
|
132
|
+
* `__kind` parser cannot type them (a scalar cannot carry a
|
|
133
|
+
* discriminator) and refuses them loudly; validation goes through the
|
|
134
|
+
* emitted JSON Schema (ajv / Pydantic). `root` and a non-empty `fields`
|
|
135
|
+
* are mutually exclusive.
|
|
136
|
+
* - `inline_object.open` — `additionalProperties: true`; fixes the
|
|
137
|
+
* open-empty-object defect where an open `inline_object{fields:{}}`
|
|
138
|
+
* materialized as CLOSED (schema_proposal / item_presentation class).
|
|
139
|
+
*
|
|
140
|
+
* 2026-07-15 input-semantics extension (W3-A, agent-input bridge) — the
|
|
141
|
+
* constructs the Wave-1 sufficiency survey of all 1,529 live agent variables
|
|
142
|
+
* showed FieldSchema could not carry, added so `AgentVariable` ⇄ kind
|
|
143
|
+
* conversion is faithful:
|
|
144
|
+
* - `FieldBase.description` — human guidance; round-trips JSON Schema
|
|
145
|
+
* `description` and VariableDefinition `helpText`.
|
|
146
|
+
* - `FieldBase.default` — the field's default VALUE (JSON Schema `default`,
|
|
147
|
+
* VariableDefinition `defaultValue`). Annotation-level: validators never
|
|
148
|
+
* apply it; emitters carry it verbatim.
|
|
149
|
+
* - `enum.open` — "one of these options OR any string" (the FE's
|
|
150
|
+
* `allowOther`). Emits as `anyOf: [{type:"string", enum}, {type:"string"}]`
|
|
151
|
+
* so the option set survives instead of widening to bare `string`.
|
|
152
|
+
* - `number` bounds `min`/`max`/`step` — JSON Schema
|
|
153
|
+
* `minimum`/`maximum`/`multipleOf` (number/slider components).
|
|
154
|
+
* - `string[].values` (+ `open`) — an items-enum: array of strings drawn
|
|
155
|
+
* from an option set (checkbox components), `open` meaning the set is
|
|
156
|
+
* advisory (`allowOther` on a multi-select).
|
|
157
|
+
* Picklist bindings, scope bindings, and media component identity are
|
|
158
|
+
* PROVENANCE, not structure — they never enter FieldSchema; the bridge
|
|
159
|
+
* carries them out-of-band (see convert/kind-variable-bridge.ts sidecar).
|
|
160
|
+
*/
|
|
161
|
+
/** System discriminator — hardcoded, not part of per-kind field schemas. */
|
|
162
|
+
declare const KIND_KEY = "__kind";
|
|
163
|
+
type ScalarFieldType = "string" | "number" | "boolean";
|
|
164
|
+
type ArrayItemScalarType = "string" | "number" | "boolean";
|
|
165
|
+
/** Value domain of a `record` field — typed scalars, or any JSON value. */
|
|
166
|
+
type RecordValueType = ArrayItemScalarType | "json";
|
|
167
|
+
type FieldBase = {
|
|
168
|
+
required?: boolean;
|
|
169
|
+
nullable?: boolean;
|
|
170
|
+
/** Human guidance — JSON Schema `description` / variable `helpText`. */
|
|
171
|
+
description?: string;
|
|
172
|
+
/**
|
|
173
|
+
* Default VALUE (JSON Schema `default` / variable `defaultValue`).
|
|
174
|
+
* Annotation-level: validators never apply it; emitters carry it verbatim.
|
|
175
|
+
*/
|
|
176
|
+
default?: unknown;
|
|
177
|
+
};
|
|
178
|
+
type FieldSchema = (FieldBase & {
|
|
179
|
+
type: "string" | "boolean";
|
|
180
|
+
}) | (FieldBase & {
|
|
181
|
+
type: "number";
|
|
182
|
+
/** Inclusive lower bound — JSON Schema `minimum`. */
|
|
183
|
+
min?: number;
|
|
184
|
+
/** Inclusive upper bound — JSON Schema `maximum`. */
|
|
185
|
+
max?: number;
|
|
186
|
+
/** Increment — JSON Schema `multipleOf`. Annotation-level in the parser. */
|
|
187
|
+
step?: number;
|
|
188
|
+
}) | (FieldBase & {
|
|
189
|
+
type: "string[]";
|
|
190
|
+
/** Items-enum: each item must be one of these values (checkbox option sets). */
|
|
191
|
+
values?: string[];
|
|
192
|
+
/** With `values`: the set is advisory — any string item is also legal (`allowOther`). */
|
|
193
|
+
open?: boolean;
|
|
194
|
+
}) | (FieldBase & {
|
|
195
|
+
type: "number[]" | "boolean[]";
|
|
196
|
+
}) | (FieldBase & {
|
|
197
|
+
type: "json";
|
|
198
|
+
}) | (FieldBase & {
|
|
199
|
+
type: "json[]";
|
|
200
|
+
}) | (FieldBase & {
|
|
201
|
+
type: "array";
|
|
202
|
+
itemKinds: string[];
|
|
203
|
+
}) | (FieldBase & {
|
|
204
|
+
type: "object";
|
|
205
|
+
kind: string;
|
|
206
|
+
}) | (FieldBase & {
|
|
207
|
+
type: "inline_object";
|
|
208
|
+
fields: Record<string, FieldSchema>;
|
|
209
|
+
/** additionalProperties: true — unknown keys are legal, not residue-only. */
|
|
210
|
+
open?: boolean;
|
|
211
|
+
}) | (FieldBase & {
|
|
212
|
+
type: "record";
|
|
213
|
+
values: RecordValueType;
|
|
214
|
+
}) | (FieldBase & {
|
|
215
|
+
type: "enum";
|
|
216
|
+
values: string[];
|
|
217
|
+
/** "One of these OR any string" — the option set is advisory (`allowOther`). */
|
|
218
|
+
open?: boolean;
|
|
219
|
+
}) | (FieldBase & {
|
|
220
|
+
type: "union";
|
|
221
|
+
scalars: Array<"string" | "number" | "boolean">;
|
|
222
|
+
/** Object union members — kind refs (anyOf of $refs), may mix with scalars. */
|
|
223
|
+
kinds?: string[];
|
|
224
|
+
});
|
|
225
|
+
/**
|
|
226
|
+
* Domain fields only — __kind is enforced by the parser via KindSchema.kind
|
|
227
|
+
* (block slug). A kind with `root` set has NO field map (fields stays `{}`):
|
|
228
|
+
* its value is the root field's type at the top level. See the module header.
|
|
229
|
+
*/
|
|
230
|
+
type KindSchema = {
|
|
231
|
+
kind: string;
|
|
232
|
+
fields: Record<string, FieldSchema>;
|
|
233
|
+
/** Non-object root form — mutually exclusive with a non-empty `fields`. */
|
|
234
|
+
root?: FieldSchema;
|
|
235
|
+
};
|
|
236
|
+
declare function readObjectKind(value: Record<string, unknown>): string | null;
|
|
237
|
+
declare function isScalarArrayType(type: FieldSchema["type"]): type is "string[]" | "number[]" | "boolean[]";
|
|
238
|
+
declare function scalarArrayItemType(type: "string[]" | "number[]" | "boolean[]"): ArrayItemScalarType;
|
|
239
|
+
/**
|
|
240
|
+
* Does this field's value domain accept ANY JSON shape (object/array/scalar/
|
|
241
|
+
* null alike)? True for `json` and `json[]` ITEMS — the parser treats the
|
|
242
|
+
* subtree under such a field as opaque (no kind identification, no raw_object
|
|
243
|
+
* degradation: unknown structure is the declared contract, not a failure).
|
|
244
|
+
*/
|
|
245
|
+
declare function isJsonAnyField(field: FieldSchema): boolean;
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* KindStreamParser — the streaming, schema-validating, __kind-discriminated
|
|
249
|
+
* JSON parser at the heart of content-ir.
|
|
250
|
+
*
|
|
251
|
+
* Frame-stack pushdown parser over JsonStreamTokenizer tokens. Every value is
|
|
252
|
+
* path-addressed; every object resolves a kind against the schema registry;
|
|
253
|
+
* schema-shaped `block_snapshot` events fire on field arrival so renderers
|
|
254
|
+
* get live partials; unknown/invalid structures degrade to `raw_object`
|
|
255
|
+
* instead of failing the stream.
|
|
256
|
+
*
|
|
257
|
+
* The pushdown discipline (commit → descend → never re-ask → backtrack):
|
|
258
|
+
* - SPECULATIVE DESCENT: when a parent field schema predicts a child's kind
|
|
259
|
+
* ({type:"object", kind} or {type:"array", itemKinds:[K]} with one member),
|
|
260
|
+
* the child commits to that kind THE INSTANT `{` opens and renders a
|
|
261
|
+
* placeholder snapshot. `__kind` arrival confirms (no-op), re-tags (allowed
|
|
262
|
+
* sibling kind), or backtracks to raw (contradiction). An object under a
|
|
263
|
+
* predicting parent doesn't even need `__kind` — prediction alone types it.
|
|
264
|
+
* - PENDING SCHEMA: an identified kind whose schema isn't loaded holds the
|
|
265
|
+
* node open (`pending_schema` event, fields keep accumulating), fires the
|
|
266
|
+
* resolver's cold fetch, and upgrades in place via `notifySchemaArrived` —
|
|
267
|
+
* even after the node (or the whole region) has closed.
|
|
268
|
+
* - POP-UP-ONE-LEVEL: node-scoped problems (duplicate key, schema violation,
|
|
269
|
+
* disallowed itemKind) mark THAT node raw and keep parsing the parent.
|
|
270
|
+
* Only grammar/tokenizer errors are region-fatal — and the host degrades
|
|
271
|
+
* the region to a plain code block, never the stream.
|
|
272
|
+
*/
|
|
273
|
+
|
|
274
|
+
/** Kept as the historical name for the parser's event paths. */
|
|
275
|
+
type JsonPath = IrPath;
|
|
276
|
+
/**
|
|
277
|
+
* Schema source abstraction. A plain Record works for static sets; the
|
|
278
|
+
* registry supplies a resolver whose `request` fires a cold fetch and later
|
|
279
|
+
* calls `parser.notifySchemaArrived`.
|
|
280
|
+
*/
|
|
281
|
+
interface SchemaResolver {
|
|
282
|
+
get(kind: string): KindSchema | undefined;
|
|
283
|
+
/** Fire-and-forget cold fetch. Absent = static source, unknown kinds go raw. */
|
|
284
|
+
request?(kind: string): void;
|
|
285
|
+
/**
|
|
286
|
+
* Optional per-resolver override for legacy root-key recognition. Most hosts
|
|
287
|
+
* leave this unset and register the lookup once via
|
|
288
|
+
* `setJsonRootKeyLookup` — see that function for why.
|
|
289
|
+
*/
|
|
290
|
+
kindForJsonRootKey?(key: string): string | null;
|
|
291
|
+
}
|
|
292
|
+
declare function setJsonRootKeyLookup(lookup: ((key: string) => string | null) | null): void;
|
|
293
|
+
type KindStreamEvent = {
|
|
294
|
+
type: "kind_identified";
|
|
295
|
+
kind: string;
|
|
296
|
+
path: JsonPath;
|
|
297
|
+
/** True when committed from the parent schema before __kind arrived. */
|
|
298
|
+
speculative?: boolean;
|
|
299
|
+
at: number;
|
|
300
|
+
} | {
|
|
301
|
+
type: "pending_kind";
|
|
302
|
+
path: JsonPath;
|
|
303
|
+
at: number;
|
|
304
|
+
} | {
|
|
305
|
+
type: "kind_wait_end";
|
|
306
|
+
path: JsonPath;
|
|
307
|
+
outcome: "identified" | "raw_fallback";
|
|
308
|
+
kind?: string;
|
|
309
|
+
reason?: string;
|
|
310
|
+
at: number;
|
|
311
|
+
} | {
|
|
312
|
+
type: "pending_schema";
|
|
313
|
+
kind: string;
|
|
314
|
+
path: JsonPath;
|
|
315
|
+
at: number;
|
|
316
|
+
} | {
|
|
317
|
+
type: "field";
|
|
318
|
+
kind: string;
|
|
319
|
+
path: JsonPath;
|
|
320
|
+
key: string;
|
|
321
|
+
value: unknown;
|
|
322
|
+
at: number;
|
|
323
|
+
} | {
|
|
324
|
+
type: "object_start";
|
|
325
|
+
path: JsonPath;
|
|
326
|
+
at: number;
|
|
327
|
+
} | {
|
|
328
|
+
type: "object_complete";
|
|
329
|
+
kind: string;
|
|
330
|
+
path: JsonPath;
|
|
331
|
+
value: Record<string, unknown>;
|
|
332
|
+
at: number;
|
|
333
|
+
} | {
|
|
334
|
+
type: "raw_object";
|
|
335
|
+
path: JsonPath;
|
|
336
|
+
value: unknown;
|
|
337
|
+
reason: string;
|
|
338
|
+
/**
|
|
339
|
+
* The kind that WAS identified for this node before it degraded raw —
|
|
340
|
+
* present ONLY for schema-availability failures ("no schema registered"
|
|
341
|
+
* for an identified/declared kind), never for structural failures
|
|
342
|
+
* (missing __kind, duplicate keys, schema violations). Consumers use it
|
|
343
|
+
* to preserve a known-but-unrenderable kind on the envelope so the
|
|
344
|
+
* render seam can route to the generic viewer (or upgrade when the
|
|
345
|
+
* schema arrives late) instead of dumping raw JSON.
|
|
346
|
+
*/
|
|
347
|
+
kind?: string;
|
|
348
|
+
at: number;
|
|
349
|
+
} | {
|
|
350
|
+
type: "optional_field_missing";
|
|
351
|
+
kind: string;
|
|
352
|
+
path: JsonPath;
|
|
353
|
+
field: string;
|
|
354
|
+
at: number;
|
|
355
|
+
} | {
|
|
356
|
+
type: "extra_field";
|
|
357
|
+
kind: string;
|
|
358
|
+
path: JsonPath;
|
|
359
|
+
field: string;
|
|
360
|
+
at: number;
|
|
361
|
+
} | {
|
|
362
|
+
type: "block_snapshot";
|
|
363
|
+
kind: string;
|
|
364
|
+
path: JsonPath;
|
|
365
|
+
value: Record<string, unknown>;
|
|
366
|
+
residue: IrResidue | null;
|
|
367
|
+
complete: boolean;
|
|
368
|
+
at: number;
|
|
369
|
+
} | {
|
|
370
|
+
type: "array_start";
|
|
371
|
+
path: JsonPath;
|
|
372
|
+
field: string;
|
|
373
|
+
at: number;
|
|
374
|
+
} | {
|
|
375
|
+
type: "complete";
|
|
376
|
+
kind: string;
|
|
377
|
+
value: unknown;
|
|
378
|
+
at: number;
|
|
379
|
+
} | {
|
|
380
|
+
type: "error";
|
|
381
|
+
reason: string;
|
|
382
|
+
at: number;
|
|
383
|
+
};
|
|
384
|
+
type KindStreamParserOptions = {
|
|
385
|
+
onEvent: (event: KindStreamEvent) => void;
|
|
386
|
+
schemas: Record<string, KindSchema> | SchemaResolver;
|
|
387
|
+
/**
|
|
388
|
+
* Known-context root prediction (e.g. "this agent's output schema is
|
|
389
|
+
* flashcard_set"). The root commits speculatively at `{` open — the Option-1
|
|
390
|
+
* provenance path for agents whose schemas don't carry __kind yet.
|
|
391
|
+
*/
|
|
392
|
+
expectedRootKind?: string;
|
|
393
|
+
};
|
|
394
|
+
declare class KindStreamParser {
|
|
395
|
+
private readonly options;
|
|
396
|
+
private readonly resolver;
|
|
397
|
+
private readonly stack;
|
|
398
|
+
private readonly objectKinds;
|
|
399
|
+
private readonly inlineSchemas;
|
|
400
|
+
private readonly recordSchemas;
|
|
401
|
+
private readonly rawObjectPaths;
|
|
402
|
+
/**
|
|
403
|
+
* Subtrees whose value domain is "any JSON" by schema (`json` / `json[]`
|
|
404
|
+
* fields, `record` with `values:"json"` members). OPAQUE by contract: no
|
|
405
|
+
* kind identification, no pending_kind, no raw_object degradation — unknown
|
|
406
|
+
* structure here is the declared shape, not a failure. Propagates to every
|
|
407
|
+
* descendant compound.
|
|
408
|
+
*/
|
|
409
|
+
private readonly opaquePaths;
|
|
410
|
+
private readonly deferredFields;
|
|
411
|
+
private readonly awaitingKindPaths;
|
|
412
|
+
/** Paths whose kind came from parent-schema prediction, unconfirmed so far. */
|
|
413
|
+
private readonly speculativeKinds;
|
|
414
|
+
/**
|
|
415
|
+
* Kind named by the root object's FIRST key through the `json_root_key`
|
|
416
|
+
* surface registry — a candidate, adopted only at root finalize.
|
|
417
|
+
*/
|
|
418
|
+
private rootSurfaceKind;
|
|
419
|
+
/** kind → paths (by key) waiting for the resolver's cold fetch. */
|
|
420
|
+
private readonly pendingSchemaPaths;
|
|
421
|
+
/** Pending-schema paths whose object already closed. */
|
|
422
|
+
private readonly closedPendingPaths;
|
|
423
|
+
/** Schemas delivered via notifySchemaArrived (overlay over the resolver). */
|
|
424
|
+
private readonly arrivedSchemas;
|
|
425
|
+
private tokenizer;
|
|
426
|
+
private root;
|
|
427
|
+
private rootKind;
|
|
428
|
+
private rootDone;
|
|
429
|
+
private failed;
|
|
430
|
+
constructor(options: KindStreamParserOptions);
|
|
431
|
+
push(chunk: string): void;
|
|
432
|
+
end(): void;
|
|
433
|
+
private resolvePendingSchemasAsRaw;
|
|
434
|
+
/** True once the root value has closed — the region is fully consumed. */
|
|
435
|
+
get isComplete(): boolean;
|
|
436
|
+
get hasFailed(): boolean;
|
|
437
|
+
/**
|
|
438
|
+
* Upgrade-in-place: the registry's cold fetch answered. Pending nodes for
|
|
439
|
+
* this kind validate and complete (closed nodes retroactively); a null
|
|
440
|
+
* schema (fetch miss) drops them to raw. Safe to call after end().
|
|
441
|
+
*/
|
|
442
|
+
notifySchemaArrived(kind: string, schema: KindSchema | null): void;
|
|
443
|
+
private handleToken;
|
|
444
|
+
private handleString;
|
|
445
|
+
private handlePunctuation;
|
|
446
|
+
private beginCompound;
|
|
447
|
+
/**
|
|
448
|
+
* THE JSON-ROOT-KEY SURFACE — `content_ir.kind_surface` rows of type
|
|
449
|
+
* `json_root_key`, live at last (they were inert phantom rows until
|
|
450
|
+
* 2026-08-20).
|
|
451
|
+
*
|
|
452
|
+
* A legacy payload such as `{"quiz_title": ...}` carries no `__kind`, but
|
|
453
|
+
* the ONE surface registry knows exactly which kind that root key names —
|
|
454
|
+
* the same lookup the SERVER performs before adapting the payload
|
|
455
|
+
* (`aidream .../processing/blocks/envelope.py`). Consulting it HERE, in the
|
|
456
|
+
* shared parser core, is what makes one place decide it: both hosts — the
|
|
457
|
+
* one-shot `normalizeJsonRegion` (DB reload / reconcile) and the live
|
|
458
|
+
* `openParseSession` (streaming) — build their parser through
|
|
459
|
+
* `createKindStreamParser`, so neither passes an option and neither can
|
|
460
|
+
* drift from the other.
|
|
461
|
+
*
|
|
462
|
+
* Recorded at the first root key; ADOPTED only when the root object closes
|
|
463
|
+
* (`completeTypedObject`). That is the surface registry's complete-only
|
|
464
|
+
* convergence law, and every json_root_key row is `streaming:false` — these
|
|
465
|
+
* legacy shapes are recognised by their whole payload, so speculating
|
|
466
|
+
* mid-stream would flash a kind component over an object that may never
|
|
467
|
+
* satisfy the schema. An explicit `expectedRootKind` (an agent's declared
|
|
468
|
+
* output schema) is stronger context and always wins; an actual `__kind`
|
|
469
|
+
* still wins over both.
|
|
470
|
+
*/
|
|
471
|
+
private noteRootSurfaceKind;
|
|
472
|
+
/**
|
|
473
|
+
* Prediction from the parent schema: object field → declared kind; array
|
|
474
|
+
* item → sole itemKind; root → expectedRootKind. Only when the schema is
|
|
475
|
+
* actually resolvable (a prediction we can't validate against is not a
|
|
476
|
+
* commitment worth making).
|
|
477
|
+
*/
|
|
478
|
+
private resolveSpeculativeKind;
|
|
479
|
+
/**
|
|
480
|
+
* A schema usable for OBJECT speculation/snapshots — root-form kinds
|
|
481
|
+
* (non-object data-only shapes) are never a valid object commitment.
|
|
482
|
+
*/
|
|
483
|
+
private lookupObjectSchema;
|
|
484
|
+
private beginScalar;
|
|
485
|
+
private placeValue;
|
|
486
|
+
private acceptObjectKey;
|
|
487
|
+
private acceptColon;
|
|
488
|
+
private acceptComma;
|
|
489
|
+
private closeCompound;
|
|
490
|
+
/**
|
|
491
|
+
* True when a value placed at `path` sits directly under a json-any
|
|
492
|
+
* placement: a `json`/`json[]` FIELD, or a member of a `record` whose
|
|
493
|
+
* values are `"json"`. (Deeper descendants inherit via `opaquePaths`.)
|
|
494
|
+
*/
|
|
495
|
+
private isJsonAnyPlacement;
|
|
496
|
+
private onValueFinalized;
|
|
497
|
+
/** __kind arrived for an object — confirm speculation, identify, or backtrack. */
|
|
498
|
+
private onKindDiscriminatorArrived;
|
|
499
|
+
/** A contradicted speculation may re-tag only where the new kind is legal. */
|
|
500
|
+
private speculativeRetagAllowed;
|
|
501
|
+
private addPendingSchemaPath;
|
|
502
|
+
private completeTypedObject;
|
|
503
|
+
/** Validate + complete an object whose value carries __kind. */
|
|
504
|
+
private finalizeTypedObject;
|
|
505
|
+
/** Validate + complete an object typed purely by parent prediction. */
|
|
506
|
+
private finalizeSpeculatedObject;
|
|
507
|
+
private validateArrayItemKind;
|
|
508
|
+
private completeRoot;
|
|
509
|
+
/** Mark a node raw (node-scoped failure) without killing the stream. */
|
|
510
|
+
private markNodeRaw;
|
|
511
|
+
private emitRawObject;
|
|
512
|
+
private clearKindWait;
|
|
513
|
+
private emitFieldIfReady;
|
|
514
|
+
private flushDeferredFields;
|
|
515
|
+
private getLiveObjectValue;
|
|
516
|
+
private emitBlockSnapshotForObject;
|
|
517
|
+
/**
|
|
518
|
+
* On `complete` snapshots (and post-close schema upgrades) the frame has
|
|
519
|
+
* already been popped — the finalized value lives in the root tree.
|
|
520
|
+
*/
|
|
521
|
+
private getFinalizedObjectValue;
|
|
522
|
+
private validateFieldPlacement;
|
|
523
|
+
private emitSchemaNotices;
|
|
524
|
+
private validateObjectAgainstSchema;
|
|
525
|
+
private validateFinalFieldValue;
|
|
526
|
+
private validateValueAgainstField;
|
|
527
|
+
private validateScalarField;
|
|
528
|
+
private validateRecordScalar;
|
|
529
|
+
private validateRecordObject;
|
|
530
|
+
private validateObjectAgainstFields;
|
|
531
|
+
private registerObjectContext;
|
|
532
|
+
private resolveParentFieldSchema;
|
|
533
|
+
private getObjectKindForPath;
|
|
534
|
+
private getDirectObjectKind;
|
|
535
|
+
private isAllowedSchemaField;
|
|
536
|
+
private parentFieldName;
|
|
537
|
+
private fieldKeyFromPath;
|
|
538
|
+
private isKindFieldPath;
|
|
539
|
+
private pathKey;
|
|
540
|
+
private lookupSchema;
|
|
541
|
+
private fail;
|
|
542
|
+
private emit;
|
|
543
|
+
private currentFrame;
|
|
544
|
+
}
|
|
545
|
+
declare function createKindStreamParser(options: KindStreamParserOptions): KindStreamParser;
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* IrTree — the immutable, structurally-shared view of a parsed region.
|
|
549
|
+
*
|
|
550
|
+
* Consumes KindStreamEvents and maintains one node per kind-resolved path.
|
|
551
|
+
* Node values are STABILIZED: compound children that are themselves kind
|
|
552
|
+
* nodes are substituted by their current tree value, so an update to
|
|
553
|
+
* cards[7] produces new identities for root → cards → cards[7] ONLY;
|
|
554
|
+
* cards[0..6] keep referential identity and memoized components bail out.
|
|
555
|
+
*
|
|
556
|
+
* Parser frame values are NEVER exposed — everything handed out is rebuilt
|
|
557
|
+
* copy-on-write, so it is safe to freeze (Redux dev-mode immutability) and
|
|
558
|
+
* safe to hold across renders.
|
|
559
|
+
*
|
|
560
|
+
* Both the live ParseSession and the one-shot normalizer build their
|
|
561
|
+
* CanonicalBlockIR from this tree — one assembly path, structurally
|
|
562
|
+
* identical output for stream and static input.
|
|
563
|
+
*/
|
|
564
|
+
|
|
565
|
+
interface IrTreeNode {
|
|
566
|
+
kind: string;
|
|
567
|
+
kindState: IrKindState;
|
|
568
|
+
path: IrPath;
|
|
569
|
+
pathKey: string;
|
|
570
|
+
/** Stabilized immutable snapshot value (schema fields + __kind only). */
|
|
571
|
+
value: Record<string, unknown>;
|
|
572
|
+
residue: IrResidue | null;
|
|
573
|
+
complete: boolean;
|
|
574
|
+
/** Bumps on every update to this node (incl. child propagation). */
|
|
575
|
+
version: number;
|
|
576
|
+
}
|
|
577
|
+
declare class IrTree {
|
|
578
|
+
private readonly nodes;
|
|
579
|
+
private readonly rawPaths;
|
|
580
|
+
private readonly dirty;
|
|
581
|
+
/**
|
|
582
|
+
* KIND PRESERVATION (streaming db/cloud kinds): pathKey → identified kind
|
|
583
|
+
* for nodes whose kind is KNOWN (kind_identified / pending_schema) but that
|
|
584
|
+
* have no snapshot node yet because the schema is still cold-fetching.
|
|
585
|
+
* Without this, the envelope reports `kind: ""` for the whole pending
|
|
586
|
+
* window and the render seam can only show raw JSON.
|
|
587
|
+
*/
|
|
588
|
+
private readonly identifiedKinds;
|
|
589
|
+
/** pathKeys currently waiting on a schema cold fetch. */
|
|
590
|
+
private readonly pendingSchemaPaths;
|
|
591
|
+
/**
|
|
592
|
+
* Early top-level scalar fields (title, loading_message, …) captured for
|
|
593
|
+
* identified-but-schema-pending nodes — the loading-component fuel. Scalars
|
|
594
|
+
* only (no live parser references can escape through here). Superseded the
|
|
595
|
+
* moment a real snapshot node exists.
|
|
596
|
+
*/
|
|
597
|
+
private readonly earlyFields;
|
|
598
|
+
/**
|
|
599
|
+
* pathKey → identified kind preserved through a SCHEMA-AVAILABILITY raw
|
|
600
|
+
* fallback (parser stamped `kind` on the raw_object event). Structural raws
|
|
601
|
+
* (missing __kind, duplicate key, validation failure) never land here.
|
|
602
|
+
*/
|
|
603
|
+
private readonly rawKinds;
|
|
604
|
+
private regionStatus;
|
|
605
|
+
private errorReason;
|
|
606
|
+
private rootRawValue;
|
|
607
|
+
/** Notices for degrades that tried to erase already-published data. */
|
|
608
|
+
private readonly rescueNotices;
|
|
609
|
+
private completedKind;
|
|
610
|
+
get status(): "streaming" | "complete" | "error";
|
|
611
|
+
applyEvent(event: KindStreamEvent): void;
|
|
612
|
+
getNode(pathKey: string): IrTreeNode | null;
|
|
613
|
+
listNodes(): IrTreeNode[];
|
|
614
|
+
isRawPath(pathKey: string): boolean;
|
|
615
|
+
/** Dirty pathKeys since the last drain — the flush/notify unit. */
|
|
616
|
+
drainDirty(): string[];
|
|
617
|
+
hasDirty(): boolean;
|
|
618
|
+
private upsertNode;
|
|
619
|
+
/**
|
|
620
|
+
* Substitute kind-node children with their current tree values so sibling
|
|
621
|
+
* identities are stable; deep-copy everything else so no live parser
|
|
622
|
+
* reference ever escapes.
|
|
623
|
+
*/
|
|
624
|
+
private stabilizeValue;
|
|
625
|
+
/**
|
|
626
|
+
* COW spine rebuild: replace the child's slot in each ancestor kind-node's
|
|
627
|
+
* value, shallow-copying only the containers along the way. Siblings keep
|
|
628
|
+
* identity; every ancestor gets a new value identity + version bump.
|
|
629
|
+
*/
|
|
630
|
+
private propagateToAncestors;
|
|
631
|
+
private findNearestAncestorNode;
|
|
632
|
+
private cloneAlong;
|
|
633
|
+
private markRaw;
|
|
634
|
+
/**
|
|
635
|
+
* A rescue means a degrade tried to erase data a user could already see —
|
|
636
|
+
* an upstream defect. It rides the envelope as a notice so it surfaces in the
|
|
637
|
+
* Error Inspector instead of being silently absorbed (`core/` is a pure
|
|
638
|
+
* kernel: no console, no capture — the notice IS the alarm).
|
|
639
|
+
*/
|
|
640
|
+
private recordRescue;
|
|
641
|
+
/**
|
|
642
|
+
* Assemble the canonical envelope. ONE code path for stream + one-shot.
|
|
643
|
+
* Callers supply the fingerprint (one-shot hashes the source; live sessions
|
|
644
|
+
* keep an incremental hasher so no per-flush re-hash happens).
|
|
645
|
+
*/
|
|
646
|
+
buildEnvelope(fingerprint: string): CanonicalBlockIR;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Character-level incremental JSON tokenizer. Survives chunk boundaries
|
|
651
|
+
* mid-token (strings, escapes, unicode sequences, primitives). Pure — no
|
|
652
|
+
* React, no Redux, no IO.
|
|
653
|
+
*
|
|
654
|
+
* Moved from app/(dev)/demos/json-block-detector/reusable-logic.ts (the demo
|
|
655
|
+
* is now a consumer of this library).
|
|
656
|
+
*/
|
|
657
|
+
type Punct = "{" | "}" | "[" | "]" | ":" | ",";
|
|
658
|
+
type JsonToken = {
|
|
659
|
+
type: "punct";
|
|
660
|
+
value: Punct;
|
|
661
|
+
at: number;
|
|
662
|
+
} | {
|
|
663
|
+
type: "string";
|
|
664
|
+
value: string;
|
|
665
|
+
at: number;
|
|
666
|
+
} | {
|
|
667
|
+
type: "number";
|
|
668
|
+
value: number;
|
|
669
|
+
at: number;
|
|
670
|
+
} | {
|
|
671
|
+
type: "boolean";
|
|
672
|
+
value: boolean;
|
|
673
|
+
at: number;
|
|
674
|
+
} | {
|
|
675
|
+
type: "null";
|
|
676
|
+
value: null;
|
|
677
|
+
at: number;
|
|
678
|
+
};
|
|
679
|
+
declare class JsonStreamTokenizer {
|
|
680
|
+
private readonly onToken;
|
|
681
|
+
private mode;
|
|
682
|
+
private pos;
|
|
683
|
+
private stringBuffer;
|
|
684
|
+
private primitiveBuffer;
|
|
685
|
+
private unicodeBuffer;
|
|
686
|
+
private tokenStart;
|
|
687
|
+
constructor(onToken: (token: JsonToken) => void);
|
|
688
|
+
get position(): number;
|
|
689
|
+
push(chunk: string): void;
|
|
690
|
+
end(): void;
|
|
691
|
+
private handleNormalChar;
|
|
692
|
+
private emitPrimitive;
|
|
693
|
+
private isDelimiter;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Compliant snapshot builder — the guarantee that a renderer always receives
|
|
698
|
+
* a well-formed, schema-shaped object even mid-stream.
|
|
699
|
+
*
|
|
700
|
+
* ZERO DATA LOSS: unknown keys are NOT merged into the snapshot value (they
|
|
701
|
+
* would be indistinguishable from schema fields). They are returned on the
|
|
702
|
+
* residue channel and re-merged only at wire-serialization time via
|
|
703
|
+
* `mergeResidueIntoValue`.
|
|
704
|
+
*/
|
|
705
|
+
|
|
706
|
+
/** Placeholder for a required field not yet received during streaming. */
|
|
707
|
+
declare function emptyValueForFieldSchema(field: FieldSchema): unknown;
|
|
708
|
+
interface CompliantKindSnapshot {
|
|
709
|
+
/** Schema fields + __kind only. Required-but-missing fields hold typed placeholders. */
|
|
710
|
+
value: Record<string, unknown>;
|
|
711
|
+
/** Unknown keys + missing optionals. Null when both channels are empty. */
|
|
712
|
+
residue: IrResidue | null;
|
|
713
|
+
}
|
|
714
|
+
declare function buildCompliantKindSnapshot(schema: KindSchema, partial: Record<string, unknown>): CompliantKindSnapshot;
|
|
715
|
+
/**
|
|
716
|
+
* Wire/round-trip form: schema fields + unknown keys back together, exactly
|
|
717
|
+
* as the source carried them. `residue.extra` wins nothing — snapshot value
|
|
718
|
+
* and extras are disjoint by construction.
|
|
719
|
+
*/
|
|
720
|
+
declare function mergeResidueIntoValue(value: Record<string, unknown>, residue: IrResidue | null): Record<string, unknown>;
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Idempotent normalizer — the "recognizes its own work" property.
|
|
724
|
+
*
|
|
725
|
+
* THE LAW: anything already carrying a current CanonicalBlockIR envelope is
|
|
726
|
+
* returned BY REFERENCE — zero reprocessing, reference equality holds, React
|
|
727
|
+
* bails out. Only raw text (a detected region's source) is ever parsed, and
|
|
728
|
+
* it is parsed exactly once per fingerprint.
|
|
729
|
+
*
|
|
730
|
+
* `normalizeJsonRegion` is the one-shot mode: the same KindStreamParser that
|
|
731
|
+
* powers live streams, run over a complete region string (DB reloads,
|
|
732
|
+
* reconcile passes), assembled through the same IrTree the live session uses
|
|
733
|
+
* — stream and static output are structurally identical by construction.
|
|
734
|
+
*/
|
|
735
|
+
|
|
736
|
+
declare function isCanonicalBlockIR(value: unknown): value is CanonicalBlockIR;
|
|
737
|
+
/**
|
|
738
|
+
* The idempotence fast path: return the existing envelope by reference when
|
|
739
|
+
* it still describes this source text; null means "parse needed".
|
|
740
|
+
*/
|
|
741
|
+
declare function reuseEnvelopeIfCurrent(source: string, candidate: unknown): CanonicalBlockIR | null;
|
|
742
|
+
interface NormalizeJsonRegionOptions {
|
|
743
|
+
schemas: Record<string, KindSchema> | SchemaResolver;
|
|
744
|
+
/** Known-context root prediction (agent output schema, fence hint). */
|
|
745
|
+
expectedRootKind?: string;
|
|
746
|
+
/**
|
|
747
|
+
* Pass a previously persisted envelope (message metadata, artifact row);
|
|
748
|
+
* when its fingerprint matches, it is returned as-is and nothing parses.
|
|
749
|
+
*/
|
|
750
|
+
existing?: unknown;
|
|
751
|
+
}
|
|
752
|
+
interface CompleteValueEnvelopeOptions {
|
|
753
|
+
/**
|
|
754
|
+
* Wire discriminator recorded on the root node — which syntax established
|
|
755
|
+
* the kind. Defaults to the JSON `__kind` key; XML surfaces converging at
|
|
756
|
+
* region finalize pass `xmlDiscriminator(tag)` so round-trip serializers
|
|
757
|
+
* know the original arrival format.
|
|
758
|
+
*/
|
|
759
|
+
discriminator?: IrDiscriminator;
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Build a resolved, complete envelope directly from an already-structured
|
|
763
|
+
* value (a persisted artifact's `content.data` object, or a completed XML
|
|
764
|
+
* region's strategy output). This is the zero-reprocessing rehydration path:
|
|
765
|
+
* the value IS the reconstructed region value (schema fields + residue extras
|
|
766
|
+
* merged at persist time), so no tokenizer/parser run is needed — the
|
|
767
|
+
* envelope wraps it verbatim. The fingerprint hashes the canonical value
|
|
768
|
+
* serialization, so any two paths producing the same value produce the SAME
|
|
769
|
+
* envelope (stream ≡ static by construction).
|
|
770
|
+
*/
|
|
771
|
+
declare function envelopeFromCompleteValue(value: Record<string, unknown>, kind: string, options?: CompleteValueEnvelopeOptions): CanonicalBlockIR;
|
|
772
|
+
/** One-shot: complete region text in → canonical envelope out. */
|
|
773
|
+
declare function normalizeJsonRegion(source: string, options: NormalizeJsonRegionOptions): CanonicalBlockIR;
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Stable, fast content fingerprint for IR envelopes.
|
|
777
|
+
*
|
|
778
|
+
* Used as the idempotence / cache key: a persisted CanonicalBlockIR is only
|
|
779
|
+
* reused when its fingerprint matches the region source text it claims to
|
|
780
|
+
* represent. Not cryptographic — collision resistance at the "same message,
|
|
781
|
+
* same block" scale is all that's required, and it must be synchronous and
|
|
782
|
+
* dependency-free (runs per region on the hot streaming path).
|
|
783
|
+
*
|
|
784
|
+
* FNV-1a 32-bit, applied twice with different seeds and concatenated, so a
|
|
785
|
+
* single 32-bit collision doesn't alias two regions.
|
|
786
|
+
*
|
|
787
|
+
* `createFingerprinter` is the incremental form for live streams: feeding
|
|
788
|
+
* chunks one at a time yields EXACTLY the same fingerprint as
|
|
789
|
+
* `fingerprintText` over the concatenation — sessions never re-hash the
|
|
790
|
+
* whole source per flush.
|
|
791
|
+
*/
|
|
792
|
+
interface Fingerprinter {
|
|
793
|
+
push(chunk: string): void;
|
|
794
|
+
/** Fingerprint of everything pushed so far. */
|
|
795
|
+
current(): string;
|
|
796
|
+
}
|
|
797
|
+
declare function createFingerprinter(): Fingerprinter;
|
|
798
|
+
declare function fingerprintText(source: string): string;
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Discriminator abstraction — how a node's kind is established from the wire.
|
|
802
|
+
*
|
|
803
|
+
* The parser never hardcodes "kind comes from a JSON key". JSON resolves via
|
|
804
|
+
* the `__kind` field (implemented today); XML resolves via tag → registry
|
|
805
|
+
* alias (Phase 6 — contract only). `IrStructuredNode.discriminator` records
|
|
806
|
+
* which resolver produced each node so renderers and round-trip serializers
|
|
807
|
+
* can reconstruct the original wire format per node.
|
|
808
|
+
*/
|
|
809
|
+
|
|
810
|
+
declare const JSON_DISCRIMINATOR: IrDiscriminator;
|
|
811
|
+
declare function xmlDiscriminator(tag: string): IrDiscriminator;
|
|
812
|
+
declare function fenceDiscriminator(language: string): IrDiscriminator;
|
|
813
|
+
/** What a resolver can say about an opening compound value. */
|
|
814
|
+
type KindResolution = {
|
|
815
|
+
outcome: "kind";
|
|
816
|
+
kind: string;
|
|
817
|
+
} | {
|
|
818
|
+
outcome: "pending";
|
|
819
|
+
} | {
|
|
820
|
+
outcome: "raw";
|
|
821
|
+
reason: string;
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* IrEnvelopeCache — the persisted envelope cache carried on MESSAGE PARTS.
|
|
826
|
+
*
|
|
827
|
+
* Phase 5 (reload without re-parse): at stream end, `assembleMessageParts`
|
|
828
|
+
* stamps every completed JSON region's CanonicalBlockIR onto the committed
|
|
829
|
+
* `CxTextContent.metadata.__ir` as this cache — keyed by fingerprint, because
|
|
830
|
+
* one text part can embed several regions. On reload the splitter's envelope
|
|
831
|
+
* memo consults the seeded cache (`registry/region-envelope-memo.ts`) and
|
|
832
|
+
* reuses the persisted envelope BY REFERENCE via `reuseEnvelopeIfCurrent`
|
|
833
|
+
* (exact fingerprint match of the detected region source) — zero re-parse.
|
|
834
|
+
*
|
|
835
|
+
* Shape discipline: the SAME `__ir` metadata key carries two shapes at two
|
|
836
|
+
* levels, disambiguated by validators that reject each other —
|
|
837
|
+
* - render block metadata → a single CanonicalBlockIR (`isCanonicalBlockIR`)
|
|
838
|
+
* - message part metadata → this cache (`isIrEnvelopeCache`)
|
|
839
|
+
*
|
|
840
|
+
* Pure kernel module: types + validators only. No React/Redux/Supabase.
|
|
841
|
+
*/
|
|
842
|
+
|
|
843
|
+
/** Cache version. Bump = migration point for persisted part caches. */
|
|
844
|
+
declare const IR_ENVELOPE_CACHE_VERSION: 1;
|
|
845
|
+
/**
|
|
846
|
+
* The envelope cache persisted on a message part's `metadata.__ir`.
|
|
847
|
+
* `blocks` maps each region-source fingerprint to its complete envelope.
|
|
848
|
+
*/
|
|
849
|
+
interface IrEnvelopeCache {
|
|
850
|
+
v: typeof IR_ENVELOPE_CACHE_VERSION;
|
|
851
|
+
blocks: Record<string, CanonicalBlockIR>;
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Strict whole-cache validation: every entry must be a complete
|
|
855
|
+
* CanonicalBlockIR keyed by its OWN fingerprint. A cache failing this guard
|
|
856
|
+
* came from a buggy or foreign writer — callers surface that loudly and seed
|
|
857
|
+
* nothing (a half-trusted cache is how poisoned envelopes reach renderers).
|
|
858
|
+
*/
|
|
859
|
+
declare function isIrEnvelopeCache(value: unknown): value is IrEnvelopeCache;
|
|
860
|
+
/**
|
|
861
|
+
* Build the persistable cache from a run's collected envelopes. Only
|
|
862
|
+
* complete envelopes are cacheable (a streaming/error envelope can never be
|
|
863
|
+
* reused — its fingerprint doesn't describe a finished region). Duplicate
|
|
864
|
+
* regions (same fingerprint) collapse to one entry. Engine-agnostic by
|
|
865
|
+
* design: an aidream-built `engine: "py-block-detector"` envelope is cached
|
|
866
|
+
* identically to an FE-parsed one. Returns null when nothing qualifies so
|
|
867
|
+
* callers skip the metadata stamp entirely.
|
|
868
|
+
*/
|
|
869
|
+
declare function envelopeCacheFromEnvelopes(envelopes: readonly CanonicalBlockIR[]): IrEnvelopeCache | null;
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* PURE envelope ingestion — reading `metadata.__ir` off a block's metadata
|
|
873
|
+
* and gating SERVER-BUILT envelopes at the wire boundary. Twin-safe: no
|
|
874
|
+
* React / Redux / Supabase / host diagnostics — side effects (seeding the
|
|
875
|
+
* region-envelope memo, screaming to the Error Inspector) are injected by
|
|
876
|
+
* the host through `InboundEnvelopeHooks`.
|
|
877
|
+
*
|
|
878
|
+
* The frontend host shell is `redux/render-block-envelope.ts` (binds the
|
|
879
|
+
* hooks to `seedEnvelope` + `captureError`); aidream's Workflow Studio binds
|
|
880
|
+
* its own. Contract: features/content-ir/docs/PYTHON_ENVELOPE_CONTRACT.md.
|
|
881
|
+
*/
|
|
882
|
+
|
|
883
|
+
/** Read a CanonicalBlockIR envelope off a block's metadata (or anything). */
|
|
884
|
+
declare function readEnvelope(metadata: Record<string, unknown> | null | undefined): CanonicalBlockIR | null;
|
|
885
|
+
/** Pure classification of inbound metadata carrying (or not) an envelope. */
|
|
886
|
+
type InboundEnvelopeVerdict = {
|
|
887
|
+
/** No `__ir` key — zero-touch pass, same reference back. */
|
|
888
|
+
outcome: "absent";
|
|
889
|
+
metadata: Record<string, unknown> | undefined;
|
|
890
|
+
} | {
|
|
891
|
+
/** Valid CanonicalBlockIR — same reference back (idempotence law). */
|
|
892
|
+
outcome: "valid";
|
|
893
|
+
metadata: Record<string, unknown>;
|
|
894
|
+
envelope: CanonicalBlockIR;
|
|
895
|
+
} | {
|
|
896
|
+
/** Malformed/foreign `__ir` — a COPY with `__ir` stripped. */
|
|
897
|
+
outcome: "malformed";
|
|
898
|
+
metadata: Record<string, unknown>;
|
|
899
|
+
engine: string;
|
|
900
|
+
raw: unknown;
|
|
901
|
+
};
|
|
902
|
+
declare function classifyInboundEnvelopeMetadata(metadata: Record<string, unknown> | null | undefined): InboundEnvelopeVerdict;
|
|
903
|
+
/** Host-injected side effects for the inbound gate. */
|
|
904
|
+
interface InboundEnvelopeHooks {
|
|
905
|
+
/** Called with every VALID envelope so later re-splits reuse it by reference. */
|
|
906
|
+
seedEnvelope?: (envelope: CanonicalBlockIR) => void;
|
|
907
|
+
/** Called LOUDLY for every malformed envelope — a bad envelope is a defect. */
|
|
908
|
+
reportMalformed?: (info: {
|
|
909
|
+
blockId: string;
|
|
910
|
+
engine: string;
|
|
911
|
+
raw: unknown;
|
|
912
|
+
}) => void;
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Ingest guard for SERVER-BUILT envelopes riding `metadata.__ir` on a
|
|
916
|
+
* `render_block` event.
|
|
917
|
+
*
|
|
918
|
+
* - No `__ir` key → the SAME metadata reference back (zero-touch pass).
|
|
919
|
+
* - Valid CanonicalBlockIR → the SAME metadata reference back (reuse-by-
|
|
920
|
+
* reference — the idempotence law) AND `hooks.seedEnvelope` fires so any
|
|
921
|
+
* later re-split of the same region source reuses it instead of parsing.
|
|
922
|
+
* - Malformed/foreign `__ir` → a COPY with `__ir` stripped, plus a loud
|
|
923
|
+
* `hooks.reportMalformed`. A bad envelope must never poison kind routing
|
|
924
|
+
* or the persistence cache; dropping it degrades that block to the
|
|
925
|
+
* ordinary content-driven path, nothing more.
|
|
926
|
+
*/
|
|
927
|
+
declare function sanitizeInboundEnvelopeMetadata(metadata: Record<string, unknown> | null | undefined, context: {
|
|
928
|
+
blockId: string;
|
|
929
|
+
}, hooks?: InboundEnvelopeHooks): Record<string, unknown> | undefined;
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Pure envelope→value functions (the zero-data-loss wire round-trip).
|
|
933
|
+
*
|
|
934
|
+
* These live in core/ (the pure kernel) because every layer needs them —
|
|
935
|
+
* including kinds/legacy-bridge-utils, whose bridges are built at MODULE
|
|
936
|
+
* SCOPE inside kinds/<slug>.ts files that system-kinds.ts imports. When these
|
|
937
|
+
* functions lived in redux/render-block-envelope.ts, that file's memo/capture
|
|
938
|
+
* imports closed a module cycle (kinds → bridge-utils → render-block-envelope
|
|
939
|
+
* → region-envelope-memo → kind-registry → system-kinds → kinds), which made
|
|
940
|
+
* SYSTEM_KIND_DEFINITIONS entry-point-fragile (undefined defs when a kinds
|
|
941
|
+
* module was the import entry). Keeping them here makes every kinds module
|
|
942
|
+
* cycle-free BY CONSTRUCTION. redux/render-block-envelope re-exports them for
|
|
943
|
+
* its existing consumers.
|
|
944
|
+
*/
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Rebuild the region's full data: walk the root value and merge each node's
|
|
948
|
+
* residue extras back in (root residue + nodeIndex residues). This is the
|
|
949
|
+
* zero-data-loss read: nothing the model emitted is missing, whether or not
|
|
950
|
+
* a schema knew about it.
|
|
951
|
+
*/
|
|
952
|
+
declare function reconstructRegionValue(envelope: CanonicalBlockIR): Record<string, unknown>;
|
|
953
|
+
/** Deep-remove the __kind discriminator (the parser injects it into snapshots). */
|
|
954
|
+
declare function stripKindDeep(value: unknown): unknown;
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* ContentRegion — the handshake between a host detector and this library.
|
|
958
|
+
*
|
|
959
|
+
* Boundary rule (the answer to the single-root problem): the HOST detector
|
|
960
|
+
* (StreamBlockAccumulator on streams, content-splitter-v2 on DB loads) finds
|
|
961
|
+
* where a structured region begins and ends inside prose; the kind parser
|
|
962
|
+
* owns everything INSIDE the region. A fresh parser is created per region, so
|
|
963
|
+
* "one root object" is true within a region by construction, and multiple
|
|
964
|
+
* blocks per message are just multiple regions.
|
|
965
|
+
*
|
|
966
|
+
* Through Phase 6 the host's fence-close / brace-count logic remains the
|
|
967
|
+
* region-end oracle (bit-level parity with today). In Phase 7 the parser's
|
|
968
|
+
* own frame stack becomes the oracle and the mirrored counters are deleted.
|
|
969
|
+
*/
|
|
970
|
+
type RegionFormat = "json";
|
|
971
|
+
type RegionSourceKind = "fence" | "bare";
|
|
972
|
+
interface ContentRegionInit {
|
|
973
|
+
regionId: string;
|
|
974
|
+
format: RegionFormat;
|
|
975
|
+
sourceKind: RegionSourceKind;
|
|
976
|
+
}
|
|
977
|
+
/** How a region ended. Truncation is a normal outcome, never stream-fatal. */
|
|
978
|
+
type RegionEndReason = "closed" | "truncated" | "aborted";
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* Schema-shape helpers — structural depth and layout hints derived from a
|
|
982
|
+
* KindSchema. Drives the generic renderer's flat / grid / nested layout and
|
|
983
|
+
* human labels for slugs.
|
|
984
|
+
*
|
|
985
|
+
* Moved from app/(dev)/demos/json-block-detector/schema-structure.ts.
|
|
986
|
+
*/
|
|
987
|
+
|
|
988
|
+
/** Human label from a slug or field key (snake_case → Title Case + acronym fixes). */
|
|
989
|
+
declare function formatBlockLabel(key: string): string;
|
|
990
|
+
/**
|
|
991
|
+
* Structural nesting depth of a kind schema — drives generic renderer layout.
|
|
992
|
+
* 0 = flat card (scalars only)
|
|
993
|
+
* 1 = grid (one array/object nesting level)
|
|
994
|
+
* 2+ = expandable grid (parent → child → grandchild)
|
|
995
|
+
*/
|
|
996
|
+
declare function schemaStructureDepth(schema: KindSchema, allSchemas: Record<string, KindSchema>, visiting?: Set<string>): number;
|
|
997
|
+
type SchemaLayoutMode = "flat" | "grid" | "nested";
|
|
998
|
+
declare function schemaLayoutMode(schema: KindSchema, allSchemas: Record<string, KindSchema>): SchemaLayoutMode;
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* ParseSession — one live region parse per stream identity.
|
|
1002
|
+
*
|
|
1003
|
+
* Owns the parser + IrTree OUTSIDE React and Redux. Writers push chunks;
|
|
1004
|
+
* N readers subscribe per path (useIrNode / selectors) and are notified on
|
|
1005
|
+
* the host's flush cadence — never per token.
|
|
1006
|
+
*
|
|
1007
|
+
* Notification contract: `write()`/`end()` never notify. The host (demo rAF
|
|
1008
|
+
* loop, process-stream's dispatchBatch tick) calls `flushNotify()` to publish
|
|
1009
|
+
* dirty paths. Late schema arrivals (registry cold fetch) notify themselves —
|
|
1010
|
+
* they happen outside any streaming tick.
|
|
1011
|
+
*/
|
|
1012
|
+
|
|
1013
|
+
interface ParseSessionOptions {
|
|
1014
|
+
identity: string;
|
|
1015
|
+
schemas: Record<string, KindSchema> | SchemaResolver;
|
|
1016
|
+
expectedRootKind?: string;
|
|
1017
|
+
/**
|
|
1018
|
+
* Registry hook: subscribe to cold-fetch answers, return an unsubscribe.
|
|
1019
|
+
* The session forwards arrivals into the parser (upgrade-in-place) and
|
|
1020
|
+
* notifies affected readers immediately.
|
|
1021
|
+
*/
|
|
1022
|
+
onSchemaArrived?: (deliver: (kind: string, schema: KindSchema | null) => void) => () => void;
|
|
1023
|
+
/** Observe every parser event (demo inspector, telemetry). */
|
|
1024
|
+
onEvent?: (event: KindStreamEvent) => void;
|
|
1025
|
+
}
|
|
1026
|
+
type Listener = () => void;
|
|
1027
|
+
declare class ParseSession {
|
|
1028
|
+
readonly identity: string;
|
|
1029
|
+
private readonly parser;
|
|
1030
|
+
private readonly tree;
|
|
1031
|
+
private readonly listeners;
|
|
1032
|
+
private readonly anyListeners;
|
|
1033
|
+
private readonly unsubscribeSchemaArrivals;
|
|
1034
|
+
private readonly fingerprinter;
|
|
1035
|
+
private ended;
|
|
1036
|
+
constructor(options: ParseSessionOptions);
|
|
1037
|
+
/** The single writer's input. Does NOT notify — host flushes on its cadence. */
|
|
1038
|
+
write(chunk: string): void;
|
|
1039
|
+
end(): void;
|
|
1040
|
+
get isEnded(): boolean;
|
|
1041
|
+
get status(): "streaming" | "complete" | "error";
|
|
1042
|
+
getNode(pathKey: string): IrTreeNode | null;
|
|
1043
|
+
listNodes(): IrTreeNode[];
|
|
1044
|
+
isRawPath(pathKey: string): boolean;
|
|
1045
|
+
subscribe(pathKey: string, listener: Listener): () => void;
|
|
1046
|
+
/** Structural subscription: fires when ANY node changes (mount lists). */
|
|
1047
|
+
subscribeAny(listener: Listener): () => void;
|
|
1048
|
+
/** Publish dirty paths to their readers. Host-cadence, coalesced. */
|
|
1049
|
+
flushNotify(): void;
|
|
1050
|
+
buildEnvelope(): CanonicalBlockIR;
|
|
1051
|
+
dispose(): void;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* Session manager — one writer per stream identity, N readers, mechanically
|
|
1056
|
+
* enforced: a second `openParseSession` for a live identity THROWS. Readers
|
|
1057
|
+
* resolve sessions by identity and never gain write access.
|
|
1058
|
+
*
|
|
1059
|
+
* Identities: `"{requestId}:{blockId}"` for agent-stream regions; any unique
|
|
1060
|
+
* string for other hosts (demo runs, notes, one-shot upgrades).
|
|
1061
|
+
*
|
|
1062
|
+
* Ended sessions stay resolvable for late readers (reload happens via the
|
|
1063
|
+
* persisted envelope, but in-session remounts read here) until disposed.
|
|
1064
|
+
*/
|
|
1065
|
+
|
|
1066
|
+
declare function openParseSession(options: ParseSessionOptions): ParseSession;
|
|
1067
|
+
declare function getParseSession(identity: string): ParseSession | null;
|
|
1068
|
+
declare function disposeParseSession(identity: string): void;
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* KindDefinition — one kind, many facets. THE canonical registry entry.
|
|
1072
|
+
*
|
|
1073
|
+
* This does NOT merge BlockComponentRegistry or artifact-type-registry; the
|
|
1074
|
+
* `legacyBlockType` and `artifact` facets are FACADES pointing into them, so
|
|
1075
|
+
* migration is incremental and each registry keeps its own job.
|
|
1076
|
+
*/
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* The uniform props contract for kind-driven block components. This is what
|
|
1080
|
+
* de-special-cases flashcards: no component receives bespoke glue — every
|
|
1081
|
+
* kind component gets exactly this shape.
|
|
1082
|
+
*/
|
|
1083
|
+
interface KindBlockProps {
|
|
1084
|
+
kind: string;
|
|
1085
|
+
schema: KindSchema;
|
|
1086
|
+
/** Compliant snapshot value (schema fields + __kind). */
|
|
1087
|
+
data: Record<string, unknown>;
|
|
1088
|
+
status: "streaming" | "complete" | "error";
|
|
1089
|
+
residue: IrResidue | null;
|
|
1090
|
+
path: IrPath;
|
|
1091
|
+
/** ParseSession identity for live child subscriptions; null after reload. */
|
|
1092
|
+
identity: string | null;
|
|
1093
|
+
/** Child-kind schema lookup (replaces passing allSchemas around). */
|
|
1094
|
+
resolve: (kind: string) => KindSchema | undefined;
|
|
1095
|
+
}
|
|
1096
|
+
type KindTier = "eager" | "warm" | "cold";
|
|
1097
|
+
interface KindDefinition {
|
|
1098
|
+
/** Canonical slug — THE key. */
|
|
1099
|
+
kind: string;
|
|
1100
|
+
/** null until the warm/cold fetch delivers it. */
|
|
1101
|
+
schema: KindSchema | null;
|
|
1102
|
+
schemaSource: "system" | "flexible_data" | "content_ir";
|
|
1103
|
+
tier: KindTier;
|
|
1104
|
+
/** Component facet — lazy-loaded renderer for this kind. */
|
|
1105
|
+
component?: {
|
|
1106
|
+
load: () => Promise<{
|
|
1107
|
+
default: ComponentType<KindBlockProps>;
|
|
1108
|
+
}>;
|
|
1109
|
+
};
|
|
1110
|
+
/** Facade → BlockComponentRegistry type string (e.g. "flashcards"). */
|
|
1111
|
+
legacyBlockType?: string;
|
|
1112
|
+
/**
|
|
1113
|
+
* Legacy-bridge facet: derive the existing component's `serverData` from a
|
|
1114
|
+
* canonical envelope. This is what lets a kind light up the REAL component
|
|
1115
|
+
* (FlashcardsBlock, …) with zero component changes during migration.
|
|
1116
|
+
*/
|
|
1117
|
+
toLegacyServerData?: (envelope: CanonicalBlockIR) => Record<string, unknown> | undefined;
|
|
1118
|
+
/** Facade → artifact-type-registry canvasType. */
|
|
1119
|
+
artifact?: {
|
|
1120
|
+
canvasType: string;
|
|
1121
|
+
};
|
|
1122
|
+
/**
|
|
1123
|
+
* Markdown export facet — the FORWARD leg of artifact ⇄ markdown. Receives
|
|
1124
|
+
* the ZERO-LOSS reconstructed value object (the CALLER reconstructs — for
|
|
1125
|
+
* persisted structured artifacts `content.data` already IS that object;
|
|
1126
|
+
* this facet only renders it; `__kind` discriminators may still be
|
|
1127
|
+
* present and must be ignored). MUST produce human-readable markdown
|
|
1128
|
+
* (headings / lists / bold), never a JSON dump — a fenced json body under
|
|
1129
|
+
* a heading is acceptable only for inherently-code payloads (e.g. a
|
|
1130
|
+
* schema_proposal's JSON Schema). Unknown extra keys the renderer doesn't
|
|
1131
|
+
* understand MUST be appended under a small "Additional details"
|
|
1132
|
+
* (key: value) section so nothing silently vanishes. Kinds without this
|
|
1133
|
+
* facet fall back to `genericKindMarkdown` (kinds/kind-markdown-utils.ts).
|
|
1134
|
+
*/
|
|
1135
|
+
toMarkdown?: (value: Record<string, unknown>) => string;
|
|
1136
|
+
persistence?: {
|
|
1137
|
+
persistStructured: boolean;
|
|
1138
|
+
};
|
|
1139
|
+
/** Future XML tags / kind aliases resolving to this kind. */
|
|
1140
|
+
discriminatorAliases?: string[];
|
|
1141
|
+
/**
|
|
1142
|
+
* PARTIAL-READY opt-in — the streaming partial-kinds posture.
|
|
1143
|
+
*
|
|
1144
|
+
* While a structured region streams, the server may announce a PROVISIONAL
|
|
1145
|
+
* instance of this kind on `metadata.__ir_partial` (valid, closed JSON that
|
|
1146
|
+
* may be missing required fields). The default posture is WITHHOLD: a
|
|
1147
|
+
* provisional value is never routed to a component, and the block keeps its
|
|
1148
|
+
* loading skeleton until the region completes — because a component that
|
|
1149
|
+
* throws on an absent field must not be handed one.
|
|
1150
|
+
*
|
|
1151
|
+
* Setting this to `true` declares "this kind's component renders a partial
|
|
1152
|
+
* value without throwing", and the provisional value is routed to the SAME
|
|
1153
|
+
* component that renders the final one, filling in as tokens arrive. A kind
|
|
1154
|
+
* with a `toLegacyServerData` bridge must ALSO pass `{ provisional: true }`
|
|
1155
|
+
* to `makeCompleteEnvelopeBridge` (pinned by a test). A component that
|
|
1156
|
+
* throws anyway is caught, screams, and permanently drops this kind back to
|
|
1157
|
+
* withhold for the session.
|
|
1158
|
+
*
|
|
1159
|
+
* Contract: common-docs/systems/content-ir-system/STREAMING_PARTIAL_KINDS.md
|
|
1160
|
+
*/
|
|
1161
|
+
partialReady?: boolean;
|
|
1162
|
+
/**
|
|
1163
|
+
* Loading-library slug (`kind_definition.metadata.loading_component`) —
|
|
1164
|
+
* which of the ~20 hardcoded loading components renders while this kind's
|
|
1165
|
+
* instance streams in / its schema+component fetch is in flight. Null or
|
|
1166
|
+
* unknown slugs fall back to the generic structured skeleton.
|
|
1167
|
+
*/
|
|
1168
|
+
loadingComponent?: string | null;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* Kind ↔ content_ir storage shape — the pure, DB-free reshaping between the
|
|
1173
|
+
* consumed `KindSchema` and the `content_ir.kind_definition.data` array +
|
|
1174
|
+
* `content_ir.kind_edge` rows.
|
|
1175
|
+
*
|
|
1176
|
+
* Two exact inverses:
|
|
1177
|
+
* - `kindSchemaToStorage` — the MIGRATION / write direction: a parsed
|
|
1178
|
+
* `KindSchema` (fields Record, targets inline) → an ORDERED `data` array
|
|
1179
|
+
* (targets stripped) + externalized `kind_edge` specs (targets, keyed by
|
|
1180
|
+
* field PATH). This is what pours `flexible_data` (and future authored
|
|
1181
|
+
* kinds) into the canonical tables.
|
|
1182
|
+
* - `storageToKindSchema` — the ADAPTER / read direction: `data` array +
|
|
1183
|
+
* edges → the same `KindSchema` the parser/emitter consume, re-attaching
|
|
1184
|
+
* `object.kind` / `array.itemKinds` from the edges by path.
|
|
1185
|
+
*
|
|
1186
|
+
* Design invariants (KIND_REGISTRY_STORAGE.md §4):
|
|
1187
|
+
* - The `data` element is `FieldSchema` + `name`, MINUS ref targets. Order is
|
|
1188
|
+
* intrinsic to the array (fixes the jsonb key-reorder bug).
|
|
1189
|
+
* - `kind_edge` is the SINGLE source of truth for kind→kind refs: `object`
|
|
1190
|
+
* → one edge (position null); `array` → N edges (union) ordered by
|
|
1191
|
+
* `position`. `field_name` is a dot-PATH so a ref nested inside an
|
|
1192
|
+
* `inline_object` still gets a distinct, collision-free edge.
|
|
1193
|
+
* - `inline_object` is structural: its `fields` is an ordered array, it never
|
|
1194
|
+
* becomes a registry row and never carries `__kind`. Its nested refs DO get
|
|
1195
|
+
* edges (path-prefixed) so cascade/pinning see every dependency. Its
|
|
1196
|
+
* `open` flag persists on the element (losing it is the open-empty-object
|
|
1197
|
+
* defect).
|
|
1198
|
+
* - `union.kinds` refs externalize to edges exactly like `array.itemKinds`;
|
|
1199
|
+
* the stored element keeps `hasKinds: true` so lost edges scream on read.
|
|
1200
|
+
* - A NON-OBJECT ROOT (`KindSchema.root`) stores as ONE reserved element
|
|
1201
|
+
* named `ROOT_STORAGE_NAME` ("__root") — the only element allowed in that
|
|
1202
|
+
* kind's `data`. Real fields may never use the reserved name.
|
|
1203
|
+
*
|
|
1204
|
+
* NO imports of supabase / the DB — this is pure and unit-tested by round-trip
|
|
1205
|
+
* (`__tests__/kind-storage-transform.test.ts`).
|
|
1206
|
+
*/
|
|
1207
|
+
|
|
1208
|
+
/**
|
|
1209
|
+
* Reserved `data[]` element name for a NON-OBJECT ROOT form (`KindSchema.root`).
|
|
1210
|
+
* A root-form kind stores exactly one element under this name; the read
|
|
1211
|
+
* direction reconstructs `{ root }` instead of a field map. The write
|
|
1212
|
+
* direction rejects any REAL field with this name — the name is the marker.
|
|
1213
|
+
*/
|
|
1214
|
+
declare const ROOT_STORAGE_NAME = "__root";
|
|
1215
|
+
type StoredFieldBase = {
|
|
1216
|
+
name: string;
|
|
1217
|
+
required?: boolean;
|
|
1218
|
+
nullable?: boolean;
|
|
1219
|
+
/** Human guidance — mirrors FieldBase.description. */
|
|
1220
|
+
description?: string;
|
|
1221
|
+
/** Default VALUE (annotation-level) — mirrors FieldBase.default. */
|
|
1222
|
+
default?: unknown;
|
|
1223
|
+
};
|
|
1224
|
+
/** One element of `kind_definition.data` — FieldSchema + name, ref targets removed. */
|
|
1225
|
+
type StoredFieldElement = (StoredFieldBase & {
|
|
1226
|
+
type: "string" | "boolean";
|
|
1227
|
+
}) | (StoredFieldBase & {
|
|
1228
|
+
type: "number";
|
|
1229
|
+
min?: number;
|
|
1230
|
+
max?: number;
|
|
1231
|
+
step?: number;
|
|
1232
|
+
}) | (StoredFieldBase & {
|
|
1233
|
+
type: "string[]";
|
|
1234
|
+
values?: string[];
|
|
1235
|
+
open?: boolean;
|
|
1236
|
+
}) | (StoredFieldBase & {
|
|
1237
|
+
type: "number[]" | "boolean[]";
|
|
1238
|
+
}) | (StoredFieldBase & {
|
|
1239
|
+
type: "json";
|
|
1240
|
+
}) | (StoredFieldBase & {
|
|
1241
|
+
type: "json[]";
|
|
1242
|
+
}) | (StoredFieldBase & {
|
|
1243
|
+
type: "array";
|
|
1244
|
+
}) | (StoredFieldBase & {
|
|
1245
|
+
type: "object";
|
|
1246
|
+
}) | (StoredFieldBase & {
|
|
1247
|
+
type: "inline_object";
|
|
1248
|
+
fields: StoredFieldElement[];
|
|
1249
|
+
open?: boolean;
|
|
1250
|
+
}) | (StoredFieldBase & {
|
|
1251
|
+
type: "record";
|
|
1252
|
+
values: RecordValueType;
|
|
1253
|
+
}) | (StoredFieldBase & {
|
|
1254
|
+
type: "enum";
|
|
1255
|
+
values: string[];
|
|
1256
|
+
open?: boolean;
|
|
1257
|
+
}) | (StoredFieldBase & {
|
|
1258
|
+
type: "union";
|
|
1259
|
+
scalars: Array<"string" | "number" | "boolean">;
|
|
1260
|
+
/** Marker that this union's kind refs live in edges (positions ordered). */
|
|
1261
|
+
hasKinds?: boolean;
|
|
1262
|
+
});
|
|
1263
|
+
/** One `content_ir.kind_edge` row (child resolved to an id at insert time). */
|
|
1264
|
+
type KindEdgeSpec = {
|
|
1265
|
+
/** Field PATH in the parent's `data` (dot-notation into inline_objects). */
|
|
1266
|
+
fieldPath: string;
|
|
1267
|
+
/** Child kind slug — insert resolves this to `child_definition_id`. */
|
|
1268
|
+
childKind: string;
|
|
1269
|
+
/** Union (anyOf) ordering for array refs; null for a single object ref. */
|
|
1270
|
+
position: number | null;
|
|
1271
|
+
};
|
|
1272
|
+
/** The full write payload for one kind: the ordered data array + its edges. */
|
|
1273
|
+
type KindStorageShape = {
|
|
1274
|
+
data: StoredFieldElement[];
|
|
1275
|
+
edges: KindEdgeSpec[];
|
|
1276
|
+
};
|
|
1277
|
+
declare class KindStorageError extends Error {
|
|
1278
|
+
constructor(message: string);
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1281
|
+
* MIGRATION / write direction. Order of `data` follows `Object.entries(fields)`
|
|
1282
|
+
* — the caller supplies the KindSchema whose field order is authoritative
|
|
1283
|
+
* (prefer the compiled `system-kinds.ts` order for system kinds during the
|
|
1284
|
+
* one-time flexible_data migration; jsonb order is the fallback for user kinds).
|
|
1285
|
+
*/
|
|
1286
|
+
declare function kindSchemaToStorage(schema: KindSchema): KindStorageShape;
|
|
1287
|
+
/** ADAPTER / read direction — the exact inverse of `kindSchemaToStorage`. */
|
|
1288
|
+
declare function storageToKindSchema(kind: string, shape: KindStorageShape): KindSchema;
|
|
1289
|
+
|
|
1290
|
+
/**
|
|
1291
|
+
* The dual gate — Arman's law as executable code: a kind is only `is_active`
|
|
1292
|
+
* when its canonical `sample_data` passes BOTH systems.
|
|
1293
|
+
*
|
|
1294
|
+
* 1. Structural (Pydantic): the sample validates against the kind's
|
|
1295
|
+
* `emitted_json_schema`. Python's Pydantic is the AUTHORITATIVE owner of
|
|
1296
|
+
* this leg, but it reads the SAME materialized `emitted_json_schema` — so
|
|
1297
|
+
* this TS ajv check and the Python check validate the same sample against
|
|
1298
|
+
* the same schema and agree by construction. A disagreement IS the
|
|
1299
|
+
* screamer (a schema Pydantic can't express, or an ajv/Pydantic gap).
|
|
1300
|
+
* 2. Render (UI): the sample lights up the kind's real component. TS is the
|
|
1301
|
+
* AUTHORITATIVE owner of this leg. It is a PROXY for a DOM render, not a
|
|
1302
|
+
* DOM render. Exactly what it checks, and nothing more:
|
|
1303
|
+
* · the kind has something to render with (`legacyBlockType` or
|
|
1304
|
+
* `component`);
|
|
1305
|
+
* · for BRIDGED kinds, `toLegacyServerData(sample)` returns a plain
|
|
1306
|
+
* object that is SEMANTICALLY non-empty — see
|
|
1307
|
+
* `describeUnrenderableBridgeOutput` for the exact predicate;
|
|
1308
|
+
* · BRIDGELESS kinds pass with a recorded caveat — nothing is verified.
|
|
1309
|
+
*
|
|
1310
|
+
* This catches the 2026-07-04 "No flashcards available yet" class: a
|
|
1311
|
+
* bridge that returns `undefined`, `{}`, `{language:"json"}` (the raw
|
|
1312
|
+
* code-region annotation), or an object whose every value is empty.
|
|
1313
|
+
*
|
|
1314
|
+
* What it does NOT do — stated plainly, because a gate that overclaims is
|
|
1315
|
+
* worse than no gate: it does not mount the component, and it cannot know
|
|
1316
|
+
* WHICH key carries the payload. `{title:"Cell Biology", cards:[]}` passes
|
|
1317
|
+
* this leg on `title` alone. Proving the payload key is populated needs
|
|
1318
|
+
* per-kind knowledge the gate deliberately does not have; a DOM-level
|
|
1319
|
+
* render check is the deeper leg, deferred to an RTL harness.
|
|
1320
|
+
*
|
|
1321
|
+
* Both legs necessary, neither sufficient. Fail either → `isActive: false` and
|
|
1322
|
+
* the caller reports it loudly (Error Inspector, `content-ir`) and holds the
|
|
1323
|
+
* row out of production. This module is PURE (deps injected) so it runs in the
|
|
1324
|
+
* harness, in CI, and in a browser author-save alike.
|
|
1325
|
+
*
|
|
1326
|
+
* Ownership split (KIND_REGISTRY_STORAGE.md §2): the caller writes the outcome
|
|
1327
|
+
* to the LIVE `content_ir.kind_definition` row's `is_active`; the canonical
|
|
1328
|
+
* `_version_capture` trigger snapshots that state into `history.row_versions`
|
|
1329
|
+
* (never a post-hoc history mutation).
|
|
1330
|
+
*/
|
|
1331
|
+
|
|
1332
|
+
/** The facets the render leg needs — a structural subset of KindDefinition. */
|
|
1333
|
+
interface DualGateDefinition {
|
|
1334
|
+
legacyBlockType?: string;
|
|
1335
|
+
toLegacyServerData?: (envelope: CanonicalBlockIR) => Record<string, unknown> | undefined;
|
|
1336
|
+
component?: {
|
|
1337
|
+
load: () => Promise<unknown>;
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* The render leg's second satisfier: an ACTIVE `role='output'` component
|
|
1342
|
+
* resolved from `content_ir.kind_component`. Feed this from
|
|
1343
|
+
* `resolveComponent(kind, "web", "output")`.
|
|
1344
|
+
*
|
|
1345
|
+
* Why this exists: the render leg originally consulted only the compiled TS
|
|
1346
|
+
* registry, so an agent-authored kind whose renderer is a `source='db'` row was
|
|
1347
|
+
* structurally unable to pass — `definition` came back null and the gate said
|
|
1348
|
+
* "no component" about a kind that had a live, working one. Six authored kinds
|
|
1349
|
+
* (wine_tasting, employee_card, employee_roster, employee_of_the_week,
|
|
1350
|
+
* flashcard_deck, arman_video_prompt) sat permanently inactive because of it.
|
|
1351
|
+
*
|
|
1352
|
+
* NOT a mirror of `content_ir.evaluate_kind_activation`. The SQL render leg is
|
|
1353
|
+
* presence-only — it checks that an active `role='output'` row exists, because
|
|
1354
|
+
* SQL cannot execute a TypeScript bridge. THIS leg is strictly stronger: for a
|
|
1355
|
+
* compiled kind it also runs `toLegacyServerData` and rejects semantically
|
|
1356
|
+
* empty output (the "No <kind> available" class).
|
|
1357
|
+
*
|
|
1358
|
+
* The asymmetry is deliberate and bounded: SQL is the FLOOR (necessary, and
|
|
1359
|
+
* sufficient for DB-authored components, which own no bridge), while this leg
|
|
1360
|
+
* is the CEILING for compiled kinds. Consequence to know: activating a COMPILED
|
|
1361
|
+
* kind whose bridge is broken would pass the RPC and fail here. Compiled kinds
|
|
1362
|
+
* are activated by developers through `scripts/shape/activate-kinds.ts`, which
|
|
1363
|
+
* runs this leg — the studio control only ever reaches owner-authored kinds.
|
|
1364
|
+
* If that ever stops being true, the browser control must run this gate before
|
|
1365
|
+
* enabling its button.
|
|
1366
|
+
*/
|
|
1367
|
+
interface DualGateResolvedComponent {
|
|
1368
|
+
componentKey: string;
|
|
1369
|
+
/** The row's own render-trust verdict (R6). Inactive rows do not satisfy. */
|
|
1370
|
+
isActive: boolean;
|
|
1371
|
+
/** "bundled" | "db" — reported in the leg detail, never gates the verdict. */
|
|
1372
|
+
source: string;
|
|
1373
|
+
}
|
|
1374
|
+
interface DualGateInput {
|
|
1375
|
+
kind: string;
|
|
1376
|
+
/** The canonical instance (kind_definition.sample_data). */
|
|
1377
|
+
sample: Record<string, unknown>;
|
|
1378
|
+
/** The materialized kind_definition.emitted_json_schema (plain, no __kind). */
|
|
1379
|
+
emittedJsonSchema: unknown;
|
|
1380
|
+
/** The registry definition for the kind (or null when unregistered). */
|
|
1381
|
+
definition: DualGateDefinition | null;
|
|
1382
|
+
/**
|
|
1383
|
+
* The resolver's `(kind, web, output)` answer, when the caller has one.
|
|
1384
|
+
* Omit (or pass null) to check the compiled registry alone.
|
|
1385
|
+
*/
|
|
1386
|
+
resolvedComponent?: DualGateResolvedComponent | null;
|
|
1387
|
+
/**
|
|
1388
|
+
* True when the kind is a generated data-only contract
|
|
1389
|
+
* (`metadata.family` ∈ workflow_io | tool_io | action_io | agent_io).
|
|
1390
|
+
* Those are passed between nodes and never rendered, so the render leg is
|
|
1391
|
+
* structurally inapplicable — the same `n/a` doctrine the shape doctor uses.
|
|
1392
|
+
* Failing them would be noise, and noise erodes the gate.
|
|
1393
|
+
*/
|
|
1394
|
+
dataOnly?: boolean;
|
|
1395
|
+
}
|
|
1396
|
+
interface LegResult {
|
|
1397
|
+
ok: boolean;
|
|
1398
|
+
detail?: string;
|
|
1399
|
+
}
|
|
1400
|
+
interface DualGateResult {
|
|
1401
|
+
/** True only when BOTH legs pass — the value the caller writes to is_active. */
|
|
1402
|
+
isActive: boolean;
|
|
1403
|
+
structural: LegResult;
|
|
1404
|
+
render: LegResult;
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* The structural leg, exported on its own so the shape doctor
|
|
1408
|
+
* (`shape-doctor.ts`) RECOMPUTES gate validation with the exact same ajv
|
|
1409
|
+
* config + `__kind`-stripping semantics as activation — never a parallel
|
|
1410
|
+
* validator. `sample` is `unknown` (not `Record`) because kind examples may
|
|
1411
|
+
* legitimately be scalars/arrays (workflow I/O kinds like `text`/`number`).
|
|
1412
|
+
*/
|
|
1413
|
+
declare function validateStructuralLeg(sample: unknown, emittedJsonSchema: unknown): LegResult;
|
|
1414
|
+
declare function runKindDualGate(input: DualGateInput): DualGateResult;
|
|
1415
|
+
/**
|
|
1416
|
+
* One-line, Error-Inspector-ready reason for a failed gate (empty string when
|
|
1417
|
+
* it passed). The caller feeds this to `captureError({ source: "content-ir" })`
|
|
1418
|
+
* and sets `is_active=false`.
|
|
1419
|
+
*/
|
|
1420
|
+
declare function describeDualGateFailure(kind: string, result: DualGateResult): string;
|
|
1421
|
+
|
|
1422
|
+
/**
|
|
1423
|
+
* OpenAI/provider JSON Schema → KindSchema converter + __kind injection.
|
|
1424
|
+
*
|
|
1425
|
+
* The strategic keystone: `buildAgentSchemaWithRenderBlockSupport` turns "a
|
|
1426
|
+
* user/agent defined an output schema" into "the platform has a render
|
|
1427
|
+
* contract for it" by injecting the `__kind` discriminator into the agent's
|
|
1428
|
+
* output schema (root + array items), so the model emits self-identifying
|
|
1429
|
+
* objects from then on.
|
|
1430
|
+
*
|
|
1431
|
+
* Moved from app/(dev)/demos/json-block-detector/schema-converter.ts.
|
|
1432
|
+
*/
|
|
1433
|
+
|
|
1434
|
+
type JsonSchemaNode = Record<string, unknown>;
|
|
1435
|
+
type ConversionProblem = {
|
|
1436
|
+
severity: "error" | "warning" | "info";
|
|
1437
|
+
path: string;
|
|
1438
|
+
message: string;
|
|
1439
|
+
};
|
|
1440
|
+
type DroppedMetadata = {
|
|
1441
|
+
path: string;
|
|
1442
|
+
dropped: Record<string, unknown>;
|
|
1443
|
+
};
|
|
1444
|
+
type FieldComparison = {
|
|
1445
|
+
field: string;
|
|
1446
|
+
aiPresent: boolean;
|
|
1447
|
+
blockPresent: boolean;
|
|
1448
|
+
aiSummary: string | null;
|
|
1449
|
+
blockSummary: string | null;
|
|
1450
|
+
status: "match" | "ai_only" | "block_only" | "type_mismatch" | "ai_richer" | "block_richer";
|
|
1451
|
+
detail?: string;
|
|
1452
|
+
};
|
|
1453
|
+
/** One standalone flexible_data block schema row. */
|
|
1454
|
+
type BlockSchemaDraft = {
|
|
1455
|
+
slug: string;
|
|
1456
|
+
label: string;
|
|
1457
|
+
fields: Record<string, FieldSchema>;
|
|
1458
|
+
/**
|
|
1459
|
+
* NON-OBJECT ROOT form (KindSchema.root): the kind's value is this field's
|
|
1460
|
+
* shape at the top level (scalar / array / json / OPEN object). When set,
|
|
1461
|
+
* `fields` is empty — mirror of `KindSchema.root`.
|
|
1462
|
+
*/
|
|
1463
|
+
root?: FieldSchema;
|
|
1464
|
+
};
|
|
1465
|
+
type ArrayItemKindBinding = {
|
|
1466
|
+
arrayField: string;
|
|
1467
|
+
itemKindSlug: string;
|
|
1468
|
+
};
|
|
1469
|
+
type SchemaConversionResult = {
|
|
1470
|
+
schemaName: string | null;
|
|
1471
|
+
strict: boolean | null;
|
|
1472
|
+
/** Every block schema row required for the converted shape (1..n). */
|
|
1473
|
+
blockSchemas: BlockSchemaDraft[];
|
|
1474
|
+
/** OPTION 2 — same input shape with __kind injected for agent output. */
|
|
1475
|
+
agentSchemaWithKinds: unknown | null;
|
|
1476
|
+
problems: ConversionProblem[];
|
|
1477
|
+
droppedMetadata: DroppedMetadata[];
|
|
1478
|
+
comparisons: FieldComparison[];
|
|
1479
|
+
};
|
|
1480
|
+
type SavePlanEntry = {
|
|
1481
|
+
draft: BlockSchemaDraft;
|
|
1482
|
+
existsInDb: boolean;
|
|
1483
|
+
willSave: boolean;
|
|
1484
|
+
};
|
|
1485
|
+
type ItemKindRefCheck = {
|
|
1486
|
+
parentSlug: string;
|
|
1487
|
+
field: string;
|
|
1488
|
+
itemKind: string;
|
|
1489
|
+
satisfied: boolean;
|
|
1490
|
+
source: "batch" | "database" | "missing";
|
|
1491
|
+
};
|
|
1492
|
+
type SavePlanValidation = {
|
|
1493
|
+
entries: SavePlanEntry[];
|
|
1494
|
+
itemKindRefs: ItemKindRefCheck[];
|
|
1495
|
+
newCount: number;
|
|
1496
|
+
canSave: boolean;
|
|
1497
|
+
errors: string[];
|
|
1498
|
+
};
|
|
1499
|
+
/**
|
|
1500
|
+
* Inject the `__kind` discriminator into one object schema (const when
|
|
1501
|
+
* strict, enum otherwise; `__kind` prepended to required; strict also pins
|
|
1502
|
+
* additionalProperties:false). Shared with the REVERSE converter
|
|
1503
|
+
* (kind-to-json-schema.ts) so both directions stamp identical discriminators.
|
|
1504
|
+
*/
|
|
1505
|
+
declare function injectKindIntoObjectSchema(objectSchema: JsonSchemaNode, kindSlug: string, strict: boolean): JsonSchemaNode;
|
|
1506
|
+
declare function buildAgentSchemaWithRenderBlockSupport(input: unknown, rootKindSlug: string, arrayBindings: ArrayItemKindBinding[], strict: boolean): unknown | null;
|
|
1507
|
+
declare function normalizeAiSchemaInput(input: unknown): {
|
|
1508
|
+
name: string | null;
|
|
1509
|
+
strict: boolean | null;
|
|
1510
|
+
rootSchema: JsonSchemaNode | null;
|
|
1511
|
+
parseErrors: string[];
|
|
1512
|
+
};
|
|
1513
|
+
type ConversionCore = {
|
|
1514
|
+
schemaName: string | null;
|
|
1515
|
+
strict: boolean | null;
|
|
1516
|
+
blockSchemas: BlockSchemaDraft[];
|
|
1517
|
+
problems: ConversionProblem[];
|
|
1518
|
+
droppedMetadata: DroppedMetadata[];
|
|
1519
|
+
};
|
|
1520
|
+
declare function convertAiSchemaToBlockFields(schemaName: string, rootSchema: JsonSchemaNode, strict: boolean): ConversionCore;
|
|
1521
|
+
declare function compareWithExistingKindSchema(convertedFields: Record<string, FieldSchema>, existing: KindSchema | null): FieldComparison[];
|
|
1522
|
+
declare function runSchemaConversion(input: unknown, existingSchemas: Record<string, KindSchema>): SchemaConversionResult & {
|
|
1523
|
+
parseErrors: string[];
|
|
1524
|
+
};
|
|
1525
|
+
declare function validateBlockSchemaSavePlan(blockSchemas: BlockSchemaDraft[], existingSlugs: string[], hasConversionErrors: boolean): SavePlanValidation;
|
|
1526
|
+
declare function fieldsToDbPayload(fields: Record<string, FieldSchema>): Record<string, unknown>;
|
|
1527
|
+
declare function isDuplicateBlockSlug(slug: string, entries: Array<{
|
|
1528
|
+
slug: string;
|
|
1529
|
+
}>): boolean;
|
|
1530
|
+
|
|
1531
|
+
/**
|
|
1532
|
+
* KindSchema → provider-ready JSON Schema — the REVERSE of the OpenAI
|
|
1533
|
+
* converter in openai-schema-converter.ts.
|
|
1534
|
+
*
|
|
1535
|
+
* The requested root kind renders INLINE as the top-level object; every
|
|
1536
|
+
* OTHER kind it transitively references (via `{type:"object", kind}` and
|
|
1537
|
+
* `{type:"array", itemKinds}` fields, including inside inline_objects)
|
|
1538
|
+
* becomes a `$defs` entry referenced with `$ref: "#/$defs/<slug>"`.
|
|
1539
|
+
* References back to the ROOT kind use `"#"` — the standard JSON Schema
|
|
1540
|
+
* recursive-root reference (OpenAI structured outputs support both forms).
|
|
1541
|
+
* Collection is cycle-safe: each kind enters the closure exactly once.
|
|
1542
|
+
*
|
|
1543
|
+
* Round-trip contract — output fed through `runSchemaConversion`
|
|
1544
|
+
* (openai-schema-converter.ts) reproduces the source KindSchemas for the
|
|
1545
|
+
* root and every array-item child (enforced by
|
|
1546
|
+
* __tests__/kind-to-json-schema.test.ts). The 2026-07-15 expressivity
|
|
1547
|
+
* extension closed the three historical asymmetries: `record` now has a
|
|
1548
|
+
* forward target (properties-less objects → record scalar/json), multi-
|
|
1549
|
+
* itemKind arrays read back through items-anyOf when each variant declares
|
|
1550
|
+
* its `__kind`, and nullable anyOf preserves the member list + nullable flag
|
|
1551
|
+
* instead of flattening to `string`. Remaining accepted narrowings:
|
|
1552
|
+
* - multi-kind array items emitted as $refs (not inline __kind objects)
|
|
1553
|
+
* still cannot be read back — the forward converter rejects $ref.
|
|
1554
|
+
* - a nullable object-REF (`anyOf: [$ref, {type:"null"}]`) degrades for the
|
|
1555
|
+
* same $ref reason.
|
|
1556
|
+
* - JSON Schema `integer` narrows to `number` (the map_result precedent).
|
|
1557
|
+
*/
|
|
1558
|
+
|
|
1559
|
+
type KindJsonSchemaOptions = {
|
|
1560
|
+
/**
|
|
1561
|
+
* Provider strict mode: additionalProperties:false on every kind object
|
|
1562
|
+
* and inline object, and `const` (vs `enum`) __kind discriminators —
|
|
1563
|
+
* mirrors makeKindJsonSchemaProperty / injectKindIntoObjectSchema.
|
|
1564
|
+
*/
|
|
1565
|
+
strict?: boolean;
|
|
1566
|
+
/**
|
|
1567
|
+
* Inject the `__kind` discriminator (+ required) into the root and every
|
|
1568
|
+
* $defs kind object. Default true — this is what makes the emitted
|
|
1569
|
+
* objects self-identifying for the render pipeline.
|
|
1570
|
+
*/
|
|
1571
|
+
injectKind?: boolean;
|
|
1572
|
+
};
|
|
1573
|
+
type KindJsonSchemaExport = {
|
|
1574
|
+
/** The root kind slug — doubles as the provider schema name. */
|
|
1575
|
+
name: string;
|
|
1576
|
+
schema: JsonSchemaNode;
|
|
1577
|
+
strict: boolean;
|
|
1578
|
+
/**
|
|
1579
|
+
* Referenced kinds the resolver could not supply. Each still gets a
|
|
1580
|
+
* permissive `$defs` object stub (never additionalProperties:false, even
|
|
1581
|
+
* in strict mode — a __kind-only strict object would reject every real
|
|
1582
|
+
* payload) so the export stays structurally valid. Surface these loudly.
|
|
1583
|
+
*/
|
|
1584
|
+
unresolved: string[];
|
|
1585
|
+
};
|
|
1586
|
+
/**
|
|
1587
|
+
* Every kind referenced by a field map — `{type:"object", kind}` refs and
|
|
1588
|
+
* `{type:"array", itemKinds}` members, recursing through inline_objects.
|
|
1589
|
+
* First-sighting order, deduplicated. Shared read helper (the registry
|
|
1590
|
+
* catalog builds its uses / used-by graph from this).
|
|
1591
|
+
*/
|
|
1592
|
+
declare function collectReferencedKinds(fields: Record<string, FieldSchema>): string[];
|
|
1593
|
+
/**
|
|
1594
|
+
* Schema-level referenced kinds — field-map refs PLUS the refs a non-object
|
|
1595
|
+
* root form carries. Use this (not `collectReferencedKinds(schema.fields)`)
|
|
1596
|
+
* whenever the schema in hand may be root-form.
|
|
1597
|
+
*/
|
|
1598
|
+
declare function collectSchemaReferencedKinds(schema: KindSchema): string[];
|
|
1599
|
+
declare function kindSchemaToJsonSchema(kind: string, resolve: (kind: string) => KindSchema | undefined, options?: KindJsonSchemaOptions): KindJsonSchemaExport | null;
|
|
1600
|
+
|
|
1601
|
+
export { type ArrayItemKindBinding, type ArrayItemScalarType, type BlockSchemaDraft, type CanonicalBlockIR, type CanonicalContent, type CanonicalSegment, type CompleteValueEnvelopeOptions, type CompliantKindSnapshot, type ContentRegionInit, type ConversionProblem, type DroppedMetadata, type DualGateDefinition, type DualGateInput, type DualGateResolvedComponent, type DualGateResult, type FieldComparison, type FieldSchema, type Fingerprinter, IR_ENVELOPE_CACHE_VERSION, IR_ENVELOPE_KEY, IR_VERSION, type InboundEnvelopeHooks, type InboundEnvelopeVerdict, type IrDiscriminator, type IrEnvelopeCache, type IrKindState, type IrPath, type IrResidue, type IrStructuredNode, IrTree, type IrTreeNode, type ItemKindRefCheck, JSON_DISCRIMINATOR, type JsonPath, type JsonSchemaNode, JsonStreamTokenizer, type JsonToken, KIND_KEY, type KindBlockProps, type KindDefinition, type KindEdgeSpec, type KindJsonSchemaExport, type KindJsonSchemaOptions, type KindResolution, type KindSchema, KindStorageError, type KindStorageShape, type KindStreamEvent, KindStreamParser, type KindStreamParserOptions, type KindTier, type LegResult, type NormalizeJsonRegionOptions, ParseSession, type ParseSessionOptions, type Punct, ROOT_STORAGE_NAME, type RecordValueType, type RegionEndReason, type RegionFormat, type RegionSourceKind, type SavePlanEntry, type SavePlanValidation, type ScalarFieldType, type SchemaConversionResult, type SchemaLayoutMode, type SchemaResolver, type StoredFieldElement, 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 };
|