@ai-matrx/content-ir 0.16.2 → 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,11 +1,66 @@
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
+
3
53
  ## 0.16.2
4
54
 
5
55
  - **Fixed (`/source` `pairDisplayMath`, `pendingDisplayMathOpener`):** a
6
56
  `$$...$$` / `$$…$$` placeholder in prose was paired as display math. New
7
57
  `isMathPlaceholder(inner)`; such a pair is consumed whole as literal text.
8
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.
9
64
  - Consumer action: none — refresh to latest.
10
65
 
11
66
  ## 0.16.1
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,32 +5288,8 @@ function singleDollarMathEnd(text, open, limit = text.length) {
5288
5288
  return isSingleDollarMath(content, text[close + 1]) ? close + 1 : -1;
5289
5289
  }
5290
5290
 
5291
- // source/math-pairs.ts
5292
- var MAX_MATH_SPAN = 600;
5293
- var STRUCTURAL_MARKDOWN = /\]\(|https?:\/\/|\*\*|(?:^|\n)[ \t]{0,3}#{1,6}[ \t]|(?:^|\n)[ \t]*[-*+][ \t]+|(?:^|\n)[ \t]*\d+[.)][ \t]/;
5294
- var LATEX_COMMAND = /\\[a-zA-Z]/;
5295
- var PROSE_WORD = /[A-Za-z]{3,}/g;
5296
- var PROSE_WORD_LIMIT = 6;
5297
- function isMathPlaceholder(inner) {
5298
- return /^[\s.…·⋯_-]*$/.test(inner) && /[.…·⋯]/.test(inner);
5299
- }
5300
- function opensFlowBlock(text, open) {
5301
- for (let k = open - 1; k >= 0 && text[k] !== "\n"; k--) if (text[k] !== " " && text[k] !== " ") return false;
5302
- for (let k = open + 2; k < text.length && text[k] !== "\n"; k++) if (text[k] !== " " && text[k] !== " " && text[k] !== "\r") return false;
5303
- return true;
5304
- }
5305
- function looksLikeDisplayMath(inner, flow = false) {
5306
- const s = inner.trim();
5307
- if (!s) return false;
5308
- if (STRUCTURAL_MARKDOWN.test(s)) return false;
5309
- if (s.length > MAX_MATH_SPAN) return false;
5310
- if (!flow && /\n[ \t]*\n/.test(inner)) return false;
5311
- if (LATEX_COMMAND.test(s)) {
5312
- return (s.match(PROSE_WORD) ?? []).length < PROSE_WORD_LIMIT * 3;
5313
- }
5314
- return (s.match(PROSE_WORD) ?? []).length < PROSE_WORD_LIMIT;
5315
- }
5316
- function codeRanges(text) {
5291
+ // source/code-ranges.ts
5292
+ function findCodeRanges(text) {
5317
5293
  const ranges = [];
5318
5294
  const n = text.length;
5319
5295
  let fence = null;
@@ -5345,7 +5321,7 @@ function codeRanges(text) {
5345
5321
  }
5346
5322
  const [s0] = runs[r];
5347
5323
  const [s1, l1] = runs[j];
5348
- ranges.push([s0, s1 + l1]);
5324
+ ranges.push({ start: s0, end: s1 + l1, kind: "span" });
5349
5325
  r = j + 1;
5350
5326
  }
5351
5327
  };
@@ -5371,7 +5347,7 @@ function codeRanges(text) {
5371
5347
  closes = indent <= 3 && t >= fence.len && trimFenceLine(trimmed.slice(t)) === "";
5372
5348
  }
5373
5349
  if (closes) {
5374
- ranges.push([fence.start, lineEnd]);
5350
+ ranges.push({ start: fence.start, end: lineEnd, kind: "fence" });
5375
5351
  fence = null;
5376
5352
  }
5377
5353
  } else {
@@ -5391,9 +5367,106 @@ function codeRanges(text) {
5391
5367
  if (nl === -1) break;
5392
5368
  pos = nl + 1;
5393
5369
  }
5394
- if (fence) ranges.push([fence.start, n]);
5370
+ if (fence) ranges.push({ start: fence.start, end: n, kind: "fence" });
5395
5371
  else if (para !== -1) spans(para, n);
5396
- return ranges.sort((a, b) => a[0] - b[0]);
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
+
5443
+ // source/math-pairs.ts
5444
+ var MAX_MATH_SPAN = 600;
5445
+ var STRUCTURAL_MARKDOWN = /\]\(|https?:\/\/|\*\*|(?:^|\n)[ \t]{0,3}#{1,6}[ \t]|(?:^|\n)[ \t]*[-*+][ \t]+|(?:^|\n)[ \t]*\d+[.)][ \t]/;
5446
+ var LATEX_COMMAND = /\\[a-zA-Z]/;
5447
+ var PROSE_WORD = /[A-Za-z]{3,}/g;
5448
+ var PROSE_WORD_LIMIT = 6;
5449
+ function isMathPlaceholder(inner) {
5450
+ return /^[\s.…·⋯_-]*$/.test(inner) && /[.…·⋯]/.test(inner);
5451
+ }
5452
+ function opensFlowBlock(text, open) {
5453
+ for (let k = open - 1; k >= 0 && text[k] !== "\n"; k--) if (text[k] !== " " && text[k] !== " ") return false;
5454
+ for (let k = open + 2; k < text.length && text[k] !== "\n"; k++) if (text[k] !== " " && text[k] !== " " && text[k] !== "\r") return false;
5455
+ return true;
5456
+ }
5457
+ function looksLikeDisplayMath(inner, flow = false) {
5458
+ const s = inner.trim();
5459
+ if (!s) return false;
5460
+ if (STRUCTURAL_MARKDOWN.test(s)) return false;
5461
+ if (s.length > MAX_MATH_SPAN) return false;
5462
+ if (!flow && /\n[ \t]*\n/.test(inner)) return false;
5463
+ if (LATEX_COMMAND.test(s)) {
5464
+ return (s.match(PROSE_WORD) ?? []).length < PROSE_WORD_LIMIT * 3;
5465
+ }
5466
+ return (s.match(PROSE_WORD) ?? []).length < PROSE_WORD_LIMIT;
5467
+ }
5468
+ function codeRanges(text) {
5469
+ return findCodeRanges(text).map((r) => [r.start, r.end]);
5397
5470
  }
5398
5471
  function pendingDisplayMathOpener(text) {
5399
5472
  if (!text.includes("$$")) return -1;
@@ -5805,6 +5878,14 @@ var Tokenizer = class {
5805
5878
  this.provisionalFrom = streaming ? pendingDisplayMathOpener(text) : -1;
5806
5879
  if (this.provisionalFrom === -1) this.provisionalFrom = Number.POSITIVE_INFINITY;
5807
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
+ }
5808
5889
  for (const pad of ["`", "``", "```", "~~~"]) {
5809
5890
  const alt = pairDisplayMath(text + pad);
5810
5891
  for (const [open, close] of this.mathPairs) {
@@ -6160,8 +6241,8 @@ var Tokenizer = class {
6160
6241
  last = k;
6161
6242
  continue;
6162
6243
  }
6163
- if (FOOTNOTE_DEF.test(line.trimStart()) || this.detectBlock(this.lineStarts[k], k, false)) break;
6164
6244
  if (this.isOpenTail(k)) return this.heldFootnote(p, open[1]);
6245
+ if (FOOTNOTE_DEF.test(line.trimStart()) || this.detectBlock(this.lineStarts[k], k, false)) break;
6165
6246
  last = k;
6166
6247
  }
6167
6248
  const end = this.contentEnds[last];
@@ -7211,6 +7292,7 @@ exports.executesAtOutputRoot = executesAtOutputRoot;
7211
7292
  exports.fenceDiscriminator = fenceDiscriminator;
7212
7293
  exports.fenceNestsInnerFences = fenceNestsInnerFences;
7213
7294
  exports.fieldsToDbPayload = fieldsToDbPayload;
7295
+ exports.findCodeRanges = findCodeRanges;
7214
7296
  exports.fingerprintText = fingerprintText;
7215
7297
  exports.formatBlockLabel = formatBlockLabel;
7216
7298
  exports.getParseSession = getParseSession;