@ai-matrx/content-ir 0.3.0 → 0.6.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 CHANGED
@@ -1,5 +1,77 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0 — 2026-08-29
4
+
5
+ - **`$ref` / `$defs` resolution — a kind's child kinds are no longer invisible
6
+ to JSON Schema → KindSchema.** `convertAiSchemaToBlockFields` resolves `#`,
7
+ `#/$defs/<name>` and `#/definitions/<name>` in field position, in array
8
+ `items`, and inside `anyOf`/`oneOf` (including `items: {anyOf: [$ref, …]}`,
9
+ the multi-kind array). Referenced defs are registered as their own drafts,
10
+ with a cycle brake so a self-referencing kind terminates. An unresolvable
11
+ ref is a loud error, never a guess.
12
+
13
+ WHY: `kindSchemaToJsonSchema` externalizes every child kind as a `$def`, and
14
+ the reverse direction rejected `$ref` outright — so every
15
+ `{type:"object", kind}` and `{type:"array", itemKinds}` field came back
16
+ EMPTY. Measured over the 62 live kinds carrying both a stored field list and
17
+ a schema, 36 lost fields this way (sections, slides, nodes, edges, criteria,
18
+ questions, segments, …).
19
+
20
+ - **A `__kind`-carrying record reads back as a record.** An object whose only
21
+ declared property is `__kind`, with typed `additionalProperties`, is the
22
+ forward converter's emission for `{type:"record", values}` — it was widening
23
+ to an open `inline_object` and losing the value type.
24
+
25
+ - Live-corpus result: schema-derived KindSchemas now match the stored field
26
+ list byte-for-byte for 48 of 62 kinds, with 1 strictly richer. The remaining
27
+ 13 are DIVERGENCE BETWEEN THE TWO STORED COPIES, not conversion gaps — 4
28
+ stored lists carry `__kind` as a data field (it is the discriminator, never
29
+ a field), and 9 disagree on enums, bounds, required flags or descriptions
30
+ the schema states and the copy does not. That is the evidence for retiring
31
+ the stored copy.
32
+
33
+ ## 0.5.0 — 2026-08-29
34
+
35
+ - **A multi-type union is no longer silently narrowed to its first member.**
36
+ `resolvePrimaryType` now reports every non-null member, and
37
+ `convertAiSchemaToBlockFields` branches on them: a union spanning
38
+ objects/arrays becomes `json`, a scalar-only union becomes
39
+ `{type:"union", scalars}`, and anything unnameable stays `json` with a
40
+ warning. Single-typed fields are untouched.
41
+
42
+ WHY: `type: ["string","number","boolean","object","array","null"]` —
43
+ pydantic's `Any`, and the commonest construct in user-authored kinds —
44
+ converted to plain `string`. The loss was invisible and it inverted the
45
+ field's meaning: an `estimated_count` of `1` then FAILED validation, so a
46
+ correct payload read as a broken instance. Widening is safe; silently
47
+ narrowing is not.
48
+
49
+ - New `__tests__/json-schema-to-kind.test.ts` pins JSON Schema → KindSchema
50
+ over the constructs a stored field list historically could not hold —
51
+ pydantic-Any unions, `items: {}` arrays, arrays of `__kind` children,
52
+ nested `__kind` objects, and markerless inline objects. All convert with
53
+ zero errors, which is the evidence that `emitted_json_schema` is sufficient
54
+ on its own and a second stored copy of a kind's fields is unnecessary.
55
+
56
+ ## 0.4.0 — 2026-08-29
57
+
58
+ - **`unverified` — "we never checked it" is no longer "we checked it and it
59
+ failed."** `IrKindState` gains `unverified`, and `raw_object` events carry a
60
+ `cause` (`"unverified" | "invalid"`, absent reads as `"invalid"`). The four
61
+ no-schema-registered degrades — the two pending-schema resolutions, plus the
62
+ typed and speculated finalizers — now report `unverified`; every validation
63
+ failure, array-item mismatch, duplicate key, bad placement, and contradicted
64
+ speculation still reports `invalid`. `IrTree` carries the cause onto
65
+ `root.kindState` and into `nodeIndex`.
66
+
67
+ WHY THIS EXISTS: `raw` meant both things, and on 2026-08-28 the render route
68
+ began reading `raw` as "broken instance" and diverting it away from the
69
+ kind's component. Every kind whose schema cannot be reconstructed — nested
70
+ objects, arrays of child kinds, loose fields — degrades on the no-schema
71
+ path, so ~221 live kinds with purpose-built components silently started
72
+ rendering as key/value dumps. Consumers MUST NOT treat `unverified` as a
73
+ failure: the value is intact and was never examined.
74
+
3
75
  ## 0.3.0 — 2026-08-29
