@ai-matrx/content-ir 0.16.1 → 0.17.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,68 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.17.0 — 2026-09-26
4
+
5
+ **`/source` streaming: what "settled" promises, pinned — and the one
6
+ documented exception** (verify-RC-B3 § Re-verification (0.16.0): G3, G6, H1,
7
+ H2). The verifier's mutations M1–M13 each turn
8
+ `__tests__/source-attack.test.ts` red ON ITS OWN — including the three
9
+ holdbacks the 0.16.0 suite could not see (M9 unclosed backtick, M10 open `{`
10
+ line, M12 pending `$$`), the `$$` open-tail rule (M11) and the held footnote
11
+ (M13). Unmutated: green in CI and in 10 consecutive long runs under 24 CPU
12
+ spinners.
13
+
14
+ - **G3** — the suite's bounds are counts, never the clock, so no test may be
15
+ failed by the clock either: an explicit suite-wide timeout (15 min CI, 1 h
16
+ long). 0.16.0's long mode died on vitest's default 5 s timeout while every
17
+ work assertion held.
18
+ - **G6** — each streaming holdback is pinned directly and by a flicker
19
+ message, and a new property checks, over random documents with and without
20
+ a final newline, that every island a stream shows as settled is the one-shot
21
+ island.
22
+ - **H1** — a footnote whose continuation is undecided (it ends the text, or
23
+ the next line is still arriving — even one that currently looks like a
24
+ block) is held open (`complete: false`), never shown settled.
25
+ - **H2 (chair ruling, 2026-09-26)** — a late orphan `</thinking>` /
26
+ `</think>` / `</reasoning>` closer is the ONE documented exception: it folds
27
+ the text before it into a thinking region retroactively. Consumers re-read
28
+ the finished text one-shot when a stream completes (README § streaming
29
+ mode); matrx-frontend's chat renderer does the same for its accumulator.
30
+ - The property found and fixed further settle-then-change classes, each now
31
+ held back in streaming mode: a lone allowed HTML tag at any block start on
32
+ the open line; an undecided single-`$` span (everything from its opener);
33
+ a code span the final paragraph may still close (`pendingCodeSpanStart`,
34
+ counting a single trailing newline as still-open) — held only for the `$$`
35
+ readings it can change; `$$` pairs that a completing backtick / tilde run
36
+ would re-pair. The paragraph's end is always computed exactly as one-shot
37
+ computes it; only what is reported stops at the provisional point.
38
+
39
+ **New (`/source` `findCodeRanges`, `CodeRange`): THE ONE code-range rule**
40
+ (RC-B10 one-canonical). Fenced blocks and inline code spans by CommonMark —
41
+ a fence opens only at line start (≤3 spaces) and closes by the one
42
+ fence-nesting rule; a code span pairs equal backtick runs within one
43
+ paragraph. Moved out of `math-pairs` (with `pendingCodeSpanStart`) so every
44
+ package asks one function: `@ai-matrx/kit` delimiter-guard and
45
+ `@ai-matrx/chat` speakable text now import it. Shared vectors:
46
+ `__tests__/fixtures/code-range-vectors.json` (read by content-ir and kit).
47
+ Guard: `__tests__/one-code-range-rule.test.ts` fails on any NEW fence/code-span
48
+ detection in `apps/shared` (shrink-only census of the existing walkers).
49
+
50
+ **Consumer action:** streaming consumers re-tokenize one-shot on completion
51
+ (the H2 contract). Stored text is unaffected.
52
+
53
+ ## 0.16.2
54
+
55
+ - **Fixed (`/source` `pairDisplayMath`, `pendingDisplayMathOpener`):** a
56
+ `$$...$$` / `$$…$$` placeholder in prose was paired as display math. New
57
+ `isMathPlaceholder(inner)`; such a pair is consumed whole as literal text.
58
+ Same rule as `@ai-matrx/kit` `delimiter-guard`.
59
+ - **Fixed (`/source` `pairDisplayMath`): an inline triple backtick in prose
60
+ hid every later `$$` formula.** Code is now found the CommonMark way — a fence
61
+ opens only at line start (≤3 spaces), closes by the one fence-nesting rule
62
+ (`classifyInnerFenceLine`), and a code span pairs equal backtick runs inside
63
+ one paragraph. `tokenizeSource` math islands inherit it.
64
+ - Consumer action: none — refresh to latest.
65
+
3
66
  ## 0.16.1
