@ai-matrx/content-ir 0.15.1 → 0.16.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,40 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.16.0 — 2026-09-25
4
+
5
+ **`/source` — a deterministic attack suite, and streaming that only ever
6
+ grows** (verify-RC-B3 § Re-verification (0.15.0): G3, G4, G5). The verifier's
7
+ planted mutations M1–M6 plus M7 (the trailing-`$` streaming guard removed)
8
+ and M8 (the footnote open-tail rule removed) each turn
9
+ `__tests__/source-attack.test.ts` red ON ITS OWN; unmutated code is green.
10
+
11
+ - **G3 — work, not wall-clock.** `tokenizeSource(text, { meter })` (diagnostic)
12
+ counts every character the tokenizer's own loops visit; the attack suite adds
13
+ native string / regex work (indexOf / includes / lastIndexOf spans, trim,
14
+ split, replace, regex exec / test to where they stop) under a V8 cost model,
15
+ and asserts work at 4× input stays under 6× — deterministic on any host. One
16
+ generous wall-clock smoke bound remains (1 MB mixed in < 20 s). The meter
17
+ immediately caught two real quadratics, both fixed: the new pending-`$$`
18
+ scan (206 s at 1 MB of `$$\n`, now linear) — and it replaced a timing
19
+ check that flaked 3 of 12 runs under load.
20
+ - **G4** — a streamed `$x$` that the next byte turns into currency (`$x$5`)
21
+ never settles as math first; guarded by a message in the suite.
22
+ - **G5 — trailing islands only grow.** The suite asserts that a line-grown
23
+ island (tree, callout, footnote, HTML block, directive) keeps its start and
24
+ type and never shrinks, at every next prefix, over random documents. Fixed
25
+ classes it found: a footnote absorbing an unterminated line that becomes
26
+ the next note (`[^n]: short\n[`); a one-tag HTML line still arriving; a
27
+ trailing lone CR (the first half of CRLF) counted as content; text after an
28
+ unclosed backtick run while its paragraph can still grow; an open last line
29
+ starting with `{`; and anything after a `$$` that could still find its
30
+ partner (`pendingDisplayMathOpener` — the earliest pairable unpaired token).
31
+ In streaming mode all of these are provisional prose until decided.
32
+ - Distinct-document counting: the structure attack counts the distinct
33
+ documents it tokenized (mulberry32) and requires ≥ 95% of its rounds.
34
+
35
+ **Consumer action:** none for stored text. Streaming callers see fewer
36
+ islands near the tail until the text decides them.
37
+
3
38
  ## 0.15.1
4
39
 
5
40
  - **Fixed (`/source` `pairDisplayMath`):** a stray `$$` in prose paired with
package/dist/index.cjs CHANGED
@@ -5321,6 +5321,50 @@ function codeRanges(text) {
5321
5321
  }
5322
5322
  return ranges.sort((a, b) => a[0] - b[0]);
5323
5323
  }
