@tangle-network/agent-eval 0.198.0 → 0.198.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +3 -1
  3. package/dist/adapters/http.d.ts +1 -1
  4. package/dist/analyst/index.d.ts +1 -1
  5. package/dist/analyst/index.js +1 -1
  6. package/dist/{benchmark-command-D8E5TaPC.js → benchmark-command-DKQuLrrD.js} +2 -2
  7. package/dist/{benchmark-command-D8E5TaPC.js.map → benchmark-command-DKQuLrrD.js.map} +1 -1
  8. package/dist/benchmarks/index.d.ts +2 -2
  9. package/dist/benchmarks/index.js +2 -2
  10. package/dist/campaign/index.d.ts +2 -2
  11. package/dist/campaign/index.js +5 -5
  12. package/dist/{campaign-Dlg2iOzH.js → campaign-DRdKwxm-.js} +6 -6
  13. package/dist/{campaign-Dlg2iOzH.js.map → campaign-DRdKwxm-.js.map} +1 -1
  14. package/dist/cli.js +16 -12
  15. package/dist/cli.js.map +1 -1
  16. package/dist/contract/index.d.ts +4 -4
  17. package/dist/contract/index.js +3 -3
  18. package/dist/{define-agent-eval-By9PImpa.d.ts → define-agent-eval-9JYoCkDI.d.ts} +3 -3
  19. package/dist/{define-agent-eval-By9PImpa.d.ts.map → define-agent-eval-9JYoCkDI.d.ts.map} +1 -1
  20. package/dist/{define-agent-eval-DvBgSzAI.js → define-agent-eval-DkfcqaCF.js} +2 -2
  21. package/dist/{define-agent-eval-DvBgSzAI.js.map → define-agent-eval-DkfcqaCF.js.map} +1 -1
  22. package/dist/{index-BhoOCduB.d.ts → index-BXJHJ_jE.d.ts} +2 -2
  23. package/dist/{index-BhoOCduB.d.ts.map → index-BXJHJ_jE.d.ts.map} +1 -1
  24. package/dist/{index-D-tI1vgf.d.ts → index-DzQ6T34s.d.ts} +3 -3
  25. package/dist/{index-D-tI1vgf.d.ts.map → index-DzQ6T34s.d.ts.map} +1 -1
  26. package/dist/index.d.ts +2 -2
  27. package/dist/index.js +4 -4
  28. package/dist/{llm-judge-BYWXsxhZ.js → llm-judge-3ncNcWNT.js} +3 -3
  29. package/dist/{llm-judge-BYWXsxhZ.js.map → llm-judge-3ncNcWNT.js.map} +1 -1
  30. package/dist/openapi.json +1 -1
  31. package/dist/{produced-state-DOfmCEwP.js → produced-state-qIhqN3V5.js} +2 -2
  32. package/dist/{produced-state-DOfmCEwP.js.map → produced-state-qIhqN3V5.js.map} +1 -1
  33. package/dist/search/index.d.ts +39 -14
  34. package/dist/search/index.d.ts.map +1 -1
  35. package/dist/search/index.js +2 -2
  36. package/dist/{search-kernel-D8_kxChy.js → search-kernel-CumYAjA3.js} +55 -27
  37. package/dist/search-kernel-CumYAjA3.js.map +1 -0
  38. package/dist/{search-kernel-CuvbakC_.d.ts → search-kernel-TLoiMMcq.d.ts} +15 -6
  39. package/dist/search-kernel-TLoiMMcq.d.ts.map +1 -0
  40. package/dist/{search-summary-Dmppg8kV.js → search-summary-dJOaZ3Dw.js} +2 -2
  41. package/dist/{search-summary-Dmppg8kV.js.map → search-summary-dJOaZ3Dw.js.map} +1 -1
  42. package/dist/{skillopt-optimization-method-BMP2qV_1.js → skillopt-optimization-method-BL_j6mX8.js} +2 -2
  43. package/dist/{skillopt-optimization-method-BMP2qV_1.js.map → skillopt-optimization-method-BL_j6mX8.js.map} +1 -1
  44. package/dist/{tree-DHHWsH-X.js → tree-DxGfhISA.js} +115 -60
  45. package/dist/tree-DxGfhISA.js.map +1 -0
  46. package/docs/search-ledger.md +6 -4
  47. package/docs/trace-contracts.md +1 -3
  48. package/package.json +1 -1
  49. package/dist/search-kernel-CuvbakC_.d.ts.map +0 -1
  50. package/dist/search-kernel-D8_kxChy.js.map +0 -1
  51. package/dist/tree-DHHWsH-X.js.map +0 -1
