@ai-matrx/content-ir 0.19.20 → 0.19.25

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.
@@ -219,12 +219,81 @@ type KindSchemaFromJsonResult = {
219
219
  /** Conversion notes. `severity: "error"` entries are worth screaming about. */
220
220
  problems: ConversionProblem[];
221
221
  };
222
+ type KindSchemaFromJsonOptions = {
223
+ /**
224
+ * The registry row's `metadata.disposition`. A `record` kind's schema gains the four
225
+ * control keys (`_replaces`, `_new`, `_record_id`, `_records`) as optional render fields
226
+ * (`withControlKeys`, KINDS-GLUE wave 2 §1.4), so an agent's patch, batch or signal draws
227
+ * through its kind instead of reading as unknown keys. Any other value adds nothing.
228
+ */
229
+ disposition?: string | null;
230
+ };
222
231
  /**
223
232
  * Derive `kind`'s field schema from its JSON Schema.
224
233
  *
225
234
  * Pure and synchronous — no registry, no IO — so a host can call it from a
226
235
  * warm load, a cold fetch, a reducer, or a test.
227
236
  */
228
- declare function kindSchemaFromJsonSchema(kind: string, jsonSchema: unknown): KindSchemaFromJsonResult;
237
+ declare function kindSchemaFromJsonSchema(kind: string, jsonSchema: unknown, options?: KindSchemaFromJsonOptions): KindSchemaFromJsonResult;
238
+
239
+ /**
240
+ * The four control keys an agent may put beside a kind's values — the TypeScript twin of
241
+ * `matrx_graph.kind_control_keys.with_control_keys` (aidream). ONE definition, two languages:
242
+ * for the same input both produce the same JSON (guard W2-G7, the shared golden
243
+ * `packages/matrx-graph/tests/fixtures/kind_control_keys_golden.json`).
244
+ *
245
+ * `_replaces` ("this replaces that output"), `_new` ("this is something new"), `_record_id`
246
+ * ("change this record") and `_records` ("several records") are never part of a kind and no
247
+ * registry row carries them — they are added where a schema is USED, for `record` kinds and
248
+ * table kinds only:
249
+ *
250
+ * - `withControlKeys(schema, { mode: "stored" })` — the JSON Schema a value is validated
251
+ * against: all four declared optional (nullable), and the root's required Fields lifted when
252
+ * the value is a patch (`_record_id`) or a batch (`_records`) — one branch APPENDED to `allOf`,
253
+ * never replacing it.
254
+ * - `withControlKeys(schema, { mode: "authored", many, allowEdit })` — what the model is shown:
255
+ * `_replaces`/`_new` always, `_record_id` only with `allowEdit`, and with `many` the root is
256
+ * the batch `{__kind, _records: [<record without __kind>], ...}`. Signals are OPTIONAL — the
257
+ * provider translators widen them to required-and-nullable on the wire and prune a null back.
258
+ * - `withControlKeyFields(kindSchema)` — the render-level twin: the four keys as optional
259
+ * render fields of a `KindSchema`, so a patch, a batch or a signal is drawn by its kind's
260
+ * component instead of being reported as unknown keys. The web schema is never stricter
261
+ * than the store.
262
+ *
263
+ * Design: common-docs `projects/data-doctrine-adoption/v6/KINDS-GLUE-WAVE2-DESIGN.md` §1.4.
264
+ */
265
+
266
+ /** The four keys, in the order they are declared. */
267
+ declare const CONTROL_KEYS: readonly ["_replaces", "_new", "_record_id", "_records"];
268
+ type ControlKey = (typeof CONTROL_KEYS)[number];
269
+ /** The one disposition whose kinds take the control keys (table kinds are always `record`). */
270
+ declare const RECORD_DISPOSITION = "record";
271
+ type ControlKeysMode = "stored" | "authored";
272
+ type WithControlKeysOptions = {
273
+ mode: ControlKeysMode;
274
+ /** Authored mode: the root becomes the batch `{__kind, _records: [...]}`. */
275
+ many?: boolean;
276
+ /** Authored mode: the model may name `_record_id` (the run may edit its own outputs). */
277
+ allowEdit?: boolean;
278
+ };
279
+ /** Whether a kind with this disposition takes the control keys. */
280
+ declare function controlKeysApplyTo(disposition: string | null | undefined): boolean;
281
+ /**
282
+ * A copy of `schema` that carries the control keys for `mode`. A schema that is not an object
283
+ * with `properties` (a root-form kind, the "any value" `{}`) is returned unchanged.
284
+ */
285
+ declare function withControlKeys<T>(schema: T, options: WithControlKeysOptions): T;
286
+ /** The render fields the control keys add to a `KindSchema` — all optional, none stricter than the store. */
287
+ declare const CONTROL_KEY_FIELDS: Readonly<Record<ControlKey, FieldSchema>>;
288
+ /**
289
+ * `schema` with the four control keys as optional render fields. A root-form kind (no field
290
+ * map) is returned unchanged. Keys the kind itself declares are left as the kind declares them.
291
+ */
292
+ declare function withControlKeyFields(schema: KindSchema): KindSchema;
293
+ /** Split a value into (values without the control keys, the control keys present). */
294
+ declare function stripControlKeys(value: Record<string, unknown>): {
295
+ values: Record<string, unknown>;
296
+ signals: Partial<Record<ControlKey, unknown>>;
297
+ };
229
298
 
