@ai-matrx/content-ir 0.3.0 → 0.5.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,47 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0 — 2026-08-29
4
+
5
+ - **A multi-type union is no longer silently narrowed to its first member.**
6
+ `resolvePrimaryType` now reports every non-null member, and
7
+ `convertAiSchemaToBlockFields` branches on them: a union spanning
8
+ objects/arrays becomes `json`, a scalar-only union becomes
9
+ `{type:"union", scalars}`, and anything unnameable stays `json` with a
10
+ warning. Single-typed fields are untouched.
11
+
12
+ WHY: `type: ["string","number","boolean","object","array","null"]` —
13
+ pydantic's `Any`, and the commonest construct in user-authored kinds —
14
+ converted to plain `string`. The loss was invisible and it inverted the
15
+ field's meaning: an `estimated_count` of `1` then FAILED validation, so a
16
+ correct payload read as a broken instance. Widening is safe; silently
17
+ narrowing is not.
18
+
19
+ - New `__tests__/json-schema-to-kind.test.ts` pins JSON Schema → KindSchema
20
+ over the constructs a stored field list historically could not hold —
21
+ pydantic-Any unions, `items: {}` arrays, arrays of `__kind` children,
22
+ nested `__kind` objects, and markerless inline objects. All convert with
23
+ zero errors, which is the evidence that `emitted_json_schema` is sufficient
24
+ on its own and a second stored copy of a kind's fields is unnecessary.
25
+
26
+ ## 0.4.0 — 2026-08-29
27
+
28
+ - **`unverified` — "we never checked it" is no longer "we checked it and it
29
+ failed."** `IrKindState` gains `unverified`, and `raw_object` events carry a
30
+ `cause` (`"unverified" | "invalid"`, absent reads as `"invalid"`). The four
31
+ no-schema-registered degrades — the two pending-schema resolutions, plus the
32
+ typed and speculated finalizers — now report `unverified`; every validation
33
+ failure, array-item mismatch, duplicate key, bad placement, and contradicted
34
+ speculation still reports `invalid`. `IrTree` carries the cause onto
35
+ `root.kindState` and into `nodeIndex`.
36
+
37
+ WHY THIS EXISTS: `raw` meant both things, and on 2026-08-28 the render route
38
+ began reading `raw` as "broken instance" and diverting it away from the
39
+ kind's component. Every kind whose schema cannot be reconstructed — nested
40
+ objects, arrays of child kinds, loose fields — degrades on the no-schema
41
+ path, so ~221 live kinds with purpose-built components silently started
42
+ rendering as key/value dumps. Consumers MUST NOT treat `unverified` as a
43
+ failure: the value is intact and was never examined.
44
+
3
45
  ## 0.3.0 — 2026-08-29
4
46
 
5
47
  - **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();
@@ -3331,7 +3360,34 @@ function convertPropertyCore(fieldName, node, required, path, ctx) {
3331
3360
  });
3332
3361
  return { ...requiredNullableFlags(required), type: "json" };
3333
3362
  }
3334
- const { type, nullable } = resolvePrimaryType(node);
3363
+ const { type, nullable, members } = resolvePrimaryType(node);
3364
+ if (members.length > 1) {
3365
+ const objectish = members.some((m) => m === "object" || m === "array");
3366
+ if (objectish) {
3367
+ ctx.problems.push({
3368
+ severity: "info",
3369
+ path,
3370
+ message: `Union of [${members.join(", ")}] at "${path || "(root)"}" spans objects/arrays \u2014 carried as any JSON value (json).`
3371
+ });
3372
+ return { ...requiredNullableFlags(required), type: "json" };
3373
+ }
3374
+ const scalars = members.filter(
3375
+ (m) => m === "string" || m === "number" || m === "boolean"
3376
+ );
3377
+ if (scalars.length === members.length && scalars.length > 1) {
3378
+ return {
3379
+ ...requiredNullableFlags(required, nullable),
3380
+ type: "union",
3381
+ scalars
3382
+ };
3383
+ }
3384
+ ctx.problems.push({
3385
+ severity: "warning",
3386
+ path,
3387
+ 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}".`
3388
+ });
3389
+ return { ...requiredNullableFlags(required), type: "json" };
3390
+ }
3335
3391
  if (type === "string") {
3336
3392
  if (Array.isArray(node.enum)) {
3337
3393
  const values = node.enum.filter(