4
67
 
5
68
  Automatic changed-only republish (docs/metadata drift since the last tag — see
package/README.md CHANGED
@@ -43,6 +43,30 @@ import {
43
43
 
44
44
  The package has no browser, React runtime, application store, database client, routing, or renderer dependency. `ajv` is its only runtime dependency. Host-specific registry fetching, component compilation, rendering, and error capture remain adapters around this kernel.
45
45
 
46
+ ## `/source` streaming mode: what "settled" promises
47
+
48
+ `tokenizeSource(text)` reads stored text; `tokenizeSource(prefix, { streaming: true })`
49
+ reads a message still arriving. In streaming mode anything the next bytes could
50
+ still change is held back as plain prose (an open last line, an unclosed code
51
+ span, a `$$` that can still pair, a footnote whose continuation is undecided, …),
52
+ so **an island reported `complete` is the island the finished text has** — with
53
+ ONE documented exception:
54
+
55
+ - **A late orphan reasoning closer.** Models do emit closer-only reasoning (the
56
+ `<thinking>` opener lost upstream). When a `</thinking>` / `</think>` /
57
+ `</reasoning>` arrives with no opener, the one-shot reading folds everything
58
+ before it into a thinking region — retroactively. No streaming reader can
59
+ know that in advance short of holding ALL prose provisional until the stream
60
+ ends, which costs more than it saves (chair ruling, 2026-09-26; common-docs
61
+ `projects/rich-content-unification/REGISTER.md`, RC-B3 rulings).
62
+
63
+ **Consumer contract:** when a stream completes, re-read the finished text in
64
+ one-shot mode (`tokenizeSource(text)`, no `streaming`) and show THAT — the
65
+ final screen always equals the one-shot result, which is also what a reload
66
+ shows. Stored text never goes through streaming mode, so none of this can
67
+ change a stored byte. (matrx-frontend's chat renderer does the same for its
68
+ own live accumulator: `components/mardown-display/chat-markdown/settle-stream-blocks.ts`.)
69
+
46
70
  ## Release gate
47
71
 
48
72
  ```bash
package/dist/index.cjs CHANGED
@@ -5288,12 +5288,167 @@ function singleDollarMathEnd(text, open, limit = text.length) {
5288
5288
  return isSingleDollarMath(content, text[close + 1]) ? close + 1 : -1;
5289
5289
  }
5290
5290
 
5291
+ // source/code-ranges.ts
5292
+ function findCodeRanges(text) {
5293
+ const ranges = [];
5294
+ const n = text.length;
5295
+ let fence = null;
5296
+ let para = -1;
5297
+ const spans = (from, to) => {
5298
+ const runs = [];
5299
+ for (let k = from; k < to; ) {
5300
+ if (text.charCodeAt(k) !== 96) {
5301
+ k++;
5302
+ continue;
5303
+ }
5304
+ let len = 1;
5305
+ while (k + len < to && text.charCodeAt(k + len) === 96) len++;
5306
+ runs.push([k, len]);
5307
+ k += len;
5308
+ }
5309
+ const nextSame = new Array(runs.length).fill(-1);
5310
+ const last = /* @__PURE__ */ new Map();
5311
+ for (let r = runs.length - 1; r >= 0; r--) {
5312
+ const len = runs[r][1];
5313
+ nextSame[r] = last.get(len) ?? -1;
5314
+ last.set(len, r);
5315
+ }
5316
+ for (let r = 0; r < runs.length; ) {
5317
+ const j = nextSame[r];
5318
+ if (j === -1) {
5319
+ r++;
5320
+ continue;
5321
+ }
5322
+ const [s0] = runs[r];
5323
+ const [s1, l1] = runs[j];
5324
+ ranges.push({ start: s0, end: s1 + l1, kind: "span" });
5325
+ r = j + 1;
5326
+ }
5327
+ };
5328
+ let pos = 0;
5329
+ while (pos <= n) {
5330
+ const nl = text.indexOf("\n", pos);
5331
+ const lineEnd = nl === -1 ? n : nl;
5332
+ const line = text.slice(pos, lineEnd);
5333
+ let indent = 0;
5334
+ while (indent < line.length && line[indent] === " ") indent++;
5335
+ const body = line.slice(indent);
5336
+ if (fence) {
5337
+ const trimmed = trimFenceLine(line);
5338
+ let closes = false;
5339
+ if (fence.char === "`") {
5340
+ const kind = classifyInnerFenceLine(trimmed, fence.len, fence.nests, fence.depth);
5341
+ if (kind === "open-nested") fence.depth++;
5342
+ else if (kind === "close-nested") fence.depth--;
5343
+ else if (kind === "close-outer") closes = indent <= 3;
5344
+ } else {
5345
+ let t = 0;
5346
+ while (t < trimmed.length && trimmed[t] === "~") t++;
5347
+ closes = indent <= 3 && t >= fence.len && trimFenceLine(trimmed.slice(t)) === "";
5348
+ }
5349
+ if (closes) {
5350
+ ranges.push({ start: fence.start, end: lineEnd, kind: "fence" });
5351
+ fence = null;
5352
+ }
5353
+ } else {
5354
+ const opener = indent <= 3 ? /^(`{3,}|~{3,})(.*)$/.exec(body) : null;
5355
+ const info = opener ? opener[2] : "";
5356
+ if (opener && !(opener[1][0] === "`" && info.includes("`"))) {
5357
+ if (para !== -1) spans(para, pos);
5358
+ para = -1;
5359
+ const char = opener[1][0];
5360
+ const parsed = char === "`" ? parseFenceOpener(line) : null;
5361
+ fence = { char, len: opener[1].length, start: pos, nests: fenceNestsInnerFences(parsed?.lang), depth: 0 };
5362
+ } else if (body.trim() === "") {
5363
+ if (para !== -1) spans(para, pos);
5364
+ para = -1;
5365
+ } else if (para === -1) para = pos;
5366
+ }
5367
+ if (nl === -1) break;
5368
+ pos = nl + 1;
5369
+ }
5370
+ if (fence) ranges.push({ start: fence.start, end: n, kind: "fence" });
5371
+ else if (para !== -1) spans(para, n);
5372
+ return ranges.sort((a, b) => a.start - b.start);
5373
+ }
5374
+ function pendingCodeSpanStart(text) {
5375
+ const n = text.length;
5376
+ if (n === 0 || /\n[ \t]*\n[ \t]*$/.test(text)) return -1;
5377
+ let fence = null;
5378
+ let para = -1;
5379
+ let pos = 0;
5380
+ while (pos <= n) {
5381
+ const nl = text.indexOf("\n", pos);
5382
+ const lineEnd = nl === -1 ? n : nl;
5383
+ const line = text.slice(pos, lineEnd);
5384
+ let indent = 0;
5385
+ while (indent < line.length && line[indent] === " ") indent++;
5386
+ const body = line.slice(indent);
5387
+ if (fence) {
5388
+ const trimmed = trimFenceLine(line);
5389
+ let closes = false;
5390
+ if (fence.char === "`") {
5391
+ const kind = classifyInnerFenceLine(trimmed, fence.len, fence.nests, fence.depth);
5392
+ if (kind === "open-nested") fence.depth++;
5393
+ else if (kind === "close-nested") fence.depth--;
5394
+ else if (kind === "close-outer") closes = indent <= 3;
5395
+ } else {
5396
+ let t = 0;
5397
+ while (t < trimmed.length && trimmed[t] === "~") t++;
5398
+ closes = indent <= 3 && t >= fence.len && trimFenceLine(trimmed.slice(t)) === "";
5399
+ }
5400
+ if (closes) fence = null;
5401
+ } else {
5402
+ const opener = indent <= 3 ? /^(`{3,}|~{3,})(.*)$/.exec(body) : null;
5403
+ const info = opener ? opener[2] : "";
5404
+ if (opener && !(opener[1][0] === "`" && info.includes("`"))) {
5405
+ para = -1;
5406
+ const char = opener[1][0];
5407
+ const parsed = char === "`" ? parseFenceOpener(line) : null;
5408
+ fence = { char, len: opener[1].length, nests: fenceNestsInnerFences(parsed?.lang), depth: 0 };
5409
+ } else if (body.trim() === "") {
5410
+ if (!(nl === -1 && line === "")) para = -1;
5411
+ } else if (para === -1) para = pos;
5412
+ }
5413
+ if (nl === -1) break;
5414
+ pos = nl + 1;
5415
+ }
5416
+ if (fence || para === -1) return -1;
5417
+ const runs = [];
5418
+ for (let k = para; k < n; ) {
5419
+ if (text.charCodeAt(k) !== 96) {
5420
+ k++;
5421
+ continue;
5422
+ }
5423
+ let len = 1;
5424
+ while (k + len < n && text.charCodeAt(k + len) === 96) len++;
5425
+ runs.push([k, len]);
5426
+ k += len;
5427
+ }
5428
+ const growing = (r) => {
5429
+ const [start, len] = runs[r];
5430
+ return start + len === n;
5431
+ };
5432
+ for (let r = 0; r < runs.length; ) {
5433
+ if (growing(r)) return runs[r][0];
5434
+ const len = runs[r][1];
5435
+ let j = r + 1;
5436
+ while (j < runs.length && (runs[j][1] !== len || growing(j))) j++;
5437
+ if (j >= runs.length) return runs[r][0];
5438
+ r = j + 1;
5439
+ }
5440
+ return -1;
5441
+ }
5442
+
5291
5443
  // source/math-pairs.ts
