@hviana/sema 0.8.2 → 0.8.5

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.
Files changed (126) hide show
  1. package/AGENTS.md +38 -37
  2. package/README.md +17 -38
  3. package/TRADEMARKS.md +0 -1
  4. package/dist/example/demo.js +85 -34
  5. package/dist/src/config.d.ts +11 -0
  6. package/dist/src/config.js +2 -0
  7. package/dist/src/geometry.d.ts +21 -10
  8. package/dist/src/geometry.js +21 -12
  9. package/dist/src/meter.d.ts +62 -0
  10. package/dist/src/meter.js +62 -0
  11. package/dist/src/mind/articulation.js +1 -1
  12. package/dist/src/mind/attention.d.ts +4 -0
  13. package/dist/src/mind/attention.js +167 -17
  14. package/dist/src/mind/canonical.d.ts +16 -0
  15. package/dist/src/mind/canonical.js +41 -0
  16. package/dist/src/mind/derivation.d.ts +201 -0
  17. package/dist/src/mind/derivation.js +327 -0
  18. package/dist/src/mind/graph-search.d.ts +2 -1
  19. package/dist/src/mind/graph-search.js +70 -29
  20. package/dist/src/mind/match.d.ts +3 -1
  21. package/dist/src/mind/match.js +7 -3
  22. package/dist/src/mind/mechanisms/alu.js +0 -2
  23. package/dist/src/mind/mechanisms/cast.d.ts +1 -5
  24. package/dist/src/mind/mechanisms/cast.js +16 -19
  25. package/dist/src/mind/mechanisms/confluence.d.ts +0 -3
  26. package/dist/src/mind/mechanisms/confluence.js +27 -9
  27. package/dist/src/mind/mechanisms/cover.js +17 -20
  28. package/dist/src/mind/mechanisms/extraction.d.ts +0 -1
  29. package/dist/src/mind/mechanisms/extraction.js +13 -8
  30. package/dist/src/mind/mechanisms/prefix-completion.js +0 -1
  31. package/dist/src/mind/mechanisms/recall.d.ts +0 -1
  32. package/dist/src/mind/mechanisms/recall.js +40 -13
  33. package/dist/src/mind/mechanisms/reference.js +3 -4
  34. package/dist/src/mind/mind.d.ts +4 -2
  35. package/dist/src/mind/mind.js +5 -4
  36. package/dist/src/mind/pipeline-mechanism.d.ts +7 -3
  37. package/dist/src/mind/pipeline.js +136 -44
  38. package/dist/src/mind/primitives.js +9 -1
  39. package/dist/src/mind/rationale.d.ts +21 -5
  40. package/dist/src/mind/rationale.js +16 -21
  41. package/dist/src/mind/reasoning.d.ts +12 -20
  42. package/dist/src/mind/reasoning.js +190 -106
  43. package/dist/src/mind/recognition.js +4 -8
  44. package/dist/src/mind/resonance.js +20 -1
  45. package/dist/src/mind/trace.js +1 -0
  46. package/dist/src/mind/traverse.js +6 -2
  47. package/dist/src/mind/types.d.ts +36 -13
  48. package/dist/src/mind/types.js +6 -3
  49. package/docs/INDEX.md +23 -24
  50. package/docs/INVARIANTS.md +16 -17
  51. package/docs/architecture/bounded-reads.md +5 -5
  52. package/docs/architecture/closure.md +65 -0
  53. package/docs/architecture/commonality.md +29 -20
  54. package/docs/architecture/cost-model.md +7 -7
  55. package/docs/architecture/determinism.md +7 -7
  56. package/docs/architecture/exact-vs-approximate.md +4 -4
  57. package/docs/architecture/factored-machinery.md +14 -14
  58. package/docs/architecture/match-project.md +2 -3
  59. package/docs/architecture/mechanism-market.md +16 -16
  60. package/docs/architecture/meter.md +10 -11
  61. package/docs/architecture/store.md +4 -4
  62. package/docs/architecture/thresholds.md +1 -1
  63. package/docs/failures/tempting-but-wrong.md +14 -5
  64. package/docs/harness/gates.md +7 -7
  65. package/docs/mechanisms/cast.md +2 -2
  66. package/docs/mechanisms/cover.md +4 -5
  67. package/docs/mechanisms/extraction.md +7 -7
  68. package/docs/mechanisms/recall.md +8 -9
  69. package/example/demo.ts +90 -37
  70. package/jsr.json +1 -1
  71. package/package.json +1 -1
  72. package/src/alu/README.md +11 -12
  73. package/src/config.ts +13 -0
  74. package/src/geometry.ts +21 -13
  75. package/src/meter.ts +62 -0
  76. package/src/mind/articulation.ts +0 -1
  77. package/src/mind/attention.ts +169 -17
  78. package/src/mind/canonical.ts +43 -0
  79. package/src/mind/derivation.ts +473 -0
  80. package/src/mind/graph-search.ts +76 -34
  81. package/src/mind/match.ts +7 -3
  82. package/src/mind/mechanisms/alu.ts +0 -2
  83. package/src/mind/mechanisms/cast.ts +20 -22
  84. package/src/mind/mechanisms/confluence.ts +27 -13
  85. package/src/mind/mechanisms/cover.ts +17 -20
  86. package/src/mind/mechanisms/extraction.ts +13 -9
  87. package/src/mind/mechanisms/prefix-completion.ts +0 -1
  88. package/src/mind/mechanisms/recall.ts +39 -13
  89. package/src/mind/mechanisms/reference.ts +2 -3
  90. package/src/mind/mind.ts +6 -4
  91. package/src/mind/pipeline-mechanism.ts +7 -3
  92. package/src/mind/pipeline.ts +160 -52
  93. package/src/mind/primitives.ts +9 -1
  94. package/src/mind/rationale.ts +27 -23
  95. package/src/mind/reasoning.ts +227 -120
  96. package/src/mind/recognition.ts +4 -8
  97. package/src/mind/resonance.ts +19 -1
  98. package/src/mind/trace.ts +1 -0
  99. package/src/mind/traverse.ts +7 -5
  100. package/src/mind/types.ts +41 -15
  101. package/test/105-derive-through-reports-its-refusal.test.mjs +24 -0
  102. package/test/118-the-join-reaches-a-key-off-the-cut.test.mjs +74 -0
  103. package/test/119-the-work-does-not-grow-with-the-corpus.test.mjs +122 -0
  104. package/test/120-composition-is-consequence.test.mjs +132 -0
  105. package/test/121-the-extension-does-not-grow-with-the-corpus.test.mjs +128 -0
  106. package/test/122-the-climb-search-does-not-grow-with-the-corpus.test.mjs +117 -0
  107. package/test/123-the-paired-formulas-agree.test.mjs +90 -0
  108. package/test/125-the-post-grounding-branch-publishes-its-operand.test.mjs +51 -0
  109. package/test/126-the-pipeline-does-not-name-mechanisms.test.mjs +42 -0
  110. package/test/128-the-leads-somewhere-pair-agrees.test.mjs +83 -0
  111. package/test/129-the-trace-payload-shape.test.mjs +164 -0
  112. package/test/133-the-decision-point-renders-the-state.test.mjs +204 -0
  113. package/test/134-the-law-explains-the-engines-own-refusal.test.mjs +237 -0
  114. package/test/135-one-law-any-producer.test.mjs +289 -0
  115. package/test/136-the-two-named-limits.test.mjs +205 -0
  116. package/test/137-the-law-lives-once-and-below.test.mjs +400 -0
  117. package/test/138-the-remainder-drains-only-what-a-move-declares.test.mjs +62 -0
  118. package/test/139-the-witness-is-engagement-not-explanation.test.mjs +51 -0
  119. package/test/140-irrelevant-supply-does-not-change-an-answer.test.mjs +48 -0
  120. package/test/141-the-question-is-paid-at-construction.test.mjs +98 -0
  121. package/test/32-confluence.test.mjs +68 -0
  122. package/test/36-already-answered-fusion.test.mjs +20 -2
  123. package/test/37-cluster-dispersion-fusion.test.mjs +30 -3
  124. package/test/38-reason-restate-guard.test.mjs +28 -2
  125. package/test/43-cast-analog-seat.test.mjs +10 -0
  126. package/test/55-cost-meter.test.mjs +862 -0
