@ai-matrx/content-ir 0.5.0 → 0.7.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,48 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0 — 2026-08-29
4
+
5
+ - **`kindSchemaFromJsonSchema` — the registry-facing door for deriving a
6
+ kind's fields from its schema.** One schema in; the kind's `KindSchema` out,
7
+ plus every child kind the schema declared inline or through `$defs`, plus
8
+ the conversion problems. Pure and synchronous, so a host can call it from a
9
+ warm load, a cold fetch, a reducer, or a test. Accepts a bare JSON Schema
10
+ and the provider `{name, schema, strict}` envelope alike.
11
+
12
+ This is what makes a stored second copy of a kind's fields unnecessary. It
13
+ never returns a partial guess: an inexpressible field widens to `json`, and
14
+ only a structurally unusable root yields null.
15
+
16
+ ## 0.6.0 — 2026-08-29
17
+
18
+ - **`$ref` / `$defs` resolution — a kind's child kinds are no longer invisible
19
+ to JSON Schema → KindSchema.** `convertAiSchemaToBlockFields` resolves `#`,
20
+ `#/$defs/<name>` and `#/definitions/<name>` in field position, in array
21
+ `items`, and inside `anyOf`/`oneOf` (including `items: {anyOf: [$ref, …]}`,
22
+ the multi-kind array). Referenced defs are registered as their own drafts,
23
+ with a cycle brake so a self-referencing kind terminates. An unresolvable
24
+ ref is a loud error, never a guess.
25
+
26
+ WHY: `kindSchemaToJsonSchema` externalizes every child kind as a `$def`, and
27
+ the reverse direction rejected `$ref` outright — so every
28
+ `{type:"object", kind}` and `{type:"array", itemKinds}` field came back
29
+ EMPTY. Measured over the 62 live kinds carrying both a stored field list and
30
+ a schema, 36 lost fields this way (sections, slides, nodes, edges, criteria,
31
+ questions, segments, …).
32
+
33
+ - **A `__kind`-carrying record reads back as a record.** An object whose only
34
+ declared property is `__kind`, with typed `additionalProperties`, is the
35
+ forward converter's emission for `{type:"record", values}` — it was widening
36
+ to an open `inline_object` and losing the value type.
37
+
38
+ - Live-corpus result: schema-derived KindSchemas now match the stored field
39
+ list byte-for-byte for 48 of 62 kinds, with 1 strictly richer. The remaining
40
+ 13 are DIVERGENCE BETWEEN THE TWO STORED COPIES, not conversion gaps — 4
41
+ stored lists carry `__kind` as a data field (it is the discriminator, never
42
+ a field), and 9 disagree on enums, bounds, required flags or descriptions
43
+ the schema states and the copy does not. That is the evidence for retiring
44
+ the stored copy.
45
+
3
46
  ## 0.5.0 — 2026-08-29
4
47
 
5
48
  - **A multi-type union is no longer silently narrowed to its first member.**
package/dist/index.cjs CHANGED
@@ -3177,6 +3177,29 @@ function buildAgentSchemaWithRenderBlockSupport(input, rootKindSlug, arrayBindin
3177
3177
  }
3178
3178
  return updatedRoot;
3179
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
+ }
3180
3203
  function isAnyValueSchema(node) {
3181
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;
3182
3205
  }
@@ -3213,12 +3236,21 @@ function convertProperty(fieldName, node, required, path, ctx) {
3213
3236
  }
3214
3237
  function convertPropertyCore(fieldName, node, required, path, ctx) {
3215
3238
  if (typeof node.$ref === "string") {
3216
- ctx.problems.push({
3217
- severity: "error",
3218
- path,
3219
- message: `$ref is not supported ("${node.$ref}"). Inline the schema manually.`
3220
- });
3221
- 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
+ };
3222
3254
  }
3223
3255
  if (Array.isArray(node.anyOf) || Array.isArray(node.oneOf)) {
3224
3256
  const variants = node.anyOf ?? node.oneOf;
@@ -3234,12 +3266,18 @@ function convertPropertyCore(fieldName, node, required, path, ctx) {
3234
3266
  continue;
3235
3267
  }
3236
3268
  if (typeof variant.$ref === "string") {
3237
- ctx.problems.push({
3238
- severity: "error",
3239
- path,
3240
- message: `$ref inside anyOf/oneOf is not supported ("${variant.$ref}"). Inline the schema manually.`
3241
- });
3242
- 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);
3243
3281
  continue;
3244
3282
  }
3245
3283
  const { type: type2, nullable: nullable2 } = resolvePrimaryType(variant);
