@ai-matrx/content-ir 0.18.6 → 0.19.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,12 +1,28 @@
1
1
  # Changelog
2
2
 
3
- ## 0.18.6
3
+ ## 0.19.0 — kind validation behind a schema-source port (Matrx Alchemy ALC-13)
4
+
5
+ `/registry` (and the root) now export `createKindValidator(source, options?)`,
6
+ `SchemaSourcePort`, `SchemaUnavailableReason`, `KindValidationResult`,
7
+ `KindValidator` and `Json`. This is matrx-frontend's `validateAgainstKind`,
8
+ moved into the package with its database read inverted: the host implements
9
+ `SchemaSourcePort.kindSchema(kind, signal)` once and answers `{ schema,
10
+ version }` or `{ unavailable: "kind_not_registered" | "schema_unavailable" |
11
+ "schema_invalid" | "catalog_unreachable" }`. The verdict (`checked`, `ok`,
12
+ `errors`, `degradedReason`) and its messages are unchanged; a source that
13
+ throws reads as `catalog_unreachable`; an uncompilable schema reads as
14
+ `schema_invalid`. The schema cache (30 s default, `invalidate`,
15
+ `cachedSchema`) belongs to each validator instance, never to the module.
4
16
 
5
- Automatic changed-only republish (docs/metadata drift since the last tag — see
6
- `git diff npm/content-ir-core/v0.18.5..npm/content-ir-core/v0.18.6 -- apps/shared/content-ir-core`).
7
- No source changes intended and no consumer action required.
17
+ ### Consumer action
18
+
19
+ matrx-frontend: implement `SchemaSourcePort` once over
20
+ `content_ir.kind_definition`, build one validator with `createKindValidator`,
21
+ repoint `features/surfaces/runtime/surface-writeback.ts` and
22
+ `build-tool-injection.ts` to it, and delete
23
+ `features/content-ir/registry/validate-against-kind.ts`.
8
24
 
9
- ## 0.19.0
25
+ ### Also in 0.19.0 — the RC-B3 fix round (RC-B3 lane)
10
26
 
11
27
  **Carries: RC-B3 fix round (verify-RC-B3 § 0.18.3) and RC-B3r R3 — builder:
12
28
  the RC-B3 standard lane.** (aidream 1174edb640 — RC-B10, the print lane —
@@ -16,7 +32,7 @@ below — shipped in 0.18.4; both under generic "no source changes" notes.)
16
32
  **`/source` streaming: settled means settled** (F3/F4). Found with the
17
33
  verifier's generator (realistic model fragments, every prefix, with and
18
34
  without a final newline), now permanent in `__tests__/source-attack.test.ts`
19
- (`LLM-fragment documents…`; 12,000 documents in `SOURCE_ATTACK=long`) plus 27
35
+ (`LLM-fragment documents…`; 12,000 documents in `SOURCE_ATTACK=long`) plus 30
20
36
  pinned reproducers. `tokenizeSource(prefix, { streaming: true })` now also
21
37
  holds back, as prose or as an open island:
22
38
  - lines a box-drawing tree still forming at the end may claim as its root
@@ -24,12 +40,14 @@ holds back, as prose or as an open island:
24
40
  - a paragraph cut short by a block that may still dissolve (a `$$` pair
25
41
  closing on the open line, an unbalanced JSON head, any opener still
26
42
  arriving on the open line) — and a footnote before one;
27
- - everything inline on the open last line, and a block island ending there;
43
+ - a block island ending on the open last line (inlines there are covered by
44
+ the tree-root hold);
28
45
  - a nested-fence closer found only by the strict-CommonMark retry, a tilde
29
46
  fence whose inner block may still grow past it, braces that balance on
30
47
  the open last line;
31
48
  - a code span that a blank-so-far or `>`-only open line may still close
32
- (`pendingCodeSpanStart`, `source/code-ranges.ts`).
49
+ (`pendingCodeSpanStart`, `source/code-ranges.ts`);
50
+ - a block whose opener is provisional text, and a paragraph break there.
33
51
  The two lone-HTML-tag holdbacks (M-N6/N7) collapsed into the open-tail block
34
52
  rule. Each holdback turns the attack suite red on its own when removed
35
53
  (planted mutations on a scratch copy). The H2 exception is unchanged.
@@ -53,6 +71,12 @@ guard case `echo $HOME $PATH`).
53
71
  `singleDollarAfter(text, close)` instead of `text[close + 1]` (a one-character
54
72
  `after` still works; it just cannot see a variable chain).
