@ai-matrx/content-ir 0.14.3 → 0.15.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
@@ -6,6 +6,44 @@ Automatic changed-only republish (docs/metadata drift since the last tag — see
6
6
  `git diff npm/content-ir-core/v0.14.2..npm/content-ir-core/v0.14.3 -- apps/shared/content-ir-core`).
7
7
  No source changes intended and no consumer action required.
8
8
 
9
+ ## 0.15.0 — 2026-09-25
10
+
11
+ **`/source` — the attack suite stands alone; the last twin drift and flicker
12
+ closed** (verify-RC-B3 § Re-verification (0.14.2): G1–G3, R4′, R2′). Proof:
13
+ the verifier's six planted mutations (M1 skip the in-range island check, M2
14
+ integrity default off, M3 streaming ignored, M4 code-point offset off by one,
15
+ M5 unbounded `}}` scan, M6 `islandEdit` accepts any span) each turn
16
+ `__tests__/source-attack.test.ts` red ON ITS OWN; unmutated code is green in
17
+ CI and in four consecutive `SOURCE_ATTACK=long` runs on a heavily loaded host.
18
+
19
+ - **G1** — the streaming check covers every complete island in every prefix,
20
+ the LAST one included, keyed by span AND block/inline; only line-grown
21
+ constructs (tree, callout, footnote, HTML block, directive) may grow as the
22
+ trailing block. A third message carries the R2′ case and the extended syntax.
23
+ - **G2** — save verdicts are checked against an independent oracle (which
24
+ islands survive in the expected text), exactly: `island_edit` vs
25
+ `integrity` vs accepted; two-sided edits must map an island exactly (never
26
+ through a deleted range); mangling must be `island_edit`; forged
27
+ `island:true` spans must be `misaligned`.
28
+ - **G3** — timing compares the MINIMUM of interleaved runs at a 4× size step
29
+ (linear ≈ 4×, quadratic ≈ 16×, bound 8×) and fails only if every one of
30
+ three attempts breaches.
31
+ - **`island_edit` is now also the verdict** when an island inside the edited
32
+ range survives only as ordinary text (its bytes matched a non-island copy,
33
+ or the edit turned it into code) — previously reported as `integrity`.
34
+ `integrity` now means an island OUTSIDE the edited ranges was disturbed.
35
+ - **R2′** — streaming: a `$$` block whose closer sits on the still-open last
36
+ line is not settled (the rest of that line decides block vs inline); a
37
+ closing `$` that is the last byte so far is not yet math.
38
+ - **R4′** — `FENCE_OPENER_WHITESPACE` + `parseFenceOpener`: the opener parse
39
+ (JavaScript `trim()` / `/\s/`, written out — proven equal to `/\s/` over
40
+ every BMP code point) shared with the Python twin, whose block detector now
41
+ reads `markdown\u001f` as the renderer does. Vectors gained an `openers`
42
+ section and four exotic-whitespace documents (old Python: red).
43
+
44
+ **Consumer action:** code that branches on `SourceSpliceError.code` treats a
45
+ carried island that no longer stands as `island_edit` (it was `integrity`).
46
+
9
47
  ## 0.14.2 — 2026-09-25
10
48
 
11
49
  **`/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++;
@@ -5955,8 +5973,7 @@ var Tokenizer = class {
5955
5973
  */
5956
5974
  backtickFence(p, li, t) {
5957
5975
  const openTicks = backtickRunLength(t, 0);
5958
- const info = t.slice(openTicks).trim();
5959
- const lang = info.split(/\s/)[0] ?? "";
5976
+ const lang = parseFenceOpener(t)?.lang ?? "";
5960
5977
  const isJson = lang === "json";
5961
5978
  const normalized = normalizeFenceLanguage(lang);
5962
5979
  const meta = { fence: "`", ticks: openTicks, lang };
