@ai-matrx/content-ir 0.14.3 → 0.15.1

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,59 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.1
4
+
5
+ - **Fixed (`/source` `pairDisplayMath`):** a stray `$$` in prose paired with
6
+ the next paragraph's formula when a LaTeX command sat between them. A `$$`
7
+ with text beside it never crosses a blank line (TeX's rule), checked on the
8
+ untrimmed span; a `$$` alone on its line still opens a flow block that may
9
+ span blank lines. `looksLikeDisplayMath(inner, flow?)` gains the `flow`
10
+ flag. Same rule as `@ai-matrx/kit` `delimiter-guard` (one math dialect).
11
+ - Consumer action: none — refresh to latest.
12
+
3
13
  ## 0.14.3
4
14
 
5
15
  Automatic changed-only republish (docs/metadata drift since the last tag — see
6
16
  `git diff npm/content-ir-core/v0.14.2..npm/content-ir-core/v0.14.3 -- apps/shared/content-ir-core`).
7
17
  No source changes intended and no consumer action required.
8
18
 
19
+ ## 0.15.0 — 2026-09-25
20
+
21
+ **`/source` — the attack suite stands alone; the last twin drift and flicker
22
+ closed** (verify-RC-B3 § Re-verification (0.14.2): G1–G3, R4′, R2′). Proof:
23
+ the verifier's six planted mutations (M1 skip the in-range island check, M2
24
+ integrity default off, M3 streaming ignored, M4 code-point offset off by one,
25
+ M5 unbounded `}}` scan, M6 `islandEdit` accepts any span) each turn
26
+ `__tests__/source-attack.test.ts` red ON ITS OWN; unmutated code is green in
27
+ CI and in four consecutive `SOURCE_ATTACK=long` runs on a heavily loaded host.
28
+
29
+ - **G1** — the streaming check covers every complete island in every prefix,
30
+ the LAST one included, keyed by span AND block/inline; only line-grown
31
+ constructs (tree, callout, footnote, HTML block, directive) may grow as the
32
+ trailing block. A third message carries the R2′ case and the extended syntax.
33
+ - **G2** — save verdicts are checked against an independent oracle (which
34
+ islands survive in the expected text), exactly: `island_edit` vs
35
+ `integrity` vs accepted; two-sided edits must map an island exactly (never
36
+ through a deleted range); mangling must be `island_edit`; forged
37
+ `island:true` spans must be `misaligned`.
38
+ - **G3** — timing compares the MINIMUM of interleaved runs at a 4× size step
39
+ (linear ≈ 4×, quadratic ≈ 16×, bound 8×) and fails only if every one of
40
+ three attempts breaches.
41
+ - **`island_edit` is now also the verdict** when an island inside the edited
42
+ range survives only as ordinary text (its bytes matched a non-island copy,
43
+ or the edit turned it into code) — previously reported as `integrity`.
44
+ `integrity` now means an island OUTSIDE the edited ranges was disturbed.
45
+ - **R2′** — streaming: a `$$` block whose closer sits on the still-open last
46
+ line is not settled (the rest of that line decides block vs inline); a
47
+ closing `$` that is the last byte so far is not yet math.
48
+ - **R4′** — `FENCE_OPENER_WHITESPACE` + `parseFenceOpener`: the opener parse
49
+ (JavaScript `trim()` / `/\s/`, written out — proven equal to `/\s/` over
50
+ every BMP code point) shared with the Python twin, whose block detector now
51
+ reads `markdown\u001f` as the renderer does. Vectors gained an `openers`
52
+ section and four exotic-whitespace documents (old Python: red).
53
+
54
+ **Consumer action:** code that branches on `SourceSpliceError.code` treats a
55
+ carried island that no longer stands as `island_edit` (it was `integrity`).
56
+
9
57
  ## 0.14.2 — 2026-09-25
10
58
 
11
59
  **`/source` — a tilde fence locks what the renderer draws inside it**
package/dist/index.cjs CHANGED
@@ -5195,6 +5195,24 @@ function trimFenceLine(line) {
5195
5195
  while (end > start && isFenceWhitespace(line[end - 1])) end--;
5196
5196
  return start === 0 && end === line.length ? line : line.slice(start, end);
5197
5197
  }