4
76
 
5
77
  - **KIND PRESERVATION now covers STRUCTURAL raws.** A node that degrades to
package/dist/index.cjs CHANGED
@@ -111,11 +111,20 @@ var IrTree = class {
111
111
  */
112
112
  earlyFields = /* @__PURE__ */ new Map();
113
113
  /**
114
- * pathKey → identified kind preserved through a SCHEMA-AVAILABILITY raw
115
- * fallback (parser stamped `kind` on the raw_object event). Structural raws
116
- * (missing __kind, duplicate key, validation failure) never land here.
114
+ * pathKey → identified kind preserved through a raw fallback (parser stamped
115
+ * `kind` on the raw_object event) schema-availability degrades and, since
116
+ * 2026-08-29, structural ones too. Only a node nothing ever identified (no
117
+ * `__kind` at all) is absent here.
117
118
  */
118
119
  rawKinds = /* @__PURE__ */ new Map();
120
+ /**
121
+ * pathKey → WHY the node degraded. `"unverified"` means no schema was
122
+ * available and nothing was ever checked; `"invalid"` means a check ran and
123
+ * failed. THE RENDER ROUTE BRANCHES ON THIS — see `IrKindState`. Absent =
124
+ * `"invalid"`, so a parser that predates the `cause` field (or any consumer
125
+ * hand-building events) keeps the strict, safe reading.
126
+ */
127
+ rawCauses = /* @__PURE__ */ new Map();
119
128
  regionStatus = "streaming";
120
129
  errorReason = null;
121
130
  rootRawValue = null;
@@ -168,6 +177,7 @@ var IrTree = class {
168
177
  this.pendingSchemaPaths.delete(pathKey);
169
178
  this.earlyFields.delete(pathKey);
170
179
  if (event.kind) this.rawKinds.set(pathKey, event.kind);
180
+ this.rawCauses.set(pathKey, event.cause ?? "invalid");
171
181
  this.markRaw(event.path, event.reason, event.value);
172
182
  return;
173
183
  }
@@ -366,10 +376,11 @@ var IrTree = class {
366
376
  const isRaw = rootRawReason !== null;
367
377
  const identifiedKind = this.identifiedKinds.get("") ?? "";
368
378
  const rootKind = isRaw ? this.rawKinds.get("") ?? "" : rootNode?.kind ?? (this.completedKind || identifiedKind);
379
+ const rootRawState = this.rawCauses.get("") === "unverified" ? "unverified" : "raw";
369
380
  const root = {
370
381
  role: "structured",
371
382
  kind: rootKind,
372
- kindState: isRaw ? "raw" : rootNode ? rootNode.kindState : this.pendingSchemaPaths.has("") ? "pending_schema" : this.regionStatus === "streaming" ? identifiedKind ? "pending_schema" : "pending_kind" : "raw",
383
+ kindState: isRaw ? rootRawState : rootNode ? rootNode.kindState : this.pendingSchemaPaths.has("") ? "pending_schema" : this.regionStatus === "streaming" ? identifiedKind ? "pending_schema" : "pending_kind" : "raw",
373
384
  discriminator: JSON_DISCRIMINATOR,
374
385
  path: [],
375
386
  status: this.regionStatus,
@@ -392,7 +403,7 @@ var IrTree = class {
392
403
  if (pathKey === "") continue;
393
404
  nodeIndex[pathKey] = {
394
405
  kind: this.rawKinds.get(pathKey) ?? "",
395
- kindState: "raw",
406
+ kindState: this.rawCauses.get(pathKey) === "unverified" ? "unverified" : "raw",
396
407
  status: "complete"
397
408
  };
398
409
  }
@@ -732,7 +743,8 @@ var KindStreamParser = class {
732
743
  safeCopy(value),
733
744
  `No block schema registered for "${kind}".`,
734
745
  at,
735
- kind
746
+ kind,
747
+ "unverified"
736
748
  );
737
749
  }
738
750
  }
@@ -766,7 +778,8 @@ var KindStreamParser = class {
766
778
  safeCopy(value ?? {}),
767
779
  `No block schema registered for "${kind}".`,
768
780
  at,
769
- kind
781
+ kind,
782
+ "unverified"
770
783
  );
771
784
  this.closedPendingPaths.delete(pathKey);
772
785
  continue;
@@ -1312,7 +1325,8 @@ var KindStreamParser = class {
1312
1325
  objectValue,
1313
1326
  `No block schema registered for "${kind}".`,
1314
1327
  at,
1315
- kind
1328
+ kind,
1329
+ "unverified"
1316
1330
  );
1317
1331
  return;
1318
1332
  }