@@ -6086,6 +6103,7 @@ var Tokenizer = class {
6086
6103
  const endLine = this.lineOf(close);
6087
6104
  const endCe = this.contentEnds[endLine];
6088
6105
  if (endLine !== li && !this.isBlank(end, endCe)) return null;
6106
+ if (this.isOpenTail(endLine)) return this.island("math_block", p, this.n, false, { delimiter: "$$" });
6089
6107
  return this.island("math_block", p, end, true, { delimiter: "$$" });
6090
6108
  }
6091
6109
  /** `<artifact …>`, `<decision …>`, … at the start of a line. */
@@ -6415,7 +6433,7 @@ var Tokenizer = class {
6415
6433
  continue;
6416
6434
  }
6417
6435
  const end = singleDollarMathEnd(text, i, pe);
6418
- if (end > 0) {
6436
+ if (end > 0 && !(this.streaming && end === this.n)) {
6419
6437
  inlines.push(this.island("math_inline", i, end, true, { delimiter: "$" }));
6420
6438
  i = end;
6421
6439
  continue;
@@ -6825,11 +6843,17 @@ function spliceSave(original, edits, options = {}) {
6825
6843
  else newIslandsByStart.set(island.start, [island.raw]);
6826
6844
  }
6827
6845
  const disturbed = [];
6846
+ const carried = (island) => sorted.some((edit) => !edit.island && island.start >= edit.start && island.end <= edit.end);
6847
+ const changedInside = [];
6828
6848
  for (const island of islands) {
6829
6849
  if (replacedIslands.has(`${island.start}:${island.end}`)) continue;
6830
6850
  const mappedStart = mapPosition(changes, island.start, { assoc: 1 }).pos;
6831
6851
  const candidates = newIslandsByStart.get(mappedStart);
6832
6852
  if (!candidates || !candidates.includes(island.raw)) {
6853
+ if (carried(island)) {
6854
+ changedInside.push(island);
6855
+ continue;
6856
+ }
6833
6857
  disturbed.push({
6834
6858
  islandType: island.islandType,
6835
6859
  inline: island.inline,
@@ -6839,6 +6863,14 @@ function spliceSave(original, edits, options = {}) {
6839
6863
  });
6840
6864
  }
6841
6865
  }
6866
+ if (changedInside.length > 0) {
6867
+ const first = changedInside[0];
6868
+ const name = first.meta.tag ?? first.meta.name ?? first.meta.lang ?? "";
6869
+ throw new SourceSpliceError(
6870
+ "island_edit",
6871
+ `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()`
6872
+ );
6873
+ }
6842
6874
  const integrity = {
6843
6875
  ok: bytesOutsideEditsIdentical && disturbed.length === 0,
6844
6876
  bytesOutsideEditsIdentical,
@@ -6896,6 +6928,7 @@ exports.CAPABILITY_BY_CLASS = CAPABILITY_BY_CLASS;
6896
6928
  exports.CLASSES = CLASSES;
6897
6929
  exports.DIRECTIVE_VERSION = DIRECTIVE_VERSION;
6898
6930
  exports.DirectiveDecodeError = DirectiveDecodeError;
6931
+ exports.FENCE_OPENER_WHITESPACE = FENCE_OPENER_WHITESPACE;
6899
6932
  exports.FENCE_WHITESPACE = FENCE_WHITESPACE;
6900
6933
  exports.IN_CONTENT_CLASSES = IN_CONTENT_CLASSES;
6901
6934
  exports.IR_ENVELOPE_CACHE_VERSION = IR_ENVELOPE_CACHE_VERSION;
@@ -6998,6 +7031,7 @@ exports.nounTitleColumn = nounTitleColumn;
6998
7031
  exports.openParseSession = openParseSession;
6999
7032
  exports.pairDisplayMath = pairDisplayMath;
7000
7033
  exports.parseDirectiveSlug = parseDirectiveSlug;
7034
+ exports.parseFenceOpener = parseFenceOpener;
7001
7035
  exports.readEnvelope = readEnvelope;
7002
7036
  exports.readNodeOutcomeValue = readNodeOutcomeValue;
7003
7037
  exports.readObjectKind = readObjectKind;