@ai-matrx/content-ir 0.2.2 → 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,63 @@
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
+
45
+ ## 0.3.0 — 2026-08-29
46
+
47
+ - **KIND PRESERVATION now covers STRUCTURAL raws.** A node that degrades to
48
+ `raw_object` because its value failed schema validation, carried a duplicate
49
+ key, or violated the parent's `itemKinds` keeps its identified kind on the
50
+ raw event, the envelope root (`root.kind` with `kindState: "raw"`), and the
51
+ `nodeIndex` entry for nested children. Previously only schema-availability
52
+ raws preserved the kind, so a payload that was off by ONE field rendered as
53
+ anonymous raw JSON with no repaint subscription and no route — the single
54
+ most damaging class of the 2026-08-29 "kind slips through the cracks" audit
55
+ (crack #9). The render seam's registration boundary is unchanged: an
56
+ unregistered slug is still never claimed. Consumers should route a
57
+ registered kind-preserved raw to their generic floor and surface
58
+ `residue.notices` (the exact `raw_fallback` reason is recorded there).
59
+ Pinned by two new kind-parser tests.
60
+
3
61
  ## 0.2.2 — 2026-08-24
4
62
 
5
63
  - `rehydrateRunResult` now rejects nested entries whose `__kind` is not
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,
@@ -390,7 +401,11 @@ var IrTree = class {
390
401
  }
391
402
  for (const [pathKey] of this.rawPaths) {
392
403
  if (pathKey === "") continue;
393
- nodeIndex[pathKey] = { kind: "", kindState: "raw", status: "complete" };
404
+ nodeIndex[pathKey] = {
405
+ kind: this.rawKinds.get(pathKey) ?? "",
406
+ kindState: this.rawCauses.get(pathKey) === "unverified" ? "unverified" : "raw",
407
+ status: "complete"
408
+ };
394
409
  }
395
410
  return {
396
411
  v: IR_VERSION,
@@ -728,7 +743,8 @@ var KindStreamParser = class {
728
743
  safeCopy(value),
729
744
  `No block schema registered for "${kind}".`,
730
745
  at,
731
- kind
746
+ kind,
747
+ "unverified"
732
748
  );
733
749
  }
734
750
  }
@@ -762,7 +778,8 @@ var KindStreamParser = class {
762
778
  safeCopy(value ?? {}),
763
779
  `No block schema registered for "${kind}".`,
764
780
  at,
765
- kind
781
+ kind,
782
+ "unverified"
766
783
  );
767
784
  this.closedPendingPaths.delete(pathKey);
768
785
  continue;
@@ -905,7 +922,7 @@ var KindStreamParser = class {
905
922
  * Recorded at the first root key; ADOPTED only when the root object closes
906
923
  * (`completeTypedObject`). That is the surface registry's complete-only
907
924
  * convergence law, and every json_root_key row is `streaming:false` — these
908
- * legacy shapes are recognised by their whole payload, so speculating
925
+ * legacy shapes are recognized by their whole payload, so speculating
909
926
  * mid-stream would flash a kind component over an object that may never
910
927
  * satisfy the schema. An explicit `expectedRootKind` (an agent's declared
911
928
  * output schema) is stronger context and always wins; an actual `__kind`
@@ -1308,18 +1325,19 @@ var KindStreamParser = class {
1308
1325
  objectValue,
1309
1326
  `No block schema registered for "${kind}".`,
1310
1327
  at,
1311
- kind
1328
+ kind,
1329
+ "unverified"
1312
1330
  );
1313
1331
  return;
1314
1332
  }
1315
1333
  const arrayItemError = this.validateArrayItemKind(path, kind);
1316
1334
  if (arrayItemError) {
1317
- this.emitRawObject(path, objectValue, arrayItemError, at);
1335
+ this.emitRawObject(path, objectValue, arrayItemError, at, kind);
1318
1336
  return;
1319
1337
  }
1320
1338
  const outcome = this.validateObjectAgainstSchema(objectValue, schema);