5198
+ var FENCE_OPENER_WHITESPACE = " \n\v\f\r \xA0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF";
5199
+ function isOpenerWhitespace(ch) {
5200
+ return ch !== void 0 && ch !== "" && FENCE_OPENER_WHITESPACE.includes(ch);
5201
+ }
5202
+ function parseFenceOpener(line) {
5203
+ let start = 0;
5204
+ let end = line.length;
5205
+ while (start < end && isOpenerWhitespace(line[start])) start++;
5206
+ while (end > start && isOpenerWhitespace(line[end - 1])) end--;
5207
+ let ticks = 0;
5208
+ while (start + ticks < end && line[start + ticks] === "`") ticks++;
5209
+ if (ticks < 3) return null;
5210
+ let infoStart = start + ticks;
5211
+ while (infoStart < end && isOpenerWhitespace(line[infoStart])) infoStart++;
5212
+ let langEnd = infoStart;
5213
+ while (langEnd < end && !isOpenerWhitespace(line[langEnd])) langEnd++;
5214
+ return { ticks, lang: line.slice(infoStart, langEnd) };
5215
+ }
5198
5216
  function classifyInnerFenceLine(trimmed, openTicks, nests, nestedDepth) {
5199
5217
  let ticks = 0;
5200
5218
  while (ticks < trimmed.length && trimmed[ticks] === "`") ticks++;
@@ -5276,15 +5294,20 @@ var STRUCTURAL_MARKDOWN = /\]\(|https?:\/\/|\*\*|(?:^|\n)[ \t]{0,3}#{1,6}[ \t]|(
5276
5294
  var LATEX_COMMAND = /\\[a-zA-Z]/;
5277
5295
  var PROSE_WORD = /[A-Za-z]{3,}/g;
5278
5296
  var PROSE_WORD_LIMIT = 6;
5279
- function looksLikeDisplayMath(inner) {
5297
+ function opensFlowBlock(text, open) {
5298
+ for (let k = open - 1; k >= 0 && text[k] !== "\n"; k--) if (text[k] !== " " && text[k] !== " ") return false;
5299
+ for (let k = open + 2; k < text.length && text[k] !== "\n"; k++) if (text[k] !== " " && text[k] !== " " && text[k] !== "\r") return false;
5300
+ return true;
5301
+ }
5302
+ function looksLikeDisplayMath(inner, flow = false) {
5280
5303
  const s = inner.trim();
5281
5304
  if (!s) return false;
5282
5305
  if (STRUCTURAL_MARKDOWN.test(s)) return false;
5283
5306
  if (s.length > MAX_MATH_SPAN) return false;
5307
+ if (!flow && /\n[ \t]*\n/.test(inner)) return false;
5284
5308
  if (LATEX_COMMAND.test(s)) {
5285
5309
  return (s.match(PROSE_WORD) ?? []).length < PROSE_WORD_LIMIT * 3;
5286
5310
  }
5287
- if (/\n[ \t]*\n/.test(s)) return false;
5288
5311
  return (s.match(PROSE_WORD) ?? []).length < PROSE_WORD_LIMIT;
5289
5312
  }
5290
5313
  function codeRanges(text) {
@@ -5318,7 +5341,7 @@ function pairDisplayMath(text) {
5318
5341
  const open = tokens[j];
5319
5342
  const close = tokens[j + 1];
5320
5343
  if (close === void 0) break;
5321
- if (looksLikeDisplayMath(text.slice(open + 2, close))) {
5344
+ if (looksLikeDisplayMath(text.slice(open + 2, close), opensFlowBlock(text, open))) {
5322
5345
  pairs.set(open, close);
5323
5346
  j += 2;
5324
5347
  continue;
@@ -5955,8 +5978,7 @@ var Tokenizer = class {
5955
5978
  */
5956
5979
  backtickFence(p, li, t) {
5957
5980
  const openTicks = backtickRunLength(t, 0);
5958
- const info = t.slice(openTicks).trim();
5959
- const lang = info.split(/\s/)[0] ?? "";
5981
+ const lang = parseFenceOpener(t)?.lang ?? "";
5960
5982
  const isJson = lang === "json";
5961
5983
  const normalized = normalizeFenceLanguage(lang);
5962
5984
  const meta = { fence: "`", ticks: openTicks, lang };
@@ -6086,6 +6108,7 @@ var Tokenizer = class {
6086
6108
  const endLine = this.lineOf(close);
6087
6109
  const endCe = this.contentEnds[endLine];
6088
6110
  if (endLine !== li && !this.isBlank(end, endCe)) return null;
6111
+ if (this.isOpenTail(endLine)) return this.island("math_block", p, this.n, false, { delimiter: "$$" });
6089
6112
  return this.island("math_block", p, end, true, { delimiter: "$$" });
6090
6113
  }
6091
6114
  /** `<artifact …>`, `<decision …>`, … at the start of a line. */
@@ -6415,7 +6438,7 @@ var Tokenizer = class {
6415
6438
  continue;
6416
6439
  }
6417
6440
  const end = singleDollarMathEnd(text, i, pe);
6418
- if (end > 0) {
6441
+ if (end > 0 && !(this.streaming && end === this.n)) {
6419
6442
  inlines.push(this.island("math_inline", i, end, true, { delimiter: "$" }));
6420
6443
  i = end;
6421
6444
  continue;
@@ -6825,11 +6848,17 @@ function spliceSave(original, edits, options = {}) {
6825
6848
  else newIslandsByStart.set(island.start, [island.raw]);
6826
6849
  }
6827
6850
  const disturbed = [];
6851
+ const carried = (island) => sorted.some((edit) => !edit.island && island.start >= edit.start && island.end <= edit.end);
6852
+ const changedInside = [];
6828
6853
  for (const island of islands) {
6829
6854
  if (replacedIslands.has(`${island.start}:${island.end}`)) continue;
6830
6855
  const mappedStart = mapPosition(changes, island.start, { assoc: 1 }).pos;
6831
6856
  const candidates = newIslandsByStart.get(mappedStart);
6832
6857
  if (!candidates || !candidates.includes(island.raw)) {
6858
+ if (carried(island)) {
6859
+ changedInside.push(island);
6860
+ continue;
6861
+ }
6833
6862
  disturbed.push({
6834
6863
  islandType: island.islandType,
6835
6864
  inline: island.inline,
@@ -6839,6 +6868,14 @@ function spliceSave(original, edits, options = {}) {
6839
6868
  });
6840
6869
  }
6841
6870
  }
6871
+ if (changedInside.length > 0) {
6872
+ const first = changedInside[0];
6873
+ const name = first.meta.tag ?? first.meta.name ?? first.meta.lang ?? "";
6874
+ throw new SourceSpliceError(
6875
+ "island_edit",
6876
+ `the edit changes the protected ${first.islandType}${name ? ` (${String(name)})` : ""} island at [${first.start}, ${first.end}) \u2014 its bytes survive only as ordinary text${changedInside.length > 1 ? ` (and ${changedInside.length - 1} more)` : ""}; islands change only through islandEdit()`
6877
+ );
6878
+ }
6842
6879
  const integrity = {
6843
6880
  ok: bytesOutsideEditsIdentical && disturbed.length === 0,
6844
6881
  bytesOutsideEditsIdentical,
@@ -6896,6 +6933,7 @@ exports.CAPABILITY_BY_CLASS = CAPABILITY_BY_CLASS;
6896
6933
  exports.CLASSES = CLASSES;
6897
6934
  exports.DIRECTIVE_VERSION = DIRECTIVE_VERSION;
6898
6935
  exports.DirectiveDecodeError = DirectiveDecodeError;
6936
+ exports.FENCE_OPENER_WHITESPACE = FENCE_OPENER_WHITESPACE;
6899
6937
  exports.FENCE_WHITESPACE = FENCE_WHITESPACE;
6900
6938
  exports.IN_CONTENT_CLASSES = IN_CONTENT_CLASSES;
6901
6939
  exports.IR_ENVELOPE_CACHE_VERSION = IR_ENVELOPE_CACHE_VERSION;
@@ -6998,6 +7036,7 @@ exports.nounTitleColumn = nounTitleColumn;
6998
7036
  exports.openParseSession = openParseSession;
6999
7037
  exports.pairDisplayMath = pairDisplayMath;
7000
7038
  exports.parseDirectiveSlug = parseDirectiveSlug;
7039
+ exports.parseFenceOpener = parseFenceOpener;
7001
7040
  exports.readEnvelope = readEnvelope;
7002
7041
  exports.readNodeOutcomeValue = readNodeOutcomeValue;
7003
7042
  exports.readObjectKind = readObjectKind;