55
73
 
74
+ ## 0.18.6
75
+
76
+ Automatic republish. Despite the generated note, this version DID carry source
77
+ changes: most of the 0.19.0 work above (through aidream cd35751bea). 0.19.0
78
+ completes it (aidream d6241c5496, f1e9156e82).
79
+
56
80
  ## 0.18.5
57
81
 
58
82
  Automatic republish. Despite the generated note, this version DID carry source
package/README.md CHANGED
@@ -15,7 +15,7 @@ The root export is the full surface and always will be. Since 0.10.0 each intern
15
15
  | `@ai-matrx/content-ir` | everything below |
16
16
  | `@ai-matrx/content-ir/core` | IR types + tree, JSON tokenizer, kind parser, snapshots, normalize, fingerprint, discriminator, envelope read/value/cache, region, schema structure |
17
17
  | `@ai-matrx/content-ir/session` | `ParseSession` and the one-writer-per-identity registry |
18
- | `@ai-matrx/content-ir/registry` | kind registry types, storage transform, the dual gate |
18
+ | `@ai-matrx/content-ir/registry` | kind registry types, storage transform, the dual gate, `createKindValidator` over a host `SchemaSourcePort` |
19
19
  | `@ai-matrx/content-ir/convert` | kind ⇄ JSON Schema, the OpenAI structured-output converter |
20
20
  | `@ai-matrx/content-ir/wire` | partial-kind events, runtime wrapper readers, emit payload |
21
21
  | `@ai-matrx/content-ir/directives` | Kind Directives: slug grammar, the ONE decoder |
package/dist/index.cjs CHANGED
@@ -2968,6 +2968,93 @@ function describeDualGateFailure(kind, result) {
2968
2968
  return `kind "${kind}" failed the dual gate \u2014 ${parts.join(" | ")}`;
2969
2969
  }
2970
2970
 
2971
+ // registry/kind-validator.ts
2972
+ var SCHEMA_COMPILE_FAILURE_PREFIX = "emitted_json_schema failed to compile:";
2973
+ var DEFAULT_CACHE_TTL_MS = 3e4;
2974
+ function unavailableMessage(kind, reason, detail) {
2975
+ switch (reason) {
2976
+ case "kind_not_registered":
2977
+ return `kind "${kind}" is not registered in content_ir.kind_definition`;
2978
+ case "schema_unavailable":
2979
+ return `kind "${kind}" has no emitted_json_schema \u2014 nothing to validate against`;
2980
+ case "schema_invalid":
2981
+ return `kind "${kind}" has an uncompilable emitted_json_schema${detail ? `: ${detail}` : ""}`;
2982
+ case "catalog_unreachable":
2983
+ return `could not read the contract for kind "${kind}"${detail ? `: ${detail}` : ""}`;
2984
+ }
2985
+ }
2986
+ function degraded(kind, reason, detail) {
2987
+ return {
2988
+ kind,
2989
+ checked: false,
2990
+ ok: false,
2991
+ errors: [unavailableMessage(kind, reason, detail)],
2992
+ degradedReason: reason
2993
+ };
2994
+ }
2995
+ function createKindValidator(source, options = {}) {
2996
+ const ttl = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
2997
+ const now = options.now ?? Date.now;
2998
+ const cache = /* @__PURE__ */ new Map();
2999
+ async function fetchSchema(kind, signal) {
3000
+ const cached = cache.get(kind);
3001
+ if (cached && now() - cached.fetchedAt <= ttl) {
3002
+ return { schema: cached.schema, version: cached.version };
3003
+ }
3004
+ let answer;
3005
+ try {
3006
+ answer = await source.kindSchema(kind, signal);
3007
+ } catch (err) {
3008
+ return {
3009
+ unavailable: "catalog_unreachable",
3010
+ detail: err instanceof Error ? err.message : String(err)
3011
+ };
3012
+ }
3013
+ if ("unavailable" in answer) return { unavailable: answer.unavailable };
3014
+ if (answer.schema == null) {
3015
+ return { unavailable: "schema_unavailable" };
3016
+ }
3017
+ cache.set(kind, {
3018
+ schema: answer.schema,
3019
+ version: answer.version,
3020
+ fetchedAt: now()
3021
+ });
3022
+ return answer;
3023
+ }
3024
+ return {
3025
+ async validate(value, kind, signal = new AbortController().signal) {
3026
+ if (!kind || typeof kind !== "string") {
3027
+ throw new TypeError(
3028
+ `validateAgainstKind: "kind" must be a non-empty slug (received ${typeof kind})`
3029
+ );
3030
+ }
3031
+ const fetched = await fetchSchema(kind, signal);
3032
+ if ("unavailable" in fetched) {
3033
+ return degraded(kind, fetched.unavailable, fetched.detail);
3034
+ }
3035
+ const leg = validateStructuralLeg(value, fetched.schema);
3036
+ if (!leg.ok && leg.detail?.startsWith(SCHEMA_COMPILE_FAILURE_PREFIX)) {
3037
+ return degraded(kind, "schema_invalid", leg.detail);
3038
+ }
3039
+ return {
3040
+ kind,
3041
+ checked: true,
3042
+ ok: leg.ok,
3043
+ errors: leg.ok ? [] : [leg.detail ?? "validation failed"],
3044
+ degradedReason: null
3045
+ };
3046
+ },
3047
+ async cachedSchema(kind, signal = new AbortController().signal) {
3048
+ const fetched = await fetchSchema(kind, signal);
3049
+ return "unavailable" in fetched ? null : fetched.schema;
3050
+ },
3051
+ invalidate(kind) {
3052
+ if (kind) cache.delete(kind);
3053
+ else cache.clear();
3054
+ }
3055
+ };
3056
+ }
3057
+
2971
3058
  // convert/openai-schema-converter.ts