@@ -1346,7 +1360,8 @@ var KindStreamParser = class {
1346
1360
  objectValue,
1347
1361
  `No block schema registered for "${kind}".`,
1348
1362
  at,
1349
- kind
1363
+ kind,
1364
+ "unverified"
1350
1365
  );
1351
1366
  return;
1352
1367
  }
@@ -1419,7 +1434,15 @@ var KindStreamParser = class {
1419
1434
  this.speculativeKinds.delete(pathKey);
1420
1435
  this.emitRawObject(path, safeCopy(liveValue), reason, at, identifiedKind);
1421
1436
  }
1422
- emitRawObject(path, value, reason, at, identifiedKind) {
1437
+ /**
1438
+ * Degrade ONE node off the resolved path.
1439
+ *
1440
+ * `cause` defaults to `"invalid"` deliberately: every call site that omits
1441
+ * it is a real failure (validation, duplicate key, placement, contradicted
1442
+ * speculation). ONLY the "no schema registered" sites pass `"unverified"`,
1443
+ * and they are the reason the parameter exists — see `IrKindState`.
1444
+ */
1445
+ emitRawObject(path, value, reason, at, identifiedKind, cause = "invalid") {
1423
1446
  const pathKey = this.pathKey(path);
1424
1447
  if (this.rawObjectPaths.has(pathKey)) return;
1425
1448
  this.rawObjectPaths.add(pathKey);
@@ -1430,6 +1453,7 @@ var KindStreamParser = class {
1430
1453
  value,
1431
1454
  reason,
1432
1455
  ...identifiedKind !== void 0 && { kind: identifiedKind },
1456
+ cause,
1433
1457
  at
1434
1458
  });
1435
1459
  }
