@tangle-network/agent-eval 0.197.0 → 0.198.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.
Files changed (148) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/active-curriculum-B_Lc9kfO.js +61 -0
  3. package/dist/active-curriculum-B_Lc9kfO.js.map +1 -0
  4. package/dist/adapters/http.d.ts +2 -2
  5. package/dist/analyst/index.d.ts +5 -5
  6. package/dist/analyst/index.js +3 -3
  7. package/dist/{analyze-runs-B6gv7vAT.js → analyze-runs-DFbp2gB8.js} +5 -5
  8. package/dist/{analyze-runs-B6gv7vAT.js.map → analyze-runs-DFbp2gB8.js.map} +1 -1
  9. package/dist/{benchmark-command-DB7pxlks.js → benchmark-command-D8E5TaPC.js} +6 -5
  10. package/dist/{benchmark-command-DB7pxlks.js.map → benchmark-command-D8E5TaPC.js.map} +1 -1
  11. package/dist/benchmarks/index.d.ts +4 -3
  12. package/dist/benchmarks/index.d.ts.map +1 -1
  13. package/dist/benchmarks/index.js +3 -3
  14. package/dist/campaign/index.d.ts +5 -4
  15. package/dist/campaign/index.js +13 -11
  16. package/dist/{campaign-BUFSOI36.js → campaign-Dlg2iOzH.js} +13 -12
  17. package/dist/{campaign-BUFSOI36.js.map → campaign-Dlg2iOzH.js.map} +1 -1
  18. package/dist/{campaign-evidence-DJc60F9C.js → campaign-evidence-jL_U7Ikv.js} +2 -2
  19. package/dist/{campaign-evidence-DJc60F9C.js.map → campaign-evidence-jL_U7Ikv.js.map} +1 -1
  20. package/dist/cli.js +243 -13
  21. package/dist/cli.js.map +1 -1
  22. package/dist/contract/index.d.ts +7 -6
  23. package/dist/contract/index.js +9 -9
  24. package/dist/{default-registry-D2csUBBQ.d.ts → default-registry-oK9hHfX_.d.ts} +3 -3
  25. package/dist/{default-registry-D2csUBBQ.d.ts.map → default-registry-oK9hHfX_.d.ts.map} +1 -1
  26. package/dist/{define-agent-eval-ulRlLvse.d.ts → define-agent-eval-By9PImpa.d.ts} +5 -4
  27. package/dist/define-agent-eval-By9PImpa.d.ts.map +1 -0
  28. package/dist/{define-agent-eval-4J02MdF7.js → define-agent-eval-DvBgSzAI.js} +6 -6
  29. package/dist/{define-agent-eval-4J02MdF7.js.map → define-agent-eval-DvBgSzAI.js.map} +1 -1
  30. package/dist/diagnosis/index.js +1 -1
  31. package/dist/{dspy-rlm-engine-k_QdEBZo.js → dspy-rlm-engine-B5T43BkZ.js} +2 -2
  32. package/dist/{dspy-rlm-engine-k_QdEBZo.js.map → dspy-rlm-engine-B5T43BkZ.js.map} +1 -1
  33. package/dist/{engine-De2hfuiQ.d.ts → engine-Dc996kAg.d.ts} +2 -2
  34. package/dist/{engine-De2hfuiQ.d.ts.map → engine-Dc996kAg.d.ts.map} +1 -1
  35. package/dist/{eval-campaign-CXT2WYUe.js → eval-campaign-GapmPHIA.js} +2 -2
  36. package/dist/{eval-campaign-CXT2WYUe.js.map → eval-campaign-GapmPHIA.js.map} +1 -1
  37. package/dist/evaluation.d.ts +1 -1
  38. package/dist/experiment/index.d.ts +1 -1
  39. package/dist/experiment/index.js +8 -6
  40. package/dist/experiment/index.js.map +1 -1
  41. package/dist/{external-optimizer-process-_YIECYJ7.js → external-optimizer-process-CEXcCKqd.js} +2 -2
  42. package/dist/{external-optimizer-process-_YIECYJ7.js.map → external-optimizer-process-CEXcCKqd.js.map} +1 -1
  43. package/dist/{external-optimizer-subprocess-BrghTvEp.js → external-optimizer-subprocess-laM1-UDV.js} +2 -61
  44. package/dist/external-optimizer-subprocess-laM1-UDV.js.map +1 -0
  45. package/dist/fuzz.d.ts.map +1 -1
  46. package/dist/fuzz.js +2 -3
  47. package/dist/fuzz.js.map +1 -1
  48. package/dist/hosted/index.js +1 -1
  49. package/dist/{index-DJKZEldg.d.ts → index-BhoOCduB.d.ts} +12 -947
  50. package/dist/index-BhoOCduB.d.ts.map +1 -0
  51. package/dist/{index-CRsmcAGl.d.ts → index-D-tI1vgf.d.ts} +6 -6
  52. package/dist/{index-CRsmcAGl.d.ts.map → index-D-tI1vgf.d.ts.map} +1 -1
  53. package/dist/{index-kKECJfib.d.ts → index-lbwvO9vJ.d.ts} +3 -3
  54. package/dist/{index-kKECJfib.d.ts.map → index-lbwvO9vJ.d.ts.map} +1 -1
  55. package/dist/index.d.ts +9 -9
  56. package/dist/index.js +15 -13
  57. package/dist/index.js.map +1 -1
  58. package/dist/jev.d.ts +1 -1
  59. package/dist/{llm-judge-fhkFQL1N.js → llm-judge-BYWXsxhZ.js} +415 -1981
  60. package/dist/llm-judge-BYWXsxhZ.js.map +1 -0
  61. package/dist/{matrix-C1X-fIVX.d.ts → matrix-BnksRaDD.d.ts} +2 -2
  62. package/dist/{matrix-C1X-fIVX.d.ts.map → matrix-BnksRaDD.d.ts.map} +1 -1
  63. package/dist/multiplicity-D9cjmStq.js +101 -0
  64. package/dist/multiplicity-D9cjmStq.js.map +1 -0
  65. package/dist/multishot/golden/index.d.ts +1 -1
  66. package/dist/multishot/golden/index.js +1 -1
  67. package/dist/multishot/index.d.ts +2 -2
  68. package/dist/multishot/index.js +1 -1
  69. package/dist/openapi.json +1 -1
  70. package/dist/{paired-arms-JSDcs2zx.js → paired-arms-DKZFVPt8.js} +4 -398
  71. package/dist/paired-arms-DKZFVPt8.js.map +1 -0
  72. package/dist/paired-binary-fzkwENI4.js +399 -0
  73. package/dist/paired-binary-fzkwENI4.js.map +1 -0
  74. package/dist/{paired-promotion-decision-DTKl6-G8.js → paired-promotion-decision-D5B1Cxc2.js} +78 -5
  75. package/dist/paired-promotion-decision-D5B1Cxc2.js.map +1 -0
  76. package/dist/{power-and-mde-BJOpt6pq.js → power-and-mde-K_CmO3dc.js} +3 -101
  77. package/dist/power-and-mde-K_CmO3dc.js.map +1 -0
  78. package/dist/{produced-state-1RnbLrSa.js → produced-state-DOfmCEwP.js} +3 -3
  79. package/dist/{produced-state-1RnbLrSa.js.map → produced-state-DOfmCEwP.js.map} +1 -1
  80. package/dist/{promotion-policy-Cy5OV8BV.js → promotion-policy-C6FAJvmZ.js} +3 -3
  81. package/dist/{promotion-policy-Cy5OV8BV.js.map → promotion-policy-C6FAJvmZ.js.map} +1 -1
  82. package/dist/reporting.js +3 -3
  83. package/dist/rl.d.ts +17 -39
  84. package/dist/rl.d.ts.map +1 -1
  85. package/dist/rl.js +5 -5
  86. package/dist/search/index.d.ts +1162 -0
  87. package/dist/search/index.d.ts.map +1 -0
  88. package/dist/search/index.js +187 -0
  89. package/dist/search/index.js.map +1 -0
  90. package/dist/search-kernel-CuvbakC_.d.ts +1176 -0
  91. package/dist/search-kernel-CuvbakC_.d.ts.map +1 -0
  92. package/dist/search-kernel-D8_kxChy.js +2399 -0
  93. package/dist/search-kernel-D8_kxChy.js.map +1 -0
  94. package/dist/{search-ledger-9W0g5FP6.js → search-ledger-BnUytR8X.js} +3 -1
  95. package/dist/search-ledger-BnUytR8X.js.map +1 -0
  96. package/dist/{search-shipper-CYbmJQx8.js → search-shipper-Cn8cxcyp.js} +2 -2
  97. package/dist/{search-shipper-CYbmJQx8.js.map → search-shipper-Cn8cxcyp.js.map} +1 -1
  98. package/dist/search-summary-Dmppg8kV.js +133 -0
  99. package/dist/search-summary-Dmppg8kV.js.map +1 -0
  100. package/dist/{semantic-concept-judge-Bh5lvKGg.js → semantic-concept-judge-2SE_kXGF.js} +2 -2
  101. package/dist/{semantic-concept-judge-Bh5lvKGg.js.map → semantic-concept-judge-2SE_kXGF.js.map} +1 -1
  102. package/dist/{sequential-C21_PP7F.js → sequential-Yz3OSskl.js} +2 -2
  103. package/dist/{sequential-C21_PP7F.js.map → sequential-Yz3OSskl.js.map} +1 -1
  104. package/dist/single-run-lock-fVu3MZWH.js +62 -0
  105. package/dist/single-run-lock-fVu3MZWH.js.map +1 -0
  106. package/dist/{skillopt-optimization-method-CHUcZ8w6.js → skillopt-optimization-method-BMP2qV_1.js} +8 -7
  107. package/dist/{skillopt-optimization-method-CHUcZ8w6.js.map → skillopt-optimization-method-BMP2qV_1.js.map} +1 -1
  108. package/dist/{statistical-heldout-DANVmyug.js → statistical-heldout-BAFDCo07.js} +2 -2
  109. package/dist/{statistical-heldout-DANVmyug.js.map → statistical-heldout-BAFDCo07.js.map} +1 -1
  110. package/dist/{statistical-heldout-BedVSZb2.d.ts → statistical-heldout-CMpxMjlQ.d.ts} +2 -2
  111. package/dist/{statistical-heldout-BedVSZb2.d.ts.map → statistical-heldout-CMpxMjlQ.d.ts.map} +1 -1
  112. package/dist/{store-tool-spans-CawP-WxM.d.ts → store-tool-spans-CUwWSXoj.d.ts} +3 -3
  113. package/dist/{store-tool-spans-CawP-WxM.d.ts.map → store-tool-spans-CUwWSXoj.d.ts.map} +1 -1
  114. package/dist/{summary-report-BMvou2oZ.js → summary-report-BeYnvddy.js} +4 -3
  115. package/dist/{summary-report-BMvou2oZ.js.map → summary-report-BeYnvddy.js.map} +1 -1
  116. package/dist/{tool-groups-B4JVrH3g.d.ts → tool-groups-DVJAR1GE.d.ts} +2 -2
  117. package/dist/tool-groups-DVJAR1GE.d.ts.map +1 -0
  118. package/dist/trace-repair/index.d.ts +1 -1
  119. package/dist/traces.d.ts +2 -2
  120. package/dist/tree-DHHWsH-X.js +4045 -0
  121. package/dist/tree-DHHWsH-X.js.map +1 -0
  122. package/dist/{types-B3jzCp0p.js → types-B3jzCp0p2.js} +1 -1
  123. package/dist/types-B3jzCp0p2.js.map +1 -0
  124. package/dist/{types-CjFDKMDe.d.ts → types-D94EanDL.d.ts} +2 -2
  125. package/dist/{types-CjFDKMDe.d.ts.map → types-D94EanDL.d.ts.map} +1 -1
  126. package/dist/{types-B2673PBj.d.ts → types-EBUav9Bu.d.ts} +8 -4
  127. package/dist/types-EBUav9Bu.d.ts.map +1 -0
  128. package/docs/campaign-proposers.md +2 -0
  129. package/docs/design/statistics-decisions.md +1 -1
  130. package/docs/search-ledger.md +143 -8
  131. package/package.json +6 -1
  132. package/dist/active-curriculum-OjIWgrUJ.js +0 -204
  133. package/dist/active-curriculum-OjIWgrUJ.js.map +0 -1
  134. package/dist/define-agent-eval-ulRlLvse.d.ts.map +0 -1
  135. package/dist/external-optimizer-subprocess-BrghTvEp.js.map +0 -1
  136. package/dist/index-DJKZEldg.d.ts.map +0 -1
  137. package/dist/llm-judge-fhkFQL1N.js.map +0 -1
  138. package/dist/paired-arms-JSDcs2zx.js.map +0 -1
  139. package/dist/paired-delta-test-BF-FEP8V.js +0 -78
  140. package/dist/paired-delta-test-BF-FEP8V.js.map +0 -1
  141. package/dist/paired-promotion-decision-DTKl6-G8.js.map +0 -1
  142. package/dist/power-and-mde-BJOpt6pq.js.map +0 -1
  143. package/dist/search-ledger-9W0g5FP6.js.map +0 -1
  144. package/dist/search-summary-CcavvYLe.js +0 -378
  145. package/dist/search-summary-CcavvYLe.js.map +0 -1
  146. package/dist/tool-groups-B4JVrH3g.d.ts.map +0 -1
  147. package/dist/types-B2673PBj.d.ts.map +0 -1
  148. package/dist/types-B3jzCp0p.js.map +0 -1