@@ -1,10 +1,10 @@
1
1
  import { a as hashCanonical, i as compareCodeUnits, r as canonicalString } from "./canonical-BsZVgBhW.js";
2
- import { s as zQuantile } from "./internal-BMFSR8Ns.js";
2
+ import { a as medianInPlace, s as zQuantile } from "./internal-BMFSR8Ns.js";
3
3
  import { t as mulberry32 } from "./random-Dn5fPWkt.js";
4
4
  import { r as holm } from "./multiplicity-D9cjmStq.js";
5
5
  import { a as pairedSignTest, n as DECISION_PAIRED_DELTA_STATISTIC } from "./paired-tests-C8iCsioC.js";
6
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";
7
+ import { C as searchEstimateMethod, D as paretoFrontier, T as seedFromDigest, _ as rankingSplit, a as searchPolicyView, b as estimateNode, g as INSUFFICIENT_FROM, u as verifySearchClaim, v as summarizeSamples, w as searchPosterior } from "./search-kernel-CumYAjA3.js";
8
8
  import { f as searchUnitScores } from "./search-ledger-BnUytR8X.js";
9
9
  import { defineInlineResource } from "@tangle-network/agent-interface";
10
10
  //#region src/search/lenses/edit-credit.ts
@@ -594,7 +594,7 @@ function lineageRows(state, nodes, byId, contents, carried) {
594
594
  */
595
595
  function pairedEstimate(input) {
596
596
  const pairs = input.carrier.length;
597
- const method = methodFor(pairs);
597
+ const method = searchEstimateMethod(pairs);
598
598
  const base = {
599
599
  against: input.against,
600
600
  split: input.split,
@@ -663,12 +663,6 @@ const chanceReusable = (() => {
663
663
  return size;
664
664
  };
665
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
666
  /** The worse end of the interval, as an improvement; -Infinity without one. */
673
667
  function improvementLow(credit, sign) {
674
668
  if (credit.interval === null) return Number.NEGATIVE_INFINITY;
@@ -765,7 +759,7 @@ function interactionPairs(input) {
765
759
  }
766
760
  function interactionEstimate(genes, steps, values, sign) {
767
761
  const units = values.length;
768
- const method = methodFor(units);
762
+ const method = searchEstimateMethod(units);
769
763
  const base = {
770
764
  genes,
771
765
  steps,
@@ -933,6 +927,25 @@ function plain(value) {
933
927
  }
934
928
  //#endregion
935
929
  //#region src/search/lenses/front.ts
930
+ /**
931
+ * `front(state, axes)`: the Pareto front over a node's score and its known
932
+ * cost per cell, with room for caller-declared axes (search-tree-design §12).
933
+ *
934
+ * Reuses the existing frontier primitive (`../../pareto`, already the parent
935
+ * selector behind `crowdedFrontierParent` in `search-policy.ts`) rather than
936
+ * a second dominance implementation.
937
+ *
938
+ * The cost axis is the node's cost per attempted cell on the ranking split,
939
+ * not its total spend: an allocator gives a node more cells as it advances,
940
+ * so a total would rank a node by how far it was measured, and a node pruned
941
+ * after its screen would look cheap only for having run fewer cells. A node
942
+ * enters the frontier only when its score rests on 2 or more units (one unit
943
+ * is no estimate, §6.4), every attempted cell on the split has a known cost
944
+ * (an unknown cost is never floored to its known partial or imputed), and it
945
+ * was not decided `invalid` (a judge integrity or admission failure, whose
946
+ * score a reward hack may have produced; such a node never becomes a parent,
947
+ * §6.6).
948
+ */
936
949
  function front(state, options = {}) {
937
950
  const split = options.split ?? rankingSplit(state);
938
951
  const direction = state.header?.objective.direction ?? "maximize";
@@ -940,15 +953,31 @@ function front(state, options = {}) {
940
953
  const rows = state.nodes().map((node) => {
941
954
  const units = state.unitScores(node.nodeId, split);
942
955
  const score = units.length > 0 ? units.reduce((sum, u) => sum + u.mean, 0) / units.length : null;
956
+ let attempted = 0;
957
+ let spent = 0;
958
+ let known = true;
959
+ for (const cell of state.cells({ nodeId: node.nodeId })) {
960
+ if (cell.split !== split || cell.attempts === 0) continue;
961
+ attempted += 1;
962
+ spent += cell.spentUsd;
963
+ if (!cell.costKnown) known = false;
964
+ }
965
+ const extra = extraAxes.map((axis) => axis.value(state, node.nodeId));
966
+ const costPerCellUsd = known && attempted > 0 ? spent / attempted : null;
967
+ const excluded = node.status === "invalid" ? "invalid" : score === null ? "unscored" : units.length < 2 ? "one-unit" : costPerCellUsd === null ? "unknown-cost" : extra.every((value) => Number.isFinite(value)) ? null : "extra-axis";
943
968
  return {
944
969
  nodeId: node.nodeId,
945
970
  score,
971
+ units: units.length,
972
+ method: searchEstimateMethod(units.length),
973
+ costPerCellUsd,
946
974
  knownCostUsd: node.spend.knownUsd,
947
975
  costKnown: node.spend.unknownCostCells === 0,
948
- extra: extraAxes.map((axis) => axis.value(state, node.nodeId))
976
+ excluded,
977
+ extra
949
978
  };
950
979
  });
951
- const eligible = rows.filter((row) => row.score !== null && row.costKnown && row.extra.every((value) => Number.isFinite(value)));
980
+ const eligible = rows.filter((row) => row.excluded === null);
952
981
  const objectives = [
953
982
  {
954
983
  name: "score",
@@ -956,9 +985,9 @@ function front(state, options = {}) {
956
985
  value: (row) => row.score
957
986
  },
958
987
  {
959
- name: "knownCostUsd",
988
+ name: "costPerCellUsd",
960
989
  direction: "minimize",
961
- value: (row) => row.knownCostUsd
990
+ value: (row) => row.costPerCellUsd
962
991
  },
963
992
  ...extraAxes.map((axis, index) => ({
964
993
  name: axis.name,
@@ -967,13 +996,20 @@ function front(state, options = {}) {
967
996
  }))
968
997
  ];
969
998
  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
- }));
999
+ const exclusions = {
1000
+ invalid: 0,
1001
+ unscored: 0,
1002
+ "one-unit": 0,
1003
+ "unknown-cost": 0,
1004
+ "extra-axis": 0
1005
+ };
1006
+ const outRows = rows.map(({ extra: _extra, ...row }) => {
1007
+ if (row.excluded) exclusions[row.excluded] += 1;
1008
+ return {
1009
+ ...row,
1010
+ onFront: frontierIds.has(row.nodeId)
1011
+ };
1012
+ });
977
1013
  const signalValue = {};
978
1014
  for (const row of outRows) signalValue[row.nodeId] = row.onFront ? 1 : 0;
979
1015
  return {
@@ -982,7 +1018,8 @@ function front(state, options = {}) {
982
1018
  axes: objectives.map((o) => o.name),
983
1019
  rows: outRows,
984
1020
  frontierSize: frontierIds.size,
985
- excludedCount: rows.length - eligible.length
1021
+ excludedCount: rows.length - eligible.length,
1022
+ exclusions
986
1023
  },
987
1024
  signal: {
988
1025
  name: "front.membership",
@@ -990,13 +1027,6 @@ function front(state, options = {}) {
990
1027
  }
991
1028
  };
992
1029
  }
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
1030
  /**
1001
1031
  * Nodes whose screen finished, in registration order, root first: the
1002
1032
  * ledger's reading of the kernel's `screened` list. A node counts once it is
@@ -1034,10 +1064,7 @@ function round9(value) {
1034
1064
  return rounded === 0 ? 0 : rounded;
1035
1065
  }
1036
1066
  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;
1067
+ return values.length === 0 ? null : medianInPlace([...values]);
1041
1068
  }
1042
1069
  /** At most `cap` items, evenly spaced by position: a deterministic sample
1043
1070
  * that keeps a quadratic step bounded on a large search. */
@@ -1446,11 +1473,11 @@ function lineEditDistance(a, b) {
1446
1473
  }
1447
1474
  return max;
1448
1475
  }
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";
1476
+ 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, and nodes decided invalid, 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
1477
  /** Scored nodes a surface or a basin count needs: the library's minimum
1451
1478
  * sample for anything descriptive, as for units. */
1452
1479
  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";
1480
+ const BASIN_METHOD = "0-dimensional persistence of node scores on the symmetric k-nearest-neighbour graph of placed nodes (not decided invalid, 2 or more units shared with the root), 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
1481
  /** Nodes the basin graph holds: evenly spaced by registration, plus the best. */
1455
1482
  const BASIN_NODES = 2e3;
1456
1483
  const BASIN_TOP = 100;
@@ -1506,7 +1533,7 @@ function landscape(state, embed = lineageEdits(), options = {}) {
1506
1533
  score: entry.mean === null || entry.pairs < 2 && node.nodeId !== rootNodeId ? null : round9(entry.mean),
1507
1534
  standardError: entry.variance === null ? null : round9(Math.sqrt(entry.variance)),
1508
1535
  pairs: entry.pairs,
1509
- method: estimateMethodFor(entry.pairs),
1536
+ method: searchEstimateMethod(entry.pairs),
1510
1537
  basin: null
1511
1538
  };
1512
1539
  });
@@ -1522,7 +1549,7 @@ function landscape(state, embed = lineageEdits(), options = {}) {
1522
1549
  });
1523
1550
  }
1524
1551
  }
1525
- const scored = records.filter((record) => record.x !== null && record.score !== null && record.pairs >= 2);
1552
+ const scored = records.filter((record) => record.x !== null && record.score !== null && record.pairs >= 2 && record.status !== "invalid");
1526
1553
  const gridResult = krige(records, scored, new Map(posterior.nodes.map((entry) => [entry.nodeId, entry.variance])), columns, surfaceNodes);
1527
1554
  const fullOf = /* @__PURE__ */ new Map();
1528
1555
  nodes.forEach((node, position) => {
@@ -1785,7 +1812,7 @@ function distanceRows(nodes, embed, adjacency) {
1785
1812
  function krige(records, scored, variances, columns, surfaceNodes) {
1786
1813
  if (scored.length < SURFACE_MIN_NODES) return {
1787
1814
  grid: null,
1788
- insufficient: `${plural$1(scored.length, "placed node")} share 2 or more units with the root; a surface needs ${SURFACE_MIN_NODES}`
1815
+ insufficient: `${plural$1(scored.length, "placed node")} not decided invalid share 2 or more units with the root; a surface needs ${SURFACE_MIN_NODES}`
1789
1816
  };
1790
1817
  const noiseOf = (node) => variances.get(node.nodeId) ?? null;
1791
1818
  if (scored.some((node) => noiseOf(node) === null)) return {
@@ -2013,7 +2040,7 @@ function graphBasins(records, scoredNodes, fullOf, space, pooledVariance, degree
2013
2040
  peaks: []
2014
2041
  });
2015
2042
  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}`);
2043
+ if (scored.length < SURFACE_MIN_NODES) return empty(`${plural$1(scored.length, "placed node")} not decided invalid share 2 or more units with the root; basins need ${SURFACE_MIN_NODES}`);
2017
2044
  if (pooledVariance === null || pooledVariance <= 0) return empty("no pooled between-unit variance, so the noise a peak must clear is unknown");
2018
2045
  const sample = new Set(evenSample(scored, BASIN_NODES));
2019
2046
  for (const node of [...scored].sort((left, right) => right.score - left.score || left.ordinal - right.ordinal).slice(0, BASIN_TOP)) sample.add(node);
@@ -3045,7 +3072,7 @@ function skillManifold(state, k = "auto", options = {}) {
3045
3072
  } else leaderRows = matrix.rows.map((row, index) => ({
3046
3073
  row,
3047
3074
  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);
3075
+ })).filter(({ row }) => row.observed.size >= leaderFloor && row.status !== "invalid").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
3076
  const leaderSet = new Set(leaderRows);
3050
3077
  const next = intrinsic.value === 0 ? {
3051
3078
  ranked: [],
@@ -3858,9 +3885,13 @@ function fixedOrDash(value) {
3858
3885
  }
3859
3886
  //#endregion
3860
3887
  //#region src/search/lenses/task-matrix.ts
3888
+ const SPECIALIST_GAIN_METHOD = `per unit cluster, the best node's mean over the cluster's units minus the mean of the node means, in the objective's direction and the metric's units, among nodes not decided invalid that scored every unit of the cluster; reported from 2 such nodes and ${INSUFFICIENT_FROM} units; a point value with no interval: the largest of several noisy means exceeds their mean even when no node specializes, so read it against the spread of the node means`;
3861
3889
  function taskMatrix(state, options = {}) {
3862
3890
  const split = options.split ?? rankingSplit(state);
3891
+ const direction = state.header?.objective.direction ?? "maximize";
3892
+ const sign = direction === "maximize" ? 1 : -1;
3863
3893
  const nodeIds = state.nodeIds();
3894
+ const invalid = new Set(state.nodes().filter((node) => node.status === "invalid").map((node) => node.nodeId));
3864
3895
  const byNodeUnit = /* @__PURE__ */ new Map();
3865
3896
  const unitSet = /* @__PURE__ */ new Set();
3866
3897
  for (const nodeId of nodeIds) {
@@ -3871,7 +3902,7 @@ function taskMatrix(state, options = {}) {
3871
3902
  }
3872
3903
  byNodeUnit.set(nodeId, units);
3873
3904
  }
3874
- const unitIds = [...unitSet].sort((a, b) => a.localeCompare(b));
3905
+ const unitIds = [...unitSet].sort(compareCodeUnits);
3875
3906
  const nodeClusters = clusterBy(nodeIds, (a, b) => nodeDistance(byNodeUnit, a, b));
3876
3907
  const unitClusters = clusterBy(unitIds, (a, b) => unitDistance(byNodeUnit, nodeIds, a, b));
3877
3908
  const orderedNodeIds = nodeClusters.flatMap((c) => c.members);
@@ -3886,25 +3917,43 @@ function taskMatrix(state, options = {}) {
3886
3917
  });
3887
3918
  }
3888
3919
  const specialistGain = unitClusters.map((cluster) => {
3889
- const nodeMeans = [];
3920
+ const contributing = [];
3890
3921
  for (const nodeId of nodeIds) {
3922
+ if (invalid.has(nodeId)) continue;
3891
3923
  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);
3924
+ let total = 0;
3925
+ let complete = true;
3926
+ for (const unitId of cluster.members) {
3927
+ const value = units.get(unitId);
3928
+ if (value === void 0) {
3929
+ complete = false;
3930
+ break;
3931
+ }
3932
+ total += value;
3933
+ }
3934
+ if (complete) contributing.push({
3935
+ nodeId,
3936
+ oriented: sign * total / cluster.members.length
3937
+ });
3894
3938
  }
3895
- if (nodeMeans.length < 2) return {
3939
+ let best = null;
3940
+ for (const entry of contributing) if (best === null || entry.oriented > best.oriented) best = entry;
3941
+ const row = {
3896
3942
  clusterId: cluster.id,
3897
3943
  unitIds: cluster.members,
3944
+ bestNodeId: best?.nodeId ?? null,
3945
+ nodesContributing: contributing.length
3946
+ };
3947
+ if (contributing.length < 2 || cluster.members.length < INSUFFICIENT_FROM) return {
3948
+ ...row,
3898
3949
  gain: null,
3899
- nodesContributing: nodeMeans.length
3950
+ insufficient: contributing.length < 2 ? `${contributing.length} node${contributing.length === 1 ? "" : "s"} scored every unit of the cluster; a gain needs 2` : `${cluster.members.length} of ${INSUFFICIENT_FROM} units`
3900
3951
  };
3901
- const mean = nodeMeans.reduce((a, b) => a + b, 0) / nodeMeans.length;
3902
- const gain = Math.max(...nodeMeans) - mean;
3952
+ const mean = contributing.reduce((sum, entry) => sum + entry.oriented, 0) / contributing.length;
3903
3953
  return {
3904
- clusterId: cluster.id,
3905
- unitIds: cluster.members,
3906
- gain,
3907
- nodesContributing: nodeMeans.length
3954
+ ...row,
3955
+ gain: best.oriented - mean,
3956
+ insufficient: null
3908
3957
  };
3909
3958
  });
3910
3959
  const signalValue = {};
@@ -3912,6 +3961,8 @@ function taskMatrix(state, options = {}) {
3912
3961
  return {
3913
3962
  data: {
3914
3963
  split,
3964
+ direction,
3965
+ method: SPECIALIST_GAIN_METHOD,
3915
3966
  nodeIds: orderedNodeIds,
3916
3967
  unitIds: orderedUnitIds,
3917
3968
  nodeClusters,
@@ -3936,7 +3987,7 @@ function nodeDistance(byNodeUnit, a, b) {
3936
3987
  sumSquares += (valueA - valueB) ** 2;
3937
3988
  shared += 1;
3938
3989
  }
3939
- return shared === 0 ? Number.POSITIVE_INFINITY : Math.sqrt(sumSquares);
3990
+ return shared === 0 ? Number.POSITIVE_INFINITY : Math.sqrt(sumSquares / shared);
3940
3991
  }
3941
3992
  function unitDistance(byNodeUnit, nodeIds, a, b) {
3942
3993
  let sumSquares = 0;
@@ -3949,15 +4000,17 @@ function unitDistance(byNodeUnit, nodeIds, a, b) {
3949
4000
  sumSquares += (valueA - valueB) ** 2;
3950
4001
  shared += 1;
3951
4002
  }
3952
- return shared === 0 ? Number.POSITIVE_INFINITY : Math.sqrt(sumSquares);
4003
+ return shared === 0 ? Number.POSITIVE_INFINITY : Math.sqrt(sumSquares / shared);
3953
4004
  }
3954
4005
  /**
3955
4006
  * Single-linkage agglomerative clustering. The cutoff is the median of the
3956
4007
  * finite pairwise distances measured before any merge — a threshold the data
3957
4008
  * 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.
4009
+ * distance (no shared observation) can never merge. Stopped at a cutoff,
4010
+ * single linkage yields the connected components of the graph whose edges
4011
+ * are the pairs at most the cutoff apart, so the merge order cannot change
4012
+ * the result. The clusters and their members are sorted by UTF-16 code unit,
4013
+ * never by the host's locale, so a cluster id is the same on every machine.
3961
4014
  */
3962
4015
  function clusterBy(ids, distance) {
3963
4016
  let clusters = ids.map((id) => [id]);
@@ -3987,10 +4040,10 @@ function clusterBy(ids, distance) {
3987
4040
  clusters = clusters.filter((_, index) => index !== best.i && index !== best.j);
3988
4041
  clusters.push(merged);
3989
4042
  }
3990
- return clusters.map(toCluster).sort((a, b) => a.id.localeCompare(b.id));
4043
+ return clusters.map(toCluster).sort((a, b) => compareCodeUnits(a.id, b.id));
3991
4044
  }
3992
4045
  function toCluster(members) {
3993
- const sorted = [...members].sort((a, b) => a.localeCompare(b));
4046
+ const sorted = [...members].sort(compareCodeUnits);
3994
4047
  return {
3995
4048
  id: sorted[0],
3996
4049
  members: sorted
@@ -4023,6 +4076,8 @@ function tree(state) {
4023
4076
  depth: node.depth,
4024
4077
  cellCount: node.cellCount,
4025
4078
  knownCostUsd: node.spend.knownUsd,
4079
+ floorUsd: node.spend.floorUsd,
4080
+ unknownCostCells: node.spend.unknownCostCells,
4026
4081
  operator: primaryOperator.get(node.nodeId) ?? null,
4027
4082
  children: (childrenOf.get(nodeId) ?? []).map((childId) => build(childId))
4028
4083
  };
@@ -4042,4 +4097,4 @@ function tree(state) {
4042
4097
  //#endregion
4043
4098
  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
4099
 
4045
- //# sourceMappingURL=tree-DHHWsH-X.js.map
4100
+ //# sourceMappingURL=tree-DxGfhISA.js.map