@@ -2999,21 +3023,26 @@ function fieldSchemaSummary(field) {
2999
3023
  function resolvePrimaryType(node) {
3000
3024
  const raw = node.type;
3001
3025
  if (typeof raw === "string") {
3002
- return { type: raw === "integer" ? "number" : raw, nullable: false };
3026
+ const t = raw === "integer" ? "number" : raw;
3027
+ return { type: t, nullable: false, members: [t] };
3003
3028
  }
3004
3029
  if (Array.isArray(raw)) {
3005
3030
  const types = raw.filter((t) => typeof t === "string");
3006
3031
  const nullable = types.includes("null");
3007
- const primary = types.find((t) => t !== "null") ?? (nullable && types.length === 1 ? "null" : null);
3008
- if (primary === "integer") {
3009
- return { type: "number", nullable };
3010
- }
3011
- return { type: primary ?? null, nullable };
3012
- }
3013
- if (node.enum) return { type: "string", nullable: false };
3014
- if (node.properties) return { type: "object", nullable: false };
3015
- if (node.items) return { type: "array", nullable: false };
3016
- return { type: null, nullable: false };
3032
+ const members = [
3033
+ ...new Set(
3034
+ types.filter((t) => t !== "null").map((t) => t === "integer" ? "number" : t)
3035
+ )
3036
+ ];
3037
+ const primary = members[0] ?? (nullable && types.length === 1 ? "null" : null);
3038
+ return { type: primary ?? null, nullable, members };
3039
+ }
3040
+ if (node.enum) return { type: "string", nullable: false, members: ["string"] };
3041
+ if (node.properties)
3042
+ return { type: "object", nullable: false, members: ["object"] };
3043
+ if (node.items)
3044
+ return { type: "array", nullable: false, members: ["array"] };
3045
+ return { type: null, nullable: false, members: [] };
3017
3046
  }
3018
3047
  function carriedMetadataKeys(field) {
3019
3048
  if (field === null) return /* @__PURE__ */ new Set();
@@ -3148,6 +3177,29 @@ function buildAgentSchemaWithRenderBlockSupport(input, rootKindSlug, arrayBindin
3148
3177
  }
3149
3178
  return updatedRoot;
3150
3179
  }
3180
+ function resolveRef(ref, ctx) {
3181
+ if (ref === "#" || ref === "#/") {
3182
+ return { slug: ctx.schemaName, node: ctx.rootSchema };
3183
+ }
3184
+ const match = /^#\/(?:\$defs|definitions)\/(.+)$/.exec(ref);
3185
+ if (!match) return null;
3186
+ const name = decodeURIComponent(
3187
+ (match[1] ?? "").replace(/~1/g, "/").replace(/~0/g, "~")
3188
+ );
3189
+ const node = ctx.defs[name];
3190
+ if (!isRecord4(node)) return null;
3191
+ const declared = isRecord4(node.properties) ? readBlockKindFromProperties(node.properties) : null;
3192
+ return { slug: declared ?? name, node };
3193
+ }
3194
+ function refToObjectField(resolved, path, ctx) {
3195
+ if (ctx.refsInProgress.has(resolved.slug)) return;
3196
+ ctx.refsInProgress.add(resolved.slug);
3197
+ try {
3198
+ registerDeclaredKindDraft(resolved.slug, resolved.node, path, ctx);
3199
+ } finally {
3200
+ ctx.refsInProgress.delete(resolved.slug);
3201
+ }
3202
+ }
3151
3203
  function isAnyValueSchema(node) {
3152
3204
  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;
3153
3205
  }
@@ -3184,12 +3236,21 @@ function convertProperty(fieldName, node, required, path, ctx) {
3184
3236
  }