1321
1339
  if (outcome.error) {
1322
- this.emitRawObject(path, objectValue, outcome.error, at);
1340
+ this.emitRawObject(path, objectValue, outcome.error, at, kind);
1323
1341
  return;
1324
1342
  }
1325
1343
  this.objectKinds.set(pathKey, kind);
@@ -1342,7 +1360,8 @@ var KindStreamParser = class {
1342
1360
  objectValue,
1343
1361
  `No block schema registered for "${kind}".`,
1344
1362
  at,
1345
- kind
1363
+ kind,
1364
+ "unverified"
1346
1365
  );
1347
1366
  return;
1348
1367
  }
@@ -1351,7 +1370,7 @@ var KindStreamParser = class {
1351
1370
  schema
1352
1371
  );
1353
1372
  if (outcome.error) {
1354
- this.emitRawObject(path, objectValue, outcome.error, at);
1373
+ this.emitRawObject(path, objectValue, outcome.error, at, kind);
1355
1374
  return;
1356
1375
  }
1357
1376
  this.emitSchemaNotices(path, kind, outcome, at);
@@ -1411,10 +1430,19 @@ var KindStreamParser = class {
1411
1430
  markNodeRaw(path, liveValue, reason, at) {
1412
1431
  const pathKey = this.pathKey(path);
1413
1432
  if (this.rawObjectPaths.has(pathKey)) return;
1433
+ const identifiedKind = this.objectKinds.get(pathKey) ?? (typeof liveValue === "object" && liveValue !== null && !Array.isArray(liveValue) ? readObjectKind(liveValue) ?? void 0 : void 0);
1414
1434
  this.speculativeKinds.delete(pathKey);
1415
- this.emitRawObject(path, safeCopy(liveValue), reason, at);
1435
+ this.emitRawObject(path, safeCopy(liveValue), reason, at, identifiedKind);
1416
1436
  }
1417
- 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") {
1418
1446
  const pathKey = this.pathKey(path);
1419
1447
  if (this.rawObjectPaths.has(pathKey)) return;
1420
1448
  this.rawObjectPaths.add(pathKey);
@@ -1425,6 +1453,7 @@ var KindStreamParser = class {
1425
1453
  value,
1426
1454
  reason,
1427
1455
  ...identifiedKind !== void 0 && { kind: identifiedKind },
1456
+ cause,
1428
1457
  at
1429
1458
  });
1430
1459
  }
@@ -2994,21 +3023,26 @@ function fieldSchemaSummary(field) {
2994
3023
  function resolvePrimaryType(node) {
2995
3024
  const raw = node.type;
2996
3025
  if (typeof raw === "string") {
2997
- return { type: raw === "integer" ? "number" : raw, nullable: false };
3026
+ const t = raw === "integer" ? "number" : raw;
3027
+ return { type: t, nullable: false, members: [t] };
2998
3028
  }
2999
3029
  if (Array.isArray(raw)) {
3000
3030
  const types = raw.filter((t) => typeof t === "string");
3001
3031
  const nullable = types.includes("null");
3002
- const primary = types.find((t) => t !== "null") ?? (nullable && types.length === 1 ? "null" : null);
3003
- if (primary === "integer") {
3004
- return { type: "number", nullable };
3005
- }
3006
- return { type: primary ?? null, nullable };
3007
- }
3008
- if (node.enum) return { type: "string", nullable: false };
3009
- if (node.properties) return { type: "object", nullable: false };
3010
- if (node.items) return { type: "array", nullable: false };
3011
- 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: [] };
3012
3046
  }
3013
3047
  function carriedMetadataKeys(field) {
3014
3048
  if (field === null) return /* @__PURE__ */ new Set();
@@ -3326,7 +3360,34 @@ function convertPropertyCore(fieldName, node, required, path, ctx) {
3326
3360
  });
3327
3361
  return { ...requiredNullableFlags(required), type: "json" };
3328
3362
  }
3329
- 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
+ }
3330
3391
  if (type === "string") {
3331
3392
  if (Array.isArray(node.enum)) {
3332
3393
  const values = node.enum.filter(