@@ -64,6 +64,7 @@ import {
64
64
  } from "./junction.js";
65
65
  import type { Vec } from "../vec.js";
66
66
  import { indexOf } from "../bytes.js";
67
+ import { restates } from "./derivation.js";
67
68
  import type { RationaleItem } from "./rationale.js";
68
69
  import { rDeriv, rItem, rNode, traceDerivation } from "./trace.js";
69
70
 
@@ -180,6 +181,10 @@ export interface ConsensusAnchorTrace {
180
181
 
181
182
  pooledVote: number;
182
183
  idfVote: number;
184
+ /** The LARGEST single-region contribution behind this anchor — the bar
185
+ * recall's own gate reads (mechanisms/recall.ts). Published so the one
186
+ * decision-making quantity the climb computes is not invisible. */
187
+ peak: number;
183
188
 
184
189
  candidateBreadth: number;
185
190
  contributingVotes: number;
@@ -1243,15 +1248,18 @@ export async function voteRegions(
1243
1248
  }
1244
1249
  contrastiveMargin = margin;
1245
1250
  // Scaled by what this region does NOT address — see `cov` above.
1246
- const noiseFloor = estimatorNoise(ctx.store.D) * (1 - cov);
1247
- if (margin <= noiseFloor) {
1251
+ // The bar THIS gate applies: the estimator's noise scaled by what the
1252
+ // region does NOT address (`cov`). ONE definition, used by the rejection
1253
+ // path below and by the voted payload — the trace reports the applied bar.
1254
+ const appliedFloor = estimatorNoise(ctx.store.D) * (1 - cov);
1255
+ if (margin <= appliedFloor) {
1248
1256
  recordRegion("contrastive-margin-rejection", {
1249
1257
  selected,
1250
1258
  reachNode: voterId,
1251
1259
  idf,
1252
1260
  dfWeight: wf,
1253
1261
  contrastiveMargin: margin,
1254
- contrastiveNoiseFloor: noiseFloor,
1262
+ contrastiveNoiseFloor: appliedFloor,
1255
1263
  ...(contrastiveRival ? { contrastiveRival } : {}),
1256
1264
  });
1257
1265
  continue;
@@ -1305,7 +1313,12 @@ export async function voteRegions(
1305
1313
  ...(contrastiveMargin !== undefined
1306
1314
  ? {
1307
1315
  contrastiveMargin,
1308
- contrastiveNoiseFloor: estimatorNoise(ctx.store.D),
1316
+ // THE BAR THE GATE ACTUALLY APPLIED — the same expression
1317
+ // the rejection path's `appliedFloor` defines, inline here because
1318
+ // this payload is built in a scope that does not carry that local.
1319
+ // Publishing the raw estimatorNoise(D) instead made a region that
1320
+ // PASSED look closer to its limit than it was.
1321
+ contrastiveNoiseFloor: estimatorNoise(ctx.store.D) * (1 - cov),
1309
1322
  ...(contrastiveRival ? { contrastiveRival } : {}),
1310
1323
  }
1311
1324
  : {}),
@@ -1438,7 +1451,15 @@ export function poolVotes(
1438
1451
  },
1439
1452
  pool,
1440
1453
  };
1441
- lightestDerivation(system);
1454
+ // THE SEARCH WAS THE ONE LAYER WITH NO TIME. The climb's phases are timed
1455
+ // (voteRegions, structuralResonance, crossRegion) but the pooled derivation
1456
+ // was not, so any cost or gain inside it stayed invisible.
1457
+ // `timeSync`, not `time`: the search is SYNCHRONOUS, and wrapping it in a
1458
+ // promise only to time it would make the profiled path wait where the
1459
+ // unprofiled one does not (meter.ts's own contract).
1460
+ if (ctx.meter) {
1461
+ ctx.meter.timeSync("climb.derivation", () => lightestDerivation(system));
1462
+ } else lightestDerivation(system);
1442
1463
 
1443
1464
  const votes = new Map<number, number>();
1444
1465
  const votesIdf = new Map<number, number>();
@@ -1476,12 +1497,24 @@ export function poolVotes(
1476
1497
  // The LARGEST single region's contribution to this anchor's pooled vote.
1477
1498
  // The pool is a SUM (deliberately — see the pooling note above), so it says
1478
1499
  // how much evidence there is in total, never whether any ONE place in the
1479
- // query carries evidence on its own. Consumers that hold an anchor to
1480
- // consensusFloor(N) = ln(N) + 1/2 need the latter: that bar prices ONE
1481
- // region's maximally-discriminative evidence (ln N is the IDF of content
1482
- // reaching a single context), so comparing a six-region sum against it is a
1483
- // dimensional error. Recorded here, beside the count, because this is the
1484
- // only place the per-region contributions are still separable.
1500
+ // query carries evidence on its own. Recorded here, beside the count,
1501
+ // because this is the only place the per-region contributions are still
1502
+ // separable.
1503
+ //
1504
+ // THE BAR IS THE POOLED FLOOR, AND IT WAS ONCE CLAIMED OTHERWISE HERE.
1505
+ // This comment used to say that holding an anchor to consensusFloor(N)
1506
+ // "prices ONE region's evidence", so comparing a six-region sum against it
1507
+ // was "a dimensional error". THAT WAS FALSE. `thresholds.md` §2 derives
1508
+ // `consensusFloor` as the POOLED-vote significance floor ("each region
1509
+ // contributes at most ln(N/c) <= ln(N); ln(N) + 1/2 demands ..."), and the
1510
+ // climb weights by IDF, so the sum and the floor are in ONE dimension —
1511
+ // which is exactly why `recall.ts` gates `forest[0].idfVote` against it and
1512
+ // why `commitVotes` does too. The other two weighting modes DO leave that
1513
+ // dimension (by at most ln 2, two-sided: `direct` deflates a region and
1514
+ // `combined` inflates it), and the gates therefore read the IDF sum, which
1515
+ // is mode-independent; `test/55` tests 19 and 20 pin both halves — the sum
1516
+ // as the reading the bar is derived for, and the absence of any gate
1517
+ // inversion across the three modes.
1485
1518
  const regionPeak = new Map<number, number>();
1486
1519
  const steps: DerivationStep[] = [];
1487
1520
  let order = 0;
@@ -1665,6 +1698,7 @@ export function commitVotes(
1665
1698
  anchor,
1666
1699
  vote,
1667
1700
  peak: regionPeak.get(anchor) ?? 0,
1701
+ idfVote: votesIdf.get(anchor) ?? 0,
1668
1702
  start: s.start,
1669
1703
  end: s.end,
1670
1704
  breadth: (regionSupport.get(anchor) ?? 0) / totalRegions,
@@ -1674,6 +1708,16 @@ export function commitVotes(
1674
1708
  ),
1675
1709
  };
1676
1710
  })
1711
+ // THE ORDER IS NOT A PREFERENCE: with equal evidence it decides ADMISSION,
1712
+ // through the stable sort and the first-come overlap absorption below.
1713
+ // Measured on test/34's corpus, query "red": the two candidates (`red
1714
+ // circle` and `red square`) carry IDENTICAL `vote` and IDENTICAL `idfVote`
1715
+ // (1.3863 each, three seeds), so this comparator leaves them tied and the
1716
+ // stable sort keeps the ENUMERATION order — which is corpus-determined and
1717
+ // admits `red square`, 60/60 seeds. Adding an id tie-break (`|| a.anchor -
1718
+ // b.anchor`) picks `red circle` instead and makes a single region reach the
1719
+ // JOINT context, which is the premise `test/34` exists to protect. The
1720
+ // gates read IDF; this line only decides who gets looked at first.
1677
1721
  .sort((a, b) => b.vote - a.vote);
1678
1722
  const overlaps = (a: Attention, b: Attention) =>
1679
1723
  a.start < b.end && b.start < a.end;
@@ -1719,6 +1763,13 @@ export function commitVotes(
1719
1763
  rank,
1720
1764
  pooledVote: point.vote,
1721
1765
  idfVote: votesIdf.get(point.anchor) ?? 0,
1766
+ // The LARGEST single-region contribution behind this anchor — the bar
1767
+ // recall's own gate reads (mechanisms/recall.ts: forest[0].peak > LN2),
1768
+ // and until now the only decision-making quantity the climb computed and
1769
+ // did not publish. `regionPeak` reached `ranked` (see its build below)
1770
+ // and stopped there. Published, not recomputed: the value is the one the
1771
+ // climb already carries.
1772
+ peak: point.peak,
1722
1773
  candidateBreadth: regions.length,
1723
1774
  contributingVotes: regionAxioms.get(point.anchor) ?? 0,
1724
1775
  contributingEvidence: regionSupport.get(point.anchor) ?? 0,
@@ -1748,6 +1799,18 @@ export function commitVotes(
1748
1799
  let passesConsensusFloor: boolean | undefined;
1749
1800
  let pastLeadingSaturation: boolean | undefined;
1750
1801
  let tiedWithDominant: boolean | undefined;
1802
+ // ── ONE OF THREE ADMISSIONS, AND THEY ARE NOT THE SAME READING ────────
1803
+ // This block admits by VOTES: per-region evidence pooled, gated on the
1804
+ // natural break and on consensusFloor, with the dominant allowed to bypass
1805
+ // both. `structuralResonance` admits by a MARGIN over the estimator's own
1806
+ // noise, and `crossRegionVotes` admits by STRUCTURE (which regions may pair
1807
+ // at all, with at least one side individually discriminative). Read
1808
+ // together they look like one policy written three times; they are three
1809
+ // different measurements of the same question ("is this evidence?"), and
1810
+ // unifying them would average three readings into one — the mistake
1811
+ // `extraction.ts` records as "do not unify the two into one machine".
1812
+ // What they DO share, and must keep sharing, is the discipline of deriving
1813
+ // every bar from D/W/N rather than choosing it (thresholds.md).
1751
1814
  const rejectionReasons: AnchorRejectionReason[] = [];
1752
1815
  if (absorbed) {
1753
1816
  status = "overlap";
@@ -1757,9 +1820,75 @@ export function commitVotes(
1757
1820
  pastLeadingSaturation = pastLeading;
1758
1821
  const vote = votesIdf.get(point.anchor) ?? 0;
1759
1822
  if (roots.length === 0) {
1760
- // The first non-overlapping root is DOMINANT and bypasses the two
1761
- // vote thresholds (it always grounds) — only the leading-saturation
1762
- // gate still applies to it.
1823
+ // THE DOMINANCE PRIVILEGE, AND THE TENSION IT CARRIES (measured).
1824
+ //
1825
+ // The first non-overlapping candidate is DOMINANT: it bypasses both
1826
+ // vote gates below and grounds on its own; only the leading-saturation
1827
+ // gate still applies to it. The privilege is load-bearing — analogies,
1828
+ // substitutions and composed contexts are precisely candidates the
1829
+ // query does NOT contain, and the engine loses them without it.
1830
+ //
1831
+ // WHICH candidate receives it, though, is decided by this loop's ORDER.
1832
+ // That order comes from `ranked`, and when two candidates carry equal
1833
+ // evidence the stable sort preserves the ENUMERATION order, so the
1834
+ // privilege is allocated by an ordering rather than by a rule.
1835
+ //
1836
+ // Measured on test/34's corpus, query "red":
1837
+ //
1838
+ // 0:#77 vote=1.3863 idf=1.3863 [0,3) | 1:#49 vote=1.3863 idf=1.3863 [0,3)
1839
+ //
1840
+ // Both candidates (`red square` #77, `red circle` #49) have IDENTICAL
1841
+ // `vote` AND IDENTICAL `idfVote` over the SAME support span, so the
1842
+ // comparator leaves them tied, the second is absorbed as "overlap", and
1843
+ // the first grounds. With this build's enumeration order that first is
1844
+ // `red square`, 60/60 seeds, and `red circle` — the JOINT context — is
1845
+ // never reached by "red" alone. That is the premise test/34 exists to
1846
+ // protect: no single region reaches the joint context, which is what
1847
+ // makes the binding query unreachable without direct region
1848
+ // interaction.
1849
+ //
1850
+ // THE TENSION: the premise therefore holds BY ENUMERATION ORDER, not by
1851
+ // a rule, so any change to this ordering can move the privilege onto the
1852
+ // joint context and let one region reach it. Measured: adding
1853
+ // `|| a.anchor - b.anchor` — the lowest-id tie-break that AGENTS.md §2
1854
+ // sanctions as an equivalent corpus-determined tie-break — does exactly
1855
+ // that: "red" then attends to `red circle`, test/34 fails 6/1, and the
1856
+ // canonical suite reports 1 failure.
1857
+ //
1858
+ // TWO ATTEMPTS TO MAKE IT A RULE, BOTH REFUTED BY MEASUREMENT:
1859
+ //
1860
+ // 1. EVIDENCE SEPARATION. Grant the privilege only when the first
1861
+ // candidate's evidence is separated from the next distinct
1862
+ // candidate's by more than the co-dominant band (sqrt(k) *
1863
+ // estimatorNoise(D)). Refuted: that band exists to ADMIT the
1864
+ // anchors the estimator cannot separate from the dominant — its own
1865
+ // documented purpose — so withholding the privilege on ties removes
1866
+ // the very case it was written for. Suite: 4 failures (the two
1867
+ // co-dominant band laws, breadth/scale invariance, test/29 D2).
1868
+ //
1869
+ // 2. QUERY-OWNED CONTENT. Grant the privilege only to a candidate
1870
+ // that IS a recognised region's identity (regions.some(r => r.id ===
1871
+ // point.anchor)). Measured: for "circle" that identity IS the
1872
+ // ranked candidate, so the privilege stays and `circle` grounds; for
1873
+ // "red" the identity is the `red` node itself while the candidates
1874
+ // are the conjunctions, so neither is privileged; for "red then
1875
+ // circle" the composed context carries idf 3.958 and clears both
1876
+ // gates on its own evidence. All four control queries came out
1877
+ // right — and the suite: 10 failures, six of them in the
1878
+ // analogy/counterfactual/CAST suites ("an analogy still transfers
1879
+ // from a structure the query never names"; "a substitute the query
1880
+ // NAMES may still be voiced"). Refuted: the privilege exists to
1881
+ // admit what the query does NOT contain, so identity is the wrong
1882
+ // axis.
1883
+ //
1884
+ // WHAT A FUTURE ATTEMPT MUST RESPECT: whatever allocates this privilege
1885
+ // has to (a) keep it available to candidates the query does not contain
1886
+ // — analogies, substitutions, compositions — and (b) not depend on the
1887
+ // estimator's ordering among anchors of equal evidence, because that
1888
+ // ordering is not a fact about the corpus. No lever satisfying both has
1889
+ // been found. Until one is, this premise rests on the enumeration order
1890
+ // recorded above, and test/34 is the only test that notices if it
1891
+ // moves.
1763
1892
  dominant = true;
1764
1893
  if (pastLeading) {
1765
1894
  status = "root";
@@ -1768,8 +1897,15 @@ export function commitVotes(
1768
1897
  rejectionReasons.push("leading-saturation");
1769
1898
  }
1770
1899
  } else {
1771
- passesNaturalBreak = vote >= rootCut;
1772
- passesConsensusFloor = vote >= floor;
1900
+ // THE FLOOR AND THE BREAK READ THE IDF WEIGHTING. `floor` is derived
1901
+ // for pooled IDF-weighted votes, and `rootCut` comes from the IDF
1902
+ // distribution (`idfDesc`), so gating the mode-dependent `vote` against
1903
+ // either let a weighting mode change an admission (measured: anchor 87,
1904
+ // inverse 2.682 admitted vs direct 1.468 refused). Reading the IDF sum
1905
+ // makes the verdict mode-independent, and changes nothing in the
1906
+ // engine's own mode, where the two readings coincide.
1907
+ passesNaturalBreak = point.idfVote >= rootCut;
1908
+ passesConsensusFloor = point.idfVote >= floor;
1773
1909
  // CO-DOMINANT — an anchor the estimator cannot separate from the
1774
1910
  // dominant inherits the dominant's exemption, because that exemption's
1775
1911
  // only warrant is being TOP, and "top" is not a fact about the corpus
@@ -2467,6 +2603,13 @@ export async function structuralResonance(
2467
2603
  });
2468
2604
  };
2469
2605
 
2606
+ // ── ADMISSION BY MARGIN, not by votes (see voteRegions' note) ─────────
2607
+ // What this site measures: how far the best ANN proposal's effective score
2608
+ // (score × semanticConfidence) stands above the runner-up's, against
2609
+ // `estimatorNoise(D)`. What it does NOT measure: how many regions voted,
2610
+ // or whether the query's regions agree — that is voteRegions' question, and
2611
+ // here a synthetic gist has already replaced them. The two bars are both
2612
+ // derived (thresholds.md), and neither is a tuning of the other.
2470
2613
  let selected: StructuralResonanceProposal | null = null;
2471
2614
  let selectedReach: AncestorReach | null = null;
2472
2615
  let selectedIdf = 0;
@@ -2582,6 +2725,15 @@ async function crossRegionVotes(
2582
2725
  // successfully reconstructed while probing one pair must not be read and
2583
2726
  // perceived again while probing another pair in the same climb.
2584
2727
  const siblingGistMemo = new Map<number, CachedSiblingGist>();
2728
+ // ── ADMISSION BY STRUCTURE, not by a bar (see voteRegions' note) ──────
2729
+ // What this site decides: WHICH regions may pair at all — a region that
2730
+ // already voted (individually discriminative), or a KNOWN non-voting one as
2731
+ // the weak side of a pair whose other side voted; never two non-voting
2732
+ // regions, and never a span contained in a maximal one whose reading is
2733
+ // exact. The bar (the container's idf) comes later, on the candidate. So
2734
+ // its "rejection reasons" name structural disqualifications — a different
2735
+ // vocabulary because it answers a different question, and the three
2736
+ // taxonomies stay separate for the same reason the readings do.
2585
2737
  const votedSpans = new Set<string>();
2586
2738
  for (const rv of rvs.votes) votedSpans.add(`${rv.start},${rv.end}`);
2587
2739
  const seen = new Set<string>();
@@ -2978,7 +3130,7 @@ async function crossRegionVotes(
2978
3130
  Math.min(li, ri),
2979
3131
  Math.max(li + left.length, ri + right.length),
2980
3132
  );
2981
- if (indexOf(query, joined, 0) >= 0) {
3133
+ if (restates(query, joined, 0)) {
2982
3134
  if (structuralTrace) structuralTrace.selfEvidenceRejected++;
2983
3135
  continue; // query says it itself
2984
3136
  }
@@ -88,6 +88,49 @@ export function leafIdPrefix(
88
88
  return ids;
89
89
  }
90
90
 
91
+ /** Which prefixes of `prefix ‖ tail` are STORED NODES — as lengths in the
92
+ * tail's own coordinates, ascending, excluding the empty one. This is the
93
+ * candidate set a rule needs to join an already-stored prefix to a suffix it
94
+ * has not stored: a key names a relation exactly when `prefix ‖ tail[0..p]` IS
95
+ * a node, and a key can end strictly inside the tail without sitting on any
96
+ * fold boundary (a stored member's end is the end of ITS OWN stream, and the
97
+ * fold never emits a cut at a stream's end). Measured: "stockholm mayor"
98
+ * exists, leads on, and its boundary 6 is in neither the tail's cuts nor the
99
+ * concatenation's.
100
+ *
101
+ * ONE cheap content-addressed probe per offset — `leafIdPrefix` walks the bytes
102
+ * once (a point probe each), `findBranch` hashes the growing kid run — and NO
103
+ * `resolve`, which is what keeps this off the O(suffix) vector folds the
104
+ * recognition path pays. It stops at the first byte that was never interned,
105
+ * which costs nothing real: a stored key's bytes are interned by construction. */
106
+ export function keyEnds(
107
+ ctx: MindContext,
108
+ prefix: Uint8Array,
109
+ tail: Uint8Array,
110
+ ): number[] {
111
+ if (prefix.length === 0 || tail.length === 0) return [];
112
+ const joined = new Uint8Array(prefix.length + tail.length);
113
+ joined.set(prefix, 0);
114
+ joined.set(tail, prefix.length);
115
+ const ids = leafIdPrefix(ctx, joined);
116
+ if (ids.length < prefix.length) return [];
117
+ const ends: number[] = [];
118
+ // The kid run GROWS by one id per offset; `findBranch` wants an array, so the
119
+ // run is built once and pushed into, never re-sliced. Re-slicing
120
+ // `ids.slice(0, prefix.length + p)` per offset made this O(|tail| ·
121
+ // (|prefix| + |tail|)) — quadratic in the tail, where the learning path this
122
+ // follows slices a run that SHRINKS. Same ends, linear copying.
123
+ const run = ids.slice(0, prefix.length);
124
+ // The loop ENDS at the first byte that was never interned (`ids.length`):
125
+ // every later prefix contains it, so none of them can be a node either — this
126
+ // is where the scan stops, not a silent truncation of the answer.
127
+ for (let p = 1; prefix.length + p <= ids.length; p++) {
128
+ run.push(ids[prefix.length + p - 1]);
129
+ if (ctx.store.findBranch(run) !== null) ends.push(p);
130
+ }
131
+ return ends;
132
+ }
133
+
91
134
  /** The canonical W-window node ids of a byte stream, offset → id — the
92
135
  * CONTENT-ADDRESSED IDENTITY of every W-sized slice, under which any content
93
136
  * two deposits share IS the same node (hash-consing paid the comparison at