3185
3237
  function convertPropertyCore(fieldName, node, required, path, ctx) {
3186
3238
  if (typeof node.$ref === "string") {
3187
- ctx.problems.push({
3188
- severity: "error",
3189
- path,
3190
- message: `$ref is not supported ("${node.$ref}"). Inline the schema manually.`
3191
- });
3192
- return null;
3239
+ const resolved = resolveRef(node.$ref, ctx);
3240
+ if (!resolved) {
3241
+ ctx.problems.push({
3242
+ severity: "error",
3243
+ path,
3244
+ message: `Unresolvable $ref ("${node.$ref}") \u2014 only "#", "#/$defs/<name>" and "#/definitions/<name>" are supported.`
3245
+ });
3246
+ return null;
3247
+ }
3248
+ refToObjectField(resolved, path, ctx);
3249
+ return {
3250
+ ...requiredNullableFlags(required),
3251
+ type: "object",
3252
+ kind: resolved.slug
3253
+ };
3193
3254
  }
3194
3255
  if (Array.isArray(node.anyOf) || Array.isArray(node.oneOf)) {
3195
3256
  const variants = node.anyOf ?? node.oneOf;
@@ -3205,12 +3266,18 @@ function convertPropertyCore(fieldName, node, required, path, ctx) {
3205
3266
  continue;
3206
3267
  }
3207
3268
  if (typeof variant.$ref === "string") {
3208
- ctx.problems.push({
3209
- severity: "error",
3210
- path,
3211
- message: `$ref inside anyOf/oneOf is not supported ("${variant.$ref}"). Inline the schema manually.`
3212
- });
3213
- unsupported = true;
3269
+ const resolvedVariant = resolveRef(variant.$ref, ctx);
3270
+ if (!resolvedVariant) {
3271
+ ctx.problems.push({
3272
+ severity: "error",
3273
+ path,
3274
+ message: `Unresolvable $ref inside anyOf/oneOf ("${variant.$ref}").`
3275
+ });
3276
+ unsupported = true;
3277
+ continue;
3278
+ }
3279
+ refToObjectField(resolvedVariant, path, ctx);
3280
+ memberKinds.push(resolvedVariant.slug);
3214
3281
  continue;
3215
3282
  }
3216
3283
  const { type: type2, nullable: nullable2 } = resolvePrimaryType(variant);
@@ -3331,7 +3398,34 @@ function convertPropertyCore(fieldName, node, required, path, ctx) {
3331
3398
  });
3332
3399
  return { ...requiredNullableFlags(required), type: "json" };
3333
3400
  }