230
- export { type ArrayItemKindBinding, type BlockSchemaDraft, type ConversionProblem, type DroppedMetadata, type FieldComparison, type ItemKindRefCheck, type JsonSchemaNode, type KindJsonSchemaExport, type KindJsonSchemaOptions, type KindSchemaFromJsonResult, type SavePlanEntry, type SavePlanValidation, type SchemaConversionResult, buildAgentSchemaWithRenderBlockSupport, collectReferencedKinds, collectSchemaReferencedKinds, compareWithExistingKindSchema, convertAiSchemaToBlockFields, fieldsToDbPayload, injectKindIntoObjectSchema, isDuplicateBlockSlug, kindSchemaFromJsonSchema, kindSchemaToJsonSchema, normalizeAiSchemaInput, runSchemaConversion, validateBlockSchemaSavePlan };
299
+ export { type ArrayItemKindBinding, type BlockSchemaDraft, CONTROL_KEYS, CONTROL_KEY_FIELDS, type ControlKey, type ControlKeysMode, type ConversionProblem, type DroppedMetadata, type FieldComparison, type ItemKindRefCheck, type JsonSchemaNode, type KindJsonSchemaExport, type KindJsonSchemaOptions, type KindSchemaFromJsonOptions, type KindSchemaFromJsonResult, RECORD_DISPOSITION, type SavePlanEntry, type SavePlanValidation, type SchemaConversionResult, type WithControlKeysOptions, buildAgentSchemaWithRenderBlockSupport, collectReferencedKinds, collectSchemaReferencedKinds, compareWithExistingKindSchema, controlKeysApplyTo, convertAiSchemaToBlockFields, fieldsToDbPayload, injectKindIntoObjectSchema, isDuplicateBlockSlug, kindSchemaFromJsonSchema, kindSchemaToJsonSchema, normalizeAiSchemaInput, runSchemaConversion, stripControlKeys, validateBlockSchemaSavePlan, withControlKeyFields, withControlKeys };
package/dist/convert.d.ts CHANGED
@@ -219,12 +219,81 @@ type KindSchemaFromJsonResult = {
219
219
  /** Conversion notes. `severity: "error"` entries are worth screaming about. */
220
220
  problems: ConversionProblem[];
221
221
  };
222
+ type KindSchemaFromJsonOptions = {
223
+ /**
224
+ * The registry row's `metadata.disposition`. A `record` kind's schema gains the four
225
+ * control keys (`_replaces`, `_new`, `_record_id`, `_records`) as optional render fields
226
+ * (`withControlKeys`, KINDS-GLUE wave 2 §1.4), so an agent's patch, batch or signal draws
227
+ * through its kind instead of reading as unknown keys. Any other value adds nothing.
228
+ */
229
+ disposition?: string | null;
230
+ };
222
231
  /**
223
232
  * Derive `kind`'s field schema from its JSON Schema.
224
233
  *
225
234
  * Pure and synchronous — no registry, no IO — so a host can call it from a
226
235
  * warm load, a cold fetch, a reducer, or a test.
227
236
  */