5324
+ function pendingDisplayMathOpener(text) {
5325
+ if (!text.includes("$$")) return -1;
5326
+ const ranges = codeRanges(text);
5327
+ const tokens = [];
5328
+ let r = 0;
5329
+ let maxEnd = -1;
5330
+ for (let i = text.indexOf("$$"); i !== -1 && i < text.length - 1; i = text.indexOf("$$", i)) {
5331
+ while (r < ranges.length && ranges[r][0] <= i) {
5332
+ maxEnd = Math.max(maxEnd, ranges[r][1]);
5333
+ r++;
5334
+ }
5335
+ if (maxEnd <= i) tokens.push(i);
5336
+ i += 2;
5337
+ }
5338
+ const paired = /* @__PURE__ */ new Set();
5339
+ let j = 0;
5340
+ while (j < tokens.length) {
5341
+ const open = tokens[j];
5342
+ const close = tokens[j + 1];
5343
+ if (close === void 0) break;
5344
+ if (looksLikeDisplayMath(text.slice(open + 2, close), opensFlowBlock(text, open))) {
5345
+ paired.add(open);
5346
+ paired.add(close);
5347
+ j += 2;
5348
+ continue;
5349
+ }
5350
+ j += 1;
5351
+ }
5352
+ let trail = 0;
5353
+ while (trail < text.length && /\s/.test(text[text.length - 1 - trail])) trail++;
5354
+ for (const token of tokens) {
5355
+ if (paired.has(token)) continue;
5356
+ let lead = 0;
5357
+ while (lead <= MAX_MATH_SPAN && /\s/.test(text[token + 2 + lead] ?? "x")) lead++;
5358
+ if (text.length - (token + 2) - lead - trail > MAX_MATH_SPAN) continue;
5359
+ const inner = text.slice(token + 2);
5360
+ const s = inner.trim();
5361
+ if (STRUCTURAL_MARKDOWN.test(s) || s.length > MAX_MATH_SPAN) continue;
5362
+ if (!opensFlowBlock(text, token) && /\n[ \t]*\n/.test(inner)) continue;
5363
+ if ((s.match(PROSE_WORD) ?? []).length >= PROSE_WORD_LIMIT * 3) continue;
5364
+ return token;
5365
+ }
5366
+ return -1;
5367
+ }
5324
5368
  function pairDisplayMath(text) {
5325
5369
  const pairs = /* @__PURE__ */ new Map();
5326
5370
  if (!text.includes("$$")) return pairs;
@@ -5669,11 +5713,15 @@ function isSplitterTreeLine(line) {
5669
5713
  }
5670
5714
  var MEDIA_REF_PREFIXES = ["[Image URL:", "[Video URL:", "[Audio URL:"];
5671
5715
  var Tokenizer = class {
5672
- constructor(text, streaming = false) {
5716
+ constructor(text, streaming = false, meter = void 0) {
5673
5717
  this.text = text;
5674
5718
  this.streaming = streaming;
5719
+ this.meter = meter;
5675
5720
  this.n = text.length;
5721
+ this.tick(text.length);
5676
5722
  this.mathPairs = pairDisplayMath(text);
5723
+ this.provisionalFrom = streaming ? pendingDisplayMathOpener(text) : -1;
5724
+ if (this.provisionalFrom === -1) this.provisionalFrom = Number.POSITIVE_INFINITY;
5677
5725
  let start = 0;
5678
5726
  for (let i = 0; i < text.length; i++) {
5679
5727
  if (text.charCodeAt(i) === 10) {
@@ -5684,13 +5732,15 @@ var Tokenizer = class {
5684
5732
  }
5685
5733
  }
5686
5734
  if (start < text.length || text.length === 0) {
5735
+ this.pendingCR = streaming && text.length > start && text.charCodeAt(text.length - 1) === 13;
5687
5736
  this.lineStarts.push(start);
5688
- this.contentEnds.push(text.length);
5737
+ this.contentEnds.push(this.pendingCR ? text.length - 1 : text.length);
5689
5738
  this.lineEnds.push(text.length);
5690
5739
  }
5691
5740
  }
5692
5741
  text;
5693
5742
  streaming;
5743
+ meter;
5694
5744
  n;
5695
5745
  lineStarts = [];
5696
5746
  contentEnds = [];
@@ -5705,6 +5755,23 @@ var Tokenizer = class {
5705
5755
  /** `$$` opener → closer for the pairs the core renders as math. */
5706
5756
  mathPairs;
5707
5757
  finder = (needle, from) => this.next(needle, from);
5758
+ /** Streaming: where provisional text starts (a pending `$$`), or +∞. */
5759
+ provisionalFrom = Number.POSITIVE_INFINITY;
5760
+ /** Streaming: the text ends in a lone CR whose LF has not arrived yet. */
5761
+ pendingCR = false;
5762
+ /**
5763
+ * Streaming: an open last line that begins with `{` is undecided — each new
5764
+ * 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).
5766
+ */
5767
+ markOpenJsonTail() {
5768
+ const last = this.lineStarts.length - 1;
5769
+ if (!this.isOpenTail(last)) return;
5770
+ const start = this.lineStarts[last];
5771
+ if (this.lineText(last).trimStart().startsWith("{")) {
5772
+ this.provisionalFrom = Math.min(this.provisionalFrom, start);
5773
+ }
5774
+ }
5708
5775
  /**
5709
5776
  * `text.indexOf(needle, from)` in O(log k): the first query for a needle
5710
5777
  * indexes all of its occurrences once, so repeated searches for a closer
@@ -5728,9 +5795,21 @@ var Tokenizer = class {
5728
5795
  }
5729
5796
  return lo < list.length ? list[lo] : -1;
5730
5797
  }
5798
+ /** Charge `n` visited characters to the diagnostic meter (complexity tests). */
5799
+ tick(n) {
5800
+ if (this.meter) this.meter.work += n;
5801
+ }
5802
+ /** Streaming only: a paragraph ending at `pe` may still be continued by a line not yet arrived. */
5803
+ paragraphMayGrow(pe) {
5804
+ if (!this.streaming) return false;
5805
+ const rest = this.n - pe;
5806
+ if (rest === 0) return true;
5807
+ const tail = this.text.slice(pe);
5808
+ return tail === "\n" || tail === "\r\n" || tail === "\r";
5809
+ }
5731
5810
  /** Streaming only: the final line, still waiting for its terminator. */
5732
5811
  isOpenTail(k) {
5733
- return this.streaming && k === this.lineStarts.length - 1 && this.lineEnds[k] === this.contentEnds[k];
5812
+ return this.streaming && k === this.lineStarts.length - 1 && (this.lineEnds[k] === this.contentEnds[k] || this.pendingCR);
5734
5813
  }
5735
5814
  get lineCount() {
5736
5815
  return this.lineStarts.length;
@@ -5755,6 +5834,7 @@ var Tokenizer = class {
5755
5834
  return this.text.slice(this.lineStarts[li], this.contentEnds[li]);
5756
5835
  }
5757
5836
  run() {
5837
+ if (this.streaming) this.markOpenJsonTail();
5758
5838
  let pos = 0;
5759
5839
  while (pos < this.n) {
5760
5840
  const li = this.lineOf(pos);
@@ -5763,6 +5843,10 @@ var Tokenizer = class {
5763
5843
  pos = this.gap(pos, li);
5764
5844
  continue;
5765
5845
  }
5846
+ if (pos >= this.provisionalFrom) {
5847
+ this.push({ kind: "prose", start: pos, end: this.n, island: null, inlines: [] });
5848
+ break;
5849
+ }
5766
5850
  const island = this.detectBlock(pos, li, true);
5767
5851
  if (island) {
5768
5852
  this.push({ kind: "island", start: island.start, end: island.end, island, inlines: [] });
@@ -5966,6 +6050,7 @@ var Tokenizer = class {
5966
6050
  continue;
5967
6051
  }
5968
6052
  if (FOOTNOTE_DEF.test(line.trimStart()) || this.detectBlock(this.lineStarts[k], k, false)) break;
6053
+ if (this.isOpenTail(k)) break;
5969
6054
  last = k;
5970
6055
  }
5971
6056
  return this.island("footnote_def", p, this.contentEnds[last], true, { id: open[1] });
@@ -6127,6 +6212,7 @@ var Tokenizer = class {
6127
6212
  const li = this.lineOf(tp);
6128
6213
  const ce = this.contentEnds[li];
6129
6214
  const tag = readXmlTag(this.text, tp, this.finder);
6215
+ this.tick(tag ? tag.raw.length : 1);
6130
6216
  let openEnd;
6131
6217
  if (tag && !tag.isClosing) {
6132
6218
  if (tag.isSelfClosing) {
@@ -6211,7 +6297,7 @@ var Tokenizer = class {
6211
6297
  return this.island("html_block", p, close.index + close[0].length, true, { tag: name });
6212
6298
  }
6213
6299
  const typeSix = HTML_BLOCK_LEVEL_TAGS.has(name);
6214
- const typeSeven = atParaStart && t.trimEnd() === tag.raw;
6300
+ const typeSeven = atParaStart && t.trimEnd() === tag.raw && !this.isOpenTail(li);
6215
6301
  if (!typeSix && !typeSeven) return null;
6216
6302
  let end = this.contentEnds[li];
6217
6303
  for (let k = li + 1; k < this.lineCount && !this.lineIsBlank(k); k++) {
@@ -6303,6 +6389,10 @@ var Tokenizer = class {
6303
6389
  let breakLine = -1;
6304
6390
  for (let k = li + 1; k < this.lineCount; k++) {
6305
6391
  if (this.lineIsBlank(k)) break;
6392
+ if (this.lineStarts[k] >= this.provisionalFrom) {
6393
+ end = this.n;
6394
+ break;
6395
+ }
6306
6396
  if (this.detectBlock(this.lineStarts[k], k, false)) {
6307
6397
  breakLine = k;
6308
6398
  break;
@@ -6368,6 +6458,7 @@ var Tokenizer = class {
6368
6458
  scanInline(ps, pe) {
6369
6459
  const text = this.text;
6370
6460
  const inlines = [];
6461
+ this.tick(pe - ps);
6371
6462
  const hasKind = text.slice(ps, pe).includes("__kind");
6372
6463
  let kindBudget = 4 * (pe - ps) + 65536;
6373
6464
  const stop = (island, orphan = false) => ({ inlines, breakIsland: island, orphan });
@@ -6378,6 +6469,7 @@ var Tokenizer = class {
6378
6469
  let runs = null;
6379
6470
  const codeSpanClose = (n, from) => {
6380
6471
  if (!runs) {
6472
+ this.tick(pe - ps);
6381
6473
  runs = /* @__PURE__ */ new Map();
6382
6474
  for (let k = ps; k < pe; ) {
6383
6475
  if (text.charCodeAt(k) !== 96) {
@@ -6404,7 +6496,8 @@ var Tokenizer = class {
6404
6496
  return at !== void 0 && at + n <= pe ? at : -1;
6405
6497
  };
6406
6498
  let i = ps;
6407
- while (i < pe) {
6499
+ const scanEnd = Math.min(pe, this.provisionalFrom);
6500
+ while (i < scanEnd) {
6408
6501
  const ch = text[i];
6409
6502
  if (ch === "\\") {
6410
6503
  const nextCh = text[i + 1];
@@ -6422,6 +6515,9 @@ var Tokenizer = class {
6422
6515
  if (ch === "`") {
6423
6516
  const n = backtickRunLength(text, i);
6424
6517
  const close = codeSpanClose(n, i + n);
6518
+ if (close === -1 && this.streaming && this.paragraphMayGrow(pe)) {
6519
+ return { inlines, breakIsland: null, orphan: false };
6520
+ }
6425
6521
  i = close === -1 ? i + n : close + n;
6426
6522
  continue;
6427
6523
  }
@@ -6438,6 +6534,7 @@ var Tokenizer = class {
6438
6534
  continue;
6439
6535
  }
6440
6536
  const end = singleDollarMathEnd(text, i, pe);
6537
+ this.tick(end > 0 ? end - i : Math.min(403, pe - i));
6441
6538
  if (end > 0 && !(this.streaming && end === this.n)) {
6442
6539
  inlines.push(this.island("math_inline", i, end, true, { delimiter: "$" }));
6443
6540
  i = end;
@@ -6465,6 +6562,7 @@ var Tokenizer = class {
6465
6562
  const limit = Math.min(pe, i + 65536);
6466
6563
  const end = matchingJsonObjectEnd(text, i, limit);
6467
6564
  kindBudget -= (end ?? limit) - i;
6565
+ this.tick((end ?? limit) - i);
6468
6566
  if (end !== null) {
6469
6567
  const kind = declaredKind(text.slice(i, end));
6470
6568
  if (kind) {
@@ -6526,6 +6624,7 @@ var Tokenizer = class {
6526
6624
  continue;
6527
6625
  }
6528
6626
  const tag = readXmlTag(text, i, this.finder);
6627
+ this.tick(tag ? tag.raw.length : 1);
6529
6628
  if (tag && i + tag.raw.length <= pe) {
6530
6629
  const name = tag.tagName;
6531
6630
  const tagEnd = i + tag.raw.length;
@@ -6574,7 +6673,7 @@ var Tokenizer = class {
6574
6673
  }
6575
6674
  };
6576
6675
  function tokenizeSource(text, options = {}) {
6577
- const raw = new Tokenizer(text, options.streaming ?? false).run();
6676
+ const raw = new Tokenizer(text, options.streaming ?? false, options.meter).run();
6578
6677
  const cp = buildCodePointIndex(text);
6579
6678
  const inline = (island) => ({
6580
6679
  islandType: island.type,
@@ -7037,6 +7136,7 @@ exports.openParseSession = openParseSession;
7037
7136
  exports.pairDisplayMath = pairDisplayMath;
7038
7137
  exports.parseDirectiveSlug = parseDirectiveSlug;
7039
7138
  exports.parseFenceOpener = parseFenceOpener;
7139
+ exports.pendingDisplayMathOpener = pendingDisplayMathOpener;
7040
7140
  exports.readEnvelope = readEnvelope;
7041
7141
  exports.readNodeOutcomeValue = readNodeOutcomeValue;
7042
7142
  exports.readObjectKind = readObjectKind;