5292
5444
  var MAX_MATH_SPAN = 600;
5293
5445
  var STRUCTURAL_MARKDOWN = /\]\(|https?:\/\/|\*\*|(?:^|\n)[ \t]{0,3}#{1,6}[ \t]|(?:^|\n)[ \t]*[-*+][ \t]+|(?:^|\n)[ \t]*\d+[.)][ \t]/;
5294
5446
  var LATEX_COMMAND = /\\[a-zA-Z]/;
5295
5447
  var PROSE_WORD = /[A-Za-z]{3,}/g;
5296
5448
  var PROSE_WORD_LIMIT = 6;
5449
+ function isMathPlaceholder(inner) {
5450
+ return /^[\s.…·⋯_-]*$/.test(inner) && /[.…·⋯]/.test(inner);
5451
+ }
5297
5452
  function opensFlowBlock(text, open) {
5298
5453
  for (let k = open - 1; k >= 0 && text[k] !== "\n"; k--) if (text[k] !== " " && text[k] !== " ") return false;
5299
5454
  for (let k = open + 2; k < text.length && text[k] !== "\n"; k++) if (text[k] !== " " && text[k] !== " " && text[k] !== "\r") return false;
@@ -5311,15 +5466,7 @@ function looksLikeDisplayMath(inner, flow = false) {
5311
5466
  return (s.match(PROSE_WORD) ?? []).length < PROSE_WORD_LIMIT;
5312
5467
  }
5313
5468
  function codeRanges(text) {
5314
- const ranges = [];
5315
- for (const re of [/```[\s\S]*?(?:```|$)/g, /~~~[\s\S]*?(?:~~~|$)/g, /`[^`\n]*`/g]) {
5316
- let m;
5317
- while ((m = re.exec(text)) !== null) {
5318
- ranges.push([m.index, m.index + m[0].length]);
5319
- if (m[0].length === 0) re.lastIndex++;
5320
- }
5321
- }
5322
- return ranges.sort((a, b) => a[0] - b[0]);
5469
+ return findCodeRanges(text).map((r) => [r.start, r.end]);
5323
5470
  }
5324
5471
  function pendingDisplayMathOpener(text) {
5325
5472
  if (!text.includes("$$")) return -1;
@@ -5341,6 +5488,10 @@ function pendingDisplayMathOpener(text) {
5341
5488
  const open = tokens[j];
5342
5489
  const close = tokens[j + 1];
5343
5490
  if (close === void 0) break;
5491
+ if (isMathPlaceholder(text.slice(open + 2, close))) {
5492
+ j += 2;
5493
+ continue;
5494
+ }
5344
5495
  if (looksLikeDisplayMath(text.slice(open + 2, close), opensFlowBlock(text, open))) {
5345
5496
  paired.add(open);
5346
5497
  paired.add(close);
@@ -5385,6 +5536,10 @@ function pairDisplayMath(text) {
5385
5536
  const open = tokens[j];
5386
5537
  const close = tokens[j + 1];
5387
5538
  if (close === void 0) break;
5539
+ if (isMathPlaceholder(text.slice(open + 2, close))) {
5540
+ j += 2;
5541
+ continue;
5542
+ }
5388
5543
  if (looksLikeDisplayMath(text.slice(open + 2, close), opensFlowBlock(text, open))) {
5389
5544
  pairs.set(open, close);
5390
5545
  j += 2;
@@ -5722,6 +5877,27 @@ var Tokenizer = class {
5722
5877
  this.mathPairs = pairDisplayMath(text);
5723
5878
  this.provisionalFrom = streaming ? pendingDisplayMathOpener(text) : -1;
5724
5879
  if (this.provisionalFrom === -1) this.provisionalFrom = Number.POSITIVE_INFINITY;
5880
+ if (streaming && text.includes("$$")) {
5881
+ const span = pendingCodeSpanStart(text);
5882
+ if (span !== -1) {
5883
+ const firstToken = text.indexOf("$$", span);
5884
+ if (firstToken !== -1) this.provisionalFrom = Math.min(this.provisionalFrom, firstToken);
5885
+ for (const [open, close] of this.mathPairs) {
5886
+ if (close >= span) this.provisionalFrom = Math.min(this.provisionalFrom, open);
5887
+ }
5888
+ }
5889
+ for (const pad of ["`", "``", "```", "~~~"]) {
5890
+ const alt = pairDisplayMath(text + pad);
5891
+ for (const [open, close] of this.mathPairs) {
5892
+ if (alt.get(open) !== close) this.provisionalFrom = Math.min(this.provisionalFrom, open);
5893
+ }
5894
+ for (const [open, close] of alt) {
5895
+ if (close < text.length && this.mathPairs.get(open) !== close) {
5896
+ this.provisionalFrom = Math.min(this.provisionalFrom, open);
5897
+ }
5898
+ }
5899
+ }
5900
+ }
5725
5901
  let start = 0;
5726
5902
  for (let i = 0; i < text.length; i++) {
5727
5903
  if (text.charCodeAt(i) === 10) {
@@ -5762,13 +5938,17 @@ var Tokenizer = class {
5762
5938
  /**
5763
5939
  * Streaming: an open last line that begins with `{` is undecided — each new
5764
5940
  * byte can balance or unbalance its braces and flip the whole line between
5765
- * prose and a JSON island — so it is provisional until it ends (G5).
5941
+ * prose and a JSON island — and so is a line that is exactly one allowed
5942
+ * HTML tag so far; both are provisional until they end (G5).
5766
5943
  */
5767
5944
  markOpenJsonTail() {
5768
5945
  const last = this.lineStarts.length - 1;
5769
5946
  if (!this.isOpenTail(last)) return;
5770
5947
  const start = this.lineStarts[last];
5771
- if (this.lineText(last).trimStart().startsWith("{")) {
5948
+ const t = this.lineText(last).trim();
5949
+ const oneTag = t.startsWith("<") ? readXmlTag(t, 0) : null;
5950
+ const loneTag = !!oneTag && oneTag.raw === t && ALLOWED_RAW_HTML_TAGS.has(oneTag.tagName.toLowerCase());
5951
+ if (t.startsWith("{") || loneTag) {
5772
5952
  this.provisionalFrom = Math.min(this.provisionalFrom, start);
5773
5953
  }
5774
5954
  }
@@ -6061,11 +6241,17 @@ var Tokenizer = class {
6061
6241
  last = k;
6062
6242
  continue;
6063
6243
  }
6244
+ if (this.isOpenTail(k)) return this.heldFootnote(p, open[1]);
6064
6245
  if (FOOTNOTE_DEF.test(line.trimStart()) || this.detectBlock(this.lineStarts[k], k, false)) break;
6065
- if (this.isOpenTail(k)) break;
6066
6246
  last = k;
6067
6247
  }
6068
- return this.island("footnote_def", p, this.contentEnds[last], true, { id: open[1] });
6248
+ const end = this.contentEnds[last];
6249
+ if (this.paragraphMayGrow(end)) return this.heldFootnote(p, open[1]);
6250
+ return this.island("footnote_def", p, end, true, { id: open[1] });
6251
+ }
6252
+ /** A footnote whose continuation is still arriving: open to the end of the text. */
6253
+ heldFootnote(p, id) {
6254
+ return this.island("footnote_def", p, this.n, false, { id });
6069
6255
  }
6070
6256
  /**
6071
6257
  * Backtick fence — the renderer splitter's close rules: JSON-string aware for
@@ -6309,7 +6495,12 @@ var Tokenizer = class {
6309
6495
  return this.island("html_block", p, close.index + close[0].length, true, { tag: name });
6310
6496
  }
6311
6497
  const typeSix = HTML_BLOCK_LEVEL_TAGS.has(name);
6312
- const typeSeven = atParaStart && t.trimEnd() === tag.raw && !this.isOpenTail(li);
6498
+ const loneTag = atParaStart && t.trimEnd() === tag.raw;
6499
+ if (loneTag && this.isOpenTail(li)) {
6500
+ this.provisionalFrom = Math.min(this.provisionalFrom, p);
6501
+ return null;
6502
+ }
6503
+ const typeSeven = loneTag;
6313
6504
  if (!typeSix && !typeSeven) return null;
6314
6505
  let end = this.contentEnds[li];
6315
6506
  for (let k = li + 1; k < this.lineCount && !this.lineIsBlank(k); k++) {
@@ -6401,10 +6592,6 @@ var Tokenizer = class {
6401
6592
  let breakLine = -1;
6402
6593
  for (let k = li + 1; k < this.lineCount; k++) {
6403
6594
  if (this.lineIsBlank(k)) break;
6404
- if (this.lineStarts[k] >= this.provisionalFrom) {
6405
- end = this.n;
6406
- break;
6407
- }
6408
6595
  if (this.detectBlock(this.lineStarts[k], k, false)) {
6409
6596
  breakLine = k;
6410
6597
  break;
@@ -6547,7 +6734,11 @@ var Tokenizer = class {
6547
6734
  }
6548
6735
  const end = singleDollarMathEnd(text, i, pe);
6549
6736
  this.tick(end > 0 ? end - i : Math.min(403, pe - i));
6550
- if (end > 0 && !(this.streaming && end === this.n)) {
6737
+ if (end > 0 && this.streaming && end === this.n) {
6738
+ this.provisionalFrom = Math.min(this.provisionalFrom, i);
6739
+ return { inlines, breakIsland: null, orphan: false };
6740
+ }
6741
+ if (end > 0) {
6551
6742
  inlines.push(this.island("math_inline", i, end, true, { delimiter: "$" }));
6552
6743
  i = end;
6553
6744
  continue;
@@ -7101,6 +7292,7 @@ exports.executesAtOutputRoot = executesAtOutputRoot;
7101
7292
  exports.fenceDiscriminator = fenceDiscriminator;
7102
7293
  exports.fenceNestsInnerFences = fenceNestsInnerFences;
7103
7294
  exports.fieldsToDbPayload = fieldsToDbPayload;
7295
+ exports.findCodeRanges = findCodeRanges;
7104
7296
  exports.fingerprintText = fingerprintText;
7105
7297
  exports.formatBlockLabel = formatBlockLabel;
7106
7298
  exports.getParseSession = getParseSession;
@@ -7117,6 +7309,7 @@ exports.isIrEnvelopeCache = isIrEnvelopeCache;
7117
7309
  exports.isJsonAnyField = isJsonAnyField;
7118
7310
  exports.isKindDirective = isKindDirective;
7119
7311
  exports.isLegacyShell = isLegacyShell;
7312
+ exports.isMathPlaceholder = isMathPlaceholder;
7120
7313
  exports.isProvisionalKind = isProvisionalKind;
7121
7314
  exports.isReservedDirectiveSlug = isReservedDirectiveSlug;
7122
7315
  exports.isScalarArrayType = isScalarArrayType;