228
- declare function kindSchemaFromJsonSchema(kind: string, jsonSchema: unknown): KindSchemaFromJsonResult;
237
+ declare function kindSchemaFromJsonSchema(kind: string, jsonSchema: unknown, options?: KindSchemaFromJsonOptions): KindSchemaFromJsonResult;
238
+
239
+ /**
240
+ * The four control keys an agent may put beside a kind's values — the TypeScript twin of
241
+ * `matrx_graph.kind_control_keys.with_control_keys` (aidream). ONE definition, two languages:
242
+ * for the same input both produce the same JSON (guard W2-G7, the shared golden
243
+ * `packages/matrx-graph/tests/fixtures/kind_control_keys_golden.json`).
244
+ *
245
+ * `_replaces` ("this replaces that output"), `_new` ("this is something new"), `_record_id`
246
+ * ("change this record") and `_records` ("several records") are never part of a kind and no
247
+ * registry row carries them — they are added where a schema is USED, for `record` kinds and
248
+ * table kinds only:
249
+ *
250
+ * - `withControlKeys(schema, { mode: "stored" })` — the JSON Schema a value is validated
251
+ * against: all four declared optional (nullable), and the root's required Fields lifted when
252
+ * the value is a patch (`_record_id`) or a batch (`_records`) — one branch APPENDED to `allOf`,
253
+ * never replacing it.
254
+ * - `withControlKeys(schema, { mode: "authored", many, allowEdit })` — what the model is shown:
255
+ * `_replaces`/`_new` always, `_record_id` only with `allowEdit`, and with `many` the root is
256
+ * the batch `{__kind, _records: [<record without __kind>], ...}`. Signals are OPTIONAL — the
257
+ * provider translators widen them to required-and-nullable on the wire and prune a null back.
258
+ * - `withControlKeyFields(kindSchema)` — the render-level twin: the four keys as optional
259
+ * render fields of a `KindSchema`, so a patch, a batch or a signal is drawn by its kind's
260
+ * component instead of being reported as unknown keys. The web schema is never stricter
261
+ * than the store.
262
+ *
263
+ * Design: common-docs `projects/data-doctrine-adoption/v6/KINDS-GLUE-WAVE2-DESIGN.md` §1.4.
264
+ */
265
+
266
+ /** The four keys, in the order they are declared. */
267
+ declare const CONTROL_KEYS: readonly ["_replaces", "_new", "_record_id", "_records"];
268
+ type ControlKey = (typeof CONTROL_KEYS)[number];
269
+ /** The one disposition whose kinds take the control keys (table kinds are always `record`). */
270
+ declare const RECORD_DISPOSITION = "record";
271
+ type ControlKeysMode = "stored" | "authored";
272
+ type WithControlKeysOptions = {
273
+ mode: ControlKeysMode;
274
+ /** Authored mode: the root becomes the batch `{__kind, _records: [...]}`. */
275
+ many?: boolean;
276
+ /** Authored mode: the model may name `_record_id` (the run may edit its own outputs). */
277
+ allowEdit?: boolean;
278
+ };
279
+ /** Whether a kind with this disposition takes the control keys. */
280
+ declare function controlKeysApplyTo(disposition: string | null | undefined): boolean;
281
+ /**
282
+ * A copy of `schema` that carries the control keys for `mode`. A schema that is not an object
283
+ * with `properties` (a root-form kind, the "any value" `{}`) is returned unchanged.
284
+ */
285
+ declare function withControlKeys<T>(schema: T, options: WithControlKeysOptions): T;
286
+ /** The render fields the control keys add to a `KindSchema` — all optional, none stricter than the store. */
287
+ declare const CONTROL_KEY_FIELDS: Readonly<Record<ControlKey, FieldSchema>>;
288
+ /**
289
+ * `schema` with the four control keys as optional render fields. A root-form kind (no field
290
+ * map) is returned unchanged. Keys the kind itself declares are left as the kind declares them.
291
+ */
292
+ declare function withControlKeyFields(schema: KindSchema): KindSchema;
293
+ /** Split a value into (values without the control keys, the control keys present). */
294
+ declare function stripControlKeys(value: Record<string, unknown>): {
295
+ values: Record<string, unknown>;
296
+ signals: Partial<Record<ControlKey, unknown>>;
297
+ };
229
298
 