@@ -0,0 +1,4045 @@
1
+ import { a as hashCanonical, i as compareCodeUnits, r as canonicalString } from "./canonical-BsZVgBhW.js";
2
+ import { s as zQuantile } from "./internal-BMFSR8Ns.js";
3
+ import { t as mulberry32 } from "./random-Dn5fPWkt.js";
4
+ import { r as holm } from "./multiplicity-D9cjmStq.js";
5
+ import { a as pairedSignTest, n as DECISION_PAIRED_DELTA_STATISTIC } from "./paired-tests-C8iCsioC.js";
6
+ import { i as pairedDeltaTest, r as minimumPairsForPairedDeltaTest } from "./paired-promotion-decision-D5B1Cxc2.js";
7
+ import { C as searchPosterior, E as paretoFrontier, _ as rankingSplit, a as searchPolicyView, b as estimateNode, g as INSUFFICIENT_FROM, u as verifySearchClaim, v as summarizeSamples, w as seedFromDigest } from "./search-kernel-D8_kxChy.js";
8
+ import { f as searchUnitScores } from "./search-ledger-BnUytR8X.js";
9
+ import { defineInlineResource } from "@tangle-network/agent-interface";
10
+ //#region src/search/lenses/edit-credit.ts
11
+ /**
12
+ * `editCredit`: the edits of a search as genes, followed down its lineage.
13
+ *
14
+ * Every lineage edge is a parent-to-child change of an artifact. The lens
15
+ * diffs the two artifacts line by line (whitespace-normalized) and cuts each
16
+ * changed run into paragraphs: an added paragraph is an `insert` gene, a
17
+ * removed paragraph a `delete` gene. A gene's id hashes its kind, its location
18
+ * in the artifact and its normalized lines, so the same edit has the same id
19
+ * wherever and whenever a proposer makes it. A node carries an insert gene
20
+ * when the paragraph is present at that location, and a delete gene when it is
21
+ * absent. Carrying is read from each node's content, so it follows merges,
22
+ * re-proposals and reverts without any bookkeeping in the record.
23
+ *
24
+ * A step is one lineage parent and its child. A gene flips on a step when the
25
+ * parent and the child disagree on carrying it. The gene's credit pairs, per
26
+ * step, the side that carries it against the side that lacks it on the units
27
+ * both scored, and averages those contrasts across its steps inside each unit.
28
+ * Only clean steps count: steps on which nothing flips except edits introduced
29
+ * together with the gene. A merge that also brings in the other parent's edits
30
+ * is therefore not evidence for any one of them, and genes that always move
31
+ * together are reported as linked, because their credits are one number.
32
+ *
33
+ * Interactions compare a gene's step contrasts where another gene is present
34
+ * against where it is absent, in both directions, and are flagged after a Holm
35
+ * correction for a factorial test rather than claimed.
36
+ *
37
+ * The lens reads node artifacts through `readArtifact`, which the caller backs
38
+ * with verified blob bytes. An artifact the caller cannot read, and a code
39
+ * surface whose patch bytes are not in the ledger, leave the node's content
40
+ * unknown: its steps yield no genes and no contrasts.
41
+ */
42
+ const CONFIDENCE = .95;
43
+ const RESAMPLES = 2e3;
44
+ const DESCRIPTIVE_FROM = minimumPairsForPairedDeltaTest(CONFIDENCE);
45
+ const INTERACTION_ALPHA = .05;
46
+ const DEFAULT_INTERACTION_GENES = 24;
47
+ /** Signal name a policy reads. */
48
+ const EDIT_CREDIT_SIGNAL = "reusableHunks";
49
+ /** The estimator every gene credit names. */
50
+ const EDIT_CREDIT_ESTIMATOR = {
51
+ uri: "npm:@tangle-network/agent-eval/search#editCredit",
52
+ revision: hashCanonical({
53
+ name: "tangle.edit-credit.2026-09",
54
+ gene: "a paragraph of whitespace-normalized lines added (insert) or removed (delete) at one artifact location",
55
+ step: "one lineage parent and its child, both with known content and neither invalid",
56
+ clean: "the gene flips on the step and every gene that flips was introduced on one edge that introduced it",
57
+ unit: "mean of a node’s scored cells in the unit; per unit, carrier-side and lacking-side means averaged over the clean steps where both sides scored it",
58
+ statistic: DECISION_PAIRED_DELTA_STATISTIC,
59
+ interval: "percentile paired bootstrap",
60
+ confidence: CONFIDENCE,
61
+ resamples: RESAMPLES,
62
+ rng: "mulberry32",
63
+ seed: "the first 32 bits of cellSetDigest",
64
+ signTest: "exact, one-sided toward improvement in the objective direction",
65
+ methods: {
66
+ none: "below 2 units",
67
+ insufficient: `below ${DESCRIPTIVE_FROM} units`,
68
+ descriptive: `below 20 units`,
69
+ bootstrap: `from 20 units`
70
+ }
71
+ })
72
+ };
73
+ /**
74
+ * The edit-credit lens. Pure: the result depends only on `state` and what
75
+ * `readArtifact` returns for its references.
76
+ */
77
+ function editCredit(state, options) {
78
+ const header = state.header;
79
+ if (!header) throw new Error(`editCredit: search ${state.searchId} has not been opened`);
80
+ const split = options.split ?? rankingSplit(state);
81
+ const direction = header.objective.direction;
82
+ const sign = direction === "maximize" ? 1 : -1;
83
+ const interactionGenes = options.interactionGenes ?? DEFAULT_INTERACTION_GENES;
84
+ if (!Number.isSafeInteger(interactionGenes) || interactionGenes < 0) throw new Error(`editCredit: interactionGenes must be a non-negative integer`);
85
+ const nodes = state.nodes();
86
+ const byId = new Map(nodes.map((node) => [node.nodeId, node]));
87
+ const contents = /* @__PURE__ */ new Map();
88
+ for (const node of nodes) contents.set(node.nodeId, nodeContent(options, node.artifact));
89
+ const known = (nodeId) => contents.get(nodeId);
90
+ const invalid = new Set(nodes.filter((node) => node.status === "invalid").map((n) => n.nodeId));
91
+ const genes = /* @__PURE__ */ new Map();
92
+ const byLocus = /* @__PURE__ */ new Map();
93
+ const introducedOn = /* @__PURE__ */ new Map();
94
+ const unknownEdges = {};
95
+ const stepList = [];
96
+ const stepKeys = /* @__PURE__ */ new Set();
97
+ let lineageEdges = 0;
98
+ let readEdges = 0;
99
+ let edgeOrdinal = 0;
100
+ for (const edge of state.edges()) {
101
+ edgeOrdinal += 1;
102
+ const child = byId.get(edge.childNodeId);
103
+ const parents = lineageParents(state, edge, child, byId);
104
+ if (parents.length === 0) continue;
105
+ lineageEdges += 1;
106
+ const missing = [child.nodeId, ...parents].map((nodeId) => contents.get(nodeId)).find((content) => !content.known);
107
+ if (missing && !missing.known) {
108
+ unknownEdges[missing.reason] = (unknownEdges[missing.reason] ?? 0) + 1;
109
+ continue;
110
+ }
111
+ readEdges += 1;
112
+ const childContent = known(child.nodeId);
113
+ const reproposal = child.edgeIds[0] !== edge.edgeId;
114
+ const wholeLineage = edge.parents.every((parent) => parent.searchId === state.searchId && parents.includes(parent.nodeId));
115
+ const cuts = /* @__PURE__ */ new Map();
116
+ for (const parentId of parents) {
117
+ const key = `${parentId}>${child.nodeId}`;
118
+ if (!stepKeys.has(key)) {
119
+ stepKeys.add(key);
120
+ stepList.push({
121
+ key,
122
+ parentId,
123
+ childId: child.nodeId,
124
+ flips: /* @__PURE__ */ new Set()
125
+ });
126
+ }
127
+ for (const cut of edgeCuts(known(parentId), childContent)) {
128
+ const locus = locusOf(cut);
129
+ if (!cuts.has(locus)) cuts.set(locus, cut);
130
+ }
131
+ }
132
+ if (!wholeLineage) continue;
133
+ const introducedHere = [];
134
+ for (const [locus, cut] of cuts) {
135
+ const existing = byLocus.get(locus);
136
+ const gene = existing ?? cut;
137
+ const childCarries = carries(childContent, gene);
138
+ const parentCarries = parents.filter((parentId) => carries(known(parentId), gene));
139
+ if (existing && !childCarries && parentCarries.length > 0) {
140
+ existing.dropped += 1;
141
+ continue;
142
+ }
143
+ if (!childCarries || parentCarries.length > 0) continue;
144
+ let record = existing;
145
+ if (!record) {
146
+ const geneId = `gene_${hashCanonical({
147
+ kind: cut.kind,
148
+ path: cut.path,
149
+ lines: cut.lines
150
+ }).slice(7).slice(0, 24)}`;
151
+ record = {
152
+ ...cut,
153
+ geneId,
154
+ locus,
155
+ introduced: [],
156
+ dropped: 0,
157
+ order: [edgeOrdinal, cut.position]
158
+ };
159
+ genes.set(geneId, record);
160
+ byLocus.set(locus, record);
161
+ }
162
+ record.introduced.push({
163
+ nodeId: child.nodeId,
164
+ edgeId: edge.edgeId,
165
+ operator: edge.operator,
166
+ parents,
167
+ reproposal
168
+ });
169
+ introducedHere.push(record.geneId);
170
+ }
171
+ introducedOn.set(edge.edgeId, introducedHere);
172
+ }
173
+ const carriersOf = /* @__PURE__ */ new Map();
174
+ const carried = /* @__PURE__ */ new Map();
175
+ for (const record of genes.values()) {
176
+ const set = /* @__PURE__ */ new Set();
177
+ for (const node of nodes) {
178
+ const content = contents.get(node.nodeId);
179
+ if (content.known && carries(content, record)) {
180
+ set.add(node.nodeId);
181
+ appendTo(carried, node.nodeId, record.geneId);
182
+ }
183
+ }
184
+ carriersOf.set(record.geneId, set);
185
+ }
186
+ for (const step of stepList) for (const [geneId, set] of carriersOf) if (set.has(step.parentId) !== set.has(step.childId)) step.flips.add(geneId);
187
+ const measurable = stepList.filter((step) => !invalid.has(step.parentId) && !invalid.has(step.childId));
188
+ const editsOf = /* @__PURE__ */ new Map();
189
+ for (const record of genes.values()) editsOf.set(record.geneId, record.introduced.map((introduction) => new Set(introducedOn.get(introduction.edgeId))));
190
+ const unitMeansOf = memo((nodeId) => {
191
+ const means = /* @__PURE__ */ new Map();
192
+ for (const unit of state.unitScores(nodeId, split)) means.set(unit.unitId, unit.mean);
193
+ return means;
194
+ });
195
+ const cellsDigestOf = memo((nodeId) => state.scoredCells(nodeId, split).map((cell) => [
196
+ cell.cellId,
197
+ cell.unitId,
198
+ cell.attempt,
199
+ cell.score
200
+ ]).sort((left, right) => compareCodeUnits(left[0], right[0])));
201
+ const cleanOf = /* @__PURE__ */ new Map();
202
+ const flipCount = /* @__PURE__ */ new Map();
203
+ for (const step of measurable) for (const geneId of step.flips) {
204
+ flipCount.set(geneId, (flipCount.get(geneId) ?? 0) + 1);
205
+ if (!editsOf.get(geneId).some((edit) => [...step.flips].every((other) => edit.has(other)))) continue;
206
+ const childCarries = carriersOf.get(geneId).has(step.childId);
207
+ appendTo(cleanOf, geneId, {
208
+ step,
209
+ carrierId: childCarries ? step.childId : step.parentId,
210
+ lackingId: childCarries ? step.parentId : step.childId
211
+ });
212
+ }
213
+ const credits = /* @__PURE__ */ new Map();
214
+ for (const record of genes.values()) {
215
+ const steps = cleanOf.get(record.geneId) ?? [];
216
+ const perUnit = /* @__PURE__ */ new Map();
217
+ for (const { carrierId, lackingId } of steps) {
218
+ const lacking = unitMeansOf(lackingId);
219
+ for (const [unitId, carrierMean] of unitMeansOf(carrierId)) {
220
+ const lackingMean = lacking.get(unitId);
221
+ if (lackingMean === void 0) continue;
222
+ const entry = perUnit.get(unitId) ?? {
223
+ carrier: 0,
224
+ lacking: 0,
225
+ count: 0
226
+ };
227
+ entry.carrier += carrierMean;
228
+ entry.lacking += lackingMean;
229
+ entry.count += 1;
230
+ perUnit.set(unitId, entry);
231
+ }
232
+ }
233
+ const unitIds = [...perUnit.keys()].sort(compareCodeUnits);
234
+ const cellSetDigest = hashCanonical({
235
+ split,
236
+ steps: steps.map(({ carrierId, lackingId }) => [
237
+ carrierId,
238
+ lackingId,
239
+ cellsDigestOf(carrierId),
240
+ cellsDigestOf(lackingId)
241
+ ]).sort((left, right) => compareCodeUnits(canonicalString(left), canonicalString(right)))
242
+ });
243
+ const carrierUnits = /* @__PURE__ */ new Set();
244
+ for (const { carrierId } of steps) for (const unitId of unitMeansOf(carrierId).keys()) carrierUnits.add(unitId);
245
+ credits.set(record.geneId, pairedEstimate({
246
+ against: `${record.geneId}:lacking`,
247
+ split,
248
+ direction,
249
+ units: carrierUnits.size,
250
+ lacking: unitIds.map((unitId) => perUnit.get(unitId).lacking / perUnit.get(unitId).count),
251
+ carrier: unitIds.map((unitId) => perUnit.get(unitId).carrier / perUnit.get(unitId).count),
252
+ cellSetDigest
253
+ }));
254
+ }
255
+ const linkageKey = (geneId) => canonicalString((cleanOf.get(geneId) ?? []).map(({ step, carrierId }) => `${step.key}:${carrierId === step.childId ? "+" : "-"}`).sort(compareCodeUnits));
256
+ const linkGroups = /* @__PURE__ */ new Map();
257
+ for (const record of [...genes.values()].sort((a, b) => compareOrder(a.order, b.order))) {
258
+ if (!cleanOf.has(record.geneId)) continue;
259
+ appendTo(linkGroups, linkageKey(record.geneId), record.geneId);
260
+ }
261
+ const geneList = [...genes.values()].map((record) => {
262
+ const clean = cleanOf.get(record.geneId) ?? [];
263
+ const group = clean.length > 0 ? linkGroups.get(linkageKey(record.geneId)) : [];
264
+ const carriers = carriersOf.get(record.geneId);
265
+ const { credit, verdict } = credits.get(record.geneId);
266
+ return {
267
+ geneId: record.geneId,
268
+ kind: record.kind,
269
+ path: record.path,
270
+ lines: record.lines,
271
+ text: record.text,
272
+ introduced: record.introduced,
273
+ dropped: record.dropped,
274
+ edit: [...editsOf.get(record.geneId)[0]].sort((a, b) => compareOrder(genes.get(a).order, genes.get(b).order)),
275
+ carriers: [...carriers].filter((nodeId) => !invalid.has(nodeId)).length,
276
+ invalidCarriers: [...carriers].filter((nodeId) => invalid.has(nodeId)).length,
277
+ steps: {
278
+ flips: flipCount.get(record.geneId) ?? 0,
279
+ clean: clean.length,
280
+ gained: clean.filter(({ step, carrierId }) => carrierId === step.childId).length,
281
+ lost: clean.filter(({ step, carrierId }) => carrierId === step.parentId).length
282
+ },
283
+ credit,
284
+ verdict,
285
+ linkage: group.length > 1 ? group[0] : null
286
+ };
287
+ });
288
+ const verdictRank = {
289
+ reusable: 0,
290
+ harmful: 1,
291
+ unresolved: 2,
292
+ insufficient: 3
293
+ };
294
+ geneList.sort((a, b) => verdictRank[a.verdict] - verdictRank[b.verdict] || improvementLow(b.credit, sign) - improvementLow(a.credit, sign) || gain(b.credit, sign) - gain(a.credit, sign) || compareOrder(genes.get(a.geneId).order, genes.get(b.geneId).order));
295
+ const counts = emptyCounts();
296
+ const editCounts = emptyCounts();
297
+ for (const gene of geneList) {
298
+ counts[gene.verdict] += 1;
299
+ if ((gene.linkage ?? (gene.steps.clean === 0 ? gene.edit[0] : gene.geneId)) === gene.geneId) editCounts[gene.verdict] += 1;
300
+ }
301
+ const interactions = interactionPairs({
302
+ genes: geneList,
303
+ cleanOf,
304
+ carriersOf,
305
+ unitMeansOf,
306
+ limit: interactionGenes,
307
+ sign
308
+ });
309
+ const skillCandidates = skills(geneList, genes, header.subject, split);
310
+ const lineage = lineageRows(state, nodes, byId, contents, carried);
311
+ const isMeasured = (gene) => gene.credit.method === "descriptive" || gene.credit.method === "bootstrap";
312
+ const measured = geneList.filter(isMeasured).length;
313
+ const measuredLeads = geneList.filter((gene) => isMeasured(gene) && (gene.linkage ?? gene.geneId) === gene.geneId);
314
+ const expectedReusable = plain(Math.round(measuredLeads.reduce((sum, gene) => sum + chanceReusable(gene.credit.pairs), 0) * 1e6) / 1e6);
315
+ const top = geneList.filter((gene) => gene.verdict === "reusable" && (gene.linkage ?? gene.geneId) === gene.geneId).map((gene) => gene.geneId);
316
+ return {
317
+ signal: measured === 0 ? {
318
+ name: EDIT_CREDIT_SIGNAL,
319
+ value: null,
320
+ basis: `insufficient: no gene has ${DESCRIPTIVE_FROM} ${split} units on clean steps (${geneList.length} genes, ${measurable.length} measurable steps)`,
321
+ top: []
322
+ } : {
323
+ name: EDIT_CREDIT_SIGNAL,
324
+ value: editCounts.reusable,
325
+ basis: `independent edits pairedDeltaTest finds improve the objective on the ${split} split (${counts.reusable} genes), of ${measuredLeads.length} edits (${measured} genes) measured on ${DESCRIPTIVE_FROM} or more units; about ${expectedReusable.toFixed(2)} would be called reusable by chance with no true effect (unadjusted)`,
326
+ top
327
+ },
328
+ data: {
329
+ split,
330
+ direction,
331
+ edges: {
332
+ lineage: lineageEdges,
333
+ read: readEdges,
334
+ unknown: unknownEdges
335
+ },
336
+ steps: {
337
+ total: stepList.length,
338
+ invalidEnd: stepList.length - measurable.length
339
+ },
340
+ nodes: {
341
+ total: nodes.length,
342
+ contentUnknown: [...contents.values()].filter((content) => !content.known).length,
343
+ invalid: invalid.size
344
+ },
345
+ genes: geneList,
346
+ counts,
347
+ editCounts,
348
+ interactions,
349
+ skillCandidates,
350
+ chance: {
351
+ measuredEdits: measuredLeads.length,
352
+ expectedReusable,
353
+ method: `per measured edit, the chance with no true effect that its verdict is reusable: the exact one-sided sign test's attained size at its unit count below 20 units (at most ${(1 - CONFIDENCE) / 2}; the point-estimate condition only lowers it), the nominal ${(1 - CONFIDENCE) / 2} of the percentile bootstrap from 20; summed, so it is the expected number of false reusable edits if every edit had no effect`
354
+ },
355
+ lineage,
356
+ method: {
357
+ estimator: EDIT_CREDIT_ESTIMATOR,
358
+ credit: `per clean step (a parent and its child on which every change belongs to one edit that introduced the gene), the carrying side against the lacking side on each unit both scored; per unit, both sides averaged over those steps; ${DECISION_PAIRED_DELTA_STATISTIC} paired delta with a ${Math.round(CONFIDENCE * 100)}% percentile bootstrap (${RESAMPLES} resamples, seeded from cellSetDigest): none below 2 units, insufficient below ${DESCRIPTIVE_FROM}, a descriptive interval and exact one-sided sign p below 20, a decision-grade interval from 20; unscored cells are absent, never zero`,
359
+ verdict: `pairedDeltaTest's own decision in each direction: reusable when it finds an improvement (below 20 units an exact one-sided sign test at ${(1 - CONFIDENCE) / 2} with the point estimate on the better side; from 20 the bootstrap interval above zero), harmful when it finds one in the other direction, unresolved otherwise, insufficient below ${DESCRIPTIVE_FROM} units`,
360
+ interaction: `per unit, a gene's clean-step contrast where both ends carry the other gene minus where neither does, averaged over both directions; ${Math.round(CONFIDENCE * 100)}% percentile bootstrap (${RESAMPLES} resamples) for spread; exact two-sided sign test, Holm-adjusted over the pairs tested, flagged at ${INTERACTION_ALPHA}`,
361
+ multiplicity: "credit is not adjusted for the number of genes: it steers which edits to reuse and test next, as selection estimates steer spend, and claims nothing; interaction flags are Holm-adjusted because pairs grow with the square of the genes",
362
+ selectionBias: "a policy chooses a parent for its scores, so a parent is high by chance on the units it was chosen on: on a step where the child gained a gene the lacking side is that parent, and credit is biased toward harmful; where the child lost it, toward reusable. Only the sealed test split is free of this, so a reusable edit still needs a held-out test before it ships"
363
+ }
364
+ }
365
+ };
366
+ }
367
+ function nodeContent(options, ref) {
368
+ const body = options.readArtifact(ref);
369
+ if (body === void 0) return {
370
+ known: false,
371
+ reason: `artifact ${ref.role} unreadable`
372
+ };
373
+ const paths = /* @__PURE__ */ new Map();
374
+ const record = body;
375
+ if (record && typeof record === "object" && record.kind === "mutable-surface") {
376
+ const surface = record.surface;
377
+ if (typeof surface === "string") addText(paths, "prompt", surface);
378
+ else if (surface?.kind === "components") for (const name of Object.keys(surface.components).sort(compareCodeUnits)) addText(paths, `component:${name}`, surface.components[name]);
379
+ else return {
380
+ known: false,
381
+ reason: "code surface: its patch bytes are not in the ledger"
382
+ };
383
+ } else flatten(body, "", paths);
384
+ const haystacks = /* @__PURE__ */ new Map();
385
+ for (const [path, text] of paths) haystacks.set(path, `\n${text.norm.filter((line) => line.length > 0).join("\n")}\n`);
386
+ return {
387
+ known: true,
388
+ paths,
389
+ haystacks
390
+ };
391
+ }
392
+ /** JSON as text per location: a string's lines at its path, an array's
393
+ * elements as lines at the array's path, any other value as one canonical
394
+ * line. Array elements share a path, so an insertion does not renumber the
395
+ * rest. */
396
+ function flatten(value, path, paths) {
397
+ const at = path === "" ? "/" : path;
398
+ if (typeof value === "string") addText(paths, at, value);
399
+ else if (Array.isArray(value)) for (const element of value) addText(paths, at, typeof element === "string" ? element : canonicalString(element));
400
+ else if (value !== null && typeof value === "object") for (const key of Object.keys(value).sort(compareCodeUnits)) {
401
+ const escaped = key.replaceAll("~", "~0").replaceAll("/", "~1");
402
+ flatten(value[key], `${path}/${escaped}`, paths);
403
+ }
404
+ else addText(paths, at, canonicalString(value));
405
+ }
406
+ function addText(paths, path, text) {
407
+ const entry = paths.get(path) ?? {
408
+ raw: [],
409
+ norm: []
410
+ };
411
+ for (const line of text.split(/\r?\n/)) {
412
+ entry.raw.push(line);
413
+ entry.norm.push(line.replace(/\s+/g, " ").trim());
414
+ }
415
+ paths.set(path, entry);
416
+ }
417
+ function carries(content, gene) {
418
+ const present = content.haystacks.get(gene.path)?.includes(gene.needle) ?? false;
419
+ return gene.kind === "insert" ? present : !present;
420
+ }
421
+ function locusOf(cut) {
422
+ return canonicalString([cut.path, cut.lines]);
423
+ }
424
+ function edgeCuts(parent, child) {
425
+ const cuts = [];
426
+ const paths = /* @__PURE__ */ new Set([...parent.paths.keys(), ...child.paths.keys()]);
427
+ let position = 0;
428
+ for (const path of [...paths].sort(compareCodeUnits)) {
429
+ const before = parent.paths.get(path) ?? {
430
+ raw: [],
431
+ norm: []
432
+ };
433
+ const after = child.paths.get(path) ?? {
434
+ raw: [],
435
+ norm: []
436
+ };
437
+ for (const run of changeRuns(before.norm, after.norm)) for (const [kind, side, start, end] of [[
438
+ "delete",
439
+ before,
440
+ run.aStart,
441
+ run.aEnd
442
+ ], [
443
+ "insert",
444
+ after,
445
+ run.bStart,
446
+ run.bEnd
447
+ ]]) for (const [from, to] of paragraphs(side.norm, start, end)) {
448
+ const lines = side.norm.slice(from, to).filter((line) => line.length > 0);
449
+ if (lines.length === 0) continue;
450
+ cuts.push({
451
+ kind,
452
+ path,
453
+ lines,
454
+ needle: `\n${lines.join("\n")}\n`,
455
+ text: side.raw.slice(from, to).join("\n").trim(),
456
+ position: position++
457
+ });
458
+ }
459
+ }
460
+ return cuts;
461
+ }
462
+ /** Paragraph ranges of `lines[start, end)`, split at blank lines. */
463
+ function paragraphs(lines, start, end) {
464
+ const ranges = [];
465
+ let from = -1;
466
+ for (let index = start; index < end; index++) if (lines[index].length === 0) {
467
+ if (from >= 0) ranges.push([from, index]);
468
+ from = -1;
469
+ } else if (from < 0) from = index;
470
+ if (from >= 0) ranges.push([from, end]);
471
+ return ranges;
472
+ }
473
+ /** Maximal runs where `a[aStart, aEnd)` became `b[bStart, bEnd)`, from a
474
+ * shortest edit script (Myers, 1986). */
475
+ function changeRuns(a, b) {
476
+ let prefix = 0;
477
+ while (prefix < a.length && prefix < b.length && a[prefix] === b[prefix]) prefix += 1;
478
+ let suffix = 0;
479
+ while (suffix < a.length - prefix && suffix < b.length - prefix && a[a.length - 1 - suffix] === b[b.length - 1 - suffix]) suffix += 1;
480
+ const innerA = a.slice(prefix, a.length - suffix);
481
+ const innerB = b.slice(prefix, b.length - suffix);
482
+ const runs = [];
483
+ let i = 0;
484
+ let j = 0;
485
+ const flush = (toI, toJ) => {
486
+ if (toI > i || toJ > j) runs.push({
487
+ aStart: prefix + i,
488
+ aEnd: prefix + toI,
489
+ bStart: prefix + j,
490
+ bEnd: prefix + toJ
491
+ });
492
+ };
493
+ for (const [mi, mj] of matches(innerA, innerB)) {
494
+ flush(mi, mj);
495
+ i = mi + 1;
496
+ j = mj + 1;
497
+ }
498
+ flush(innerA.length, innerB.length);
499
+ return runs;
500
+ }
501
+ /** Matched index pairs of a shortest edit script, in increasing order. */
502
+ function matches(a, b) {
503
+ const n = a.length;
504
+ const m = b.length;
505
+ if (n === 0 || m === 0) return [];
506
+ const offset = n + m;
507
+ const v = new Int32Array(2 * offset + 2);
508
+ const trace = [];
509
+ let done = false;
510
+ for (let d = 0; d <= n + m && !done; d++) {
511
+ trace.push(v.slice());
512
+ for (let k = -d; k <= d; k += 2) {
513
+ let x = k === -d || k !== d && v[k - 1 + offset] < v[k + 1 + offset] ? v[k + 1 + offset] : v[k - 1 + offset] + 1;
514
+ let y = x - k;
515
+ while (x < n && y < m && a[x] === b[y]) {
516
+ x += 1;
517
+ y += 1;
518
+ }
519
+ v[k + offset] = x;
520
+ if (x >= n && y >= m) {
521
+ done = true;
522
+ break;
523
+ }
524
+ }
525
+ }
526
+ const pairs = [];
527
+ let x = n;
528
+ let y = m;
529
+ for (let d = trace.length - 1; d >= 0; d--) {
530
+ const before = trace[d];
531
+ const k = x - y;
532
+ const prevK = k === -d || k !== d && before[k - 1 + offset] < before[k + 1 + offset] ? k + 1 : k - 1;
533
+ const prevX = before[prevK + offset];
534
+ const prevY = prevX - prevK;
535
+ while (x > prevX && y > prevY) {
536
+ x -= 1;
537
+ y -= 1;
538
+ pairs.push([x, y]);
539
+ }
540
+ if (d > 0) {
541
+ x = prevX;
542
+ y = prevY;
543
+ }
544
+ }
545
+ return pairs.reverse();
546
+ }
547
+ /** Parents of an edge that precede its child in this search: the edge's
548
+ * lineage. A re-proposal from the child itself or a later node adds none. */
549
+ function lineageParents(state, edge, child, byId) {
550
+ const parents = [];
551
+ for (const parent of edge.parents) {
552
+ if (parent.searchId !== state.searchId) continue;
553
+ const node = byId.get(parent.nodeId);
554
+ if (node && node.ordinal < child.ordinal && !parents.includes(node.nodeId)) parents.push(node.nodeId);
555
+ }
556
+ return parents;
557
+ }
558
+ function lineageRows(state, nodes, byId, contents, carried) {
559
+ const rows = [];
560
+ const placed = /* @__PURE__ */ new Set();
561
+ const visit = (root) => {
562
+ const stack = [root];
563
+ while (stack.length > 0) {
564
+ const node = stack.pop();
565
+ if (placed.has(node.nodeId)) continue;
566
+ placed.add(node.nodeId);
567
+ const firstEdge = node.edgeIds[0] ? state.edge(node.edgeIds[0]) : void 0;
568
+ rows.push({
569
+ nodeId: node.nodeId,
570
+ ordinal: node.ordinal,
571
+ depth: node.depth,
572
+ primaryParentId: node.primaryParentId,
573
+ parents: node.parents.filter((parent) => parent.searchId === state.searchId).map((parent) => parent.nodeId),
574
+ operator: firstEdge?.operator ?? null,
575
+ status: node.status,
576
+ carries: [...carried.get(node.nodeId) ?? []].sort(compareCodeUnits),
577
+ contentKnown: contents.get(node.nodeId).known
578
+ });
579
+ const children = node.children.map((id) => byId.get(id)).filter((child) => child.primaryParentId === node.nodeId).sort((a, b) => b.ordinal - a.ordinal);
580
+ stack.push(...children);
581
+ }
582
+ };
583
+ for (const node of nodes) if (node.primaryParentId === null) visit(node);
584
+ for (const node of nodes) visit(node);
585
+ return rows;
586
+ }
587
+ /**
588
+ * The paired contrast of per-unit carrier means against per-unit lacking
589
+ * means, staged and oriented exactly as `estimateNodeFromCells` stages and
590
+ * orients a node against another, with the verdict `pairedDeltaTest` itself
591
+ * would reach in each direction: below 20 units the exact one-sided sign test
592
+ * at α = 0.025 and a point estimate on the same side, from 20 the bootstrap
593
+ * interval clear of zero.
594
+ */
595
+ function pairedEstimate(input) {
596
+ const pairs = input.carrier.length;
597
+ const method = methodFor(pairs);
598
+ const base = {
599
+ against: input.against,
600
+ split: input.split,
601
+ units: input.units,
602
+ pairs,
603
+ method,
604
+ cellSetDigest: input.cellSetDigest,
605
+ estimator: EDIT_CREDIT_ESTIMATOR
606
+ };
607
+ if (method === "none") return {
608
+ credit: {
609
+ ...base,
610
+ delta: null,
611
+ interval: null,
612
+ exactSignP: null,
613
+ indeterminate: false
614
+ },
615
+ verdict: "insufficient"
616
+ };
617
+ const maximize = input.direction === "maximize";
618
+ const before = maximize ? input.lacking : input.carrier;
619
+ const after = maximize ? input.carrier : input.lacking;
620
+ const options = {
621
+ statistic: DECISION_PAIRED_DELTA_STATISTIC,
622
+ confidence: CONFIDENCE,
623
+ resamples: RESAMPLES,
624
+ seed: seedFromDigest(input.cellSetDigest)
625
+ };
626
+ const better = pairedDeltaTest(before, after, options);
627
+ const worse = pairedDeltaTest(after, before, options);
628
+ const { mean, low, high } = better.bootstrap;
629
+ const indeterminate = better.indeterminate;
630
+ const spread = (method === "descriptive" || method === "bootstrap") && !indeterminate;
631
+ return {
632
+ credit: {
633
+ ...base,
634
+ delta: plain(maximize ? mean : -mean),
635
+ interval: spread ? maximize ? [plain(low), plain(high)] : [plain(-high), plain(-low)] : null,
636
+ exactSignP: method === "descriptive" && !indeterminate ? better.pValue : null,
637
+ indeterminate
638
+ },
639
+ verdict: method === "insufficient" ? "insufficient" : better.significant ? "reusable" : worse.significant ? "harmful" : "unresolved"
640
+ };
641
+ }
642
+ /** The chance, with no true effect, that the verdict on `pairs` units is
643
+ * reusable: the exact sign test's attained size (the upper tail at the
644
+ * smallest positive count that clears α) below the bootstrap gate, the
645
+ * percentile bootstrap's nominal one-sided α from it. */
646
+ const chanceReusable = (() => {
647
+ const alpha = (1 - CONFIDENCE) / 2;
648
+ const cache = /* @__PURE__ */ new Map();
649
+ return (pairs) => {
650
+ if (pairs < DESCRIPTIVE_FROM) return 0;
651
+ if (pairs >= 20) return alpha;
652
+ const cached = cache.get(pairs);
653
+ if (cached !== void 0) return cached;
654
+ let size = 0;
655
+ for (let positive = 0; positive <= pairs; positive++) {
656
+ const p = pairedSignTest(Array.from({ length: pairs }, (_, index) => index < positive ? 1 : -1), "greater").pValue;
657
+ if (p <= alpha) {
658
+ size = p;
659
+ break;
660
+ }
661
+ }
662
+ cache.set(pairs, size);
663
+ return size;
664
+ };
665
+ })();
666
+ function methodFor(units) {
667
+ if (units < 2) return "none";
668
+ if (units < DESCRIPTIVE_FROM) return "insufficient";
669
+ if (units < 20) return "descriptive";
670
+ return "bootstrap";
671
+ }
672
+ /** The worse end of the interval, as an improvement; -Infinity without one. */
673
+ function improvementLow(credit, sign) {
674
+ if (credit.interval === null) return Number.NEGATIVE_INFINITY;
675
+ return sign > 0 ? credit.interval[0] : -credit.interval[1];
676
+ }
677
+ function gain(credit, sign) {
678
+ return credit.delta === null ? Number.NEGATIVE_INFINITY : sign * credit.delta;
679
+ }
680
+ function interactionPairs(input) {
681
+ const seenGroups = /* @__PURE__ */ new Set();
682
+ const considered = [...input.genes].filter((gene) => {
683
+ const group = gene.linkage ?? gene.geneId;
684
+ if (seenGroups.has(group)) return false;
685
+ seenGroups.add(group);
686
+ return gene.credit.pairs >= DESCRIPTIVE_FROM;
687
+ }).sort((a, b) => b.credit.pairs - a.credit.pairs || compareCodeUnits(a.geneId, b.geneId)).slice(0, input.limit);
688
+ /** Per unit, the mean clean-step contrast of `geneId` over `steps`. */
689
+ const contrasts = (steps) => {
690
+ const sums = /* @__PURE__ */ new Map();
691
+ for (const { carrierId, lackingId } of steps) {
692
+ const lacking = input.unitMeansOf(lackingId);
693
+ for (const [unitId, carrierMean] of input.unitMeansOf(carrierId)) {
694
+ const lackingMean = lacking.get(unitId);
695
+ if (lackingMean === void 0) continue;
696
+ const entry = sums.get(unitId) ?? {
697
+ sum: 0,
698
+ count: 0
699
+ };
700
+ entry.sum += carrierMean - lackingMean;
701
+ entry.count += 1;
702
+ sums.set(unitId, entry);
703
+ }
704
+ }
705
+ return new Map([...sums].map(([unitId, entry]) => [unitId, entry.sum / entry.count]));
706
+ };
707
+ /** `geneId`'s clean steps split by whether both ends carry `other`. */
708
+ const split = (geneId, other) => {
709
+ const carriers = input.carriersOf.get(other);
710
+ const steps = input.cleanOf.get(geneId) ?? [];
711
+ return {
712
+ withOther: steps.filter(({ step }) => carriers.has(step.parentId) && carriers.has(step.childId)),
713
+ withoutOther: steps.filter(({ step }) => !carriers.has(step.parentId) && !carriers.has(step.childId))
714
+ };
715
+ };
716
+ const pairs = [];
717
+ for (let i = 0; i < considered.length; i++) for (let j = i + 1; j < considered.length; j++) {
718
+ const first = considered[i];
719
+ const second = considered[j];
720
+ if (first.edit.includes(second.geneId) || second.edit.includes(first.geneId)) continue;
721
+ const firstSplit = split(first.geneId, second.geneId);
722
+ const secondSplit = split(second.geneId, first.geneId);
723
+ const steps = {
724
+ firstWithSecond: firstSplit.withOther.length,
725
+ firstWithoutSecond: firstSplit.withoutOther.length,
726
+ secondWithFirst: secondSplit.withOther.length,
727
+ secondWithoutFirst: secondSplit.withoutOther.length
728
+ };
729
+ const directions = [firstSplit, secondSplit].filter((direction) => direction.withOther.length > 0 && direction.withoutOther.length > 0);
730
+ if (directions.length === 0) continue;
731
+ const perUnit = /* @__PURE__ */ new Map();
732
+ for (const direction of directions) {
733
+ const withMeans = contrasts(direction.withOther);
734
+ const withoutMeans = contrasts(direction.withoutOther);
735
+ for (const [unitId, withMean] of withMeans) {
736
+ const withoutMean = withoutMeans.get(unitId);
737
+ if (withoutMean === void 0) continue;
738
+ const entry = perUnit.get(unitId) ?? {
739
+ sum: 0,
740
+ count: 0
741
+ };
742
+ entry.sum += withMean - withoutMean;
743
+ entry.count += 1;
744
+ perUnit.set(unitId, entry);
745
+ }
746
+ }
747
+ const values = [...perUnit.keys()].sort(compareCodeUnits).map((unitId) => perUnit.get(unitId).sum / perUnit.get(unitId).count);
748
+ pairs.push(interactionEstimate([first.geneId, second.geneId], steps, values, input.sign));
749
+ }
750
+ const tested = pairs.filter((pair) => pair.signP !== null);
751
+ const adjusted = holm(tested.map((pair) => pair.signP), INTERACTION_ALPHA);
752
+ tested.forEach((pair, index) => {
753
+ pair.adjustedP = adjusted.adjusted[index];
754
+ pair.interacting = adjusted.significant[index];
755
+ });
756
+ pairs.sort((a, b) => Number(b.interacting) - Number(a.interacting) || (a.adjustedP ?? 2) - (b.adjustedP ?? 2) || b.units - a.units || compareCodeUnits(a.genes.join(), b.genes.join()));
757
+ return {
758
+ genesConsidered: considered.length,
759
+ pairsWithContexts: pairs.length,
760
+ pairsTested: tested.length,
761
+ alpha: INTERACTION_ALPHA,
762
+ correction: "holm",
763
+ pairs
764
+ };
765
+ }
766
+ function interactionEstimate(genes, steps, values, sign) {
767
+ const units = values.length;
768
+ const method = methodFor(units);
769
+ const base = {
770
+ genes,
771
+ steps,
772
+ units,
773
+ method,
774
+ adjustedP: null,
775
+ interacting: false
776
+ };
777
+ if (method === "none") return {
778
+ ...base,
779
+ interaction: null,
780
+ interval: null,
781
+ indeterminate: false,
782
+ signP: null,
783
+ synergy: null
784
+ };
785
+ const interaction = plain(values.reduce((sum, value) => sum + value, 0) / units);
786
+ if (method === "insufficient") return {
787
+ ...base,
788
+ interaction,
789
+ interval: null,
790
+ indeterminate: false,
791
+ signP: null,
792
+ synergy: sign * interaction > 0
793
+ };
794
+ const test = pairedDeltaTest(values.map(() => 0), values, {
795
+ statistic: DECISION_PAIRED_DELTA_STATISTIC,
796
+ confidence: CONFIDENCE,
797
+ resamples: RESAMPLES,
798
+ seed: seedFromDigest(hashCanonical({
799
+ genes,
800
+ values
801
+ }))
802
+ });
803
+ const indeterminate = test.indeterminate;
804
+ const greater = pairedSignTest(values, "greater").pValue;
805
+ const less = pairedSignTest(values, "less").pValue;
806
+ return {
807
+ ...base,
808
+ interaction,
809
+ interval: indeterminate ? null : [plain(test.bootstrap.low), plain(test.bootstrap.high)],
810
+ indeterminate,
811
+ signP: indeterminate ? null : Math.min(1, 2 * Math.min(greater, less)),
812
+ synergy: sign * interaction > 0
813
+ };
814
+ }
815
+ function skills(genes, records, subject, split) {
816
+ const byGroup = /* @__PURE__ */ new Map();
817
+ for (const gene of genes) {
818
+ if (gene.verdict !== "reusable" || gene.kind !== "insert") continue;
819
+ appendTo(byGroup, gene.linkage ?? gene.geneId, gene);
820
+ }
821
+ const candidates = [];
822
+ for (const members of byGroup.values()) {
823
+ members.sort((a, b) => compareOrder(records.get(a.geneId).order, records.get(b.geneId).order));
824
+ const lead = members[0];
825
+ const name = `edit-${lead.geneId.slice(5, 17)}`;
826
+ const content = [
827
+ "---",
828
+ `name: ${name}`,
829
+ `description: ${JSON.stringify(`Instructions a ${subject} search kept: the edit to ${lead.path} whose carriers scored better than the nodes without it on the ${split} split.`)}`,
830
+ "---",
831
+ "",
832
+ members.map((gene) => gene.text).join("\n\n"),
833
+ ""
834
+ ].join("\n");
835
+ candidates.push({
836
+ resource: defineInlineResource(name, content),
837
+ improveOptions: {
838
+ surface: "skills",
839
+ skills: { resourceName: name }
840
+ },
841
+ genes: members.map((gene) => gene.geneId),
842
+ path: lead.path,
843
+ credit: lead.credit
844
+ });
845
+ }
846
+ return candidates;
847
+ }
848
+ /** The lens as compact text: what `agent-eval search show --edit-credit`
849
+ * prints and a proposer can read. */
850
+ function editCreditText(result, options = {}) {
851
+ const limit = options.limit ?? 10;
852
+ const { data, signal } = result;
853
+ const unknown = Object.entries(data.edges.unknown).map(([reason, count]) => `${count} ${reason}`).join(", ");
854
+ const lines = [
855
+ `edit credit — ${data.genes.length} genes from ${data.edges.read} of ${data.edges.lineage} lineage edges, ${data.steps.total - data.steps.invalidEnd} measurable steps (${data.split} split, ${data.direction})`,
856
+ ` genes: reusable ${data.counts.reusable} · harmful ${data.counts.harmful} · unresolved ${data.counts.unresolved} · insufficient ${data.counts.insufficient}`,
857
+ ` edits: reusable ${data.editCounts.reusable} · harmful ${data.editCounts.harmful} · unresolved ${data.editCounts.unresolved} · insufficient ${data.editCounts.insufficient}`,
858
+ ` signal ${signal.name} = ${signal.value ?? "null"} (${signal.basis})${signal.top.length > 0 ? `; top: ${signal.top.slice(0, 5).join(", ")}` : ""}`
859
+ ];
860
+ if (unknown.length > 0) lines.push(` edges without content: ${unknown}`);
861
+ if (data.nodes.invalid > 0) lines.push(` ${data.nodes.invalid} invalid node(s): ${data.steps.invalidEnd} step(s) excluded from every contrast`);
862
+ lines.push("", "Genes (carrying vs lacking side of each clean lineage step):");
863
+ if (data.genes.length === 0) lines.push(" none");
864
+ for (const gene of data.genes.slice(0, limit)) {
865
+ const reproposed = gene.introduced.length > 1 ? ` · introduced ${gene.introduced.length}×` : "";
866
+ const dropped = gene.dropped > 0 ? ` · dropped ${gene.dropped}×` : "";
867
+ const linked = gene.linkage && gene.linkage !== gene.geneId ? ` · linked to ${gene.linkage}` : "";
868
+ const invalid = gene.invalidCarriers > 0 ? ` · ${gene.invalidCarriers} invalid carrier(s)` : "";
869
+ lines.push(` ${gene.geneId} ${gene.verdict} ${gene.kind} ${gene.path} "${excerpt(gene.lines)}"`, ` ${gene.steps.clean} clean of ${gene.steps.flips} step(s) (+${gene.steps.gained}/−${gene.steps.lost}) · ${formatEstimate(gene.credit)}${reproposed}${dropped}${linked}${invalid}`);
870
+ }
871
+ if (data.genes.length > limit) lines.push(` … ${data.genes.length - limit} more`);
872
+ const { interactions } = data;
873
+ lines.push("", `Interactions: ${interactions.pairsTested} pair(s) tested of ${interactions.pairsWithContexts} with both contexts, among ${interactions.genesConsidered} genes (Holm, α=${interactions.alpha}):`);
874
+ const shown = interactions.pairs.filter((pair) => pair.signP !== null).slice(0, limit);
875
+ if (shown.length === 0) lines.push(" none testable");
876
+ for (const pair of shown) {
877
+ const interval = pair.interval ? ` [${pair.interval[0].toFixed(4)}, ${pair.interval[1].toFixed(4)}]` : "";
878
+ lines.push(` ${pair.genes[0]} × ${pair.genes[1]}: ${pair.interacting ? "INTERACTING" : "no flag"} ${signed$1(pair.interaction)}${interval} on ${pair.units} units, sign p=${pair.signP.toPrecision(3)}, Holm p=${pair.adjustedP.toPrecision(3)}`);
879
+ }
880
+ lines.push("", `Skill candidates (agent-runtime improve surface 'skills'):`);
881
+ if (data.skillCandidates.length === 0) lines.push(" none");
882
+ for (const candidate of data.skillCandidates.slice(0, limit)) lines.push(` ${candidate.resource.name}: ${candidate.genes.length} gene(s) at ${candidate.path} · ${formatEstimate(candidate.credit)}`);
883
+ return lines.join("\n");
884
+ }
885
+ function excerpt(lines) {
886
+ const text = lines.join(" ⏎ ");
887
+ return text.length <= 72 ? text : `${text.slice(0, 71)}…`;
888
+ }
889
+ function signed$1(value) {
890
+ if (value === null) return "null";
891
+ return `${value >= 0 ? "+" : ""}${value.toFixed(4)}`;
892
+ }
893
+ function formatEstimate(estimate) {
894
+ const units = `${estimate.pairs} unit${estimate.pairs === 1 ? "" : "s"}`;
895
+ if (estimate.method === "none") return `unknown (${units})`;
896
+ if (estimate.indeterminate) return `no measurable difference (indeterminate, ${units})`;
897
+ const delta = `Δ=${signed$1(estimate.delta)}`;
898
+ if (estimate.method === "insufficient") return `${delta} (${estimate.pairs} of ${DESCRIPTIVE_FROM} units, insufficient)`;
899
+ const interval = estimate.interval ? ` [${estimate.interval[0].toFixed(4)}, ${estimate.interval[1].toFixed(4)}]` : "";
900
+ if (estimate.method === "descriptive") return `${delta}${interval} descriptive, sign p=${estimate.exactSignP === null ? "null" : estimate.exactSignP.toPrecision(3)} (${units})`;
901
+ return `${delta}${interval} bootstrap (${units})`;
902
+ }
903
+ function emptyCounts() {
904
+ return {
905
+ reusable: 0,
906
+ harmful: 0,
907
+ unresolved: 0,
908
+ insufficient: 0
909
+ };
910
+ }
911
+ function appendTo(map, key, value) {
912
+ const list = map.get(key);
913
+ if (list) list.push(value);
914
+ else map.set(key, [value]);
915
+ }
916
+ function memo(compute) {
917
+ const cache = /* @__PURE__ */ new Map();
918
+ return (key) => {
919
+ let value = cache.get(key);
920
+ if (value === void 0) {
921
+ value = compute(key);
922
+ cache.set(key, value);
923
+ }
924
+ return value;
925
+ };
926
+ }
927
+ function compareOrder(left, right) {
928
+ return left[0] - right[0] || left[1] - right[1];
929
+ }
930
+ /** Canonical JSON has one zero; never hand it a negative one. */
931
+ function plain(value) {
932
+ return value === 0 ? 0 : value;
933
+ }
934
+ //#endregion
935
+ //#region src/search/lenses/front.ts
936
+ function front(state, options = {}) {
937
+ const split = options.split ?? rankingSplit(state);
938
+ const direction = state.header?.objective.direction ?? "maximize";
939
+ const extraAxes = options.axes ?? [];
940
+ const rows = state.nodes().map((node) => {
941
+ const units = state.unitScores(node.nodeId, split);
942
+ const score = units.length > 0 ? units.reduce((sum, u) => sum + u.mean, 0) / units.length : null;
943
+ return {
944
+ nodeId: node.nodeId,
945
+ score,
946
+ knownCostUsd: node.spend.knownUsd,
947
+ costKnown: node.spend.unknownCostCells === 0,
948
+ extra: extraAxes.map((axis) => axis.value(state, node.nodeId))
949
+ };
950
+ });
951
+ const eligible = rows.filter((row) => row.score !== null && row.costKnown && row.extra.every((value) => Number.isFinite(value)));
952
+ const objectives = [
953
+ {
954
+ name: "score",
955
+ direction,
956
+ value: (row) => row.score
957
+ },
958
+ {
959
+ name: "knownCostUsd",
960
+ direction: "minimize",
961
+ value: (row) => row.knownCostUsd
962
+ },
963
+ ...extraAxes.map((axis, index) => ({
964
+ name: axis.name,
965
+ direction: axis.direction,
966
+ value: (row) => row.extra[index]
967
+ }))
968
+ ];
969
+ const frontierIds = eligible.length > 0 ? new Set(paretoFrontier(eligible, objectives).frontier.map((r) => r.nodeId)) : /* @__PURE__ */ new Set();
970
+ const outRows = rows.map((row) => ({
971
+ nodeId: row.nodeId,
972
+ score: row.score,
973
+ knownCostUsd: row.knownCostUsd,
974
+ costKnown: row.costKnown,
975
+ onFront: frontierIds.has(row.nodeId)
976
+ }));
977
+ const signalValue = {};
978
+ for (const row of outRows) signalValue[row.nodeId] = row.onFront ? 1 : 0;
979
+ return {
980
+ data: {
981
+ split,
982
+ axes: objectives.map((o) => o.name),
983
+ rows: outRows,
984
+ frontierSize: frontierIds.size,
985
+ excludedCount: rows.length - eligible.length
986
+ },
987
+ signal: {
988
+ name: "front.membership",
989
+ value: signalValue
990
+ }
991
+ };
992
+ }
993
+ /** The method `estimateNode` reports for a paired sample of `pairs` units. */
994
+ function estimateMethodFor(pairs) {
995
+ if (pairs < 2) return "none";
996
+ if (pairs < INSUFFICIENT_FROM) return "insufficient";
997
+ if (pairs < 20) return "descriptive";
998
+ return "bootstrap";
999
+ }
1000
+ /**
1001
+ * Nodes whose screen finished, in registration order, root first: the
1002
+ * ledger's reading of the kernel's `screened` list. A node counts once it is
1003
+ * not decided invalid, has an edge, and every cell it was allocated outside a
1004
+ * rung or the claim can no longer run (scored, final, cancelled, or errored
1005
+ * with no attempt left). Cells a rung opened later do not un-screen it.
1006
+ */
1007
+ function screenedNodes(state, options = {}) {
1008
+ const maxAttempts = options.maxAttempts ?? 3;
1009
+ const screened = [];
1010
+ for (const node of state.nodes()) {
1011
+ if (node.status === "invalid" || node.edgeIds.length === 0) continue;
1012
+ let allocated = 0;
1013
+ let open = false;
1014
+ for (const cell of state.cells({ nodeId: node.nodeId })) {
1015
+ if (cell.stage === "rung" || cell.stage === "claim") continue;
1016
+ allocated += 1;
1017
+ if (!(cell.final || cell.cancelled !== null || cell.outcome === "errored" && cell.attempts >= maxAttempts)) {
1018
+ open = true;
1019
+ break;
1020
+ }
1021
+ }
1022
+ if (allocated > 0 && !open) screened.push(node.nodeId);
1023
+ }
1024
+ return screened;
1025
+ }
1026
+ /** The head's sequence, or -1 before the first entry. */
1027
+ function headSequence(state) {
1028
+ return state.head?.sequence ?? -1;
1029
+ }
1030
+ /** Rounded for JSON a person reads; 9 decimals keep the bits a renderer
1031
+ * needs and drop float noise. */
1032
+ function round9(value) {
1033
+ const rounded = Math.round(value * 1e9) / 1e9;
1034
+ return rounded === 0 ? 0 : rounded;
1035
+ }
1036
+ function median(values) {
1037
+ if (values.length === 0) return null;
1038
+ const sorted = [...values].sort((left, right) => left - right);
1039
+ const middle = Math.floor(sorted.length / 2);
1040
+ return sorted.length % 2 === 1 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
1041
+ }
1042
+ /** At most `cap` items, evenly spaced by position: a deterministic sample
1043
+ * that keeps a quadratic step bounded on a large search. */
1044
+ function evenSample(items, cap) {
1045
+ if (items.length <= cap) return [...items];
1046
+ return Array.from({ length: cap }, (_, index) => items[Math.floor(index * items.length / cap)]);
1047
+ }
1048
+ function positiveInteger(owner, name, value) {
1049
+ if (!Number.isSafeInteger(value) || value < 1) throw new TypeError(`${owner}: ${name} must be a positive integer, got ${String(value)}`);
1050
+ return value;
1051
+ }
1052
+ function plural$1(count, noun) {
1053
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
1054
+ }
1055
+ //#endregion
1056
+ //#region src/math/cholesky.ts
1057
+ /**
1058
+ * Cholesky factorization and solves for small symmetric positive-definite
1059
+ * systems (Golub and Van Loan, Matrix Computations, 4th ed., §4.2), the
1060
+ * ridge normal equations the skill-manifold lens solves once per row and
1061
+ * column of each alternating least-squares sweep.
1062
+ */
1063
+ /** The lower-triangular L with `matrix = L Lᵀ`, or null when the matrix is
1064
+ * not numerically positive definite. */
1065
+ function cholesky(matrix) {
1066
+ const n = matrix.length;
1067
+ const lower = Array.from({ length: n }, () => new Array(n).fill(0));
1068
+ for (let j = 0; j < n; j++) {
1069
+ let diagonal = matrix[j][j];
1070
+ for (let k = 0; k < j; k++) diagonal -= lower[j][k] ** 2;
1071
+ if (!(diagonal > 0) || !Number.isFinite(diagonal)) return null;
1072
+ const pivot = Math.sqrt(diagonal);
1073
+ lower[j][j] = pivot;
1074
+ for (let i = j + 1; i < n; i++) {
1075
+ let value = matrix[i][j];
1076
+ for (let k = 0; k < j; k++) value -= lower[i][k] * lower[j][k];
1077
+ lower[i][j] = value / pivot;
1078
+ }
1079
+ }
1080
+ return lower;
1081
+ }
1082
+ /** Solves `L Lᵀ x = b` for x given the Cholesky factor L. */
1083
+ function choleskySolve(lower, b) {
1084
+ const n = lower.length;
1085
+ const y = new Array(n).fill(0);
1086
+ for (let i = 0; i < n; i++) {
1087
+ let value = b[i];
1088
+ for (let k = 0; k < i; k++) value -= lower[i][k] * y[k];
1089
+ y[i] = value / lower[i][i];
1090
+ }
1091
+ const x = new Array(n).fill(0);
1092
+ for (let i = n - 1; i >= 0; i--) {
1093
+ let value = y[i];
1094
+ for (let k = i + 1; k < n; k++) value -= lower[k][i] * x[k];
1095
+ x[i] = value / lower[i][i];
1096
+ }
1097
+ return x;
1098
+ }
1099
+ /** The inverse of a symmetric positive-definite matrix from its factor. */
1100
+ function choleskyInverse(lower) {
1101
+ const n = lower.length;
1102
+ const columns = Array.from({ length: n }, (_, j) => choleskySolve(lower, Array.from({ length: n }, (_, i) => i === j ? 1 : 0)));
1103
+ return Array.from({ length: n }, (_, i) => columns.map((column) => column[i]));
1104
+ }
1105
+ //#endregion
1106
+ //#region src/math/symmetric-eigen.ts
1107
+ const MAX_SWEEPS = 100;
1108
+ /** Eigenvalues and eigenvectors of a symmetric matrix, largest value first. */
1109
+ function symmetricEigen(matrix) {
1110
+ const n = matrix.length;
1111
+ const a = matrix.map((row, index) => {
1112
+ if (row.length !== n) throw new RangeError(`symmetricEigen: row ${index} has ${row.length} entries, expected ${n}`);
1113
+ return row.map((value) => {
1114
+ if (!Number.isFinite(value)) throw new RangeError("symmetricEigen: entries must be finite");
1115
+ return value;
1116
+ });
1117
+ });
1118
+ for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) {
1119
+ const scale = Math.max(1, Math.abs(a[i][j]), Math.abs(a[j][i]));
1120
+ if (Math.abs(a[i][j] - a[j][i]) > 1e-9 * scale) throw new RangeError(`symmetricEigen: the matrix is not symmetric at (${i}, ${j})`);
1121
+ }
1122
+ const v = Array.from({ length: n }, (_, i) => Array.from({ length: n }, (_, j) => i === j ? 1 : 0));
1123
+ let total = 0;
1124
+ for (let i = 0; i < n; i++) for (let j = 0; j < n; j++) total += a[i][j] ** 2;
1125
+ const tolerance = 1e-24 * Math.max(total, Number.MIN_VALUE);
1126
+ for (let sweep = 0; sweep < MAX_SWEEPS; sweep++) {
1127
+ let off = 0;
1128
+ for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) off += a[i][j] ** 2;
1129
+ if (off <= tolerance) break;
1130
+ for (let p = 0; p < n; p++) for (let q = p + 1; q < n; q++) {
1131
+ const apq = a[p][q];
1132
+ if (apq === 0) continue;
1133
+ const theta = (a[q][q] - a[p][p]) / (2 * apq);
1134
+ const t = Math.sign(theta || 1) / (Math.abs(theta) + Math.sqrt(theta * theta + 1));
1135
+ const c = 1 / Math.sqrt(t * t + 1);
1136
+ const s = t * c;
1137
+ for (let k = 0; k < n; k++) {
1138
+ const akp = a[k][p];
1139
+ const akq = a[k][q];
1140
+ a[k][p] = c * akp - s * akq;
1141
+ a[k][q] = s * akp + c * akq;
1142
+ }
1143
+ for (let k = 0; k < n; k++) {
1144
+ const apk = a[p][k];
1145
+ const aqk = a[q][k];
1146
+ a[p][k] = c * apk - s * aqk;
1147
+ a[q][k] = s * apk + c * aqk;
1148
+ }
1149
+ for (let k = 0; k < n; k++) {
1150
+ const vkp = v[k][p];
1151
+ const vkq = v[k][q];
1152
+ v[k][p] = c * vkp - s * vkq;
1153
+ v[k][q] = s * vkp + c * vkq;
1154
+ }
1155
+ }
1156
+ }
1157
+ const order = Array.from({ length: n }, (_, index) => index).sort((left, right) => a[right][right] - a[left][left] || left - right);
1158
+ return {
1159
+ values: order.map((index) => a[index][index]),
1160
+ vectors: order.map((index) => orient$1(v.map((row) => row[index])))
1161
+ };
1162
+ }
1163
+ function orient$1(vector) {
1164
+ let pivot = 0;
1165
+ for (let index = 1; index < vector.length; index++) if (Math.abs(vector[index]) > Math.abs(vector[pivot]) + 1e-12) pivot = index;
1166
+ return (vector[pivot] ?? 0) < 0 ? vector.map((value) => value === 0 ? 0 : -value) : vector;
1167
+ }
1168
+ //#endregion
1169
+ //#region src/search/lenses/plateau.ts
1170
+ const PLATEAU_METHOD = `rise of the best improvement over the root across the last \`window\` accepted nodes (screened, no dodged unit, ${INSUFFICIENT_FROM} or more units shared with the root), divided by the best node's standard error sqrt(pooled variance / its shared units); an improvement is the mean per-unit gain over the root on shared units in the objective's direction, and the variance is the between-unit variance of those gains pooled over screened nodes with 2 or more shared units`;
1171
+ /**
1172
+ * The plateau score of a search. Nodes are taken in the view's screened
1173
+ * order, which is registration order. Insufficient with fewer than `window`
1174
+ * accepted nodes, or without a pooled variance above zero.
1175
+ */
1176
+ function searchPlateau(view, options = {}) {
1177
+ const window = options.window ?? 6;
1178
+ if (!Number.isSafeInteger(window) || window < 1) throw new TypeError(`searchPlateau: window must be a positive integer, got ${String(window)}`);
1179
+ const sign = view.direction === "maximize" ? 1 : -1;
1180
+ const root = new Map(view.unitScores(view.rootNodeId).map((unit) => [unit.unitId, unit.mean]));
1181
+ const accepted = [];
1182
+ let squares = 0;
1183
+ let degreesOfFreedom = 0;
1184
+ for (const nodeId of view.screened) {
1185
+ if (nodeId === view.rootNodeId || !view.complete(nodeId)) continue;
1186
+ const deltas = [];
1187
+ for (const unit of view.unitScores(nodeId)) {
1188
+ const base = root.get(unit.unitId);
1189
+ if (base !== void 0) deltas.push(sign * (unit.mean - base));
1190
+ }
1191
+ if (deltas.length < 2) continue;
1192
+ let total = 0;
1193
+ for (const delta of deltas) total += delta;
1194
+ const mean = total / deltas.length;
1195
+ for (const delta of deltas) squares += (delta - mean) ** 2;
1196
+ degreesOfFreedom += deltas.length - 1;
1197
+ if (deltas.length >= INSUFFICIENT_FROM) accepted.push({
1198
+ nodeId,
1199
+ gain: mean,
1200
+ pairs: deltas.length
1201
+ });
1202
+ }
1203
+ const pooledVariance = degreesOfFreedom > 0 ? squares / degreesOfFreedom : null;
1204
+ const start = Math.max(0, accepted.length - window);
1205
+ let before = null;
1206
+ for (const entry of accepted.slice(0, start)) if (before === null || entry.gain > before.gain) before = entry;
1207
+ let best = before;
1208
+ for (const entry of accepted.slice(start)) if (best === null || entry.gain > best.gain) best = entry;
1209
+ const base = {
1210
+ window,
1211
+ accepted: accepted.length,
1212
+ windowNodes: accepted.slice(start).map((entry) => entry.nodeId),
1213
+ best: best === null ? null : {
1214
+ ...best,
1215
+ gain: round9(best.gain)
1216
+ },
1217
+ pooledVariance: pooledVariance === null ? null : round9(pooledVariance),
1218
+ degreesOfFreedom,
1219
+ method: PLATEAU_METHOD
1220
+ };
1221
+ const insufficient = (reason) => ({
1222
+ ...base,
1223
+ value: null,
1224
+ rise: null,
1225
+ noise: null,
1226
+ insufficient: reason
1227
+ });
1228
+ if (accepted.length < window) return insufficient(`${accepted.length} of ${window} accepted nodes (screened, no dodged unit, ${INSUFFICIENT_FROM} or more units shared with the root)`);
1229
+ if (pooledVariance === null || pooledVariance === 0) return insufficient(pooledVariance === null ? "no pooled between-unit variance" : "every per-unit gain equals its node’s mean, so the noise is unmeasured");
1230
+ const leader = best;
1231
+ const rise = Math.max(0, leader.gain) - Math.max(0, before?.gain ?? 0);
1232
+ const noise = Math.sqrt(pooledVariance / leader.pairs);
1233
+ return {
1234
+ ...base,
1235
+ value: round9(rise / noise),
1236
+ rise: round9(rise),
1237
+ noise: round9(noise),
1238
+ insufficient: null
1239
+ };
1240
+ }
1241
+ /**
1242
+ * `base`, except that on a plateau it drafts: it proposes a fresh artifact
1243
+ * (operator `draft`, anchored on the root) instead of refining one. The
1244
+ * plateau is the `landscape` lens's signal (`searchPlateau` on this view):
1245
+ * how many standard errors the best improvement over the root rose across the
1246
+ * last `window` accepted nodes. A draft needs a measured plateau (at least
1247
+ * `window` accepted nodes and a pooled variance) and an expansion count that
1248
+ * is a multiple of `window`, so `base` expands at least `window − 1` times
1249
+ * between drafts and each draft's lineage gets a chance to lift the plateau.
1250
+ * Nodes still screening do not hold a draft back: the kernel expands while
1251
+ * its screening backlog is below twice the lane capacity, so under a full
1252
+ * pipeline some node is always screening, and the plateau reads only nodes
1253
+ * whose screen finished. The leader and patience are `base`'s.
1254
+ *
1255
+ * This is AIDE's draft trigger driven by measured progress instead of a fixed
1256
+ * count; `draftOnPlateau(aide(...))` adds it to `aide`'s own drafts. A draft
1257
+ * pays off only when `base` then expands drafts that trail the leader:
1258
+ * `incumbent` expands only its leader, so a draft that starts below it is
1259
+ * never refined.
1260
+ */
1261
+ function draftOnPlateau(base, options = {}) {
1262
+ const window = options.window ?? 6;
1263
+ const below = options.below ?? 1;
1264
+ if (!Number.isSafeInteger(window) || window < 1) throw new TypeError(`draftOnPlateau: window must be a positive integer, got ${String(window)}`);
1265
+ if (!(below > 0) || !Number.isFinite(below)) throw new TypeError(`draftOnPlateau: below must be a positive number, got ${String(below)}`);
1266
+ const name = `draft-on-plateau(${base.name},window=${window},below=${below})`;
1267
+ return {
1268
+ name,
1269
+ ...base.patience === void 0 ? {} : { patience: base.patience },
1270
+ leader: (view) => base.leader(view),
1271
+ expand(view) {
1272
+ if (view.expansions % window !== 0) return base.expand(view);
1273
+ const plateau = searchPlateau(view, { window });
1274
+ if (plateau.value === null || plateau.value >= below) return base.expand(view);
1275
+ return {
1276
+ parents: [view.rootNodeId],
1277
+ operator: "draft",
1278
+ selection: {
1279
+ rule: name,
1280
+ evidence: {
1281
+ plateau: plateau.value,
1282
+ rise: plateau.rise,
1283
+ noise: plateau.noise,
1284
+ window,
1285
+ accepted: plateau.accepted,
1286
+ expansions: view.expansions
1287
+ }
1288
+ }
1289
+ };
1290
+ }
1291
+ };
1292
+ }
1293
+ //#endregion
1294
+ //#region src/search/lenses/landscape.ts
1295
+ /**
1296
+ * The landscape lens: where each node sits in edit space, and how the score
1297
+ * varies over it.
1298
+ *
1299
+ * Nodes are placed in two dimensions by landmark classical multidimensional
1300
+ * scaling (de Silva and Tenenbaum, 2004) of a pluggable distance between
1301
+ * their profiles. The default distance counts the edits the ledger recorded
1302
+ * between two profiles along their lineage; a caller that can read the
1303
+ * surface blobs passes `surfaceTextEdits`, and a caller with a model
1304
+ * embedding passes `vectorEmbedding`. Every node's score is its improvement
1305
+ * over the root on the units both scored, in the objective's direction, with
1306
+ * the standard error its shared units give it. Kriging (Gaussian-process
1307
+ * regression with each node's own noise) interpolates the scores over a grid
1308
+ * and leaves a cell unknown where the nodes do not constrain it. Basins are
1309
+ * the peaks of the node scores on the nearest-neighbour graph that stand
1310
+ * above their saddle by more than the noise, Bonferroni-bounded over the
1311
+ * graph's local maxima (0-dimensional persistence of the superlevel sets,
1312
+ * ToMATo).
1313
+ *
1314
+ * The signal is `plateau`: how far the best improvement over the root rose
1315
+ * across the last `window` screened nodes, in units of that node's standard
1316
+ * error. Below 1 the search is on a plateau; a policy that drafts afresh when
1317
+ * it is (`draftOnPlateau`) reads the same function on its own view.
1318
+ */
1319
+ /** The default: edits recorded between two profiles along the lineage. */
1320
+ function lineageEdits() {
1321
+ return {
1322
+ kind: "lineage",
1323
+ name: "lineage-edits",
1324
+ method: "edits between the two profiles along the recorded lineage: the fewest improve, debug or merge edges joining them (a re-proposal edge counts; a draft is written afresh, so its edge is no edit of its anchor)"
1325
+ };
1326
+ }
1327
+ /** Surfaces whose content digests differ: the edit distance between two
1328
+ * profiles at the granularity of their declared surfaces. */
1329
+ function surfaceDigestEdits() {
1330
+ return {
1331
+ kind: "distance",
1332
+ name: "surface-digest-edits",
1333
+ method: "declared surfaces whose content digests differ (a surface only one node has counts)",
1334
+ distance(a, b) {
1335
+ const left = new Map(a.surfaces.map((surface) => [surface.surfaceId, surface.artifact.sha256]));
1336
+ let count = 0;
1337
+ const seen = /* @__PURE__ */ new Set();
1338
+ for (const surface of b.surfaces) {
1339
+ seen.add(surface.surfaceId);
1340
+ if (left.get(surface.surfaceId) !== surface.artifact.sha256) count += 1;
1341
+ }
1342
+ for (const surfaceId of left.keys()) if (!seen.has(surfaceId)) count += 1;
1343
+ return count;
1344
+ }
1345
+ };
1346
+ }
1347
+ /**
1348
+ * Line edits between the surfaces' texts: for each declared surface, the
1349
+ * fewest line insertions and deletions that turn one text into the other
1350
+ * (Myers, 1986), summed over surfaces. `read` returns a surface's content:
1351
+ * a string, parsed JSON (flattened with `profileTextLines`), or null when it
1352
+ * cannot be read, which leaves every distance of that node unknown. The lens
1353
+ * stays pure; the caller owns the I/O and its verification.
1354
+ */
1355
+ function surfaceTextEdits(read) {
1356
+ const lines = /* @__PURE__ */ new Map();
1357
+ const linesOf = (surface) => {
1358
+ const key = surface.artifact.sha256;
1359
+ if (!lines.has(key)) {
1360
+ const content = read(surface);
1361
+ lines.set(key, content === null || content === void 0 ? null : typeof content === "string" ? content.split("\n") : profileTextLines(content));
1362
+ }
1363
+ return lines.get(key);
1364
+ };
1365
+ return {
1366
+ kind: "distance",
1367
+ name: "surface-text-edits",
1368
+ method: "line insertions plus deletions between the surface texts (Myers diff), summed over declared surfaces; JSON is flattened to one line per leaf and per line of a string",
1369
+ distance(a, b) {
1370
+ const right = new Map(b.surfaces.map((surface) => [surface.surfaceId, surface]));
1371
+ let total = 0;
1372
+ const seen = /* @__PURE__ */ new Set();
1373
+ for (const surface of a.surfaces) {
1374
+ seen.add(surface.surfaceId);
1375
+ const own = linesOf(surface);
1376
+ if (own === null) return null;
1377
+ const other = right.get(surface.surfaceId);
1378
+ if (!other) {
1379
+ total += own.length;
1380
+ continue;
1381
+ }
1382
+ if (other.artifact.sha256 === surface.artifact.sha256) continue;
1383
+ const theirs = linesOf(other);
1384
+ if (theirs === null) return null;
1385
+ total += lineEditDistance(own, theirs);
1386
+ }
1387
+ for (const surface of b.surfaces) {
1388
+ if (seen.has(surface.surfaceId)) continue;
1389
+ const theirs = linesOf(surface);
1390
+ if (theirs === null) return null;
1391
+ total += theirs.length;
1392
+ }
1393
+ return total;
1394
+ }
1395
+ };
1396
+ }
1397
+ /** A caller's vector per node, for example a model embedding of its profile. */
1398
+ function vectorEmbedding(name, vector) {
1399
+ return {
1400
+ kind: "vector",
1401
+ name,
1402
+ method: `Euclidean distance between ${name} vectors`,
1403
+ vector
1404
+ };
1405
+ }
1406
+ /**
1407
+ * JSON as lines a diff can count: one line per scalar leaf, `path=value`, and
1408
+ * one line per line of a string, so an edit inside a long prompt counts the
1409
+ * lines it changed rather than the whole prompt. Object keys are visited in
1410
+ * code-unit order.
1411
+ */
1412
+ function profileTextLines(value, path = "$") {
1413
+ if (typeof value === "string") {
1414
+ const parts = value.split("\n");
1415
+ return parts.length === 1 ? [`${path}=${canonicalString(value)}`] : parts.map((line, index) => `${path}[${index}]=${canonicalString(line)}`);
1416
+ }
1417
+ if (Array.isArray(value)) {
1418
+ if (value.length === 0) return [`${path}=[]`];
1419
+ return value.flatMap((entry, index) => profileTextLines(entry, `${path}[${index}]`));
1420
+ }
1421
+ if (value !== null && typeof value === "object") {
1422
+ const keys = Object.keys(value).sort(compareCodeUnits);
1423
+ if (keys.length === 0) return [`${path}={}`];
1424
+ return keys.flatMap((key) => profileTextLines(value[key], `${path}.${key}`));
1425
+ }
1426
+ return [`${path}=${canonicalString(value ?? null)}`];
1427
+ }
1428
+ /** Fewest line insertions plus deletions turning `a` into `b` (Myers' O(ND)
1429
+ * greedy algorithm, which is fast when the texts are close). */
1430
+ function lineEditDistance(a, b) {
1431
+ const n = a.length;
1432
+ const m = b.length;
1433
+ const max = n + m;
1434
+ if (max === 0) return 0;
1435
+ const offset = max;
1436
+ const v = new Int32Array(2 * max + 2);
1437
+ for (let d = 0; d <= max; d++) for (let k = -d; k <= d; k += 2) {
1438
+ let x = k === -d || k !== d && v[offset + k - 1] < v[offset + k + 1] ? v[offset + k + 1] : v[offset + k - 1] + 1;
1439
+ let y = x - k;
1440
+ while (x < n && y < m && a[x] === b[y]) {
1441
+ x += 1;
1442
+ y += 1;
1443
+ }
1444
+ v[offset + k] = x;
1445
+ if (x >= n && y >= m) return d;
1446
+ }
1447
+ return max;
1448
+ }
1449
+ const GRID_METHOD = "Gaussian-process regression (ordinary kriging) of node scores: a constant mean (generalized least squares), a squared-exponential covariance whose prior variance is the between-node variance of the scores minus their mean noise and whose length scale maximizes the marginal likelihood over 0.5, 1, 2 and 4 times the median nearest-neighbour distance, and each node's own noise variance (the pooled between-unit variance over its units shared with the root; the root is the reference at exactly 0); nodes with fewer than 2 shared units are left out; a cell is null where the posterior variance exceeds half the prior variance, so the surface never extends past the nodes that support it; no surface is drawn unless the length scale raises the log-likelihood by 1 or more over independent node scores";
1450
+ /** Scored nodes a surface or a basin count needs: the library's minimum
1451
+ * sample for anything descriptive, as for units. */
1452
+ const SURFACE_MIN_NODES = INSUFFICIENT_FROM;
1453
+ const BASIN_METHOD = "0-dimensional persistence of node scores on the symmetric k-nearest-neighbour graph of placed nodes, read on the leading MDS axes that hold 80% of the positive eigenvalue mass (2 to 8), joined into one component by the shortest edge between components (ToMATo, Chazal et al. 2013): nodes enter from the highest score down, and where two components meet, the lower peak merges into the higher unless it stands above that saddle node by z standard errors of their difference, sqrt(pooled variance / shared units) per node, where z is the standard-normal quantile at 1 − 0.05 / (local maxima − 1), a Bonferroni bound over every peak that could survive, so a flat landscape of noisy nodes counts one basin at least 95% of the time; at most 2000 nodes, evenly spaced by registration plus the 100 best";
1454
+ /** Nodes the basin graph holds: evenly spaced by registration, plus the best. */
1455
+ const BASIN_NODES = 2e3;
1456
+ const BASIN_TOP = 100;
1457
+ /**
1458
+ * The landscape of a search: every node placed by `embed`, the interpolated
1459
+ * score over the placement, the basins of that surface, and the plateau
1460
+ * signal. Pure: it reads the state and calls `embed`.
1461
+ */
1462
+ function landscape(state, embed = lineageEdits(), options = {}) {
1463
+ const landmarkCap = positiveInteger("landscape", "landmarks", options.landmarks ?? 32);
1464
+ const columns = positiveInteger("landscape", "grid", options.grid ?? 24);
1465
+ const surfaceNodes = positiveInteger("landscape", "surfaceNodes", options.surfaceNodes ?? 400);
1466
+ const window = options.window ?? 6;
1467
+ const header = state.header;
1468
+ const split = options.split ?? rankingSplit(state);
1469
+ const direction = header?.objective.direction ?? "maximize";
1470
+ const plateauEmpty = (reason) => ({
1471
+ value: null,
1472
+ window,
1473
+ accepted: 0,
1474
+ windowNodes: [],
1475
+ rise: null,
1476
+ noise: null,
1477
+ best: null,
1478
+ pooledVariance: null,
1479
+ degreesOfFreedom: 0,
1480
+ method: PLATEAU_METHOD,
1481
+ insufficient: reason
1482
+ });
1483
+ const rootNodeId = header ? state.rootNodeId : null;
1484
+ if (!header || rootNodeId === null) {
1485
+ const reason = header ? "the search has no root node yet" : "the search has not been opened";
1486
+ return result$1(state, emptyData(split, direction, embed, reason, plateauEmpty(reason)));
1487
+ }
1488
+ const nodes = state.nodes();
1489
+ const index = new Map(nodes.map((node, position) => [node.nodeId, position]));
1490
+ const posterior = searchPosterior(state, { split });
1491
+ const scoreOf = new Map(posterior.nodes.map((entry) => [entry.nodeId, entry]));
1492
+ const placement = placeNodes(nodes, index, rootNodeId, embed, landmarkCap, () => lineageAdjacency(state, index));
1493
+ const records = nodes.map((node, position) => {
1494
+ const entry = scoreOf.get(node.nodeId);
1495
+ const firstEdge = node.edgeIds[0] ? state.edge(node.edgeIds[0]) : void 0;
1496
+ const at = placement.coordinates[position];
1497
+ return {
1498
+ nodeId: node.nodeId,
1499
+ ordinal: node.ordinal,
1500
+ depth: node.depth,
1501
+ status: node.status,
1502
+ operator: firstEdge?.operator ?? null,
1503
+ x: at ? round9(at[0]) : null,
1504
+ y: at ? round9(at[1]) : null,
1505
+ unplaced: at ? null : placement.unplaced.get(position) ?? "not placed",
1506
+ score: entry.mean === null || entry.pairs < 2 && node.nodeId !== rootNodeId ? null : round9(entry.mean),
1507
+ standardError: entry.variance === null ? null : round9(Math.sqrt(entry.variance)),
1508
+ pairs: entry.pairs,
1509
+ method: estimateMethodFor(entry.pairs),
1510
+ basin: null
1511
+ };
1512
+ });
1513
+ const lineage = [];
1514
+ for (const node of nodes) {
1515
+ const operator = node.edgeIds[0] ? state.edge(node.edgeIds[0])?.operator ?? "unknown" : "none";
1516
+ for (const parent of node.parents) {
1517
+ if (parent.searchId !== state.searchId) continue;
1518
+ lineage.push({
1519
+ parent: parent.nodeId,
1520
+ child: node.nodeId,
1521
+ operator
1522
+ });
1523
+ }
1524
+ }
1525
+ const scored = records.filter((record) => record.x !== null && record.score !== null && record.pairs >= 2);
1526
+ const gridResult = krige(records, scored, new Map(posterior.nodes.map((entry) => [entry.nodeId, entry.variance])), columns, surfaceNodes);
1527
+ const fullOf = /* @__PURE__ */ new Map();
1528
+ nodes.forEach((node, position) => {
1529
+ const point = placement.full[position];
1530
+ if (point) fullOf.set(node.nodeId, point);
1531
+ });
1532
+ const basins = graphBasins(records, scored, fullOf, {
1533
+ axes: placement.fullAxes,
1534
+ explained: placement.fullExplained
1535
+ }, posterior.pooledVariance, posterior.degreesOfFreedom);
1536
+ const plateau = searchPlateau(searchPolicyView(state, {
1537
+ screened: screenedNodes(state, { maxAttempts: options.maxAttempts }),
1538
+ screening: 0,
1539
+ expansions: 0,
1540
+ ...options.maxAttempts === void 0 ? {} : { maxAttempts: options.maxAttempts }
1541
+ }), { window });
1542
+ return result$1(state, {
1543
+ split,
1544
+ direction,
1545
+ embedding: {
1546
+ name: embed.name,
1547
+ method: embed.method,
1548
+ landmarks: placement.landmarks,
1549
+ landmarkDistance: placement.landmarkDistance,
1550
+ eigenvalues: placement.eigenvalues.map(round9),
1551
+ explained: placement.explained === null ? null : round9(placement.explained),
1552
+ negativeMass: placement.negativeMass === null ? null : round9(placement.negativeMass),
1553
+ placed: records.filter((record) => record.x !== null).length,
1554
+ unplaced: records.filter((record) => record.x === null).length
1555
+ },
1556
+ nodes: records,
1557
+ lineage,
1558
+ grid: gridResult.grid,
1559
+ gridInsufficient: gridResult.insufficient,
1560
+ basins,
1561
+ plateau
1562
+ });
1563
+ }
1564
+ function result$1(state, data) {
1565
+ const { plateau } = data;
1566
+ const signal = {
1567
+ name: "plateau",
1568
+ value: plateau.value,
1569
+ subject: plateau.best?.nodeId ?? null,
1570
+ method: plateau.method,
1571
+ n: plateau.accepted,
1572
+ insufficient: plateau.insufficient
1573
+ };
1574
+ return {
1575
+ lens: "landscape",
1576
+ searchId: state.searchId,
1577
+ sequence: headSequence(state),
1578
+ data,
1579
+ signal
1580
+ };
1581
+ }
1582
+ function emptyData(split, direction, embed, reason, plateau) {
1583
+ return {
1584
+ split,
1585
+ direction,
1586
+ embedding: {
1587
+ name: embed.name,
1588
+ method: embed.method,
1589
+ landmarks: 0,
1590
+ landmarkDistance: null,
1591
+ eigenvalues: [],
1592
+ explained: null,
1593
+ negativeMass: null,
1594
+ placed: 0,
1595
+ unplaced: 0
1596
+ },
1597
+ nodes: [],
1598
+ lineage: [],
1599
+ grid: null,
1600
+ gridInsufficient: reason,
1601
+ basins: {
1602
+ count: null,
1603
+ neighbours: 0,
1604
+ localMaxima: 0,
1605
+ z: null,
1606
+ graphNodes: 0,
1607
+ graphAxes: 0,
1608
+ graphExplained: null,
1609
+ method: BASIN_METHOD,
1610
+ insufficient: reason,
1611
+ peaks: []
1612
+ },
1613
+ plateau
1614
+ };
1615
+ }
1616
+ function placeNodes(nodes, index, rootNodeId, embed, landmarkCap, adjacency) {
1617
+ const n = nodes.length;
1618
+ const distancesFrom = distanceRows(nodes, embed, adjacency);
1619
+ const unplaced = /* @__PURE__ */ new Map();
1620
+ const coordinates = new Array(n).fill(void 0);
1621
+ const first = index.get(rootNodeId) ?? 0;
1622
+ const rows = [];
1623
+ const chosen = [];
1624
+ const nearest = new Array(n).fill(Number.POSITIVE_INFINITY);
1625
+ let pick = first;
1626
+ while (pick !== null && chosen.length < landmarkCap) {
1627
+ const row = distancesFrom(pick);
1628
+ rows.push(row);
1629
+ chosen.push(pick);
1630
+ for (let j = 0; j < n; j++) {
1631
+ const d = row[j];
1632
+ if (unplaced.has(j)) continue;
1633
+ if (d === null || d === void 0 || !Number.isFinite(d) || d < 0) {
1634
+ unplaced.set(j, chosen.length === 1 ? `no ${embed.name} distance to the root` : `no ${embed.name} distance to landmark ${nodes[pick].nodeId}`);
1635
+ continue;
1636
+ }
1637
+ nearest[j] = Math.min(nearest[j], d);
1638
+ }
1639
+ if (unplaced.has(first)) break;
1640
+ let next = null;
1641
+ for (let j = 0; j < n; j++) {
1642
+ if (unplaced.has(j) || chosen.includes(j) || nearest[j] <= 0) continue;
1643
+ if (next === null || nearest[j] > nearest[next]) next = j;
1644
+ }
1645
+ pick = next;
1646
+ }
1647
+ const landmarks = chosen.map((node, position) => ({
1648
+ node,
1649
+ row: rows[position]
1650
+ })).filter(({ node }) => !unplaced.has(node));
1651
+ if (landmarks.length === 0) {
1652
+ for (let j = 0; j < n; j++) if (!unplaced.has(j)) unplaced.set(j, `the root has no ${embed.name} placement`);
1653
+ return {
1654
+ coordinates,
1655
+ full: new Array(n).fill(void 0),
1656
+ fullAxes: 0,
1657
+ fullExplained: null,
1658
+ unplaced,
1659
+ landmarks: 0,
1660
+ landmarkDistance: null,
1661
+ eigenvalues: [],
1662
+ explained: null,
1663
+ negativeMass: null
1664
+ };
1665
+ }
1666
+ const L = landmarks.length;
1667
+ const squared = landmarks.map(({ row: rowA, node: a }) => landmarks.map(({ row: rowB, node: b }) => {
1668
+ const d = ((rowA[b] ?? 0) + (rowB[a] ?? 0)) / 2;
1669
+ return d * d;
1670
+ }));
1671
+ const columnMean = squared[0].map((_, j) => squared.reduce((sum, row) => sum + row[j], 0) / L);
1672
+ const grand = columnMean.reduce((sum, value) => sum + value, 0) / L;
1673
+ const b = squared.map((row, i) => row.map((value, j) => -.5 * (value - columnMean[i] - columnMean[j] + grand)));
1674
+ const between = [];
1675
+ for (let i = 0; i < L; i++) for (let j = i + 1; j < L; j++) between.push(Math.sqrt(squared[i][j]));
1676
+ const { values, vectors } = symmetricEigen(b);
1677
+ const positiveMass = values.filter((value) => value > 1e-12).reduce((sum, value) => sum + value, 0);
1678
+ const negativeMass = values.filter((value) => value < -1e-12).reduce((sum, value) => sum - value, 0);
1679
+ let fullAxes = 2;
1680
+ let held = values.slice(0, 2).reduce((sum, value) => sum + Math.max(value, 0), 0);
1681
+ while (fullAxes < FULL_AXES_MAX && positiveMass > 0 && held / positiveMass < FULL_AXES_MASS && (values[fullAxes] ?? 0) > 1e-12) {
1682
+ held += values[fullAxes];
1683
+ fullAxes += 1;
1684
+ }
1685
+ const allAxes = Array.from({ length: fullAxes }, (_, k) => (values[k] ?? 0) > 1e-12 ? {
1686
+ value: values[k],
1687
+ vector: vectors[k]
1688
+ } : null);
1689
+ const axes = allAxes.slice(0, 2);
1690
+ const full = new Array(n).fill(void 0);
1691
+ for (let j = 0; j < n; j++) {
1692
+ if (unplaced.has(j)) continue;
1693
+ const coordinate = allAxes.map((axis) => {
1694
+ if (axis === null) return 0;
1695
+ let sum = 0;
1696
+ for (let l = 0; l < L; l++) {
1697
+ const d = landmarks[l].row[j];
1698
+ sum += (d * d - columnMean[l]) * axis.vector[l];
1699
+ }
1700
+ return -.5 * sum / Math.sqrt(axis.value);
1701
+ });
1702
+ coordinates[j] = [coordinate[0], coordinate[1]];
1703
+ full[j] = coordinate;
1704
+ }
1705
+ return {
1706
+ coordinates,
1707
+ full,
1708
+ fullAxes,
1709
+ fullExplained: positiveMass > 0 ? Math.min(1, held / positiveMass) : null,
1710
+ unplaced,
1711
+ landmarks: L,
1712
+ landmarkDistance: between.length === 0 ? null : {
1713
+ median: round9(median(between)),
1714
+ max: round9(Math.max(...between))
1715
+ },
1716
+ eigenvalues: values,
1717
+ explained: positiveMass > 0 ? axes.reduce((sum, axis) => sum + (axis?.value ?? 0), 0) / positiveMass : null,
1718
+ negativeMass: positiveMass + negativeMass > 0 ? negativeMass / (positiveMass + negativeMass) : null
1719
+ };
1720
+ }
1721
+ /**
1722
+ * The undirected lineage graph by node index: an edge joins a child to each
1723
+ * parent in this search when its operator edits a parent (improve, debug,
1724
+ * merge). Every recorded edge counts, re-proposals into an existing node
1725
+ * included, since each says the child is one edit from that parent.
1726
+ */
1727
+ function lineageAdjacency(state, index) {
1728
+ const neighbours = Array.from({ length: index.size }, () => /* @__PURE__ */ new Set());
1729
+ for (const edge of state.edges()) {
1730
+ if (edge.operator !== "improve" && edge.operator !== "debug" && edge.operator !== "merge") continue;
1731
+ const child = index.get(edge.childNodeId);
1732
+ if (child === void 0) continue;
1733
+ for (const parent of edge.parents) {
1734
+ if (parent.searchId !== state.searchId) continue;
1735
+ const at = index.get(parent.nodeId);
1736
+ if (at === void 0 || at === child) continue;
1737
+ neighbours[child].add(at);
1738
+ neighbours[at].add(child);
1739
+ }
1740
+ }
1741
+ return neighbours.map((set) => [...set].sort((left, right) => left - right));
1742
+ }
1743
+ /** Distances from one node to every node, by the embedding's kind. */
1744
+ function distanceRows(nodes, embed, adjacency) {
1745
+ const n = nodes.length;
1746
+ if (embed.kind === "lineage") {
1747
+ const neighbours = adjacency();
1748
+ return (from) => {
1749
+ const hops = new Array(n).fill(null);
1750
+ hops[from] = 0;
1751
+ const queue = [from];
1752
+ for (let head = 0; head < queue.length; head++) {
1753
+ const at = queue[head];
1754
+ for (const next of neighbours[at]) {
1755
+ if (hops[next] !== null) continue;
1756
+ hops[next] = hops[at] + 1;
1757
+ queue.push(next);
1758
+ }
1759
+ }
1760
+ return hops;
1761
+ };
1762
+ }
1763
+ if (embed.kind === "vector") {
1764
+ const vectors = nodes.map((node) => embed.vector(node));
1765
+ const size = vectors.find((vector) => vector !== null)?.length ?? 0;
1766
+ return (from) => vectors.map((vector) => {
1767
+ const own = vectors[from];
1768
+ if (!own || !vector || own.length !== size || vector.length !== size) return null;
1769
+ let sum = 0;
1770
+ for (let d = 0; d < size; d++) sum += (own[d] - vector[d]) ** 2;
1771
+ return Math.sqrt(sum);
1772
+ });
1773
+ }
1774
+ return (from) => nodes.map((node, j) => j === from ? 0 : embed.distance(nodes[from], node));
1775
+ }
1776
+ /**
1777
+ * The score surface by ordinary kriging. Each scored node is a noisy
1778
+ * observation of the surface at its placement, with its own noise variance
1779
+ * (the root, the reference, is exact). The prior variance is the scores'
1780
+ * between-node variance minus their mean noise; when that is not positive the
1781
+ * nodes do not differ beyond noise and there is no surface to draw. The length
1782
+ * scale maximizes the marginal likelihood over a few multiples of the median
1783
+ * nearest-neighbour distance, never below one grid cell.
1784
+ */
1785
+ function krige(records, scored, variances, columns, surfaceNodes) {
1786
+ if (scored.length < SURFACE_MIN_NODES) return {
1787
+ grid: null,
1788
+ insufficient: `${plural$1(scored.length, "placed node")} share 2 or more units with the root; a surface needs ${SURFACE_MIN_NODES}`
1789
+ };
1790
+ const noiseOf = (node) => variances.get(node.nodeId) ?? null;
1791
+ if (scored.some((node) => noiseOf(node) === null)) return {
1792
+ grid: null,
1793
+ insufficient: "no pooled between-unit variance, so the noise of a node’s score is unknown"
1794
+ };
1795
+ let [x0, x1, y0, y1] = [
1796
+ Number.POSITIVE_INFINITY,
1797
+ Number.NEGATIVE_INFINITY,
1798
+ Number.POSITIVE_INFINITY,
1799
+ Number.NEGATIVE_INFINITY
1800
+ ];
1801
+ for (const record of records) {
1802
+ if (record.x === null || record.y === null) continue;
1803
+ x0 = Math.min(x0, record.x);
1804
+ x1 = Math.max(x1, record.x);
1805
+ y0 = Math.min(y0, record.y);
1806
+ y1 = Math.max(y1, record.y);
1807
+ }
1808
+ const span = Math.max(x1 - x0, y1 - y0);
1809
+ if (span <= 0) return {
1810
+ grid: null,
1811
+ insufficient: "every placed node sits at one point"
1812
+ };
1813
+ const pad = span * .05;
1814
+ x0 -= pad;
1815
+ x1 += pad;
1816
+ y0 -= pad;
1817
+ y1 += pad;
1818
+ const byScore = [...scored].sort((left, right) => right.score - left.score || left.ordinal - right.ordinal);
1819
+ const keep = new Set(evenSample(scored, surfaceNodes));
1820
+ const rootNode = scored.find((node) => noiseOf(node) === 0);
1821
+ for (const must of [rootNode, byScore[0]]) {
1822
+ if (!must || keep.has(must)) continue;
1823
+ const drop = [...keep].reverse().find((node) => node !== rootNode && node !== byScore[0]);
1824
+ if (drop) keep.delete(drop);
1825
+ keep.add(must);
1826
+ }
1827
+ const fit = scored.filter((node) => keep.has(node));
1828
+ const n = fit.length;
1829
+ const y = fit.map((node) => node.score);
1830
+ const noise = fit.map((node) => noiseOf(node));
1831
+ const meanScore = y.reduce((sum, value) => sum + value, 0) / n;
1832
+ const spread = y.reduce((sum, value) => sum + (value - meanScore) ** 2, 0) / (n - 1);
1833
+ const meanNoise = noise.reduce((sum, value) => sum + value, 0) / n;
1834
+ const priorVariance = spread - meanNoise;
1835
+ if (!(priorVariance > 0)) return {
1836
+ grid: null,
1837
+ insufficient: `the nodes' scores vary no more than their noise (between-node variance ${fixed(spread)}, mean noise variance ${fixed(meanNoise)}), so there is no surface beyond a flat one`
1838
+ };
1839
+ const cell = Math.max(x1 - x0, y1 - y0) / columns;
1840
+ const nearest = evenSample(fit, 2e3).map((a, _, sample) => {
1841
+ let best = Number.POSITIVE_INFINITY;
1842
+ for (const b of sample) {
1843
+ if (a === b) continue;
1844
+ const d = Math.hypot(a.x - b.x, a.y - b.y);
1845
+ if (d > 0 && d < best) best = d;
1846
+ }
1847
+ return best;
1848
+ });
1849
+ const typical = Math.max(median(nearest.filter(Number.isFinite)) ?? cell, cell);
1850
+ const d2 = fit.map((a) => fit.map((b) => (a.x - b.x) ** 2 + (a.y - b.y) ** 2));
1851
+ const jitter = priorVariance * 1e-9;
1852
+ let best = null;
1853
+ for (const multiple of [
1854
+ .5,
1855
+ 1,
1856
+ 2,
1857
+ 4
1858
+ ]) {
1859
+ const lengthScale = Math.max(typical * multiple, cell);
1860
+ const lower = cholesky(d2.map((row, i) => row.map((value, j) => priorVariance * Math.exp(-value / (2 * lengthScale * lengthScale)) + (i === j ? noise[i] + jitter : 0))));
1861
+ if (lower === null) continue;
1862
+ const ones = forward(lower, new Array(n).fill(1));
1863
+ const whiteY = forward(lower, y);
1864
+ let numerator = 0;
1865
+ let denominator = 0;
1866
+ for (let i = 0; i < n; i++) {
1867
+ numerator += ones[i] * whiteY[i];
1868
+ denominator += ones[i] * ones[i];
1869
+ }
1870
+ const mean = numerator / denominator;
1871
+ const residual = forward(lower, y.map((value) => value - mean));
1872
+ let quadratic = 0;
1873
+ let logDeterminant = 0;
1874
+ for (let i = 0; i < n; i++) {
1875
+ quadratic += residual[i] ** 2;
1876
+ logDeterminant += 2 * Math.log(lower[i][i]);
1877
+ }
1878
+ const logLikelihood = -.5 * (quadratic + logDeterminant + n * Math.log(2 * Math.PI));
1879
+ if (best === null || logLikelihood > best.logLikelihood) best = {
1880
+ lengthScale,
1881
+ lower,
1882
+ alpha: backward(lower, residual),
1883
+ mean,
1884
+ logLikelihood
1885
+ };
1886
+ }
1887
+ if (best === null) return {
1888
+ grid: null,
1889
+ insufficient: "the kriging covariance is not positive definite"
1890
+ };
1891
+ const independent = independentLogLikelihood(y, noise, priorVariance);
1892
+ if (best.logLikelihood - independent < 1) return {
1893
+ grid: null,
1894
+ insufficient: `node scores do not correlate with distance in this embedding (log-likelihood ${fixed(best.logLikelihood)} with a length scale against ${fixed(independent)} without one), so a surface would only interpolate noise`
1895
+ };
1896
+ const { lengthScale, lower, alpha, mean } = best;
1897
+ const rows = columns;
1898
+ const values = [];
1899
+ const sd = [];
1900
+ let supported = 0;
1901
+ for (let r = 0; r < rows; r++) {
1902
+ const row = [];
1903
+ const sdRow = [];
1904
+ const gy = y0 + (r + .5) * (y1 - y0) / rows;
1905
+ for (let c = 0; c < columns; c++) {
1906
+ const gx = x0 + (c + .5) * (x1 - x0) / columns;
1907
+ const k = fit.map((node) => priorVariance * Math.exp(-((node.x - gx) ** 2 + (node.y - gy) ** 2) / (2 * lengthScale * lengthScale)));
1908
+ let value = mean;
1909
+ for (let i = 0; i < n; i++) value += k[i] * alpha[i];
1910
+ const white = forward(lower, k);
1911
+ let explained = 0;
1912
+ for (const entry of white) explained += entry * entry;
1913
+ const variance = Math.max(0, priorVariance - explained);
1914
+ sdRow.push(round9(Math.sqrt(variance)));
1915
+ if (variance <= priorVariance / 2) {
1916
+ row.push(round9(value));
1917
+ supported += 1;
1918
+ } else row.push(null);
1919
+ }
1920
+ values.push(row);
1921
+ sd.push(sdRow);
1922
+ }
1923
+ return {
1924
+ grid: {
1925
+ columns,
1926
+ rows,
1927
+ x: [round9(x0), round9(x1)],
1928
+ y: [round9(y0), round9(y1)],
1929
+ values,
1930
+ sd,
1931
+ supportedCells: supported,
1932
+ model: {
1933
+ mean: round9(mean),
1934
+ priorVariance: round9(priorVariance),
1935
+ lengthScale: round9(lengthScale),
1936
+ logLikelihood: round9(best.logLikelihood),
1937
+ independentLogLikelihood: round9(independent),
1938
+ fitNodes: n
1939
+ },
1940
+ method: GRID_METHOD
1941
+ },
1942
+ insufficient: null
1943
+ };
1944
+ }
1945
+ /** Log likelihood of scores drawn independently around a generalized
1946
+ * least-squares mean, each with the prior variance plus its own noise. */
1947
+ function independentLogLikelihood(y, noise, priorVariance) {
1948
+ let numerator = 0;
1949
+ let denominator = 0;
1950
+ for (let i = 0; i < y.length; i++) {
1951
+ const variance = priorVariance + noise[i];
1952
+ numerator += y[i] / variance;
1953
+ denominator += 1 / variance;
1954
+ }
1955
+ const mean = numerator / denominator;
1956
+ let total = 0;
1957
+ for (let i = 0; i < y.length; i++) {
1958
+ const variance = priorVariance + noise[i];
1959
+ total += -.5 * ((y[i] - mean) ** 2 / variance + Math.log(2 * Math.PI * variance));
1960
+ }
1961
+ return total;
1962
+ }
1963
+ /** Solves L z = b for lower-triangular L. */
1964
+ function forward(lower, b) {
1965
+ const n = lower.length;
1966
+ const z = new Array(n).fill(0);
1967
+ for (let i = 0; i < n; i++) {
1968
+ const row = lower[i];
1969
+ let value = b[i];
1970
+ for (let k = 0; k < i; k++) value -= row[k] * z[k];
1971
+ z[i] = value / row[i];
1972
+ }
1973
+ return z;
1974
+ }
1975
+ /** Solves Lᵀ x = z for lower-triangular L. */
1976
+ function backward(lower, z) {
1977
+ const n = lower.length;
1978
+ const x = new Array(n).fill(0);
1979
+ for (let i = n - 1; i >= 0; i--) {
1980
+ let value = z[i];
1981
+ for (let k = i + 1; k < n; k++) value -= lower[k][i] * x[k];
1982
+ x[i] = value / lower[i][i];
1983
+ }
1984
+ return x;
1985
+ }
1986
+ function cellOf(grid, x, y) {
1987
+ const c = Math.min(grid.columns - 1, Math.max(0, Math.floor((x - grid.x[0]) / (grid.x[1] - grid.x[0]) * grid.columns)));
1988
+ const r = Math.min(grid.rows - 1, Math.max(0, Math.floor((y - grid.y[0]) / (grid.y[1] - grid.y[0]) * grid.rows)));
1989
+ return grid.values[r][c] === null ? null : r * grid.columns + c;
1990
+ }
1991
+ /**
1992
+ * Basins on the k-nearest-neighbour graph of the scored, placed nodes, by
1993
+ * ToMATo's sweep. Nodes enter from the highest score down (earlier
1994
+ * registration first on a tie). A node with no entered neighbour starts a
1995
+ * component, of which it is the peak; otherwise it joins its highest entered
1996
+ * neighbour's component. Where it touches another component, the one with
1997
+ * the lower peak merges into the higher unless its peak stands above this
1998
+ * node, the saddle, by two standard errors of their difference; a component
1999
+ * that survives its first saddle keeps that persistence. Every component left
2000
+ * at the end is a basin, labelled by its peak.
2001
+ */
2002
+ function graphBasins(records, scoredNodes, fullOf, space, pooledVariance, degreesOfFreedom) {
2003
+ const empty = (reason) => ({
2004
+ count: null,
2005
+ neighbours: 0,
2006
+ localMaxima: 0,
2007
+ z: null,
2008
+ graphNodes: 0,
2009
+ graphAxes: 0,
2010
+ graphExplained: null,
2011
+ method: BASIN_METHOD,
2012
+ insufficient: reason,
2013
+ peaks: []
2014
+ });
2015
+ let scored = scoredNodes;
2016
+ if (scored.length < SURFACE_MIN_NODES) return empty(`${plural$1(scored.length, "placed node")} share 2 or more units with the root; basins need ${SURFACE_MIN_NODES}`);
2017
+ if (pooledVariance === null || pooledVariance <= 0) return empty("no pooled between-unit variance, so the noise a peak must clear is unknown");
2018
+ const sample = new Set(evenSample(scored, BASIN_NODES));
2019
+ for (const node of [...scored].sort((left, right) => right.score - left.score || left.ordinal - right.ordinal).slice(0, BASIN_TOP)) sample.add(node);
2020
+ scored = scoredNodes.filter((node) => sample.has(node));
2021
+ const k = Math.min(BASIN_NEIGHBOURS, scored.length - 1);
2022
+ const points = scored.map((node) => fullOf.get(node.nodeId) ?? [node.x, node.y]);
2023
+ const adjacency = connectComponents(points, nearestNeighbours(points, k));
2024
+ const variance = (index) => pooledVariance / scored[index].pairs;
2025
+ const order = scored.map((_, index) => index).sort((left, right) => scored[right].score - scored[left].score || scored[left].ordinal - scored[right].ordinal);
2026
+ const rank = new Int32Array(scored.length);
2027
+ order.forEach((index, position) => {
2028
+ rank[index] = position;
2029
+ });
2030
+ const parent = new Int32Array(scored.length).fill(-1);
2031
+ const find = (index) => {
2032
+ let root = index;
2033
+ while (parent[root] !== root) root = parent[root];
2034
+ let at = index;
2035
+ while (parent[at] !== root) {
2036
+ const next = parent[at];
2037
+ parent[at] = root;
2038
+ at = next;
2039
+ }
2040
+ return root;
2041
+ };
2042
+ let maxima = 0;
2043
+ for (let index = 0; index < scored.length; index++) if (adjacency[index].every((neighbour) => rank[neighbour] > rank[index])) maxima += 1;
2044
+ const z = zQuantile(1 - BASIN_ALPHA / Math.max(1, maxima - 1));
2045
+ const survived = /* @__PURE__ */ new Map();
2046
+ for (const index of order) {
2047
+ let highest = -1;
2048
+ for (const neighbour of adjacency[index]) {
2049
+ if (parent[neighbour] === -1) continue;
2050
+ if (highest === -1 || rank[neighbour] < rank[highest]) highest = neighbour;
2051
+ }
2052
+ if (highest === -1) {
2053
+ parent[index] = index;
2054
+ continue;
2055
+ }
2056
+ parent[index] = find(highest);
2057
+ for (const neighbour of adjacency[index]) {
2058
+ if (parent[neighbour] === -1) continue;
2059
+ const mine = find(index);
2060
+ const theirs = find(neighbour);
2061
+ if (mine === theirs) continue;
2062
+ const [high, low] = rank[mine] < rank[theirs] ? [mine, theirs] : [theirs, mine];
2063
+ const persistence = scored[low].score - scored[index].score;
2064
+ const threshold = z * Math.sqrt(variance(low) + variance(index));
2065
+ if (persistence < threshold) parent[low] = high;
2066
+ else if (!survived.has(low)) survived.set(low, {
2067
+ saddle: index,
2068
+ persistence,
2069
+ threshold
2070
+ });
2071
+ }
2072
+ }
2073
+ const peaks = [...new Set(order.map(find))].sort((left, right) => rank[left] - rank[right]);
2074
+ const basinOf = new Map(peaks.map((peak, basin) => [peak, basin]));
2075
+ const members = new Array(peaks.length).fill(0);
2076
+ const byNode = /* @__PURE__ */ new Map();
2077
+ scored.forEach((node, index) => {
2078
+ const basin = basinOf.get(find(index));
2079
+ byNode.set(node.nodeId, basin);
2080
+ members[basin] += 1;
2081
+ });
2082
+ for (const record of records) record.basin = byNode.get(record.nodeId) ?? null;
2083
+ return {
2084
+ count: peaks.length,
2085
+ neighbours: k,
2086
+ localMaxima: maxima,
2087
+ z: round9(z),
2088
+ graphNodes: scored.length,
2089
+ graphAxes: space.axes,
2090
+ graphExplained: space.explained === null ? null : round9(space.explained),
2091
+ method: `${BASIN_METHOD}; pooled over ${degreesOfFreedom} degrees of freedom`,
2092
+ insufficient: null,
2093
+ peaks: peaks.map((peak, basin) => {
2094
+ const node = scored[peak];
2095
+ const own = survived.get(peak) ?? null;
2096
+ return {
2097
+ peak: {
2098
+ nodeId: node.nodeId,
2099
+ x: node.x,
2100
+ y: node.y,
2101
+ score: node.score,
2102
+ pairs: node.pairs
2103
+ },
2104
+ persistence: own === null ? null : round9(own.persistence),
2105
+ threshold: own === null ? null : round9(own.threshold),
2106
+ saddle: own === null ? null : scored[own.saddle].nodeId,
2107
+ nodes: members[basin]
2108
+ };
2109
+ })
2110
+ };
2111
+ }
2112
+ const BASIN_NEIGHBOURS = 6;
2113
+ /** The basin graph's space: leading MDS axes up to this share of the positive
2114
+ * eigenvalue mass, and at most this many. */
2115
+ const FULL_AXES_MASS = .8;
2116
+ const FULL_AXES_MAX = 8;
2117
+ /** Family-wise rate of a spurious basin on a flat landscape. */
2118
+ const BASIN_ALPHA = .05;
2119
+ /**
2120
+ * Joins a graph's components into one (Borůvka): each round links every
2121
+ * component to its nearest point in another component by one edge, until
2122
+ * one component remains. Without it, two regions the k-nearest-neighbour
2123
+ * graph leaves apart would each count as a basin with no saddle to test.
2124
+ */
2125
+ function connectComponents(points, adjacency) {
2126
+ const n = points.length;
2127
+ const parent = Int32Array.from({ length: n }, (_, index) => index);
2128
+ const find = (index) => {
2129
+ let root = index;
2130
+ while (parent[root] !== root) root = parent[root];
2131
+ while (parent[index] !== root) {
2132
+ const next = parent[index];
2133
+ parent[index] = root;
2134
+ index = next;
2135
+ }
2136
+ return root;
2137
+ };
2138
+ for (let a = 0; a < n; a++) for (const b of adjacency[a]) parent[find(a)] = find(b);
2139
+ const edges = adjacency.map((list) => new Set(list));
2140
+ for (;;) {
2141
+ const component = Int32Array.from({ length: n }, (_, index) => find(index));
2142
+ const best = /* @__PURE__ */ new Map();
2143
+ for (let a = 0; a < n; a++) {
2144
+ const own = component[a];
2145
+ for (let b = 0; b < n; b++) {
2146
+ if (component[b] === own) continue;
2147
+ const distance = euclidean(points[a], points[b]);
2148
+ const known = best.get(own);
2149
+ if (!known || distance < known.distance) best.set(own, {
2150
+ a,
2151
+ b,
2152
+ distance
2153
+ });
2154
+ }
2155
+ }
2156
+ if (best.size === 0) break;
2157
+ for (const { a, b } of best.values()) {
2158
+ edges[a].add(b);
2159
+ edges[b].add(a);
2160
+ parent[find(a)] = find(b);
2161
+ }
2162
+ }
2163
+ return edges.map((set) => [...set].sort((left, right) => left - right));
2164
+ }
2165
+ /**
2166
+ * The symmetric k-nearest-neighbour graph of points in any dimension: each
2167
+ * point joins its k nearest (ties by index) and every edge is kept in both
2168
+ * directions. Brute force, so callers keep n in the low thousands.
2169
+ */
2170
+ function nearestNeighbours(points, k) {
2171
+ const n = points.length;
2172
+ const adjacency = Array.from({ length: n }, () => /* @__PURE__ */ new Set());
2173
+ if (n < 2 || k < 1) return adjacency.map(() => []);
2174
+ for (let index = 0; index < n; index++) {
2175
+ const best = [];
2176
+ for (let other = 0; other < n; other++) {
2177
+ if (other === index) continue;
2178
+ const distance = euclidean(points[index], points[other]);
2179
+ if (best.length === k && distance >= best[k - 1].distance) continue;
2180
+ best.push({
2181
+ index: other,
2182
+ distance
2183
+ });
2184
+ best.sort((left, right) => left.distance - right.distance || left.index - right.index);
2185
+ if (best.length > k) best.length = k;
2186
+ }
2187
+ for (const { index: other } of best) {
2188
+ adjacency[index].add(other);
2189
+ adjacency[other].add(index);
2190
+ }
2191
+ }
2192
+ return adjacency.map((set) => [...set].sort((left, right) => left - right));
2193
+ }
2194
+ function euclidean(left, right) {
2195
+ let sum = 0;
2196
+ for (let d = 0; d < left.length; d++) sum += (left[d] - (right[d] ?? 0)) ** 2;
2197
+ return Math.sqrt(sum);
2198
+ }
2199
+ const SHADES = " .:-=+*#%@";
2200
+ /** The landscape as text for a terminal or a proposer's context: the same
2201
+ * numbers the view draws, with a coarse shaded map. */
2202
+ function formatLandscape(lens) {
2203
+ const { data } = lens;
2204
+ const lines = [];
2205
+ const { embedding } = data;
2206
+ lines.push(`Landscape (${data.split} split, larger is better; ${embedding.name}: ${embedding.method})`);
2207
+ lines.push(` placed ${embedding.placed} of ${embedding.placed + embedding.unplaced} nodes by landmark MDS on ${plural$1(embedding.landmarks, "landmark")}${embedding.landmarkDistance ? ` (landmarks lie a median ${fixed(embedding.landmarkDistance.median)} and at most ${fixed(embedding.landmarkDistance.max)} apart)` : ""}; two axes hold ${percent(embedding.explained)} of the positive eigenvalue mass (negative mass ${percent(embedding.negativeMass)})`);
2208
+ const reasons = /* @__PURE__ */ new Map();
2209
+ for (const node of data.nodes) if (node.unplaced) reasons.set(node.unplaced, (reasons.get(node.unplaced) ?? 0) + 1);
2210
+ for (const [reason, count] of reasons) lines.push(` unplaced ${count}: ${reason}`);
2211
+ const methods = /* @__PURE__ */ new Map();
2212
+ for (const node of data.nodes) methods.set(node.method, (methods.get(node.method) ?? 0) + 1);
2213
+ lines.push(` scores (improvement over the root on shared units): ${[
2214
+ "bootstrap",
2215
+ "descriptive",
2216
+ "insufficient",
2217
+ "none"
2218
+ ].map((method) => `${methods.get(method) ?? 0} ${method}`).join(", ")}`);
2219
+ if (data.grid === null) lines.push(` surface: insufficient: ${data.gridInsufficient}`);
2220
+ else {
2221
+ const { grid } = data;
2222
+ lines.push(` surface: ${grid.columns}×${grid.rows} grid kriged from ${plural$1(grid.model.fitNodes, "node")} (mean ${signed(grid.model.mean)}, prior sd ${fixed(Math.sqrt(grid.model.priorVariance))}, length scale ${fixed(grid.model.lengthScale)}); ${grid.supportedCells} of ${grid.columns * grid.rows} cells supported (posterior variance at most half the prior)`);
2223
+ }
2224
+ const { basins } = data;
2225
+ if (basins.count === null) lines.push(` basins: insufficient: ${basins.insufficient}`);
2226
+ else {
2227
+ lines.push(` basins: ${basins.count} on the ${basins.neighbours}-nearest-neighbour graph of ${plural$1(basins.graphNodes, "node")} in ${basins.graphAxes} MDS axes holding ${percent(basins.graphExplained)} of the distance structure (a lower peak counts when it stands ${fixed(basins.z)} standard errors of the difference above its saddle, Bonferroni over ${basins.localMaxima} local ${basins.localMaxima === 1 ? "maximum" : "maxima"} at 5%)`);
2228
+ basins.peaks.forEach((basin, index) => {
2229
+ const standing = basin.persistence === null ? index === 0 ? "the highest" : "a separate region of the graph" : `persistence ${fixed(basin.persistence)} ≥ ${fixed(basin.threshold)} over saddle ${basin.saddle}`;
2230
+ lines.push(` ${index + 1}. ${basin.peak.nodeId} ${signed(basin.peak.score)} on ${basin.peak.pairs} units at (${fixed(basin.peak.x)}, ${fixed(basin.peak.y)}), ${standing}; ${plural$1(basin.nodes, "node")}`);
2231
+ });
2232
+ }
2233
+ const { plateau } = data;
2234
+ if (plateau.value === null) lines.push(` plateau: insufficient: ${plateau.insufficient}`);
2235
+ else {
2236
+ const verdict = plateau.value < 1 ? "on a plateau" : "still climbing";
2237
+ lines.push(` plateau: ${fixed(plateau.value)}: the best improvement over the root rose ${signed(plateau.rise)} across the last ${plateau.window} of ${plateau.accepted} accepted nodes, one standard error ${fixed(plateau.noise)} (best ${plateau.best.nodeId} on ${plateau.best.pairs} units, ${plateau.degreesOfFreedom} pooled df): ${verdict}`);
2238
+ }
2239
+ if (data.grid !== null) {
2240
+ const { grid } = data;
2241
+ const present = grid.values.flat().filter((value) => value !== null);
2242
+ const low = Math.min(...present);
2243
+ const high = Math.max(...present);
2244
+ const marks = /* @__PURE__ */ new Map();
2245
+ basins.peaks.forEach((basin, index) => {
2246
+ const cell = cellOf(grid, basin.peak.x, basin.peak.y);
2247
+ if (cell !== null) marks.set(cell, String((index + 1) % 10));
2248
+ });
2249
+ lines.push(` map (top row is the highest y; shade ${JSON.stringify(SHADES)} runs from ${signed(low)} to ${signed(high)}; digits mark each basin's peak node):`);
2250
+ for (let r = grid.rows - 1; r >= 0; r--) {
2251
+ let row = "";
2252
+ for (let c = 0; c < grid.columns; c++) {
2253
+ const mark = marks.get(r * grid.columns + c);
2254
+ const value = grid.values[r][c];
2255
+ if (mark) row += mark;
2256
+ else if (value === null || value === void 0) row += " ";
2257
+ else {
2258
+ const level = high > low ? (value - low) / (high - low) : .5;
2259
+ row += SHADES[Math.min(9, Math.floor(level * 10))];
2260
+ }
2261
+ }
2262
+ lines.push(` |${row}|`);
2263
+ }
2264
+ }
2265
+ return lines.join("\n");
2266
+ }
2267
+ function percent(value) {
2268
+ return value === null ? "unknown" : `${Math.round(value * 1e3) / 10}%`;
2269
+ }
2270
+ function fixed(value) {
2271
+ return value.toFixed(3);
2272
+ }
2273
+ function signed(value) {
2274
+ return `${value >= 0 ? "+" : ""}${value.toFixed(3)}`;
2275
+ }
2276
+ //#endregion
2277
+ //#region src/search/lenses/meta-search.ts
2278
+ /**
2279
+ * `metaSearch(searches)`: a search of searches (search-tree-design §12).
2280
+ *
2281
+ * Every search is one node. Its genome is the configuration that ran it: the
2282
+ * expansion policy and allocator its header declares (their names carry
2283
+ * their parameters, such as patience, operator weights and rung sizes), its
2284
+ * budget, and the proposer its edges and candidate-generation operations
2285
+ * name, with the proposer's model. Its score is the held-out lift its claim
2286
+ * measured, per known dollar of the whole search.
2287
+ *
2288
+ * The held-out lift is `estimateNode` of one node against the root on the
2289
+ * sealed test split, from the test cells themselves: the shipped node on
2290
+ * `ship`; on `hold`, the first finalist not decided invalid, in the claim's
2291
+ * order, which the claim fixed from selection data before any test cell ran.
2292
+ * A hold's measured lift counts rather than being dropped: keeping only the
2293
+ * searches whose test was significant would inflate a configuration's mean.
2294
+ * A search whose claim is open, missing, `test-cannot-resolve`, contradicted
2295
+ * by its own ledger (`verifySearchClaim`), held with no finalist tested, or
2296
+ * whose lift pairs fewer than 2 test units is unscored with its reason, never
2297
+ * 0, and every configuration reports how many of its searches were scored.
2298
+ *
2299
+ * Dollars are the search's committed spend: every cell and operation, the
2300
+ * claim's test cells included. An unknown-cost cell or operation leaves only a
2301
+ * floor, so that search's lift per dollar is a bound, not a value, and stays
2302
+ * out of every estimate. A search that recorded no spend has none either.
2303
+ *
2304
+ * Searches arrange into a forest by derivation (`derivedFrom`: a search that
2305
+ * started from a node of another) and containment (`containment`: a search
2306
+ * that ran inside a cell of another). A search contained in an outer search's
2307
+ * cell belongs to that cell's node: the outer node is its configuration, and
2308
+ * the outer node's lineage is the configuration's lineage.
2309
+ *
2310
+ * Configurations group searches with one genome and one objective (subject,
2311
+ * metric, direction). A configuration's lift per dollar summarizes its scored
2312
+ * searches as independent units with `summarizeSamples` (a percentile
2313
+ * bootstrap of the mean); a configuration with one scored search carries that
2314
+ * search's own paired interval, whose units are its test units. The signal
2315
+ * names the configuration with the largest estimate within one objective.
2316
+ */
2317
+ /** The signal's name, as a policy or a view reads it. */
2318
+ const META_SEARCH_SIGNAL = "metaSearch.bestPolicyConfiguration";
2319
+ const DEFAULT_GLYPH_NODES = 64;
2320
+ /** The per-search score `metaSearchScore` computes. An outer search names it
2321
+ * as its objective's judge. */
2322
+ const META_SEARCH_SCORE_SOURCE = {
2323
+ uri: "npm:@tangle-network/agent-eval#metaSearchScore",
2324
+ revision: hashCanonical({
2325
+ name: "tangle.meta-search-score.2026-09",
2326
+ node: "the claim selection on ship; on hold, the first finalist in claim order not decided invalid",
2327
+ lift: "estimateNode(node, against root, split test), oriented so a gain is positive",
2328
+ unscored: [
2329
+ "open",
2330
+ "no claim",
2331
+ "claim the ledger contradicts (verifySearchClaim mismatch)",
2332
+ "test-cannot-resolve",
2333
+ "hold with no finalist tested",
2334
+ "fewer than 2 shared test units"
2335
+ ],
2336
+ dollars: "committed spend of every cell and operation, claim cells included",
2337
+ unknownCost: "a floor only: the lift per dollar is a bound and enters no estimate"
2338
+ })
2339
+ };
2340
+ /**
2341
+ * The genome of a search: the configuration its ledger records. Declared
2342
+ * parts come from the header; the proposer comes from what the edges and
2343
+ * candidate-generation operations name, because the header declares none.
2344
+ */
2345
+ function searchPolicyGenome(state) {
2346
+ const header = state.header;
2347
+ if (!header) throw new Error(`search ${state.searchId} has not been opened`);
2348
+ const { maxUsd, maxCells, maxNodes, maxConcurrency, reservedClaimUsd } = header.budget;
2349
+ const proposers = /* @__PURE__ */ new Map();
2350
+ const operations = /* @__PURE__ */ new Set();
2351
+ for (const edge of state.edges()) {
2352
+ if (!edge.proposer) continue;
2353
+ const { kind, name, source, operationId } = edge.proposer;
2354
+ const proposer = {
2355
+ kind,
2356
+ name,
2357
+ source
2358
+ };
2359
+ proposers.set(hashCanonical(proposer), proposer);
2360
+ if (operationId !== null) operations.add(operationId);
2361
+ }
2362
+ const executions = /* @__PURE__ */ new Map();
2363
+ for (const operationId of operations) {
2364
+ const execution = state.operation(operationId)?.execution;
2365
+ if (!execution) continue;
2366
+ const entry = execution.kind === "model" ? {
2367
+ kind: "model",
2368
+ model: execution.model
2369
+ } : {
2370
+ kind: "deterministic",
2371
+ source: execution.source
2372
+ };
2373
+ executions.set(hashCanonical(entry), entry);
2374
+ }
2375
+ return {
2376
+ expansion: withoutSearchSeed(header.policy.expansion, header.policy.seed),
2377
+ allocation: withoutSearchSeed(header.policy.allocation, header.policy.seed),
2378
+ budget: {
2379
+ maxUsd,
2380
+ maxCells,
2381
+ maxNodes,
2382
+ maxConcurrency,
2383
+ reservedClaimUsd
2384
+ },
2385
+ proposers: proposers.size > 0 ? sortedValues(proposers) : { unknown: "the search recorded no proposal, so its proposer is not in the ledger" },
2386
+ proposerExecution: executions.size > 0 ? sortedValues(executions) : { unknown: "no recorded candidate-generation operation names what ran the proposer" }
2387
+ };
2388
+ }
2389
+ /**
2390
+ * A search's held-out lift per known dollar, from its claim and its test
2391
+ * cells. Pure; it reads the test split, so a policy view (which holds no test
2392
+ * cells) cannot call it; an outer search reads it from a closed inner search.
2393
+ */
2394
+ function metaSearchScore(state) {
2395
+ return scoreSearch(state, state.closed?.claim ? verifySearchClaim(state) : null);
2396
+ }
2397
+ function scoreSearch(state, verification) {
2398
+ const header = state.header;
2399
+ if (!header) throw new Error(`search ${state.searchId} has not been opened`);
2400
+ const { spend: audit } = state.audit;
2401
+ const spend = {
2402
+ knownUsd: audit.knownUsd,
2403
+ floorUsd: audit.floorUsd,
2404
+ unknownCost: audit.unknownCostCells + audit.unknownCostOperations
2405
+ };
2406
+ const unscored = (reason, detail, decision = null) => ({
2407
+ status: "unscored",
2408
+ decision,
2409
+ reason,
2410
+ detail,
2411
+ spend
2412
+ });
2413
+ const closed = state.closed;
2414
+ if (!closed) return unscored("open", "the search is open; its claim is made when it closes");
2415
+ const claim = closed.claim;
2416
+ if (!claim) return unscored("no-claim", `the search closed (${closed.reason}) without a claim on a test split`);
2417
+ if (verification?.status === "mismatch") return unscored("claim-mismatch", `the claim differs from what its ledger supports: ${verification.differences.join("; ")}`, claim.decision);
2418
+ if (claim.decision === "test-cannot-resolve") return unscored("test-cannot-resolve", claim.reason, claim.decision);
2419
+ const root = state.rootNodeId;
2420
+ const nodeId = claim.decision === "ship" ? claim.selected : claim.finalists.find((finalist) => state.node(finalist.nodeId)?.status !== "invalid")?.nodeId ?? null;
2421
+ if (nodeId === null || nodeId === root) return unscored("no-valid-finalist", claim.finalists.length === 0 ? `hold: no finalist went to test (${claim.reason})` : `hold: every finalist was decided invalid (${claim.finalists.length})`, claim.decision);
2422
+ const estimate = estimateNode(state, nodeId, {
2423
+ against: root,
2424
+ split: "test"
2425
+ });
2426
+ if (estimate.delta === null) return unscored("insufficient", `${nodeId} and the root share ${estimate.pairs} scored test unit${estimate.pairs === 1 ? "" : "s"}; a lift needs 2`, claim.decision);
2427
+ const maximize = header.objective.direction === "maximize";
2428
+ const lift = orient(estimate.delta, maximize);
2429
+ const liftInterval = estimate.interval ? orientInterval(estimate.interval, maximize) : null;
2430
+ return {
2431
+ status: "scored",
2432
+ decision: claim.decision,
2433
+ nodeId,
2434
+ estimate,
2435
+ lift,
2436
+ liftInterval,
2437
+ spend,
2438
+ liftPerUsd: liftPerUsd(lift, liftInterval, spend)
2439
+ };
2440
+ }
2441
+ /** The lens: every search as a node of a search of searches. */
2442
+ function metaSearch(searches, options = {}) {
2443
+ const glyphNodes = options.glyphNodes ?? DEFAULT_GLYPH_NODES;
2444
+ if (!Number.isSafeInteger(glyphNodes) || glyphNodes < 0) throw new TypeError(`metaSearch: glyphNodes must be a non-negative integer, got ${glyphNodes}`);
2445
+ const byId = /* @__PURE__ */ new Map();
2446
+ for (const state of searches) {
2447
+ if (!state.header) throw new Error(`metaSearch: search ${state.searchId} has not been opened`);
2448
+ if (byId.has(state.searchId)) throw new Error(`metaSearch: search ${state.searchId} was given twice`);
2449
+ byId.set(state.searchId, state);
2450
+ }
2451
+ const entries = searches.map((state) => entry(state, byId, glyphNodes));
2452
+ const entryById = new Map(entries.map((item) => [item.searchId, item]));
2453
+ for (const item of entries) if (item.parent?.present) entryById.get(item.parent.searchId).children.push(item.searchId);
2454
+ const roots = entries.filter((item) => !item.parent?.present).map((item) => item.searchId);
2455
+ const assignDepth = (searchId, depth, seen) => {
2456
+ if (seen.has(searchId)) return;
2457
+ seen.add(searchId);
2458
+ const item = entryById.get(searchId);
2459
+ item.depth = depth;
2460
+ for (const child of item.children) assignDepth(child, depth + 1, seen);
2461
+ };
2462
+ const seen = /* @__PURE__ */ new Set();
2463
+ for (const root of roots) assignDepth(root, 0, seen);
2464
+ const configurations = configure(entries, byId);
2465
+ const objectives = [...new Set(entries.map((item) => item.objectiveKey))].sort(compareCodeUnits);
2466
+ const { objective, value } = best(configurations, objectives, options.objective);
2467
+ return {
2468
+ data: {
2469
+ searches: entries,
2470
+ roots,
2471
+ configurations,
2472
+ objectives,
2473
+ signalObjective: objective
2474
+ },
2475
+ signal: {
2476
+ name: META_SEARCH_SIGNAL,
2477
+ value
2478
+ }
2479
+ };
2480
+ }
2481
+ function entry(state, byId, glyphNodes) {
2482
+ const header = state.header;
2483
+ const genome = searchPolicyGenome(state);
2484
+ const closed = state.closed;
2485
+ let claim = null;
2486
+ const verification = closed?.claim ? verifySearchClaim(state) : null;
2487
+ if (closed?.claim) claim = {
2488
+ decision: closed.claim.decision,
2489
+ selected: closed.claim.selected,
2490
+ verification: verification?.status ?? "unknown",
2491
+ reason: closed.claim.reason
2492
+ };
2493
+ const nodes = state.nodes();
2494
+ const cells = state.cells();
2495
+ return {
2496
+ searchId: state.searchId,
2497
+ subject: header.subject,
2498
+ objective: {
2499
+ metric: header.objective.metric,
2500
+ direction: header.objective.direction
2501
+ },
2502
+ objectiveKey: objectiveKey(header),
2503
+ status: closed ? "closed" : "open",
2504
+ closeReason: closed?.reason ?? null,
2505
+ genome,
2506
+ genomeDigest: hashCanonical(genome),
2507
+ seed: header.policy.seed,
2508
+ rootArtifactDigest: nodes[0]?.artifactDigest ?? null,
2509
+ nodes: nodes.length,
2510
+ cells: {
2511
+ settled: state.audit.cells.settled,
2512
+ test: cells.filter((cell) => cell.split === "test").length
2513
+ },
2514
+ claim,
2515
+ score: scoreSearch(state, verification),
2516
+ parent: parentOf(state, byId),
2517
+ children: [],
2518
+ depth: 0,
2519
+ glyph: {
2520
+ nodes: nodes.slice(0, glyphNodes).map((node) => [
2521
+ node.nodeId,
2522
+ node.primaryParentId,
2523
+ node.status
2524
+ ]),
2525
+ omitted: Math.max(0, nodes.length - glyphNodes)
2526
+ }
2527
+ };
2528
+ }
2529
+ /** `subject · metric (direction)`: the key searches are compared within. */
2530
+ function objectiveKey(header) {
2531
+ return `${header.subject} · ${header.objective.metric} (${header.objective.direction})`;
2532
+ }
2533
+ function parentOf(state, byId) {
2534
+ const header = state.header;
2535
+ if (header.containment) {
2536
+ const { searchId, cellId, attempt } = header.containment;
2537
+ const outer = byId.get(searchId);
2538
+ return {
2539
+ relation: "contained",
2540
+ searchId,
2541
+ cellId,
2542
+ attempt,
2543
+ nodeId: outer?.cell(cellId)?.nodeId ?? null,
2544
+ present: outer !== void 0
2545
+ };
2546
+ }
2547
+ if (header.derivedFrom) {
2548
+ const { searchId, nodeId } = header.derivedFrom;
2549
+ return {
2550
+ relation: "derived",
2551
+ searchId,
2552
+ nodeId,
2553
+ present: byId.has(searchId)
2554
+ };
2555
+ }
2556
+ return null;
2557
+ }
2558
+ function configure(entries, byId) {
2559
+ const groups = /* @__PURE__ */ new Map();
2560
+ for (const item of entries) {
2561
+ const key = configurationKey(item.objectiveKey, item.genomeDigest);
2562
+ const group = groups.get(key);
2563
+ if (group) group.push(item);
2564
+ else groups.set(key, [item]);
2565
+ }
2566
+ const genomeOfOuterNode = /* @__PURE__ */ new Map();
2567
+ for (const item of entries) if (item.parent?.relation === "contained" && item.parent.nodeId !== null) genomeOfOuterNode.set(`${item.parent.searchId}\u0000${item.parent.nodeId}`, item.genomeDigest);
2568
+ return [...groups.values()].map((group) => {
2569
+ const first = group[0];
2570
+ const decisions = {
2571
+ ship: 0,
2572
+ hold: 0,
2573
+ "test-cannot-resolve": 0,
2574
+ none: 0
2575
+ };
2576
+ const unscored = {};
2577
+ const values = [];
2578
+ let single = null;
2579
+ let bounded = 0;
2580
+ for (const item of group) {
2581
+ decisions[item.claim?.decision ?? "none"] += 1;
2582
+ const score = item.score;
2583
+ if (score.status === "unscored") {
2584
+ unscored[score.reason] = (unscored[score.reason] ?? 0) + 1;
2585
+ continue;
2586
+ }
2587
+ if (score.liftPerUsd.status === "bound") bounded += 1;
2588
+ if (score.liftPerUsd.status !== "known") continue;
2589
+ values.push(score.liftPerUsd.value);
2590
+ single = {
2591
+ basis: "test-units",
2592
+ n: score.estimate.pairs,
2593
+ value: score.liftPerUsd.value,
2594
+ method: score.estimate.method,
2595
+ interval: score.liftPerUsd.interval,
2596
+ intervalMethod: score.liftPerUsd.interval ? "paired-delta-test-over-usd" : null
2597
+ };
2598
+ }
2599
+ const estimate = values.length === 1 && single ? single : fromSummary(summarizeSamples(values, seedFromDigest(first.genomeDigest)));
2600
+ const outerNodes = /* @__PURE__ */ new Map();
2601
+ for (const item of group) {
2602
+ const parent = item.parent;
2603
+ if (parent?.relation !== "contained" || parent.nodeId === null) continue;
2604
+ const key = `${parent.searchId}\u0000${parent.nodeId}`;
2605
+ if (outerNodes.has(key)) continue;
2606
+ const outer = byId.get(parent.searchId);
2607
+ const parentNodeId = outer.node(parent.nodeId).primaryParentId;
2608
+ outerNodes.set(key, {
2609
+ searchId: parent.searchId,
2610
+ nodeId: parent.nodeId,
2611
+ parentNodeId,
2612
+ parentGenomeDigest: parentNodeId === null ? null : genomeOfOuterNode.get(`${parent.searchId}\u0000${parentNodeId}`) ?? null,
2613
+ againstParent: parentNodeId === null ? null : estimateNode(outer, parent.nodeId, {
2614
+ against: parentNodeId,
2615
+ split: rankingSplit(outer)
2616
+ })
2617
+ });
2618
+ }
2619
+ return {
2620
+ genomeDigest: first.genomeDigest,
2621
+ objectiveKey: first.objectiveKey,
2622
+ genome: first.genome,
2623
+ searches: group.map((item) => item.searchId),
2624
+ decisions,
2625
+ scored: values.length,
2626
+ bounded,
2627
+ unscored,
2628
+ estimate,
2629
+ outerNodes: [...outerNodes.values()]
2630
+ };
2631
+ }).sort((left, right) => rankValue(right.estimate.value) - rankValue(left.estimate.value) || right.scored - left.scored || compareCodeUnits(left.objectiveKey, right.objectiveKey) || compareCodeUnits(left.genomeDigest, right.genomeDigest));
2632
+ }
2633
+ function best(configurations, objectives, requested) {
2634
+ const empty = (objective, reason) => ({
2635
+ objective,
2636
+ value: {
2637
+ liftPerUsd: null,
2638
+ objectiveKey: objective,
2639
+ genomeDigest: null,
2640
+ genome: null,
2641
+ interval: null,
2642
+ method: null,
2643
+ intervalMethod: null,
2644
+ basis: null,
2645
+ n: 0,
2646
+ searches: 0,
2647
+ scored: 0,
2648
+ outerNode: null,
2649
+ reason
2650
+ }
2651
+ });
2652
+ if (configurations.length === 0) return empty(null, "no search was given");
2653
+ const measured = [...new Set(configurations.filter((item) => item.estimate.value !== null).map((item) => item.objectiveKey))].sort(compareCodeUnits);
2654
+ let objective;
2655
+ if (requested !== void 0) {
2656
+ if (!objectives.includes(requested)) return empty(requested, `no search given has objective ${requested}; the searches have ${objectives.join("; ")}`);
2657
+ objective = requested;
2658
+ } else if (measured.length > 1) return empty(null, `searches with a known lift per dollar span ${measured.length} objectives (${measured.join("; ")}); a lift in one metric does not compare with another, so name one`);
2659
+ else {
2660
+ objective = measured[0] ?? (objectives.length === 1 ? objectives[0] : "");
2661
+ if (objective === "") return empty(null, `no configuration has a known lift per dollar, across ${objectives.length} objectives`);
2662
+ }
2663
+ const within = configurations.filter((item) => item.objectiveKey === objective);
2664
+ const withValue = within.filter((item) => item.estimate.value !== null);
2665
+ const top = withValue[0];
2666
+ if (!top) {
2667
+ const unscored = within.reduce((total, item) => total + Object.values(item.unscored).reduce((sum, count) => sum + (count ?? 0), 0), 0);
2668
+ const bounded = within.reduce((total, item) => total + item.bounded, 0);
2669
+ return empty(objective, `no configuration of ${objective} has a known lift per dollar: ${unscored} unscored search${unscored === 1 ? "" : "es"} and ${bounded} with only a spend floor`);
2670
+ }
2671
+ const { estimate } = top;
2672
+ const runnerUp = withValue[1];
2673
+ const coverage = `${top.scored} of ${top.searches.length} of its searches scored`;
2674
+ return {
2675
+ objective,
2676
+ value: {
2677
+ liftPerUsd: estimate.value,
2678
+ objectiveKey: objective,
2679
+ genomeDigest: top.genomeDigest,
2680
+ genome: top.genome,
2681
+ interval: estimate.interval,
2682
+ method: estimate.method,
2683
+ intervalMethod: estimate.intervalMethod,
2684
+ basis: estimate.basis,
2685
+ n: estimate.n,
2686
+ searches: top.searches.length,
2687
+ scored: top.scored,
2688
+ outerNode: top.outerNodes[0] ? {
2689
+ searchId: top.outerNodes[0].searchId,
2690
+ nodeId: top.outerNodes[0].nodeId
2691
+ } : null,
2692
+ reason: runnerUp === void 0 ? `the only configuration with a known lift per dollar (${estimate.method}, ${estimate.basis} n=${estimate.n}; ${coverage})` : `the largest of ${withValue.length} configurations by estimated lift per dollar (${estimate.method}, ${estimate.basis} n=${estimate.n}; ${coverage}); a point ranking, not a test`
2693
+ }
2694
+ };
2695
+ }
2696
+ function liftPerUsd(lift, interval, spend) {
2697
+ const committed = spend.knownUsd + spend.floorUsd;
2698
+ if (spend.unknownCost > 0) {
2699
+ if (committed <= 0) return {
2700
+ status: "unknown",
2701
+ reason: `${spend.unknownCost} cost${spend.unknownCost === 1 ? " is" : "s are"} unknown with no proven floor`
2702
+ };
2703
+ return {
2704
+ status: "bound",
2705
+ value: lift / committed,
2706
+ bound: lift >= 0 ? "at-most" : "at-least",
2707
+ reason: `${spend.unknownCost} cost${spend.unknownCost === 1 ? " is" : "s are"} known only as a floor; $${round(committed)} is the least the search spent`
2708
+ };
2709
+ }
2710
+ if (spend.knownUsd <= 0) return {
2711
+ status: "unknown",
2712
+ reason: "the search recorded no spend, so a lift per dollar is undefined"
2713
+ };
2714
+ return {
2715
+ status: "known",
2716
+ value: lift / spend.knownUsd,
2717
+ interval: interval ? [interval[0] / spend.knownUsd, interval[1] / spend.knownUsd] : null
2718
+ };
2719
+ }
2720
+ function fromSummary(summary) {
2721
+ return {
2722
+ basis: "searches",
2723
+ n: summary.n,
2724
+ value: summary.mean,
2725
+ method: summary.n < 2 ? "none" : summary.method,
2726
+ interval: summary.interval,
2727
+ intervalMethod: summary.interval ? "percentile-bootstrap-of-mean" : null
2728
+ };
2729
+ }
2730
+ /**
2731
+ * The lens as text, for an agent: the signal, every configuration best first
2732
+ * with its coverage and interval, and the forest of searches with contained
2733
+ * searches grouped under the outer node that ran them. `agent-eval search show
2734
+ * --meta` prints it.
2735
+ */
2736
+ function renderMetaSearchText(result, options = {}) {
2737
+ const perGroup = options.searchesPerGroup ?? 5;
2738
+ const { data, signal } = result;
2739
+ const lines = [];
2740
+ const contained = data.searches.filter((item) => item.parent?.relation === "contained").length;
2741
+ const derived = data.searches.filter((item) => item.parent?.relation === "derived").length;
2742
+ lines.push(`meta-search: ${plural(data.searches.length, "search", "searches")} (${contained} contained, ${derived} derived) in ${plural(data.objectives.length, "objective")}, ${plural(data.configurations.length, "configuration")}`);
2743
+ const value = signal.value;
2744
+ lines.push(value.liftPerUsd === null ? `${signal.name}: unknown — ${value.reason}` : `${signal.name}: ${value.genomeDigest.slice(0, 19)} lift per known $ ${formatNumber(value.liftPerUsd)} ${formatInterval(value.interval, value.intervalMethod)} (${value.method}, ${value.basis} n=${value.n}); ${value.reason}`);
2745
+ const index = new Map(data.configurations.map((item, position) => [configurationKey(item.objectiveKey, item.genomeDigest), position]));
2746
+ for (const objective of data.objectives) {
2747
+ lines.push("", `configurations of ${objective}, best first:`);
2748
+ for (const configuration of data.configurations) {
2749
+ if (configuration.objectiveKey !== objective) continue;
2750
+ lines.push(...configurationLines(configuration, index));
2751
+ }
2752
+ }
2753
+ lines.push("", "searches, by derivation and containment:");
2754
+ const byId = new Map(data.searches.map((item) => [item.searchId, item]));
2755
+ const walk = (searchId, depth) => {
2756
+ const item = byId.get(searchId);
2757
+ const pad = " ".repeat(depth + 1);
2758
+ lines.push(`${pad}${searchLine(item, index)}`);
2759
+ const derivedChildren = item.children.filter((child) => byId.get(child).parent?.relation === "derived");
2760
+ for (const child of derivedChildren) walk(child, depth + 1);
2761
+ const groups = /* @__PURE__ */ new Map();
2762
+ for (const child of item.children) {
2763
+ const childItem = byId.get(child);
2764
+ if (childItem.parent?.relation !== "contained") continue;
2765
+ const key = childItem.parent.nodeId ?? "(unknown node)";
2766
+ const group = groups.get(key);
2767
+ if (group) group.push(childItem);
2768
+ else groups.set(key, [childItem]);
2769
+ }
2770
+ for (const [nodeId, group] of groups) {
2771
+ const shown = group.slice(0, perGroup);
2772
+ const tally = decisionTally(group);
2773
+ lines.push(`${pad} node ${nodeId}: ${plural(group.length, "contained search", "contained searches")} (${tally})`);
2774
+ for (const child of shown) walk(child.searchId, depth + 2);
2775
+ if (group.length > shown.length) lines.push(`${" ".repeat(depth + 3)}… ${group.length - shown.length} more`);
2776
+ }
2777
+ };
2778
+ for (const root of data.roots) walk(root, 0);
2779
+ return lines.join("\n");
2780
+ }
2781
+ function configurationLines(configuration, index) {
2782
+ const { estimate, genome } = configuration;
2783
+ const decisions = Object.entries(configuration.decisions).filter(([, count]) => count > 0).map(([decision, count]) => `${decision} ${count}`).join(", ");
2784
+ const unscored = Object.entries(configuration.unscored).map(([reason, count]) => `${reason} ${count}`).join(", ");
2785
+ const value = estimate.value === null ? "lift per known $: unknown" : `lift per known $ ${formatNumber(estimate.value)} ${formatInterval(estimate.interval, estimate.intervalMethod)} (${estimate.method}, ${estimate.basis} n=${estimate.n})`;
2786
+ const lines = [` #${index.get(configurationKey(configuration.objectiveKey, configuration.genomeDigest)) + 1} ${configuration.genomeDigest.slice(0, 19)} ${genomeLine(genome)}`, ` ${plural(configuration.searches.length, "search", "searches")}: ${decisions}; scored ${configuration.scored}${configuration.bounded > 0 ? `, spend floor only ${configuration.bounded}` : ""}${unscored ? `, unscored: ${unscored}` : ""}; ${value}`];
2787
+ for (const outer of configuration.outerNodes) {
2788
+ const against = outer.againstParent;
2789
+ const parentIndex = outer.parentGenomeDigest === null ? void 0 : index.get(configurationKey(configuration.objectiveKey, outer.parentGenomeDigest));
2790
+ lines.push(against === null ? ` outer node ${outer.nodeId} in ${outer.searchId}: the outer root` : ` outer node ${outer.nodeId} in ${outer.searchId}, from ${parentIndex === void 0 ? outer.parentNodeId : `#${parentIndex + 1}`}: ${estimateText(against)}`);
2791
+ }
2792
+ return lines;
2793
+ }
2794
+ function searchLine(item, index) {
2795
+ const claim = item.claim ? `claim ${item.claim.decision} (${item.claim.verification})` : "no claim";
2796
+ const configuration = index.get(configurationKey(item.objectiveKey, item.genomeDigest));
2797
+ const score = item.score;
2798
+ const scoreText = score.status === "unscored" ? `unscored: ${score.reason} — ${score.detail}` : `lift ${formatNumber(score.lift)} ${formatInterval(score.liftInterval, score.liftInterval ? "paired-delta-test" : null)} on ${score.estimate.pairs} test units (${score.estimate.method}); ${liftPerUsdText(score.liftPerUsd)}`;
2799
+ const relation = item.parent === null ? "" : item.parent.relation === "derived" ? ` derived from ${item.parent.searchId}/${item.parent.nodeId}${item.parent.present ? "" : " (not given)"};` : item.parent.present ? "" : ` contained in ${item.parent.searchId} cell ${item.parent.cellId} (not given);`;
2800
+ return `${item.searchId} [${item.status}${item.closeReason ? `: ${item.closeReason}` : ""}; ${claim}; config #${configuration === void 0 ? "?" : configuration + 1}; ${item.nodes} nodes, ${item.cells.settled} cells, $${round(item.score.spend.knownUsd)} known${item.score.spend.unknownCost > 0 ? ` + $${round(item.score.spend.floorUsd)} floor over ${item.score.spend.unknownCost} unknown` : ""}]${relation} ${scoreText}`;
2801
+ }
2802
+ function genomeLine(genome) {
2803
+ const budget = Object.entries(genome.budget).filter(([, value]) => value !== null && value !== 0).map(([key, value]) => `${key}=${value}`).join(" ");
2804
+ const proposers = Array.isArray(genome.proposers) ? genome.proposers.map((proposer) => `${proposer.kind}:${proposer.name}`).join(", ") : "unknown";
2805
+ const execution = Array.isArray(genome.proposerExecution) ? genome.proposerExecution.map((item) => item.kind === "model" ? `model ${item.model.provider}/${"snapshot" in item.model ? item.model.snapshot : `${item.model.alias} (snapshot unknown)`}` : "deterministic").join(", ") : "unknown";
2806
+ return `expansion=${genome.expansion} allocation=${genome.allocation}${budget ? ` ${budget}` : ""} proposer=${proposers} (${execution})`;
2807
+ }
2808
+ function decisionTally(group) {
2809
+ const counts = /* @__PURE__ */ new Map();
2810
+ for (const item of group) {
2811
+ const key = item.score.status === "scored" ? `${item.score.decision} scored` : `${item.claim?.decision ?? "no claim"} unscored`;
2812
+ counts.set(key, (counts.get(key) ?? 0) + 1);
2813
+ }
2814
+ return [...counts.entries()].sort(([left], [right]) => compareCodeUnits(left, right)).map(([key, count]) => `${key} ${count}`).join(", ");
2815
+ }
2816
+ function estimateText(estimate) {
2817
+ if (estimate.delta === null) return `unknown (${plural(estimate.pairs, "shared unit")})`;
2818
+ if (estimate.method === "insufficient") return `${formatSigned(estimate.delta)} (insufficient: ${estimate.pairs} of ${INSUFFICIENT_FROM} units)`;
2819
+ return `${formatSigned(estimate.delta)} ${formatInterval(estimate.interval, estimate.interval ? "paired-delta-test" : null)} (${estimate.method}, ${estimate.pairs} ${estimate.split} units)`;
2820
+ }
2821
+ function liftPerUsdText(value) {
2822
+ if (value.status === "unknown") return `per $: unknown (${value.reason})`;
2823
+ if (value.status === "bound") return `per $: ${value.bound} ${formatNumber(value.value)} (${value.reason})`;
2824
+ return `per $: ${formatNumber(value.value)}`;
2825
+ }
2826
+ function formatInterval(interval, method) {
2827
+ if (interval === null) return "[no interval]";
2828
+ return `[${formatNumber(interval[0])}, ${formatNumber(interval[1])}]${method ? ` ${method}` : ""}`;
2829
+ }
2830
+ function formatNumber(value) {
2831
+ if (value === 0) return "0";
2832
+ return Math.abs(value) >= .01 ? value.toFixed(4) : value.toPrecision(3);
2833
+ }
2834
+ function formatSigned(value) {
2835
+ return value > 0 ? `+${formatNumber(value)}` : formatNumber(value);
2836
+ }
2837
+ function plural(count, one, many = `${one}s`) {
2838
+ return `${count} ${count === 1 ? one : many}`;
2839
+ }
2840
+ /** A policy or allocator name carries `seed=<n>` when it is seeded from the
2841
+ * search's own seed; the genome names it `seed=<search>` so searches of one
2842
+ * configuration on different seeds share a genome. */
2843
+ function withoutSearchSeed(name, seed) {
2844
+ return name.replace(new RegExp(`\\bseed=${seed}(?![\\d.])`, "g"), "seed=<search>");
2845
+ }
2846
+ function orient(delta, maximize) {
2847
+ return maximize ? delta : delta === 0 ? 0 : -delta;
2848
+ }
2849
+ function orientInterval(interval, maximize) {
2850
+ return maximize ? interval : [orient(interval[1], false), orient(interval[0], false)];
2851
+ }
2852
+ function sortedValues(map) {
2853
+ return [...map.entries()].sort(([left], [right]) => compareCodeUnits(left, right)).map(([, value]) => value);
2854
+ }
2855
+ function configurationKey(objective, genomeDigest) {
2856
+ return `${objective}\u0000${genomeDigest}`;
2857
+ }
2858
+ function rankValue(value) {
2859
+ return value === null ? Number.NEGATIVE_INFINITY : value;
2860
+ }
2861
+ function round(usd) {
2862
+ return Math.round(usd * 1e6) / 1e6;
2863
+ }
2864
+ //#endregion
2865
+ //#region src/search/lenses/skill-manifold.ts
2866
+ const METHODS = {
2867
+ identity: {
2868
+ model: "masked matrix factorization: each observed per-unit mean is b_u + P_i·Q_u on standardized scores, fitted by alternating ridge least squares over observed cells only; missing cells are masked, never filled",
2869
+ intrinsic: "k-fold cross-validation over observed cells (folds by a hash of node and unit), mean squared error of held-out cells by rank; the intrinsic dimension is the smallest rank within one standard error (across folds) of the best",
2870
+ nextUnit: "expected share of the variance of each leader pair’s contrast on the whole split that one more cell on the unit, run on both leaders, removes; each leader’s coordinates have the linear-Gaussian ridge posterior with loadings held fixed and noise equal to the cross-validated cell error; averaged over leader pairs",
2871
+ explained: "in-sample R² of each rank over the rank-0 model (unit means only), on the fitted rows; it always grows with rank, which is why the intrinsic dimension is cross-validated"
2872
+ },
2873
+ logit: {
2874
+ model: "multidimensional two-parameter item-response model: a node passes a unit with probability logistic(b_u + P_i·Q_u), its passes on the unit binomial over its cells; fitted by alternating Newton steps on the likelihood with standard-normal priors on P and Q (ridge) and a N(0, 10²) prior on b; missing cells are masked, never filled",
2875
+ intrinsic: "k-fold cross-validation over observed node-unit pass counts (folds by a hash of node and unit), binomial log loss per held-out cell by rank; the intrinsic dimension is the smallest rank within one standard error (across folds) of the best",
2876
+ nextUnit: "expected share of the variance of each leader pair’s contrast (predicted pass rate on the whole split) that one more cell on the unit, run on both leaders, removes; each leader’s coordinates have the Laplace posterior of the item-response model with loadings held fixed, and a cell’s information is its binomial variance at the leader’s predicted pass rate; averaged over leader pairs",
2877
+ explained: "in-sample McFadden pseudo-R² of each rank over the rank-0 model (unit easiness only), on the fitted rows; it always grows with rank, which is why the intrinsic dimension is cross-validated"
2878
+ }
2879
+ };
2880
+ /** The prior precision on a unit's log-odds intercept: N(0, 10²), weak
2881
+ * enough to leave any observed pass rate alone and strong enough to keep a
2882
+ * unit every node passed (or failed) finite. */
2883
+ const LOGIT_BIAS_PRECISION = .01;
2884
+ /** The largest move one Newton step makes in any coordinate, in log-odds. */
2885
+ const NEWTON_STEP_CAP = 4;
2886
+ const MAX_ITERATIONS = 300;
2887
+ /** Relative change of the objective that ends a fit. */
2888
+ const TOLERANCE = 1e-7;
2889
+ /**
2890
+ * The skill manifold of a search at rank `k` (`auto`: the cross-validated
2891
+ * intrinsic dimension, at least 1, and 1 when too few nodes cross-validate).
2892
+ * Pure: it reads the state, and `calibration` when given.
2893
+ */
2894
+ function skillManifold(state, k = "auto", options = {}) {
2895
+ const maxRank = positiveInteger("skillManifold", "maxRank", options.maxRank ?? 4);
2896
+ const folds = positiveInteger("skillManifold", "folds", options.folds ?? 5);
2897
+ if (folds < 2) throw new TypeError(`skillManifold: folds must be at least 2, got ${folds}`);
2898
+ const leaderCount = positiveInteger("skillManifold", "leaders", options.leaders ?? 3);
2899
+ const ridge = options.ridge ?? 1;
2900
+ if (!(ridge > 0) || !Number.isFinite(ridge)) throw new TypeError(`skillManifold: ridge must be a positive number, got ${String(ridge)}`);
2901
+ const sampleRows = positiveInteger("skillManifold", "sampleRows", options.sampleRows ?? 1e3);
2902
+ if (k !== "auto") positiveInteger("skillManifold", "k", k);
2903
+ const requested = options.model ?? "auto";
2904
+ if (requested !== "auto" && requested !== "linear" && requested !== "logistic") throw new TypeError(`skillManifold: model must be auto, linear or logistic, got ${String(requested)}`);
2905
+ const header = state.header;
2906
+ const split = options.split ?? rankingSplit(state);
2907
+ const direction = header?.objective.direction ?? "maximize";
2908
+ const sign = direction === "maximize" ? 1 : -1;
2909
+ const calibration = options.calibration ?? null;
2910
+ const source = calibration?.source ?? `${state.searchId}@${headSequence(state)}`;
2911
+ let link = calibration?.link ?? (requested === "logistic" ? "logit" : "identity");
2912
+ const empty = (reason, matrix) => result(state, link, {
2913
+ split,
2914
+ direction,
2915
+ mode: calibration ? "calibrated" : "fitted",
2916
+ source,
2917
+ matrix: matrix ?? {
2918
+ rows: 0,
2919
+ units: 0,
2920
+ observed: 0,
2921
+ density: null,
2922
+ excludedNodes: 0,
2923
+ excludedUnits: 0,
2924
+ fitRows: 0
2925
+ },
2926
+ model: null,
2927
+ explained: null,
2928
+ intrinsicDimension: {
2929
+ value: null,
2930
+ method: METHODS[link].intrinsic,
2931
+ n: 0,
2932
+ folds,
2933
+ curve: [],
2934
+ insufficient: reason
2935
+ },
2936
+ axes: axesNote(direction),
2937
+ noiseVariance: null,
2938
+ nodes: [],
2939
+ units: [],
2940
+ leaders: [],
2941
+ nextUnits: [],
2942
+ batch: null,
2943
+ insufficient: reason
2944
+ });
2945
+ if (!header || state.rootNodeId === null) return empty(header ? "the search has no root node yet" : "the search has not been opened");
2946
+ const matrix = scoreMatrix(state, split, options.asOfSequence, calibration);
2947
+ const shape = {
2948
+ rows: matrix.rows.length,
2949
+ units: matrix.unitIds.length,
2950
+ observed: matrix.observed,
2951
+ density: matrix.rows.length * matrix.unitIds.length > 0 ? round9(matrix.observed / (matrix.rows.length * matrix.unitIds.length)) : null,
2952
+ excludedNodes: matrix.excludedNodes,
2953
+ excludedUnits: matrix.excludedUnits,
2954
+ fitRows: 0
2955
+ };
2956
+ if (!calibration) {
2957
+ if (requested === "logistic" && !matrix.binary) throw new TypeError("skillManifold: model logistic needs every modelled cell to score 0 or 1; use auto or linear");
2958
+ link = requested === "linear" || !matrix.binary ? "identity" : "logit";
2959
+ } else if (link === "logit" && !matrix.binary) return empty(`the calibration from ${calibration.source} is an item-response model, and this search has cells that score other than 0 or 1`, shape);
2960
+ let center;
2961
+ let scale;
2962
+ let fit;
2963
+ let rank;
2964
+ let noiseVariance;
2965
+ let intrinsic;
2966
+ let explained = null;
2967
+ let iterations = 0;
2968
+ let converged = true;
2969
+ if (calibration) {
2970
+ if (matrix.rows.length === 0 || matrix.unitIds.length === 0) return empty("no node scored a calibrated unit", shape);
2971
+ center = calibration.center;
2972
+ scale = calibration.scale;
2973
+ rank = calibration.rank;
2974
+ const byUnit = new Map(calibration.units.map((unit) => [unit.unitId, unit]));
2975
+ fit = {
2976
+ rank,
2977
+ bias: matrix.unitIds.map((unitId) => byUnit.get(unitId).bias),
2978
+ loadings: matrix.unitIds.map((unitId) => [...byUnit.get(unitId).loading]),
2979
+ byRow: /* @__PURE__ */ new Map(),
2980
+ coordinates: [],
2981
+ iterations: 0,
2982
+ converged: true
2983
+ };
2984
+ placeRows(matrix, fit, center, scale, calibration.ridge, link);
2985
+ noiseVariance = calibration.noiseVariance;
2986
+ intrinsic = {
2987
+ value: null,
2988
+ method: METHODS[link].intrinsic,
2989
+ n: 0,
2990
+ folds,
2991
+ curve: [],
2992
+ insufficient: `loadings come from ${calibration.source}; its fit decided the rank (${rank})`
2993
+ };
2994
+ } else {
2995
+ if (matrix.rows.length < 3 || matrix.unitIds.length < INSUFFICIENT_FROM) return empty(`${plural$1(matrix.rows.length, "node")} and ${plural$1(matrix.unitIds.length, "unit")} qualify (a unit counts when 2 or more nodes scored it, a node when it scored 2 or more such units); a manifold needs 3 nodes and ${INSUFFICIENT_FROM} units`, shape);
2996
+ const standard = standardization(matrix);
2997
+ if (standard === null) return empty("every node scores the same on every unit, so there is no variation to factor", shape);
2998
+ ({center, scale} = link === "logit" ? {
2999
+ center: 0,
3000
+ scale: 1
3001
+ } : standard);
3002
+ const fitRows = evenSample(matrix.rows.map((_, index) => index), sampleRows);
3003
+ shape.fitRows = fitRows.length;
3004
+ const cells = observedCells(matrix, fitRows, center, scale);
3005
+ const rankCap = Math.min(maxRank, matrix.unitIds.length - 1, fitRows.length - 1);
3006
+ intrinsic = crossValidate(matrix, cells, rankCap, folds, ridge, link);
3007
+ rank = k === "auto" ? Math.max(1, intrinsic.value ?? 1) : Math.min(k, Math.max(1, rankCap));
3008
+ const fits = [];
3009
+ for (let r = 0; r <= Math.max(rank, rankCap); r++) fits.push(factor(matrix.unitIds.length, cells, r, ridge, link));
3010
+ const base = loss(fits[0], cells, link);
3011
+ explained = {
3012
+ byRank: fits.slice(1).map((entry) => ({
3013
+ rank: entry.rank,
3014
+ r2: round9(base > 0 ? 1 - loss(entry, cells, link) / base : 0)
3015
+ })),
3016
+ axisShare: [],
3017
+ method: METHODS[link].explained
3018
+ };
3019
+ fit = fits[rank];
3020
+ iterations = fit.iterations;
3021
+ converged = fit.converged;
3022
+ const cv = intrinsic.curve.find((point) => point.rank === rank);
3023
+ noiseVariance = link === "identity" && cv && Number.isFinite(cv.error) ? cv.error : null;
3024
+ const shares = principalAxes(fit, fitRows);
3025
+ explained.axisShare = shares.map(round9);
3026
+ fit.coordinates = [];
3027
+ placeRows(matrix, fit, center, scale, ridge, link);
3028
+ }
3029
+ orientAxes(fit, sign);
3030
+ const unitCount = matrix.unitIds.length;
3031
+ const meanLoading = new Array(rank).fill(0);
3032
+ for (const loading of fit.loadings) for (let d = 0; d < rank; d++) meanLoading[d] += loading[d] / unitCount;
3033
+ const meanBias = fit.bias.reduce((sum, value) => sum + value, 0) / unitCount;
3034
+ const predicted = (coordinates) => {
3035
+ if (link === "identity") return center + scale * (meanBias + dot(coordinates, meanLoading));
3036
+ let total = 0;
3037
+ for (let u = 0; u < unitCount; u++) total += logistic(fit.bias[u] + dot(coordinates, fit.loadings[u]));
3038
+ return total / unitCount;
3039
+ };
3040
+ const leaderFloor = Math.min(INSUFFICIENT_FROM, unitCount);
3041
+ let leaderRows;
3042
+ if (options.candidates) {
3043
+ const wanted = new Set(options.candidates);
3044
+ leaderRows = matrix.rows.flatMap((row, index) => wanted.has(row.nodeId) ? [index] : []);
3045
+ } else leaderRows = matrix.rows.map((row, index) => ({
3046
+ row,
3047
+ index
3048
+ })).filter(({ row }) => row.observed.size >= leaderFloor).sort((left, right) => sign * (predicted(fit.coordinates[right.index]) - predicted(fit.coordinates[left.index])) || left.row.ordinal - right.row.ordinal).slice(0, leaderCount).map(({ index }) => index);
3049
+ const leaderSet = new Set(leaderRows);
3050
+ const next = intrinsic.value === 0 ? {
3051
+ ranked: [],
3052
+ batch: null,
3053
+ insufficient: "held-out cells fit no better with any skill axis than with unit means alone (intrinsic dimension 0): the nodes do not differ beyond noise, so no unit separates them"
3054
+ } : !calibration && intrinsic.value === null ? {
3055
+ ranked: [],
3056
+ batch: null,
3057
+ insufficient: "the skill axes are not cross-validated (see the intrinsic dimension), so no unit’s information is known"
3058
+ } : nextUnits(matrix, fit, leaderRows, noiseVariance, calibration?.ridge ?? ridge, options.batch ?? null, link);
3059
+ const nodes = matrix.rows.map((row, index) => ({
3060
+ nodeId: row.nodeId,
3061
+ ordinal: row.ordinal,
3062
+ status: row.status,
3063
+ observed: row.observed.size,
3064
+ coordinates: fit.coordinates[index].map(round9),
3065
+ predictedMean: round9(predicted(fit.coordinates[index])),
3066
+ leader: leaderSet.has(index)
3067
+ }));
3068
+ const units = matrix.unitIds.map((unitId, u) => ({
3069
+ unitId,
3070
+ observedBy: matrix.observedBy[u],
3071
+ bias: round9(link === "identity" ? center + scale * fit.bias[u] : logistic(fit.bias[u])),
3072
+ intercept: round9(fit.bias[u]),
3073
+ loading: fit.loadings[u].map(round9),
3074
+ strength: round9(Math.hypot(...fit.loadings[u]))
3075
+ }));
3076
+ return result(state, link, {
3077
+ split,
3078
+ direction,
3079
+ mode: calibration ? "calibrated" : "fitted",
3080
+ source,
3081
+ matrix: shape,
3082
+ model: {
3083
+ method: METHODS[link].model,
3084
+ link,
3085
+ rank,
3086
+ ridge: calibration?.ridge ?? ridge,
3087
+ center: round9(center),
3088
+ scale: round9(scale),
3089
+ iterations,
3090
+ converged
3091
+ },
3092
+ explained,
3093
+ intrinsicDimension: intrinsic,
3094
+ axes: axesNote(direction),
3095
+ noiseVariance: noiseVariance === null ? null : round9(noiseVariance),
3096
+ nodes,
3097
+ units,
3098
+ leaders: leaderRows.map((index) => ({
3099
+ nodeId: matrix.rows[index].nodeId,
3100
+ predictedMean: round9(predicted(fit.coordinates[index])),
3101
+ observed: matrix.rows[index].observed.size
3102
+ })),
3103
+ nextUnits: next.ranked,
3104
+ batch: options.batch ? next.batch : null,
3105
+ insufficient: next.insufficient
3106
+ });
3107
+ }
3108
+ /** The loadings of a fitted manifold, for a later search over the same units
3109
+ * to place its nodes on. Null unless cross-validation found at least one skill
3110
+ * axis (intrinsic dimension 1 or more), and, for the linear model, measured
3111
+ * the cell noise: loadings of an axis the held-out cells do not support would
3112
+ * steer allocation by noise. */
3113
+ function skillCalibration(lens) {
3114
+ const { data } = lens;
3115
+ const { model } = data;
3116
+ if (data.mode !== "fitted" || model === null || model.center === null || model.scale === null || model.link === "identity" && data.noiseVariance === null || data.intrinsicDimension.value === null || data.intrinsicDimension.value < 1) return null;
3117
+ return {
3118
+ source: data.source,
3119
+ link: model.link,
3120
+ center: model.center,
3121
+ scale: model.scale,
3122
+ rank: model.rank,
3123
+ ridge: model.ridge,
3124
+ noiseVariance: data.noiseVariance,
3125
+ units: data.units.map((unit) => ({
3126
+ unitId: unit.unitId,
3127
+ bias: unit.intercept,
3128
+ loading: [...unit.loading]
3129
+ }))
3130
+ };
3131
+ }
3132
+ function result(state, link, data) {
3133
+ const best = data.nextUnits.filter((unit) => unit.measuredBy < data.leaders.length)[0] ?? data.nextUnits[0] ?? null;
3134
+ const signal = {
3135
+ name: "nextUnit",
3136
+ value: best === null ? null : best.share,
3137
+ subject: best?.unitId ?? null,
3138
+ method: METHODS[link].nextUnit,
3139
+ n: data.leaders.reduce((sum, leader) => sum + leader.observed, 0),
3140
+ insufficient: best === null ? data.insufficient ?? "no unit to rank" : null
3141
+ };
3142
+ return {
3143
+ lens: "skillManifold",
3144
+ searchId: state.searchId,
3145
+ sequence: headSequence(state),
3146
+ data,
3147
+ signal
3148
+ };
3149
+ }
3150
+ function axesNote(direction) {
3151
+ return direction === "maximize" ? "axes are principal (largest share first) and oriented so the mean unit loads positively: a larger coordinate predicts a higher score" : "axes are principal (largest share first) and oriented so the mean unit loads negatively: a larger coordinate predicts a lower (better) score";
3152
+ }
3153
+ /**
3154
+ * Per-unit means of every node on `split`. Without a calibration, a unit is
3155
+ * modelled when 2 or more nodes scored it and a node when it scored 2 or more
3156
+ * modelled units, repeated until both hold. With one, the modelled units are
3157
+ * the calibration's, and a node needs one of them.
3158
+ */
3159
+ function scoreMatrix(state, split, asOf, calibration) {
3160
+ const raw = [];
3161
+ const allUnits = /* @__PURE__ */ new Set();
3162
+ for (const node of state.nodes()) {
3163
+ const cells = asOf === void 0 ? state.scoredCells(node.nodeId, split) : scoredAsOf(state, node.nodeId, split, asOf);
3164
+ const scores = searchUnitScores(cells);
3165
+ const units = new Map(scores.map((unit) => [unit.unitId, unit.mean]));
3166
+ const counts = new Map(scores.map((unit) => [unit.unitId, {
3167
+ sum: unit.sum,
3168
+ count: unit.count
3169
+ }]));
3170
+ for (const unitId of units.keys()) allUnits.add(unitId);
3171
+ raw.push({
3172
+ nodeId: node.nodeId,
3173
+ ordinal: node.ordinal,
3174
+ status: node.status,
3175
+ units,
3176
+ counts,
3177
+ binary: cells.every((cell) => cell.score === 0 || cell.score === 1)
3178
+ });
3179
+ }
3180
+ let unitIds;
3181
+ let keep;
3182
+ if (calibration) {
3183
+ const calibrated = new Set(calibration.units.map((unit) => unit.unitId));
3184
+ unitIds = calibration.units.map((unit) => unit.unitId);
3185
+ keep = raw.filter((row) => [...row.units.keys()].some((unitId) => calibrated.has(unitId)));
3186
+ } else {
3187
+ let units = new Set(allUnits);
3188
+ keep = raw;
3189
+ for (;;) {
3190
+ const counts = /* @__PURE__ */ new Map();
3191
+ for (const row of keep) for (const unitId of row.units.keys()) if (units.has(unitId)) counts.set(unitId, (counts.get(unitId) ?? 0) + 1);
3192
+ const nextUnits = new Set([...units].filter((unitId) => (counts.get(unitId) ?? 0) >= 2));
3193
+ const nextKeep = keep.filter((row) => [...row.units.keys()].filter((unitId) => nextUnits.has(unitId)).length >= 2);
3194
+ const stable = nextUnits.size === units.size && nextKeep.length === keep.length;
3195
+ units = nextUnits;
3196
+ keep = nextKeep;
3197
+ if (stable) break;
3198
+ }
3199
+ unitIds = [...units].sort(compareCodeUnits);
3200
+ }
3201
+ const unitIndex = new Map(unitIds.map((unitId, index) => [unitId, index]));
3202
+ const observedBy = new Array(unitIds.length).fill(0);
3203
+ let observed = 0;
3204
+ const rows = keep.map((row) => {
3205
+ const map = /* @__PURE__ */ new Map();
3206
+ const counts = /* @__PURE__ */ new Map();
3207
+ for (const [unitId, mean] of row.units) {
3208
+ const index = unitIndex.get(unitId);
3209
+ if (index === void 0) continue;
3210
+ map.set(index, mean);
3211
+ counts.set(index, row.counts.get(unitId));
3212
+ observedBy[index] += 1;
3213
+ observed += 1;
3214
+ }
3215
+ return {
3216
+ nodeId: row.nodeId,
3217
+ ordinal: row.ordinal,
3218
+ status: row.status,
3219
+ observed: map,
3220
+ counts
3221
+ };
3222
+ });
3223
+ return {
3224
+ rows,
3225
+ unitIds,
3226
+ observedBy,
3227
+ observed,
3228
+ binary: keep.length > 0 && keep.every((row) => row.binary),
3229
+ excludedNodes: raw.length - rows.length,
3230
+ excludedUnits: calibration ? [...allUnits].filter((unitId) => !unitIndex.has(unitId)).length : allUnits.size - unitIds.length
3231
+ };
3232
+ }
3233
+ /** A node's scored cells on `split` whose score the ledger held at `asOf`:
3234
+ * a cell's last update is its scoring settle, so a later retry is excluded. */
3235
+ function scoredAsOf(state, nodeId, split, asOf) {
3236
+ const cells = [];
3237
+ for (const cell of state.cells({ nodeId })) {
3238
+ if (cell.split !== split || cell.score === null || cell.updatedSequence > asOf) continue;
3239
+ cells.push({
3240
+ cellId: cell.cellId,
3241
+ unitId: cell.unitId,
3242
+ attempt: cell.attempts,
3243
+ score: cell.score
3244
+ });
3245
+ }
3246
+ return cells;
3247
+ }
3248
+ function standardization(matrix) {
3249
+ let total = 0;
3250
+ const columnSum = new Array(matrix.unitIds.length).fill(0);
3251
+ for (const row of matrix.rows) for (const [u, value] of row.observed) {
3252
+ total += value;
3253
+ columnSum[u] += value;
3254
+ }
3255
+ const center = total / matrix.observed;
3256
+ let squares = 0;
3257
+ for (const row of matrix.rows) for (const [u, value] of row.observed) squares += (value - columnSum[u] / matrix.observedBy[u]) ** 2;
3258
+ const scale = Math.sqrt(squares / matrix.observed);
3259
+ return scale > 1e-12 ? {
3260
+ center,
3261
+ scale
3262
+ } : null;
3263
+ }
3264
+ function observedCells(matrix, rows, center, scale) {
3265
+ const cells = [];
3266
+ for (const row of rows) {
3267
+ const { nodeId, observed, counts } = matrix.rows[row];
3268
+ for (const [unit, value] of [...observed].sort(([left], [right]) => left - right)) {
3269
+ const { sum, count } = counts.get(unit);
3270
+ cells.push({
3271
+ row,
3272
+ unit,
3273
+ z: (value - center) / scale,
3274
+ k: sum,
3275
+ n: count,
3276
+ fold: fnv1a(`${nodeId}\u0000${matrix.unitIds[unit]}`)
3277
+ });
3278
+ }
3279
+ }
3280
+ return cells;
3281
+ }
3282
+ /** 32-bit FNV-1a: a deterministic fold assignment that depends on the cell's
3283
+ * coordinates alone, not on the order cells were read. */
3284
+ function fnv1a(text) {
3285
+ let hash = 2166136261;
3286
+ for (let index = 0; index < text.length; index++) {
3287
+ hash ^= text.charCodeAt(index);
3288
+ hash = Math.imul(hash, 16777619);
3289
+ }
3290
+ return hash >>> 0;
3291
+ }
3292
+ /** The fit of `rank` axes under `link`. */
3293
+ function factor(units, cells, rank, ridge, link) {
3294
+ return link === "identity" ? factorLinear(units, cells, rank, ridge) : factorLogistic(units, cells, rank, ridge);
3295
+ }
3296
+ function groupCells(units, cells) {
3297
+ const byUnit = Array.from({ length: units }, () => []);
3298
+ const byRow = /* @__PURE__ */ new Map();
3299
+ for (const cell of cells) {
3300
+ byUnit[cell.unit].push(cell);
3301
+ const list = byRow.get(cell.row);
3302
+ if (list) list.push(cell);
3303
+ else byRow.set(cell.row, [cell]);
3304
+ }
3305
+ return {
3306
+ byUnit,
3307
+ byRow
3308
+ };
3309
+ }
3310
+ /** Loadings start from a fixed seeded draw, so a fit is deterministic. */
3311
+ function seededLoadings(units, rank) {
3312
+ const random = mulberry32(24301 + rank);
3313
+ return Array.from({ length: units }, () => Array.from({ length: rank }, () => (random() - .5) * .2));
3314
+ }
3315
+ /**
3316
+ * Alternating ridge least squares over `cells`: rows' coordinates given the
3317
+ * loadings, then each unit's loading and (unpenalized) bias given the
3318
+ * coordinates, until the objective moves by less than 1e-7 of itself.
3319
+ */
3320
+ function factorLinear(units, cells, rank, ridge) {
3321
+ const { byUnit, byRow: rowIds } = groupCells(units, cells);
3322
+ const bias = byUnit.map((list) => list.length === 0 ? 0 : list.reduce((sum, cell) => sum + cell.z, 0) / list.length);
3323
+ const loadings = seededLoadings(units, rank);
3324
+ const coordinates = /* @__PURE__ */ new Map();
3325
+ for (const row of rowIds.keys()) coordinates.set(row, new Array(rank).fill(0));
3326
+ if (rank === 0) return {
3327
+ rank,
3328
+ bias,
3329
+ loadings,
3330
+ byRow: coordinates,
3331
+ coordinates: [],
3332
+ iterations: 0,
3333
+ converged: true
3334
+ };
3335
+ let previous = Number.POSITIVE_INFINITY;
3336
+ let iterations = 0;
3337
+ let converged = false;
3338
+ for (; iterations < MAX_ITERATIONS; iterations++) {
3339
+ for (const [row, list] of rowIds) coordinates.set(row, solveRow(list, loadings, bias, rank, ridge, "identity"));
3340
+ for (let u = 0; u < units; u++) {
3341
+ const list = byUnit[u];
3342
+ if (list.length === 0) continue;
3343
+ const size = rank + 1;
3344
+ const system = Array.from({ length: size }, (_, i) => Array.from({ length: size }, (_, j) => i === j && i < rank ? ridge : 0));
3345
+ const rhs = new Array(size).fill(0);
3346
+ for (const cell of list) {
3347
+ const point = coordinates.get(cell.row);
3348
+ for (let i = 0; i < size; i++) {
3349
+ const xi = i < rank ? point[i] : 1;
3350
+ rhs[i] += xi * cell.z;
3351
+ const line = system[i];
3352
+ for (let j = 0; j < size; j++) line[j] += xi * (j < rank ? point[j] : 1);
3353
+ }
3354
+ }
3355
+ const lower = cholesky(system);
3356
+ if (lower === null) continue;
3357
+ const solution = choleskySolve(lower, rhs);
3358
+ loadings[u] = solution.slice(0, rank);
3359
+ bias[u] = solution[rank];
3360
+ }
3361
+ let objective = 0;
3362
+ for (const cell of cells) objective += (cell.z - bias[cell.unit] - dot(coordinates.get(cell.row), loadings[cell.unit])) ** 2;
3363
+ for (const point of coordinates.values()) objective += ridge * dot(point, point);
3364
+ for (const loading of loadings) objective += ridge * dot(loading, loading);
3365
+ if (Math.abs(previous - objective) <= TOLERANCE * Math.max(objective, 1e-12)) {
3366
+ converged = true;
3367
+ iterations += 1;
3368
+ break;
3369
+ }
3370
+ previous = objective;
3371
+ }
3372
+ return {
3373
+ rank,
3374
+ bias,
3375
+ loadings,
3376
+ byRow: coordinates,
3377
+ coordinates: [],
3378
+ iterations,
3379
+ converged
3380
+ };
3381
+ }
3382
+ /**
3383
+ * The item-response fit: alternating Newton steps on the penalized binomial
3384
+ * likelihood, each row's coordinates given the loadings, then each unit's
3385
+ * loading and intercept given the coordinates, until the objective moves by
3386
+ * less than 1e-7 of itself. Priors: standard normal on coordinates and
3387
+ * loadings (scaled by `ridge`), N(0, 10²) on each intercept, which keeps a
3388
+ * unit every node passed finite. A step moves no parameter by more than 4.
3389
+ */
3390
+ function factorLogistic(units, cells, rank, ridge) {
3391
+ const { byUnit, byRow: rowIds } = groupCells(units, cells);
3392
+ const bias = byUnit.map((list) => {
3393
+ let k = 0;
3394
+ let n = 0;
3395
+ for (const cell of list) {
3396
+ k += cell.k;
3397
+ n += cell.n;
3398
+ }
3399
+ return Math.log((k + .5) / (n - k + .5));
3400
+ });
3401
+ const loadings = seededLoadings(units, rank);
3402
+ const coordinates = /* @__PURE__ */ new Map();
3403
+ for (const row of rowIds.keys()) coordinates.set(row, new Array(rank).fill(0));
3404
+ const objectiveOf = () => {
3405
+ let total = 0;
3406
+ for (const cell of cells) total += binomialLoss(cell, bias[cell.unit] + dot(coordinates.get(cell.row), loadings[cell.unit]));
3407
+ for (const point of coordinates.values()) total += ridge / 2 * dot(point, point);
3408
+ for (const loading of loadings) total += ridge / 2 * dot(loading, loading);
3409
+ for (const value of bias) total += LOGIT_BIAS_PRECISION / 2 * value * value;
3410
+ return total;
3411
+ };
3412
+ let previous = Number.POSITIVE_INFINITY;
3413
+ let iterations = 0;
3414
+ let converged = false;
3415
+ for (; iterations < MAX_ITERATIONS; iterations++) {
3416
+ if (rank > 0) for (const [row, list] of rowIds) {
3417
+ const point = coordinates.get(row);
3418
+ coordinates.set(row, newtonStep(point, list, loadings, bias, ridge));
3419
+ }
3420
+ for (let u = 0; u < units; u++) {
3421
+ const list = byUnit[u];
3422
+ if (list.length === 0) continue;
3423
+ const size = rank + 1;
3424
+ const theta = [...loadings[u], bias[u]];
3425
+ const precision = (i) => i < rank ? ridge : LOGIT_BIAS_PRECISION;
3426
+ const hessian = Array.from({ length: size }, (_, i) => Array.from({ length: size }, (_, j) => i === j ? precision(i) : 0));
3427
+ const gradient = theta.map((value, i) => -precision(i) * value);
3428
+ for (const cell of list) {
3429
+ const point = coordinates.get(cell.row);
3430
+ const p = logistic(theta[rank] + dot(point, theta));
3431
+ const weight = cell.n * p * (1 - p);
3432
+ for (let i = 0; i < size; i++) {
3433
+ const xi = i < rank ? point[i] : 1;
3434
+ gradient[i] += (cell.k - cell.n * p) * xi;
3435
+ const line = hessian[i];
3436
+ for (let j = 0; j < size; j++) line[j] += weight * xi * (j < rank ? point[j] : 1);
3437
+ }
3438
+ }
3439
+ const lower = cholesky(hessian);
3440
+ if (lower === null) continue;
3441
+ const step = capped(choleskySolve(lower, gradient));
3442
+ loadings[u] = theta.slice(0, rank).map((value, i) => value + step[i]);
3443
+ bias[u] = theta[rank] + step[rank];
3444
+ }
3445
+ const objective = objectiveOf();
3446
+ if (Math.abs(previous - objective) <= TOLERANCE * Math.max(objective, 1e-12)) {
3447
+ converged = true;
3448
+ iterations += 1;
3449
+ break;
3450
+ }
3451
+ previous = objective;
3452
+ }
3453
+ return {
3454
+ rank,
3455
+ bias,
3456
+ loadings,
3457
+ byRow: coordinates,
3458
+ coordinates: [],
3459
+ iterations,
3460
+ converged
3461
+ };
3462
+ }
3463
+ /** One Newton step on a row's coordinates under the item-response model. */
3464
+ function newtonStep(point, list, loadings, bias, ridge) {
3465
+ const rank = point.length;
3466
+ const hessian = Array.from({ length: rank }, (_, i) => Array.from({ length: rank }, (_, j) => i === j ? ridge : 0));
3467
+ const gradient = point.map((value) => -ridge * value);
3468
+ for (const cell of list) {
3469
+ const q = loadings[cell.unit];
3470
+ const p = logistic(bias[cell.unit] + dot(point, q));
3471
+ const weight = cell.n * p * (1 - p);
3472
+ for (let i = 0; i < rank; i++) {
3473
+ gradient[i] += (cell.k - cell.n * p) * q[i];
3474
+ for (let j = 0; j < rank; j++) hessian[i][j] += weight * q[i] * q[j];
3475
+ }
3476
+ }
3477
+ const step = capped(choleskySolve(cholesky(hessian), gradient));
3478
+ return point.map((value, i) => value + step[i]);
3479
+ }
3480
+ function capped(step) {
3481
+ const largest = Math.max(0, ...step.map(Math.abs));
3482
+ return largest > NEWTON_STEP_CAP ? step.map((value) => value * NEWTON_STEP_CAP / largest) : [...step];
3483
+ }
3484
+ /** −log of the binomial likelihood of `k` passes in `n` at log-odds `eta`,
3485
+ * without the binomial coefficient: n·log(1 + e^η) − k·η, computed stably. */
3486
+ function binomialLoss(cell, eta) {
3487
+ const softplus = Math.max(eta, 0) + Math.log1p(Math.exp(-Math.abs(eta)));
3488
+ return cell.n * softplus - cell.k * eta;
3489
+ }
3490
+ function logistic(eta) {
3491
+ if (eta >= 0) return 1 / (1 + Math.exp(-eta));
3492
+ const e = Math.exp(eta);
3493
+ return e / (1 + e);
3494
+ }
3495
+ /** A row's coordinates given the loadings: one ridge solve (identity), or
3496
+ * Newton steps to convergence (logit). */
3497
+ function solveRow(list, loadings, bias, rank, ridge, link) {
3498
+ if (link === "logit") {
3499
+ let point = new Array(rank).fill(0);
3500
+ for (let iteration = 0; iteration < 50; iteration++) {
3501
+ const next = newtonStep(point, list, loadings, bias, ridge);
3502
+ const moved = Math.max(0, ...next.map((value, i) => Math.abs(value - point[i])));
3503
+ point = next;
3504
+ if (moved < 1e-10) break;
3505
+ }
3506
+ return point;
3507
+ }
3508
+ const system = Array.from({ length: rank }, (_, i) => Array.from({ length: rank }, (_, j) => i === j ? ridge : 0));
3509
+ const rhs = new Array(rank).fill(0);
3510
+ for (const cell of list) {
3511
+ const q = loadings[cell.unit];
3512
+ const residual = cell.z - bias[cell.unit];
3513
+ for (let i = 0; i < rank; i++) {
3514
+ rhs[i] += q[i] * residual;
3515
+ for (let j = 0; j < rank; j++) system[i][j] += q[i] * q[j];
3516
+ }
3517
+ }
3518
+ return choleskySolve(cholesky(system), rhs);
3519
+ }
3520
+ /** The model's linear predictor: standardized mean (identity) or log-odds (logit). */
3521
+ function predict(fit, point, unit) {
3522
+ return fit.bias[unit] + (point ? dot(point, fit.loadings[unit]) : 0);
3523
+ }
3524
+ /** Squared error (identity) or binomial log loss (logit) of the fit on `cells`. */
3525
+ function loss(fit, cells, link) {
3526
+ let total = 0;
3527
+ for (const cell of cells) total += cellLoss(cell, predict(fit, fit.byRow.get(cell.row), cell.unit), link);
3528
+ return total;
3529
+ }
3530
+ function cellLoss(cell, eta, link) {
3531
+ return link === "identity" ? (cell.z - eta) ** 2 : binomialLoss(cell, eta);
3532
+ }
3533
+ /** Every matrix row placed on the fitted loadings. */
3534
+ function placeRows(matrix, fit, center, scale, ridge, link) {
3535
+ fit.coordinates = matrix.rows.map((row) => {
3536
+ if (fit.rank === 0) return [];
3537
+ return solveRow([...row.observed].map(([unit, value]) => ({
3538
+ unit,
3539
+ z: (value - center) / scale,
3540
+ k: row.counts.get(unit).sum,
3541
+ n: row.counts.get(unit).count
3542
+ })), fit.loadings, fit.bias, fit.rank, ridge, link);
3543
+ });
3544
+ }
3545
+ /**
3546
+ * Rotates the fitted rows and loadings to principal axes: the singular
3547
+ * vectors of the fitted low-rank matrix, largest first, with the scale split
3548
+ * evenly between rows and loadings. The product the model predicts is
3549
+ * unchanged. Returns each axis's share of the squared singular values.
3550
+ */
3551
+ function principalAxes(fit, fitRows) {
3552
+ const rank = fit.rank;
3553
+ const byRow = fit.byRow;
3554
+ const rows = fitRows.map((row) => byRow.get(row) ?? new Array(rank).fill(0));
3555
+ const gram = (vectors) => Array.from({ length: rank }, (_, i) => Array.from({ length: rank }, (_, j) => vectors.reduce((sum, v) => sum + v[i] * v[j], 0)));
3556
+ const lowerP = cholesky(gram(rows));
3557
+ const lowerQ = cholesky(gram(fit.loadings));
3558
+ if (lowerP === null || lowerQ === null) return [];
3559
+ const c = Array.from({ length: rank }, (_, i) => Array.from({ length: rank }, (_, j) => {
3560
+ let sum = 0;
3561
+ for (let m = 0; m < rank; m++) sum += lowerP[m][i] * lowerQ[m][j];
3562
+ return sum;
3563
+ }));
3564
+ const { values, vectors } = symmetricEigen(Array.from({ length: rank }, (_, i) => Array.from({ length: rank }, (_, j) => {
3565
+ let sum = 0;
3566
+ for (let m = 0; m < rank; m++) sum += c[m][i] * c[m][j];
3567
+ return sum;
3568
+ })));
3569
+ const sigma = values.map((value) => Math.sqrt(Math.max(value, 0)));
3570
+ if (sigma.some((value) => value <= 1e-12)) return [];
3571
+ const b = Array.from({ length: rank }, (_, i) => vectors.map((vector) => vector[i]));
3572
+ const a = Array.from({ length: rank }, (_, i) => Array.from({ length: rank }, (_, j) => {
3573
+ let sum = 0;
3574
+ for (let m = 0; m < rank; m++) sum += c[i][m] * b[m][j];
3575
+ return sum / sigma[j];
3576
+ }));
3577
+ const upperInverse = (lower) => transpose(lowerInverse(lower));
3578
+ const transformP = multiply(multiply(upperInverse(lowerP), a), diagonal(sigma.map(Math.sqrt)));
3579
+ const transformQ = multiply(multiply(upperInverse(lowerQ), b), diagonal(sigma.map(Math.sqrt)));
3580
+ for (const [row, point] of byRow) byRow.set(row, applyRow(point, transformP));
3581
+ fit.loadings = fit.loadings.map((loading) => applyRow(loading, transformQ));
3582
+ const total = values.reduce((sum, value) => sum + Math.max(value, 0), 0);
3583
+ return values.map((value) => Math.max(value, 0) / total);
3584
+ }
3585
+ /** Flips each axis so the mean loading points toward a better score. */
3586
+ function orientAxes(fit, sign) {
3587
+ for (let d = 0; d < fit.rank; d++) {
3588
+ let mean = 0;
3589
+ for (const loading of fit.loadings) mean += loading[d];
3590
+ if (sign * mean >= 0) continue;
3591
+ for (const loading of fit.loadings) loading[d] = -loading[d] || 0;
3592
+ for (const point of fit.coordinates) point[d] = -point[d] || 0;
3593
+ for (const point of fit.byRow.values()) point[d] = -point[d] || 0;
3594
+ }
3595
+ }
3596
+ function crossValidate(matrix, cells, rankCap, folds, ridge, link) {
3597
+ const rows = new Set(cells.map((cell) => cell.row)).size;
3598
+ if (rows < INSUFFICIENT_FROM || matrix.unitIds.length < INSUFFICIENT_FROM) return {
3599
+ value: null,
3600
+ method: METHODS[link].intrinsic,
3601
+ n: 0,
3602
+ folds,
3603
+ curve: [],
3604
+ insufficient: `cross-validation needs ${INSUFFICIENT_FROM} nodes and ${INSUFFICIENT_FROM} units; ${plural$1(rows, "node")} and ${plural$1(matrix.unitIds.length, "unit")} qualify`
3605
+ };
3606
+ const curve = [];
3607
+ let held = 0;
3608
+ for (let rank = 0; rank <= rankCap; rank++) {
3609
+ const foldErrors = [];
3610
+ let squares = 0;
3611
+ let count = 0;
3612
+ for (let fold = 0; fold < folds; fold++) {
3613
+ const train = cells.filter((cell) => cell.fold % folds !== fold);
3614
+ const test = cells.filter((cell) => cell.fold % folds === fold);
3615
+ const fit = factor(matrix.unitIds.length, train, rank, ridge, link);
3616
+ const trained = new Set(train.map((cell) => cell.unit));
3617
+ let foldSquares = 0;
3618
+ let foldCount = 0;
3619
+ for (const cell of test) {
3620
+ if (!trained.has(cell.unit)) continue;
3621
+ foldSquares += cellLoss(cell, predict(fit, fit.byRow.get(cell.row), cell.unit), link);
3622
+ foldCount += 1;
3623
+ }
3624
+ if (foldCount > 0) foldErrors.push(foldSquares / foldCount);
3625
+ squares += foldSquares;
3626
+ count += foldCount;
3627
+ }
3628
+ held = count;
3629
+ const error = count > 0 ? squares / count : NaN;
3630
+ const spread = foldErrors.length > 1 ? Math.sqrt(foldErrors.reduce((sum, value) => sum + (value - mean(foldErrors)) ** 2, 0) / (foldErrors.length - 1) / foldErrors.length) : NaN;
3631
+ curve.push({
3632
+ rank,
3633
+ error: round9(error),
3634
+ standardError: round9(spread)
3635
+ });
3636
+ }
3637
+ const finite = curve.filter((point) => Number.isFinite(point.error));
3638
+ if (finite.length === 0) return {
3639
+ value: null,
3640
+ method: METHODS[link].intrinsic,
3641
+ n: held,
3642
+ folds,
3643
+ curve,
3644
+ insufficient: "no held-out cell had a unit the rest of the data measured"
3645
+ };
3646
+ const best = finite.reduce((left, right) => right.error < left.error ? right : left);
3647
+ const bar = best.error + (Number.isFinite(best.standardError) ? best.standardError : 0);
3648
+ return {
3649
+ value: finite.find((point) => point.error <= bar).rank,
3650
+ method: METHODS[link].intrinsic,
3651
+ n: held,
3652
+ folds,
3653
+ curve,
3654
+ insufficient: null
3655
+ };
3656
+ }
3657
+ function nextUnits(matrix, fit, leaderRows, noiseVariance, ridge, batch, link) {
3658
+ if (leaderRows.length < 2) return {
3659
+ ranked: [],
3660
+ batch: null,
3661
+ insufficient: `${plural$1(leaderRows.length, "leader")} qualify (${INSUFFICIENT_FROM} or more modelled units); separating leaders needs 2`
3662
+ };
3663
+ if (link === "identity" && (noiseVariance === null || !(noiseVariance > 0))) return {
3664
+ ranked: [],
3665
+ batch: null,
3666
+ insufficient: "the cell noise is unmeasured, so no unit’s information is known"
3667
+ };
3668
+ if (fit.rank === 0) return {
3669
+ ranked: [],
3670
+ batch: null,
3671
+ insufficient: "rank 0: the nodes do not differ beyond noise"
3672
+ };
3673
+ const rank = fit.rank;
3674
+ const units = matrix.unitIds.length;
3675
+ const meanLoading = new Array(rank).fill(0);
3676
+ for (const loading of fit.loadings) for (let d = 0; d < rank; d++) meanLoading[d] += loading[d] / units;
3677
+ const leaders = leaderRows.map((row) => {
3678
+ const point = fit.coordinates[row];
3679
+ const information = fit.loadings.map((q, u) => {
3680
+ if (link === "identity") return 1 / noiseVariance;
3681
+ const p = logistic(fit.bias[u] + dot(point, q));
3682
+ return p * (1 - p);
3683
+ });
3684
+ const gradient = link === "identity" ? meanLoading : Array.from({ length: rank }, (_, d) => {
3685
+ let total = 0;
3686
+ for (let u = 0; u < units; u++) total += information[u] * fit.loadings[u][d] / units;
3687
+ return total;
3688
+ });
3689
+ const prior = link === "identity" ? ridge / noiseVariance : ridge;
3690
+ const system = Array.from({ length: rank }, (_, i) => Array.from({ length: rank }, (_, j) => i === j ? prior : 0));
3691
+ const counts = matrix.rows[row].counts;
3692
+ for (const unit of matrix.rows[row].observed.keys()) {
3693
+ const q = fit.loadings[unit];
3694
+ const weight = information[unit] * (link === "identity" ? 1 : counts.get(unit).count);
3695
+ for (let i = 0; i < rank; i++) for (let j = 0; j < rank; j++) system[i][j] += weight * q[i] * q[j];
3696
+ }
3697
+ return {
3698
+ information,
3699
+ gradient,
3700
+ covariance: choleskyInverse(cholesky(system))
3701
+ };
3702
+ });
3703
+ const shares = (sigmas) => {
3704
+ const toward = sigmas.map((sigma, index) => sigma.map((line) => dot(line, leaders[index].gradient)));
3705
+ const contrast = toward.map((vector, index) => dot(leaders[index].gradient, vector));
3706
+ return fit.loadings.map((q, u) => {
3707
+ const reduction = sigmas.map((sigma, index) => {
3708
+ const information = leaders[index].information[u];
3709
+ if (!(information > 0)) return 0;
3710
+ const spread = dot(q, sigma.map((line) => dot(line, q)));
3711
+ return dot(toward[index], q) ** 2 / (1 / information + spread);
3712
+ });
3713
+ let share = 0;
3714
+ let pairs = 0;
3715
+ for (let i = 0; i < sigmas.length; i++) for (let j = i + 1; j < sigmas.length; j++) {
3716
+ const variance = contrast[i] + contrast[j];
3717
+ if (variance > 0) share += (reduction[i] + reduction[j]) / variance;
3718
+ pairs += 1;
3719
+ }
3720
+ return share / pairs;
3721
+ });
3722
+ };
3723
+ const byShare = (values) => (left, right) => values[right] - values[left] || compareCodeUnits(matrix.unitIds[left], matrix.unitIds[right]);
3724
+ const covariance = leaders.map((leader) => leader.covariance);
3725
+ const first = shares(covariance);
3726
+ const ranked = matrix.unitIds.map((_, u) => u).sort(byShare(first)).map((u) => ({
3727
+ unitId: matrix.unitIds[u],
3728
+ share: round9(first[u]),
3729
+ measuredBy: leaderRows.filter((row) => matrix.rows[row].observed.has(u)).length
3730
+ }));
3731
+ if (batch === null) return {
3732
+ ranked,
3733
+ batch: null,
3734
+ insufficient: null
3735
+ };
3736
+ const among = new Set(batch.among ?? matrix.unitIds);
3737
+ const open = new Set(matrix.unitIds.flatMap((unitId, u) => among.has(unitId) ? [u] : []));
3738
+ const picked = [];
3739
+ let sigmas = covariance;
3740
+ while (picked.length < batch.count && open.size > 0) {
3741
+ const values = shares(sigmas);
3742
+ const best = [...open].sort(byShare(values))[0];
3743
+ open.delete(best);
3744
+ picked.push(matrix.unitIds[best]);
3745
+ const q = fit.loadings[best];
3746
+ sigmas = sigmas.map((sigma, index) => {
3747
+ const information = leaders[index].information[best];
3748
+ if (!(information > 0)) return sigma;
3749
+ const toward = sigma.map((line) => dot(line, q));
3750
+ const denominator = 1 / information + dot(q, toward);
3751
+ return sigma.map((line, i) => line.map((value, j) => value - toward[i] * toward[j] / denominator));
3752
+ });
3753
+ }
3754
+ return {
3755
+ ranked,
3756
+ batch: picked,
3757
+ insufficient: null
3758
+ };
3759
+ }
3760
+ /**
3761
+ * The `asha` extension that adds, at each rung, the units that best separate
3762
+ * the leaders: the greedy `nextUnit` batch from `calibration`'s loadings,
3763
+ * with this search's nodes placed on them from the cells the ledger held when
3764
+ * the rung opened. The leaders are the lens's (the top 3 by predicted mean on
3765
+ * every calibrated unit). Where the lens cannot rank (fewer than 2 leaders,
3766
+ * or no measured noise), `asha` fills the rung from its seeded order.
3767
+ */
3768
+ function nextUnitExtension(calibration, options = {}) {
3769
+ return {
3770
+ name: `next-unit(${hashCanonical(calibration).slice(7, 19)})`,
3771
+ choose({ state, asOfSequence, split, remaining, count }) {
3772
+ return skillManifold(state, calibration.rank, {
3773
+ split,
3774
+ calibration,
3775
+ asOfSequence,
3776
+ ...options.leaders === void 0 ? {} : { leaders: options.leaders },
3777
+ batch: {
3778
+ count,
3779
+ among: remaining
3780
+ }
3781
+ }).data.batch ?? [];
3782
+ }
3783
+ };
3784
+ }
3785
+ function dot(left, right) {
3786
+ let sum = 0;
3787
+ for (let index = 0; index < left.length; index++) sum += left[index] * right[index];
3788
+ return sum;
3789
+ }
3790
+ function mean(values) {
3791
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
3792
+ }
3793
+ function transpose(matrix) {
3794
+ return matrix[0] ? matrix[0].map((_, j) => matrix.map((row) => row[j])) : [];
3795
+ }
3796
+ function multiply(left, right) {
3797
+ return left.map((row) => right[0].map((_, j) => row.reduce((sum, value, m) => sum + value * right[m][j], 0)));
3798
+ }
3799
+ function diagonal(values) {
3800
+ return values.map((value, i) => values.map((_, j) => i === j ? value : 0));
3801
+ }
3802
+ function applyRow(vector, transform) {
3803
+ return transform[0].map((_, j) => vector.reduce((sum, value, m) => sum + value * transform[m][j], 0));
3804
+ }
3805
+ /** Inverse of a lower-triangular matrix by forward substitution. */
3806
+ function lowerInverse(lower) {
3807
+ const n = lower.length;
3808
+ const inverse = Array.from({ length: n }, () => new Array(n).fill(0));
3809
+ for (let j = 0; j < n; j++) {
3810
+ inverse[j][j] = 1 / lower[j][j];
3811
+ for (let i = j + 1; i < n; i++) {
3812
+ let sum = 0;
3813
+ for (let m = j; m < i; m++) sum += lower[i][m] * inverse[m][j];
3814
+ inverse[i][j] = -sum / lower[i][i];
3815
+ }
3816
+ }
3817
+ return inverse;
3818
+ }
3819
+ /** The manifold as text for a terminal or a proposer's context. */
3820
+ function formatSkillManifold(lens) {
3821
+ const { data, signal } = lens;
3822
+ const lines = [];
3823
+ const { matrix } = data;
3824
+ lines.push(`Skill manifold (${data.split} split, ${data.mode === "calibrated" ? `loadings from ${data.source}` : "loadings fitted here"})`);
3825
+ lines.push(` matrix: ${plural$1(matrix.rows, "node")} × ${plural$1(matrix.units, "unit")}, ${matrix.observed} observed cells (density ${matrix.density === null ? "unknown" : `${Math.round(matrix.density * 1e3) / 10}%`}); left out ${plural$1(matrix.excludedNodes, "node")} and ${plural$1(matrix.excludedUnits, "unit")}${matrix.fitRows > 0 && matrix.fitRows < matrix.rows ? `; factors fitted on ${matrix.fitRows} evenly spaced rows` : ""}`);
3826
+ const { intrinsicDimension: intrinsic } = data;
3827
+ if (intrinsic.value === null) lines.push(` intrinsic dimension: insufficient (${intrinsic.insufficient})`);
3828
+ else {
3829
+ const curve = intrinsic.curve.map((point) => `k=${point.rank} ${point.error.toFixed(3)}±${fixedOrDash(point.standardError)}`).join(", ");
3830
+ lines.push(` intrinsic dimension: ${intrinsic.value} (${intrinsic.folds}-fold held-out ${data.model?.link === "logit" ? "binomial log loss" : "squared error on the standardized scale"}, one-SE rule, ${intrinsic.n} held-out cells per rank: ${curve})`);
3831
+ }
3832
+ if (data.model === null) {
3833
+ if (data.insufficient !== intrinsic.insufficient) lines.push(` model: insufficient (${data.insufficient})`);
3834
+ return lines.join("\n");
3835
+ }
3836
+ const { model } = data;
3837
+ const explained = data.explained;
3838
+ const r2 = explained?.byRank.find((entry) => entry.rank === model.rank)?.r2;
3839
+ const logit = model.link === "logit";
3840
+ lines.push(` model: ${logit ? "item-response (two-parameter logistic, every cell 0 or 1)" : "linear on standardized means"}, rank ${model.rank}, ridge ${model.ridge}, ${model.iterations} ${logit ? "Newton" : "ALS"} sweeps${model.converged ? "" : " (not converged)"}${r2 === void 0 ? "" : `, in-sample ${logit ? "pseudo-R²" : "R²"} ${r2.toFixed(3)} over unit ${logit ? "easiness" : "means"}`}${explained && explained.axisShare.length > 0 ? `; axis shares ${explained.axisShare.map((share) => `${Math.round(share * 1e3) / 10}%`).join(", ")}` : ""}; ${logit ? "cell noise binomial" : `cell noise ${data.noiseVariance === null ? "unknown" : data.noiseVariance.toFixed(3)} (standardized)`}`);
3841
+ const strongest = [...data.units].sort((left, right) => right.strength - left.strength).slice(0, 5);
3842
+ lines.push(` units that separate nodes most: ${strongest.map((unit) => `${unit.unitId} (${unit.strength.toFixed(2)}, axis ${dominantAxis(unit.loading) + 1})`).join(", ")}`);
3843
+ if (data.leaders.length > 0) lines.push(` leaders by predicted split mean: ${data.leaders.map((leader) => `${leader.nodeId} ${leader.predictedMean.toFixed(3)} on ${leader.observed} units`).join("; ")}`);
3844
+ if (signal.value === null) lines.push(` next unit: insufficient (${signal.insufficient})`);
3845
+ else {
3846
+ const top = data.nextUnits.slice(0, 5).map((unit) => `${unit.unitId} ${(unit.share * 100).toFixed(1)}% (${unit.measuredBy}/${data.leaders.length} measured)`).join(", ");
3847
+ lines.push(` next unit: ${signal.subject}, removing an expected ${(signal.value * 100).toFixed(1)}% of the leaders' contrast variance per cell; ranked: ${top}`);
3848
+ }
3849
+ return lines.join("\n");
3850
+ }
3851
+ function dominantAxis(loading) {
3852
+ let best = 0;
3853
+ for (let index = 1; index < loading.length; index++) if (Math.abs(loading[index]) > Math.abs(loading[best])) best = index;
3854
+ return best;
3855
+ }
3856
+ function fixedOrDash(value) {
3857
+ return Number.isFinite(value) ? value.toFixed(3) : "–";
3858
+ }
3859
+ //#endregion
3860
+ //#region src/search/lenses/task-matrix.ts
3861
+ function taskMatrix(state, options = {}) {
3862
+ const split = options.split ?? rankingSplit(state);
3863
+ const nodeIds = state.nodeIds();
3864
+ const byNodeUnit = /* @__PURE__ */ new Map();
3865
+ const unitSet = /* @__PURE__ */ new Set();
3866
+ for (const nodeId of nodeIds) {
3867
+ const units = /* @__PURE__ */ new Map();
3868
+ for (const unit of state.unitScores(nodeId, split)) {
3869
+ units.set(unit.unitId, unit.mean);
3870
+ unitSet.add(unit.unitId);
3871
+ }
3872
+ byNodeUnit.set(nodeId, units);
3873
+ }
3874
+ const unitIds = [...unitSet].sort((a, b) => a.localeCompare(b));
3875
+ const nodeClusters = clusterBy(nodeIds, (a, b) => nodeDistance(byNodeUnit, a, b));
3876
+ const unitClusters = clusterBy(unitIds, (a, b) => unitDistance(byNodeUnit, nodeIds, a, b));
3877
+ const orderedNodeIds = nodeClusters.flatMap((c) => c.members);
3878
+ const orderedUnitIds = unitClusters.flatMap((c) => c.members);
3879
+ const cells = [];
3880
+ for (const nodeId of orderedNodeIds) {
3881
+ const units = byNodeUnit.get(nodeId);
3882
+ for (const unitId of orderedUnitIds) cells.push({
3883
+ nodeId,
3884
+ unitId,
3885
+ mean: units.get(unitId) ?? null
3886
+ });
3887
+ }
3888
+ const specialistGain = unitClusters.map((cluster) => {
3889
+ const nodeMeans = [];
3890
+ for (const nodeId of nodeIds) {
3891
+ const units = byNodeUnit.get(nodeId);
3892
+ const scored = cluster.members.map((unitId) => units.get(unitId)).filter((v) => v !== void 0);
3893
+ if (scored.length > 0) nodeMeans.push(scored.reduce((a, b) => a + b, 0) / scored.length);
3894
+ }
3895
+ if (nodeMeans.length < 2) return {
3896
+ clusterId: cluster.id,
3897
+ unitIds: cluster.members,
3898
+ gain: null,
3899
+ nodesContributing: nodeMeans.length
3900
+ };
3901
+ const mean = nodeMeans.reduce((a, b) => a + b, 0) / nodeMeans.length;
3902
+ const gain = Math.max(...nodeMeans) - mean;
3903
+ return {
3904
+ clusterId: cluster.id,
3905
+ unitIds: cluster.members,
3906
+ gain,
3907
+ nodesContributing: nodeMeans.length
3908
+ };
3909
+ });
3910
+ const signalValue = {};
3911
+ for (const row of specialistGain) if (row.gain !== null) signalValue[row.clusterId] = row.gain;
3912
+ return {
3913
+ data: {
3914
+ split,
3915
+ nodeIds: orderedNodeIds,
3916
+ unitIds: orderedUnitIds,
3917
+ nodeClusters,
3918
+ unitClusters,
3919
+ cells,
3920
+ specialistGain
3921
+ },
3922
+ signal: {
3923
+ name: "taskMatrix.specialistGain",
3924
+ value: signalValue
3925
+ }
3926
+ };
3927
+ }
3928
+ function nodeDistance(byNodeUnit, a, b) {
3929
+ const unitsA = byNodeUnit.get(a);
3930
+ const unitsB = byNodeUnit.get(b);
3931
+ let sumSquares = 0;
3932
+ let shared = 0;
3933
+ for (const [unitId, valueA] of unitsA) {
3934
+ const valueB = unitsB.get(unitId);
3935
+ if (valueB === void 0) continue;
3936
+ sumSquares += (valueA - valueB) ** 2;
3937
+ shared += 1;
3938
+ }
3939
+ return shared === 0 ? Number.POSITIVE_INFINITY : Math.sqrt(sumSquares);
3940
+ }
3941
+ function unitDistance(byNodeUnit, nodeIds, a, b) {
3942
+ let sumSquares = 0;
3943
+ let shared = 0;
3944
+ for (const nodeId of nodeIds) {
3945
+ const units = byNodeUnit.get(nodeId);
3946
+ const valueA = units.get(a);
3947
+ const valueB = units.get(b);
3948
+ if (valueA === void 0 || valueB === void 0) continue;
3949
+ sumSquares += (valueA - valueB) ** 2;
3950
+ shared += 1;
3951
+ }
3952
+ return shared === 0 ? Number.POSITIVE_INFINITY : Math.sqrt(sumSquares);
3953
+ }
3954
+ /**
3955
+ * Single-linkage agglomerative clustering. The cutoff is the median of the
3956
+ * finite pairwise distances measured before any merge — a threshold the data
3957
+ * itself sets, not a constant this lens chooses. Two items with no finite
3958
+ * distance (no shared observation) can never merge. Deterministic: ties in
3959
+ * the merge order break on the lexicographically smaller pair, and the
3960
+ * returned clusters and their members are both sorted.
3961
+ */
3962
+ function clusterBy(ids, distance) {
3963
+ let clusters = ids.map((id) => [id]);
3964
+ if (clusters.length <= 1) return clusters.map(toCluster);
3965
+ const finite = [];
3966
+ for (let i = 0; i < ids.length; i++) for (let j = i + 1; j < ids.length; j++) {
3967
+ const d = distance(ids[i], ids[j]);
3968
+ if (Number.isFinite(d)) finite.push(d);
3969
+ }
3970
+ if (finite.length === 0) return clusters.map(toCluster);
3971
+ finite.sort((a, b) => a - b);
3972
+ const cutoff = finite[Math.floor(finite.length / 2)];
3973
+ for (;;) {
3974
+ let best = null;
3975
+ for (let i = 0; i < clusters.length; i++) for (let j = i + 1; j < clusters.length; j++) {
3976
+ let d = Number.POSITIVE_INFINITY;
3977
+ for (const a of clusters[i]) for (const b of clusters[j]) d = Math.min(d, distance(a, b));
3978
+ if (d > cutoff) continue;
3979
+ if (best === null || d < best.d) best = {
3980
+ i,
3981
+ j,
3982
+ d
3983
+ };
3984
+ }
3985
+ if (!best) break;
3986
+ const merged = [...clusters[best.i], ...clusters[best.j]];
3987
+ clusters = clusters.filter((_, index) => index !== best.i && index !== best.j);
3988
+ clusters.push(merged);
3989
+ }
3990
+ return clusters.map(toCluster).sort((a, b) => a.id.localeCompare(b.id));
3991
+ }
3992
+ function toCluster(members) {
3993
+ const sorted = [...members].sort((a, b) => a.localeCompare(b));
3994
+ return {
3995
+ id: sorted[0],
3996
+ members: sorted
3997
+ };
3998
+ }
3999
+ //#endregion
4000
+ //#region src/search/lenses/tree.ts
4001
+ function tree(state) {
4002
+ const nodes = state.nodes();
4003
+ const byId = new Map(nodes.map((node) => [node.nodeId, node]));
4004
+ const childrenOf = /* @__PURE__ */ new Map();
4005
+ const topLevel = [];
4006
+ for (const node of nodes) if (node.primaryParentId !== null && byId.has(node.primaryParentId)) {
4007
+ const list = childrenOf.get(node.primaryParentId);
4008
+ if (list) list.push(node.nodeId);
4009
+ else childrenOf.set(node.primaryParentId, [node.nodeId]);
4010
+ } else topLevel.push(node.nodeId);
4011
+ const primaryOperator = /* @__PURE__ */ new Map();
4012
+ for (const edge of state.edges()) {
4013
+ const child = byId.get(edge.childNodeId);
4014
+ if (!child) continue;
4015
+ if ((edge.parents[0]?.nodeId ?? null) === child.primaryParentId && !primaryOperator.has(child.nodeId)) primaryOperator.set(child.nodeId, edge.operator);
4016
+ }
4017
+ const build = (nodeId) => {
4018
+ const node = byId.get(nodeId);
4019
+ return {
4020
+ nodeId: node.nodeId,
4021
+ status: node.status,
4022
+ rung: node.rung,
4023
+ depth: node.depth,
4024
+ cellCount: node.cellCount,
4025
+ knownCostUsd: node.spend.knownUsd,
4026
+ operator: primaryOperator.get(node.nodeId) ?? null,
4027
+ children: (childrenOf.get(nodeId) ?? []).map((childId) => build(childId))
4028
+ };
4029
+ };
4030
+ return {
4031
+ data: {
4032
+ roots: topLevel.map(build),
4033
+ nodeCount: nodes.length,
4034
+ edgeCount: state.edges().length
4035
+ },
4036
+ signal: {
4037
+ name: "tree.nodeCount",
4038
+ value: nodes.length
4039
+ }
4040
+ };
4041
+ }
4042
+ //#endregion
4043
+ export { editCreditText as A, draftOnPlateau as C, EDIT_CREDIT_ESTIMATOR as D, front as E, EDIT_CREDIT_SIGNAL as O, vectorEmbedding as S, screenedNodes as T, lineageEdits as _, skillCalibration as a, surfaceDigestEdits as b, META_SEARCH_SIGNAL as c, objectiveKey as d, renderMetaSearchText as f, lineEditDistance as g, landscape as h, nextUnitExtension as i, editCredit as k, metaSearch as l, formatLandscape as m, taskMatrix as n, skillManifold as o, searchPolicyGenome as p, formatSkillManifold as r, META_SEARCH_SCORE_SOURCE as s, tree as t, metaSearchScore as u, nearestNeighbours as v, searchPlateau as w, surfaceTextEdits as x, profileTextLines as y };
4044
+
4045
+ //# sourceMappingURL=tree-DHHWsH-X.js.map