2972
3059
  function requiredNullableFlags(required, nullable) {
2973
3060
  return {
@@ -5243,12 +5330,6 @@ function fenceOpenerOf(content) {
5243
5330
  return null;
5244
5331
  }
5245
5332
  var FenceReader = class {
5246
- constructor(opener, options = {}) {
5247
- this.opener = opener;
5248
- this.isJson = opener.char === "`" && opener.lang === "json";
5249
- this.nesting = opener.char === "`" && (options.nesting ?? fenceNestsInnerFences(opener.lang));
5250
- this.tildeCloser = opener.char === "~" ? new RegExp(`^ {0,3}~{${opener.ticks},}[ \\t]*$`) : null;
5251
- }
5252
5333
  opener;
5253
5334
  state = { inString: false, escaped: false };
5254
5335
  depth = 0;
@@ -5257,6 +5338,12 @@ var FenceReader = class {
5257
5338
  isJson;
5258
5339
  nesting;
5259
5340
  tildeCloser;
5341
+ constructor(opener, options = {}) {
5342
+ this.opener = opener;
5343
+ this.isJson = opener.char === "`" && opener.lang === "json";
5344
+ this.nesting = opener.char === "`" && (options.nesting ?? fenceNestsInnerFences(opener.lang));
5345
+ this.tildeCloser = opener.char === "~" ? new RegExp(`^ {0,3}~{${opener.ticks},}[ \\t]*$`) : null;
5346
+ }
5260
5347
  /** How many nested fences are open right now. */
5261
5348
  get nestedDepth() {
5262
5349
  return this.depth;
@@ -5874,9 +5961,6 @@ function isEscapedBacktick(line, offset) {
5874
5961
  return slashes % 2 === 1;
5875
5962
  }
5876
5963
  var XmlContainerTracker = class {
5877
- constructor(rootTag) {
5878
- this.rootTag = rootTag;
5879
- }
5880
5964
  rootTag;
5881
5965
  depth = 0;
5882
5966
  inComment = false;
@@ -5884,6 +5968,9 @@ var XmlContainerTracker = class {
5884
5968
  inlineTicks = 0;
5885
5969
  /** The open fence, read by THE one closer (source/code-ranges.ts). */
5886
5970
  fence = null;
5971
+ constructor(rootTag) {
5972
+ this.rootTag = rootTag;
5973
+ }
5887
5974
  consumeLine(line, startOffset = 0) {
5888
5975
  if (this.fence) {
5889
5976
  if (this.fence.feed(line)) this.fence = null;
@@ -6095,6 +6182,23 @@ function isSplitterTreeLine(line) {
6095
6182
  }
6096
6183
  var MEDIA_REF_PREFIXES = ["[Image URL:", "[Video URL:", "[Audio URL:"];
6097
6184
  var Tokenizer = class {
6185
+ text;
6186
+ streaming;
6187
+ meter;
6188
+ n;
6189
+ lineStarts = [];
6190
+ contentEnds = [];
6191
+ lineEnds = [];
6192
+ blocks = [];
6193
+ blockCache = /* @__PURE__ */ new Map();
6194
+ /** Lazily built: for line i, first line j >= i where brace net from i drops to <= 0. */
6195
+ braceStop = null;
6196
+ braceNet = null;
6197
+ /** Every occurrence offset of each needle searched so far (overlapping, like indexOf). */
6198
+ occurrences = /* @__PURE__ */ new Map();
6199
+ /** `$$` opener → closer for the pairs the core renders as math. */
6200
+ mathPairs;
6201
+ finder = (needle, from) => this.next(needle, from);
6098
6202
  constructor(text, streaming = false, meter = void 0) {
6099
6203
  this.text = text;
6100
6204
  this.streaming = streaming;
@@ -6141,23 +6245,6 @@ var Tokenizer = class {
6141
6245
  this.lineEnds.push(text.length);
6142
6246
  }
6143
6247
  }
6144
- text;
6145
- streaming;
6146
- meter;
6147
- n;
6148
- lineStarts = [];
6149
- contentEnds = [];
6150
- lineEnds = [];
6151
- blocks = [];
6152
- blockCache = /* @__PURE__ */ new Map();
6153
- /** Lazily built: for line i, first line j >= i where brace net from i drops to <= 0. */
6154
- braceStop = null;
6155
- braceNet = null;
6156
- /** Every occurrence offset of each needle searched so far (overlapping, like indexOf). */
6157
- occurrences = /* @__PURE__ */ new Map();
6158
- /** `$$` opener → closer for the pairs the core renders as math. */
6159
- mathPairs;
6160
- finder = (needle, from) => this.next(needle, from);
6161
6248
  /** Streaming: where provisional text starts (a pending `$$`), or +∞. */
6162
6249
  provisionalFrom = Number.POSITIVE_INFINITY;
6163
6250
  /** Streaming: the text ends in a lone CR whose LF has not arrived yet. */
@@ -6304,12 +6391,6 @@ var Tokenizer = class {
6304
6391
  const last = this.lineStarts.length - 1;
6305
6392
  if (last < 0) return;
6306
6393
  if (this.isOpenTail(last)) {
6307
- const lineStart = this.lineStarts[last];
6308
- for (const block2 of this.blocks) {
6309
- if (block2.kind === "prose" && block2.end > lineStart) {
6310
- block2.inlines = block2.inlines.filter((inline) => inline.end <= lineStart);
6311
- }
6312
- }
6313
6394
  let b = this.blocks.length - 1;
6314
6395
  while (b >= 0 && this.blocks[b].kind === "gap") b--;
6315
6396
  const block = this.blocks[b];
@@ -6915,7 +6996,9 @@ var Tokenizer = class {
6915
6996
  const block = this.detectBlock(this.lineStarts[k], k, false);
6916
6997
  if (block) {
6917
6998
  breakLine = k;
6918
- if (this.mayDissolve(block)) this.undecidedBreaks.add(end);
6999
+ if (this.mayDissolve(block) || this.streaming && block.start >= this.provisionalFrom) {
7000
+ this.undecidedBreaks.add(end);
7001
+ }
6919
7002
  break;
6920
7003
  }
6921
7004
  end = this.contentEnds[k];
@@ -7260,12 +7343,12 @@ function blockAt(blocks, offset) {
7260
7343
 
7261
7344
  // source/splice.ts
7262
7345
  var SourceSpliceError = class extends Error {
7346
+ code;
7263
7347
  constructor(code, message) {
7264
7348
  super(message);
7265
7349
  this.code = code;
7266
7350
  this.name = "SourceSpliceError";
7267
7351
  }
7268
- code;
7269
7352
  };
7270
7353
  function blockEdit(block, text) {
7271
7354
  return { start: block.start, end: block.end, text };
@@ -7597,6 +7680,7 @@ exports.compareWithExistingKindSchema = compareWithExistingKindSchema;
7597
7680
  exports.convertAiSchemaToBlockFields = convertAiSchemaToBlockFields;
7598
7681
  exports.createFingerprinter = createFingerprinter;
7599
7682
  exports.createKindStreamParser = createKindStreamParser;
7683
+ exports.createKindValidator = createKindValidator;
7600
7684
  exports.decodeDirective = decodeDirective;
7601
7685
  exports.describeDualGateFailure = describeDualGateFailure;
7602
7686
  exports.directiveDisplay = directiveDisplay;