@@ -3442,6 +3480,27 @@ function convertPropertyCore(fieldName, node, required, path, ctx) {
3442
3480
  });
3443
3481
  return null;
3444
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
+ }
3445
3504
  if (isAnyValueSchema(itemNode)) {
3446
3505
  return {
3447
3506
  ...requiredNullableFlags(required, nullable),
@@ -3476,11 +3535,29 @@ function convertPropertyCore(fieldName, node, required, path, ctx) {
3476
3535
  }
3477
3536
  const itemKinds = [];
3478
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
+ }
3479
3556
  if (!isRecord4(variant) || !isRecord4(variant.properties)) {
3480
3557
  ctx.problems.push({
3481
3558
  severity: "error",
3482
3559
  path: `${path}[]`,
3483
- 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."
3484
3561
  });
3485
3562
  return null;
3486
3563
  }
@@ -3614,6 +3691,14 @@ function convertPropertyCore(fieldName, node, required, path, ctx) {
3614
3691
  kind: referencedKind
3615
3692
  };
3616
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
+ }
3617
3702
  let open = apIsOpen;
3618
3703
  if (!apIsOpen && isRecord4(ap)) {
3619
3704
  ctx.problems.push({
@@ -3730,12 +3815,21 @@ function normalizeAiSchemaInput(input) {
3730
3815
  return { name: null, strict: null, rootSchema: null, parseErrors };
3731
3816
  }
3732
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
+ }
3733
3823
  const ctx = {
3734
3824
  schemaName,
3825
+ strict,
3735
3826
  problems: [],
3736
3827
  droppedMetadata: [],
3737
3828
  blockSchemas: [],
3738
- arrayBindings: []
3829
+ arrayBindings: [],
3830
+ defs,
3831
+ rootSchema,
3832
+ refsInProgress: /* @__PURE__ */ new Set()
3739
3833
  };
3740
3834
  ctx.droppedMetadata.push(...collectDropped(rootSchema, ""));
3741
3835
  const rootAp = rootSchema.additionalProperties;
@@ -4201,6 +4295,41 @@ function kindSchemaToJsonSchema(kind, resolve, options = {}) {
4201
4295
  return { name: kind, schema: { ...rootNode, $defs: defs }, strict, unresolved };
4202
4296
  }
4203
4297
 
4298
+ // convert/json-schema-to-kind.ts
4299
+ function kindSchemaFromJsonSchema(kind, jsonSchema) {
4300
+ if (jsonSchema === null || jsonSchema === void 0) {
4301
+ return { schema: null, children: {}, problems: [] };
4302
+ }
4303
+ const normalized = normalizeAiSchemaInput(jsonSchema);
4304
+ const rootSchema = normalized.rootSchema ?? (typeof jsonSchema === "object" && !Array.isArray(jsonSchema) ? jsonSchema : null);
4305
+ if (!rootSchema) {
4306
+ return {
4307
+ schema: null,
4308
+ children: {},
4309
+ problems: [
4310
+ {
4311
+ severity: "error",
4312
+ path: "",
4313
+ message: `Kind "${kind}": emitted_json_schema is not a JSON object.`
4314
+ }
4315
+ ]
4316
+ };
4317
+ }
4318
+ const converted = convertAiSchemaToBlockFields(kind, rootSchema, false);
4319
+ let schema = null;
4320
+ const children = {};
4321
+ for (const draft of converted.blockSchemas) {
4322
+ const asKindSchema = {
4323
+ kind: draft.slug,
4324
+ fields: draft.fields,
4325
+ ...draft.root ? { root: draft.root } : {}
4326
+ };
4327
+ if (draft.slug === kind) schema = asKindSchema;
4328
+ else children[draft.slug] = asKindSchema;
4329
+ }
4330
+ return { schema, children, problems: converted.problems };
4331
+ }
4332
+
4204
4333
  // wire/partial-kind.ts
4205
4334
  var IR_PARTIAL_KEY = "__ir_partial";
4206
4335
  var PARTIAL_STATES = [
@@ -4532,6 +4661,7 @@ exports.isJsonAnyField = isJsonAnyField;
4532
4661
  exports.isProvisionalKind = isProvisionalKind;
4533
4662
  exports.isScalarArrayType = isScalarArrayType;
4534
4663
  exports.isTerminalKindEvent = isTerminalKindEvent;
4664
+ exports.kindSchemaFromJsonSchema = kindSchemaFromJsonSchema;
4535
4665
  exports.kindSchemaToJsonSchema = kindSchemaToJsonSchema;
4536
4666
  exports.kindSchemaToStorage = kindSchemaToStorage;
4537
4667
  exports.kindVerdictOf = kindVerdictOf;