3334
- const { type, nullable } = resolvePrimaryType(node);
3401
+ const { type, nullable, members } = resolvePrimaryType(node);
3402
+ if (members.length > 1) {
3403
+ const objectish = members.some((m) => m === "object" || m === "array");
3404
+ if (objectish) {
3405
+ ctx.problems.push({
3406
+ severity: "info",
3407
+ path,
3408
+ message: `Union of [${members.join(", ")}] at "${path || "(root)"}" spans objects/arrays \u2014 carried as any JSON value (json).`
3409
+ });
3410
+ return { ...requiredNullableFlags(required), type: "json" };
3411
+ }
3412
+ const scalars = members.filter(
3413
+ (m) => m === "string" || m === "number" || m === "boolean"
3414
+ );
3415
+ if (scalars.length === members.length && scalars.length > 1) {
3416
+ return {
3417
+ ...requiredNullableFlags(required, nullable),
3418
+ type: "union",
3419
+ scalars
3420
+ };
3421
+ }
3422
+ ctx.problems.push({
3423
+ severity: "warning",
3424
+ path,
3425
+ message: `Union of [${members.join(", ")}] at "${path || "(root)"}" has no exact field type \u2014 carried as any JSON value (json) rather than narrowed to "${type}".`
3426
+ });
3427
+ return { ...requiredNullableFlags(required), type: "json" };
3428
+ }
3335
3429
  if (type === "string") {
3336
3430
  if (Array.isArray(node.enum)) {
3337
3431
  const values = node.enum.filter(
@@ -3386,6 +3480,27 @@ function convertPropertyCore(fieldName, node, required, path, ctx) {
3386
3480
  });
3387
3481
  return null;
3388
3482
  }
3483
+ if (typeof itemNode.$ref === "string") {
3484
+ const resolvedItem = resolveRef(itemNode.$ref, ctx);
3485
+ if (!resolvedItem) {
3486
+ ctx.problems.push({
3487
+ severity: "error",
3488
+ path: `${path}[]`,
3489
+ message: `Unresolvable $ref in array items ("${itemNode.$ref}").`
3490
+ });
3491
+ return null;
3492
+ }
3493
+ refToObjectField(resolvedItem, `${path}[]`, ctx);
3494
+ ctx.arrayBindings.push({
3495
+ arrayField: fieldName,
3496
+ itemKindSlug: resolvedItem.slug
3497
+ });
3498
+ return {
3499
+ ...requiredNullableFlags(required, nullable),
3500
+ type: "array",
3501
+ itemKinds: [resolvedItem.slug]
3502
+ };
3503
+ }
3389
3504
  if (isAnyValueSchema(itemNode)) {
3390
3505
  return {
3391
3506
  ...requiredNullableFlags(required, nullable),
@@ -3420,11 +3535,29 @@ function convertPropertyCore(fieldName, node, required, path, ctx) {
3420
3535
  }
3421
3536
  const itemKinds = [];
3422
3537
  for (const variant of itemVariants) {
3538
+ if (isRecord4(variant) && typeof variant.$ref === "string") {
3539
+ const resolvedVariant = resolveRef(variant.$ref, ctx);
3540
+ if (!resolvedVariant) {
3541
+ ctx.problems.push({
3542
+ severity: "error",
3543
+ path: `${path}[]`,
3544
+ message: `Unresolvable $ref in array items anyOf ("${variant.$ref}").`
3545
+ });
3546
+ return null;
3547
+ }
3548
+ itemKinds.push(resolvedVariant.slug);
3549
+ refToObjectField(resolvedVariant, `${path}[]<${resolvedVariant.slug}>`, ctx);
3550
+ ctx.arrayBindings.push({
3551
+ arrayField: fieldName,
3552
+ itemKindSlug: resolvedVariant.slug
3553
+ });
3554
+ continue;
3555
+ }
3423
3556
  if (!isRecord4(variant) || !isRecord4(variant.properties)) {
3424
3557
  ctx.problems.push({
3425
3558
  severity: "error",
3426
3559
  path: `${path}[]`,
3427
- message: "Array items anyOf/oneOf variants must be inline objects declaring __kind."
3560
+ message: "Array items anyOf/oneOf variants must be inline objects declaring __kind, or $refs to defs."
3428
3561
  });
3429
3562
  return null;
3430
3563
  }
@@ -3558,6 +3691,14 @@ function convertPropertyCore(fieldName, node, required, path, ctx) {
3558
3691
  kind: referencedKind
3559
3692
  };
3560
3693
  }
3694
+ if (Object.keys(nestedFields).length === 0 && !apIsOpen && isRecord4(ap) && KIND_KEY in node.properties) {
3695
+ const apType = resolvePrimaryType(ap).type;
3696
+ return {
3697
+ ...requiredNullableFlags(required, nullable),
3698
+ type: "record",
3699
+ values: apType === "string" || apType === "number" || apType === "boolean" ? apType : "json"
3700
+ };
3701
+ }
3561
3702
  let open = apIsOpen;
3562
3703
  if (!apIsOpen && isRecord4(ap)) {
3563
3704
  ctx.problems.push({
@@ -3674,12 +3815,21 @@ function normalizeAiSchemaInput(input) {
3674
3815
  return { name: null, strict: null, rootSchema: null, parseErrors };
3675
3816
  }
3676
3817
  function convertAiSchemaToBlockFields(schemaName, rootSchema, strict) {
3818
+ const rawDefs = isRecord4(rootSchema.$defs) ? rootSchema.$defs : isRecord4(rootSchema.definitions) ? rootSchema.definitions : {};
3819
+ const defs = {};
3820
+ for (const [name, node] of Object.entries(rawDefs)) {
3821
+ if (isRecord4(node)) defs[name] = node;
3822
+ }
3677
3823
  const ctx = {
3678
3824
  schemaName,
3825
+ strict,
3679
3826
  problems: [],
3680
3827
  droppedMetadata: [],
3681
3828
  blockSchemas: [],
3682
- arrayBindings: []
3829
+ arrayBindings: [],
3830
+ defs,
3831
+ rootSchema,
3832
+ refsInProgress: /* @__PURE__ */ new Set()
3683
3833
  };
3684
3834
  ctx.droppedMetadata.push(...collectDropped(rootSchema, ""));
3685
3835
  const rootAp = rootSchema.additionalProperties;