230
- export { type ArrayItemKindBinding, type BlockSchemaDraft, type ConversionProblem, type DroppedMetadata, type FieldComparison, type ItemKindRefCheck, type JsonSchemaNode, type KindJsonSchemaExport, type KindJsonSchemaOptions, type KindSchemaFromJsonResult, type SavePlanEntry, type SavePlanValidation, type SchemaConversionResult, buildAgentSchemaWithRenderBlockSupport, collectReferencedKinds, collectSchemaReferencedKinds, compareWithExistingKindSchema, convertAiSchemaToBlockFields, fieldsToDbPayload, injectKindIntoObjectSchema, isDuplicateBlockSlug, kindSchemaFromJsonSchema, kindSchemaToJsonSchema, normalizeAiSchemaInput, runSchemaConversion, validateBlockSchemaSavePlan };
299
+ export { type ArrayItemKindBinding, type BlockSchemaDraft, CONTROL_KEYS, CONTROL_KEY_FIELDS, type ControlKey, type ControlKeysMode, type ConversionProblem, type DroppedMetadata, type FieldComparison, type ItemKindRefCheck, type JsonSchemaNode, type KindJsonSchemaExport, type KindJsonSchemaOptions, type KindSchemaFromJsonOptions, type KindSchemaFromJsonResult, RECORD_DISPOSITION, type SavePlanEntry, type SavePlanValidation, type SchemaConversionResult, type WithControlKeysOptions, buildAgentSchemaWithRenderBlockSupport, collectReferencedKinds, collectSchemaReferencedKinds, compareWithExistingKindSchema, controlKeysApplyTo, convertAiSchemaToBlockFields, fieldsToDbPayload, injectKindIntoObjectSchema, isDuplicateBlockSlug, kindSchemaFromJsonSchema, kindSchemaToJsonSchema, normalizeAiSchemaInput, runSchemaConversion, stripControlKeys, validateBlockSchemaSavePlan, withControlKeyFields, withControlKeys };
package/dist/convert.js CHANGED
@@ -423,13 +423,13 @@ function makeKindJsonSchemaProperty(kindSlug, strict) {
423
423
  return { ...base, enum: [kindSlug] };
424
424
  }
425
425
  function injectKindIntoObjectSchema(objectSchema, kindSlug, strict) {
426
- const clone = deepClone(objectSchema);
427
- const properties = isRecord(clone.properties) ? { ...clone.properties } : {};
426
+ const clone2 = deepClone(objectSchema);
427
+ const properties = isRecord(clone2.properties) ? { ...clone2.properties } : {};
428
428
  properties[KIND_KEY] = makeKindJsonSchemaProperty(kindSlug, strict);
429
- const required = Array.isArray(clone.required) ? clone.required.filter((k) => typeof k === "string") : [];
429
+ const required = Array.isArray(clone2.required) ? clone2.required.filter((k) => typeof k === "string") : [];
430
430
  const nextRequired = required.includes(KIND_KEY) ? required : [KIND_KEY, ...required];
431
431
  return {
432
- ...clone,
432
+ ...clone2,
433
433
  properties,
434
434
  required: nextRequired,
435
435
  ...strict ? { additionalProperties: false } : {}
@@ -1626,8 +1626,132 @@ function kindSchemaToJsonSchema(kind, resolve, options = {}) {
1626
1626
  return { name: kind, schema: { ...rootNode, $defs: defs }, strict, unresolved };
1627
1627
  }
1628
1628
 
1629
+ // convert/control-keys.ts
1630
+ var CONTROL_KEYS = ["_replaces", "_new", "_record_id", "_records"];
1631
+ var RECORD_DISPOSITION = "record";
1632
+ var KIND_KEY2 = "__kind";
1633
+ var DOCUMENT_KEYS = /* @__PURE__ */ new Set(["$schema", "$id", "$defs", "definitions", "title", "description"]);
1634
+ var CONTROL = new Set(CONTROL_KEYS);
1635
+ function controlKeysApplyTo(disposition) {
1636
+ return disposition === RECORD_DISPOSITION;
1637
+ }
1638
+ function clone(value) {
1639
+ return value === void 0 ? value : JSON.parse(JSON.stringify(value));
1640
+ }
1641
+ function isObjectSchema(schema) {
1642
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) return false;
1643
+ const s = schema;
1644
+ return s.type === "object" && !!s.properties && typeof s.properties === "object" && !Array.isArray(s.properties);
1645
+ }
1646
+ function requiredOf(schema) {
1647
+ return Array.isArray(schema.required) ? schema.required : [];
1648
+ }
1649
+ function recordObject(schema) {
1650
+ const item = {};
1651
+ for (const [k, v] of Object.entries(schema)) {
1652
+ if (!DOCUMENT_KEYS.has(k)) item[k] = clone(v);
1653
+ }
1654
+ const props = {};
1655
+ for (const [k, v] of Object.entries(item.properties)) {
1656
+ if (k !== KIND_KEY2 && !CONTROL.has(k)) props[k] = v;
1657
+ }
1658
+ item.properties = props;
1659
+ const required = requiredOf(item).filter((k) => k !== KIND_KEY2 && !CONTROL.has(k));
1660
+ if (required.length > 0) item.required = required;
1661
+ else delete item.required;
1662
+ return item;
1663
+ }
1664
+ function stored(schema) {
1665
+ const out = clone(schema);
1666
+ const item = recordObject(schema);
1667
+ const props = out.properties;
1668
+ props._replaces = { type: ["string", "null"], format: "uuid" };
1669
+ props._new = { type: ["boolean", "null"] };
1670
+ props._record_id = { type: ["string", "null"], format: "uuid" };
1671
+ props._records = { type: ["array", "null"], items: item };
1672
+ const rootRequired = requiredOf(out).filter((k) => !CONTROL.has(k));
1673
+ const lifted = rootRequired.filter((k) => k !== KIND_KEY2);
1674
+ if (lifted.length > 0) {
1675
+ const kept = rootRequired.filter((k) => k === KIND_KEY2);
1676
+ if (kept.length > 0) out.required = kept;
1677
+ else delete out.required;
1678
+ const branch = {
1679
+ if: {
1680
+ anyOf: [
1681
+ { required: ["_record_id"], properties: { _record_id: { type: "string" } } },
1682
+ { required: ["_records"], properties: { _records: { type: "array" } } }
1683
+ ]
1684
+ },
1685
+ else: { required: lifted }
1686
+ };
1687
+ out.allOf = Array.isArray(out.allOf) ? [...out.allOf, branch] : [branch];
1688
+ }
1689
+ return out;
1690
+ }
1691
+ function authored(schema, many, allowEdit) {
1692
+ const signals = {
1693
+ _replaces: { type: "string", format: "uuid" },
1694
+ _new: { type: "boolean" }
1695
+ };
1696
+ if (allowEdit) signals._record_id = { type: "string", format: "uuid" };
1697
+ if (!many) {
1698
+ const out2 = clone(schema);
1699
+ Object.assign(out2.properties, clone(signals));
1700
+ const required = requiredOf(out2).filter((k) => !CONTROL.has(k));
1701
+ if (required.length > 0 || "required" in out2) out2.required = required;
1702
+ return out2;
1703
+ }
1704
+ const out = {};
1705
+ for (const [k, v] of Object.entries(schema)) {
1706
+ if (DOCUMENT_KEYS.has(k)) out[k] = clone(v);
1707
+ }
1708
+ out.type = "object";
1709
+ const kindProp = schema.properties[KIND_KEY2];
1710
+ const properties = {};
1711
+ if (kindProp !== void 0) properties[KIND_KEY2] = clone(kindProp);
1712
+ properties._records = { type: "array", items: recordObject(schema) };
1713
+ Object.assign(properties, clone(signals));
1714
+ out.properties = properties;
1715
+ out.required = [KIND_KEY2, "_records"].filter((k) => k in properties);
1716
+ out.additionalProperties = false;
1717
+ return out;
1718
+ }
1719
+ function withControlKeys(schema, options) {
1720
+ const { mode, many = false, allowEdit = false } = options;
1721
+ if (mode !== "stored" && mode !== "authored") {
1722
+ throw new Error(`withControlKeys: mode must be "stored" or "authored", got ${String(mode)}`);
1723
+ }
1724
+ if (!isObjectSchema(schema)) return schema;
1725
+ return mode === "stored" ? stored(schema) : authored(schema, many, allowEdit);
1726
+ }
1727
+ var CONTROL_KEY_FIELDS = Object.freeze({
1728
+ _replaces: { type: "string", nullable: true },
1729
+ _new: { type: "boolean", nullable: true },
1730
+ _record_id: { type: "string", nullable: true },
1731
+ // A batch's items are records of this kind WITHOUT `__kind`; the render layer reads them
1732
+ // as opaque JSON (never stricter than the store, which validates each item).
1733
+ _records: { type: "json[]", nullable: true }
1734
+ });
1735
+ function withControlKeyFields(schema) {
1736
+ if (schema.root) return schema;
1737
+ const fields = { ...schema.fields };
1738
+ for (const key of CONTROL_KEYS) {
1739
+ if (!(key in fields)) fields[key] = { ...CONTROL_KEY_FIELDS[key] };
1740
+ }
1741
+ return { ...schema, fields };
1742
+ }
1743
+ function stripControlKeys(value) {
1744
+ const values = {};
1745
+ const signals = {};
1746
+ for (const [k, v] of Object.entries(value)) {
1747
+ if (CONTROL.has(k)) signals[k] = v;
1748
+ else values[k] = v;
1749
+ }
1750
+ return { values, signals };
1751
+ }
1752
+
1629
1753
  // convert/json-schema-to-kind.ts
1630
- function kindSchemaFromJsonSchema(kind, jsonSchema) {
1754
+ function kindSchemaFromJsonSchema(kind, jsonSchema, options = {}) {
1631
1755
  if (jsonSchema === null || jsonSchema === void 0) {
1632
1756
  return { schema: null, children: {}, problems: [] };
1633
1757
  }
@@ -1658,9 +1782,12 @@ function kindSchemaFromJsonSchema(kind, jsonSchema) {
1658
1782
  if (draft.slug === kind) schema = asKindSchema;
1659
1783
  else children[draft.slug] = asKindSchema;
1660
1784
  }
1785
+ if (schema && controlKeysApplyTo(options.disposition)) {
1786
+ schema = withControlKeyFields(schema);
1787
+ }
1661
1788
  return { schema, children, problems: converted.problems };
1662
1789
  }
1663
1790
 
1664
- export { buildAgentSchemaWithRenderBlockSupport, collectReferencedKinds, collectSchemaReferencedKinds, compareWithExistingKindSchema, convertAiSchemaToBlockFields, fieldsToDbPayload, injectKindIntoObjectSchema, isDuplicateBlockSlug, kindSchemaFromJsonSchema, kindSchemaToJsonSchema, normalizeAiSchemaInput, runSchemaConversion, validateBlockSchemaSavePlan };
1791
+ export { CONTROL_KEYS, CONTROL_KEY_FIELDS, RECORD_DISPOSITION, buildAgentSchemaWithRenderBlockSupport, collectReferencedKinds, collectSchemaReferencedKinds, compareWithExistingKindSchema, controlKeysApplyTo, convertAiSchemaToBlockFields, fieldsToDbPayload, injectKindIntoObjectSchema, isDuplicateBlockSlug, kindSchemaFromJsonSchema, kindSchemaToJsonSchema, normalizeAiSchemaInput, runSchemaConversion, stripControlKeys, validateBlockSchemaSavePlan, withControlKeyFields, withControlKeys };
1665
1792
  //# sourceMappingURL=convert.js.map
1666
1793
  //# sourceMappingURL=convert.js.map