skynet-graph 1.2.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 (213) hide show
  1. package/CONTRIBUTING.md +22 -0
  2. package/LICENSE +661 -0
  3. package/README.md +245 -0
  4. package/bin/sg +20 -0
  5. package/concepts/README.md +23 -0
  6. package/concepts/_substrate/Claim/Confidence/Trusted.json +6 -0
  7. package/concepts/_substrate/Claim/Confidence.json +10 -0
  8. package/concepts/_substrate/Claim/Freshness/Stale.json +7 -0
  9. package/concepts/_substrate/Claim/Freshness.json +5 -0
  10. package/concepts/_substrate/Claim/Verification/Refuted.json +6 -0
  11. package/concepts/_substrate/Claim/Verification.json +6 -0
  12. package/concepts/_substrate/Claim.json +4 -0
  13. package/concepts/_substrate/Frontier/InBeam.json +6 -0
  14. package/concepts/_substrate/Frontier/Stuck.json +6 -0
  15. package/concepts/_substrate/Frontier.json +5 -0
  16. package/concepts/_substrate/Intake/ToTask.json +22 -0
  17. package/concepts/_substrate/Intake/Typed.json +10 -0
  18. package/concepts/_substrate/Intake.json +27 -0
  19. package/concepts/_substrate/Task/Answer.json +12 -0
  20. package/concepts/_substrate/Task/Complexity/Atomic.json +5 -0
  21. package/concepts/_substrate/Task/Complexity/Compound/Expansion.json +7 -0
  22. package/concepts/_substrate/Task/Complexity/Compound.json +6 -0
  23. package/concepts/_substrate/Task/Complexity.json +6 -0
  24. package/concepts/_substrate/Task/EvalComplexity.json +13 -0
  25. package/concepts/_substrate/Task/ReportUp.json +6 -0
  26. package/concepts/_substrate/Task/Rollup.json +10 -0
  27. package/concepts/_substrate/Task.json +5 -0
  28. package/concepts/clinical/Diagnosis/Medication.json +6 -0
  29. package/concepts/clinical/Diagnosis.json +15 -0
  30. package/concepts/clinical/Observation/LabValue/OutOfRange.json +6 -0
  31. package/concepts/clinical/Observation/LabValue.json +5 -0
  32. package/concepts/clinical/Observation.json +6 -0
  33. package/concepts/common/Document/pathBasket.json +6 -0
  34. package/concepts/common/Document.json +3 -0
  35. package/concepts/common/Edge/Distance.json +13 -0
  36. package/concepts/common/Edge/Stay/LongStay.json +8 -0
  37. package/concepts/common/Edge/Stay.json +8 -0
  38. package/concepts/common/Edge/Travel/LongTravel.json +16 -0
  39. package/concepts/common/Edge/Travel/ShortTravel.json +11 -0
  40. package/concepts/common/Edge/Travel/targetNode.json +7 -0
  41. package/concepts/common/Edge/Travel.json +7 -0
  42. package/concepts/common/Edge.json +3 -0
  43. package/concepts/common/Vertice.json +3 -0
  44. package/concepts/supply/Fulfillment.json +10 -0
  45. package/concepts/supply/Inventory/Reorder.json +7 -0
  46. package/concepts/supply/Inventory.json +5 -0
  47. package/concepts/supply/Procurement/SupplierConfirm.json +6 -0
  48. package/concepts/supply/Procurement.json +8 -0
  49. package/concepts/supply/Transport.json +5 -0
  50. package/doc/API.md +556 -0
  51. package/doc/CAPABILITIES.md +373 -0
  52. package/doc/MODELISATION.md +511 -0
  53. package/doc/architecture.md +267 -0
  54. package/doc/concept-as-graph.md +379 -0
  55. package/doc/concept-learning.md +168 -0
  56. package/doc/creative-loop-map.md +202 -0
  57. package/doc/original-2016-doc.md +148 -0
  58. package/doc/usage.md +501 -0
  59. package/lib/authoring/abstract.js +401 -0
  60. package/lib/authoring/abstraction.js +190 -0
  61. package/lib/authoring/adapt.js +393 -0
  62. package/lib/authoring/ancestry.js +216 -0
  63. package/lib/authoring/author.js +147 -0
  64. package/lib/authoring/bounded-merge.js +57 -0
  65. package/lib/authoring/canon.js +158 -0
  66. package/lib/authoring/clock.js +123 -0
  67. package/lib/authoring/combinator.js +92 -0
  68. package/lib/authoring/compete.js +76 -0
  69. package/lib/authoring/compose-hotspot.js +314 -0
  70. package/lib/authoring/compress.js +279 -0
  71. package/lib/authoring/concept-net.js +254 -0
  72. package/lib/authoring/concepts.js +66 -0
  73. package/lib/authoring/context-project.js +277 -0
  74. package/lib/authoring/contract.js +476 -0
  75. package/lib/authoring/corpus-pack.js +125 -0
  76. package/lib/authoring/cost-probe.js +86 -0
  77. package/lib/authoring/crystallize.js +555 -0
  78. package/lib/authoring/dag-decompose.js +256 -0
  79. package/lib/authoring/dataset-adapter.js +253 -0
  80. package/lib/authoring/debug-provider.js +92 -0
  81. package/lib/authoring/decompose.js +343 -0
  82. package/lib/authoring/emittability.js +236 -0
  83. package/lib/authoring/equilibrium.js +160 -0
  84. package/lib/authoring/extract.js +153 -0
  85. package/lib/authoring/forest.js +55 -0
  86. package/lib/authoring/forge-fallback.js +116 -0
  87. package/lib/authoring/givens.js +121 -0
  88. package/lib/authoring/glossary.js +177 -0
  89. package/lib/authoring/grammar-graph.js +191 -0
  90. package/lib/authoring/granularity.js +80 -0
  91. package/lib/authoring/graph-net.js +108 -0
  92. package/lib/authoring/ground.js +148 -0
  93. package/lib/authoring/higher-order.js +85 -0
  94. package/lib/authoring/hotspot.js +132 -0
  95. package/lib/authoring/hysteresis.js +85 -0
  96. package/lib/authoring/index.js +106 -0
  97. package/lib/authoring/lattice-morphism.js +61 -0
  98. package/lib/authoring/lattice-pack.js +148 -0
  99. package/lib/authoring/leaf-io.js +77 -0
  100. package/lib/authoring/library.js +156 -0
  101. package/lib/authoring/lifecycle.js +92 -0
  102. package/lib/authoring/loop.js +193 -0
  103. package/lib/authoring/master-loop.js +132 -0
  104. package/lib/authoring/mdl.js +209 -0
  105. package/lib/authoring/memo-stability.js +122 -0
  106. package/lib/authoring/method-explorer.js +138 -0
  107. package/lib/authoring/method-pack.js +181 -0
  108. package/lib/authoring/method.js +202 -0
  109. package/lib/authoring/mine.js +510 -0
  110. package/lib/authoring/mount.js +119 -0
  111. package/lib/authoring/negotiate.js +100 -0
  112. package/lib/authoring/parametric.js +139 -0
  113. package/lib/authoring/reaggregate.js +75 -0
  114. package/lib/authoring/rebalance.js +157 -0
  115. package/lib/authoring/recall.js +135 -0
  116. package/lib/authoring/registry.js +350 -0
  117. package/lib/authoring/relearn.js +147 -0
  118. package/lib/authoring/retention.js +89 -0
  119. package/lib/authoring/segment-proxy.js +114 -0
  120. package/lib/authoring/serve-leaf.js +64 -0
  121. package/lib/authoring/slot-aware-serve.js +77 -0
  122. package/lib/authoring/sound-invoke.js +59 -0
  123. package/lib/authoring/split-serve.js +74 -0
  124. package/lib/authoring/ste.js +239 -0
  125. package/lib/authoring/stock.js +101 -0
  126. package/lib/authoring/store.js +94 -0
  127. package/lib/authoring/supervise.js +138 -0
  128. package/lib/authoring/support.js +131 -0
  129. package/lib/authoring/task-mirror.js +51 -0
  130. package/lib/authoring/typed-loop.js +160 -0
  131. package/lib/authoring/validate.js +569 -0
  132. package/lib/authoring/widen.js +88 -0
  133. package/lib/combos/appliance.js +189 -0
  134. package/lib/combos/critique.js +377 -0
  135. package/lib/combos/defaults.js +86 -0
  136. package/lib/combos/durable-runner.js +115 -0
  137. package/lib/combos/forge.js +209 -0
  138. package/lib/combos/index.js +86 -0
  139. package/lib/combos/learning-library.js +253 -0
  140. package/lib/combos/mixture-serve.js +193 -0
  141. package/lib/combos/plan-loop.js +192 -0
  142. package/lib/combos/proxy-cache.js +272 -0
  143. package/lib/combos/self-mod.js +94 -0
  144. package/lib/durable/audit.js +102 -0
  145. package/lib/durable/checkpoint-store.js +550 -0
  146. package/lib/durable/fold.js +62 -0
  147. package/lib/durable/interpreter.js +242 -0
  148. package/lib/durable/xlate.js +185 -0
  149. package/lib/graph/Graph.js +2486 -0
  150. package/lib/graph/expr.js +307 -0
  151. package/lib/graph/index.js +26 -0
  152. package/lib/graph/log.js +134 -0
  153. package/lib/graph/objects/Concept.js +382 -0
  154. package/lib/graph/objects/Entity.js +503 -0
  155. package/lib/graph/objects/Node.js +57 -0
  156. package/lib/graph/objects/PathMap.js +557 -0
  157. package/lib/graph/objects/Segment.js +100 -0
  158. package/lib/graph/tasks/stabilize.js +59 -0
  159. package/lib/graph/tasks/taskflow.js +247 -0
  160. package/lib/index.js +122 -0
  161. package/lib/load.js +132 -0
  162. package/lib/providers/backends.js +117 -0
  163. package/lib/providers/borderline.js +129 -0
  164. package/lib/providers/cache.js +226 -0
  165. package/lib/providers/canonicalize.js +198 -0
  166. package/lib/providers/constat.js +107 -0
  167. package/lib/providers/geo.js +53 -0
  168. package/lib/providers/index.js +116 -0
  169. package/lib/providers/intake.js +263 -0
  170. package/lib/providers/llm-local.js +88 -0
  171. package/lib/providers/llm.js +236 -0
  172. package/lib/providers/local-host.js +197 -0
  173. package/lib/providers/merge-consistency.js +162 -0
  174. package/lib/providers/nogood.js +149 -0
  175. package/lib/providers/reason-loop.js +223 -0
  176. package/lib/providers/semiring.js +285 -0
  177. package/lib/providers/solver-fork.js +163 -0
  178. package/lib/providers/stats.js +228 -0
  179. package/lib/providers/verify.js +120 -0
  180. package/lib/runtime/index.js +135 -0
  181. package/lib/runtime/invoke-pool.js +92 -0
  182. package/lib/runtime/protocol.js +210 -0
  183. package/lib/runtime/transport-socket.js +77 -0
  184. package/lib/runtime/worker-entry.js +31 -0
  185. package/lib/sg/cli.js +601 -0
  186. package/lib/sg/log-sinks.js +214 -0
  187. package/lib/sg/mcp.js +395 -0
  188. package/lib/sg/proxy-run.js +66 -0
  189. package/lib/sg/serve-demo.js +185 -0
  190. package/lib/sg/serve.js +205 -0
  191. package/lib/sg/trace.js +118 -0
  192. package/lib/studio/protocol.js +45 -0
  193. package/lib/studio/public/app.js +310 -0
  194. package/lib/studio/public/components/ConceptEditor.js +43 -0
  195. package/lib/studio/public/components/ConceptTree.js +43 -0
  196. package/lib/studio/public/components/CorpusPanel.js +68 -0
  197. package/lib/studio/public/components/ForkTree.js +36 -0
  198. package/lib/studio/public/components/GrammarGraph.js +99 -0
  199. package/lib/studio/public/components/GraphCanvas.js +156 -0
  200. package/lib/studio/public/components/Inspector.js +48 -0
  201. package/lib/studio/public/components/LearningPanel.js +85 -0
  202. package/lib/studio/public/components/PromptConsole.js +46 -0
  203. package/lib/studio/public/components/ProviderTrace.js +39 -0
  204. package/lib/studio/public/components/SessionSplit.js +47 -0
  205. package/lib/studio/public/components/TilingOverlay.js +40 -0
  206. package/lib/studio/public/components/Timeline.js +57 -0
  207. package/lib/studio/public/index.html +26 -0
  208. package/lib/studio/public/styles.css +185 -0
  209. package/lib/studio/public/ws.js +58 -0
  210. package/lib/studio/server.js +130 -0
  211. package/lib/studio/session.js +397 -0
  212. package/lib/studio/studio.js +149 -0
  213. package/package.json +42 -0
@@ -0,0 +1,160 @@
1
+ /*
2
+ * Copyright 2026 Nathanael Braun
3
+ * @author : Nathanael BRAUN <pp9ping@gmail.com>
4
+ *
5
+ * This program is free software: you can redistribute it and/or modify
6
+ * it under the terms of the GNU Affero General Public License as published by
7
+ * the Free Software Foundation, either version 3 of the License, or
8
+ * (at your option) any later version.
9
+ *
10
+ * This program is distributed in the hope that it will be useful,
11
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ * GNU Affero General Public License for more details.
14
+ *
15
+ * You should have received a copy of the GNU Affero General Public License
16
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ */
18
+ 'use strict';
19
+ /**
20
+ * equilibrium — implicit differentiation through a forward-chaining FIXPOINT (the DEQ method),
21
+ * for learning across the COMPOSITE concept topology. Host-side, ZERO-CORE. Study
22
+ * doc/WIP/studies/2026-06-26-concepts-dynamiques-grammaire-induction.md; theory brief = Laurie.
23
+ *
24
+ * The engine's stabilization is a Picard iteration z_{t+1}=F(z_t,θ) driven to a fixpoint
25
+ * z*=F(z*,θ). To train params θ that live inside concept-nets, we want dL(z*)/dθ WITHOUT
26
+ * differentiating through the iteration. Differentiate the fixpoint condition instead
27
+ * (Almeida/Pineda 1987; DEQ, Bai-Kolter-Koltun 2019):
28
+ *
29
+ * J_z := ∂F/∂z |_{z*} (n×n) J_θ := ∂F/∂θ |_{z*} (n×p) — ONE sweep, at z*
30
+ * dz* / dθ = (I − J_z)^{-1} J_θ
31
+ * dL/dθ = (∇_z L) (I − J_z)^{-1} J_θ
32
+ *
33
+ * Reverse mode (never form the inverse): solve the adjoint (I − J_z)^T u = ∇_z L once, then
34
+ * dL/dθ = J_θ^T u. Well-posed iff ρ(J_z) < 1 (the contraction that also makes the forward
35
+ * fixpoint exist & be unique). Neumann: (I − J_z)^{-1} = Σ_k J_z^k, so u_K = Σ_{k≤K}(J_z^T)^k g
36
+ * — the truncation depth K is the exact analogue of the engine's apply-cap, and its bias ~ρ^K
37
+ * binds precisely as ρ→1. This module is the differentiable MIRROR of the composite topology;
38
+ * the real engine stays discrete (a trained net is baked back via ste.js, STE at the boundary).
39
+ *
40
+ * const fp = solveFixpoint((z) => F(z, θ, x), z0, { maxIter, tol });
41
+ * const { grad } = implicitGrad(jacZ(fp.z), jacTheta(fp.z), gradL, { mode: 'direct' });
42
+ */
43
+
44
+ // ---- small dense linear algebra (POC scale, n ≲ tens — a direct solve is free) ----
45
+
46
+ // Solve A u = b by Gaussian elimination with partial pivoting. A is n×n (row-major), b is n.
47
+ function solveLinear( A, b ) {
48
+ const n = b.length, M = A.map((row, i) => row.slice().concat(b[i])); // augmented
49
+ for ( let col = 0; col < n; col++ ) {
50
+ let piv = col;
51
+ for ( let r = col + 1; r < n; r++ ) if ( Math.abs(M[r][col]) > Math.abs(M[piv][col]) ) piv = r;
52
+ if ( Math.abs(M[piv][col]) < 1e-300 ) throw new Error('equilibrium: singular system (I − J_z not invertible; ρ ≥ 1?)');
53
+ if ( piv !== col ) { const tmp = M[piv]; M[piv] = M[col]; M[col] = tmp; }
54
+ for ( let r = 0; r < n; r++ ) {
55
+ if ( r === col ) continue;
56
+ const f = M[r][col] / M[col][col];
57
+ for ( let c = col; c <= n; c++ ) M[r][c] -= f * M[col][c];
58
+ }
59
+ }
60
+ return M.map((row, i) => row[n] / row[i]);
61
+ }
62
+
63
+ // (J^T v)[i] = Σ_j J[j][i] v[j] — transpose-times-vector (J is r×c, v is r → c-vector)
64
+ function matTvec( J, v ) {
65
+ const r = J.length, c = r ? J[0].length : 0, out = new Array(c).fill(0);
66
+ for ( let i = 0; i < r; i++ ) for ( let j = 0; j < c; j++ ) out[j] += J[i][j] * v[i];
67
+ return out;
68
+ }
69
+
70
+ /**
71
+ * Picard iteration to a fixpoint z* = F(z*). Faithful to the engine's stabilization sweep.
72
+ * @returns { z, iters, residual, converged } residual = max|z_{t+1} − z_t|.
73
+ */
74
+ function solveFixpoint( F, z0, opts ) {
75
+ opts = opts || {};
76
+ const maxIter = opts.maxIter || 1000, tol = opts.tol == null ? 1e-12 : opts.tol;
77
+ let z = z0.slice(), iters = 0, residual = Infinity;
78
+ for ( ; iters < maxIter; ) {
79
+ const zn = F(z);
80
+ residual = 0;
81
+ for ( let i = 0; i < zn.length; i++ ) residual = Math.max(residual, Math.abs(zn[i] - z[i]));
82
+ z = zn; iters++;
83
+ if ( residual < tol ) break;
84
+ }
85
+ return { z: z, iters: iters, residual: residual, converged: residual < tol };
86
+ }
87
+
88
+ /**
89
+ * Implicit (DEQ) gradient dL/dθ = J_θ^T u where (I − J_z)^T u = gradL.
90
+ * @param Jz ∂F/∂z at z* (n×n)
91
+ * @param Jtheta ∂F/∂θ at z* (n×p)
92
+ * @param gradL ∇_z L at z* (n)
93
+ * @param opts.mode 'direct' (dense solve, the gold value) | { neumann: K } (truncated series)
94
+ * @returns { grad (p), u (n) }
95
+ */
96
+ function implicitGrad( Jz, Jtheta, gradL, opts ) {
97
+ opts = opts || {};
98
+ const n = gradL.length;
99
+ let u;
100
+ if ( opts.mode && opts.mode.neumann != null ) {
101
+ // u = Σ_{k=0..K} (Jz^T)^k gradL — each step is one VJP v ↦ Jz^T v (Jacobian-free)
102
+ const K = opts.mode.neumann;
103
+ u = gradL.slice();
104
+ let term = gradL.slice();
105
+ for ( let k = 0; k < K; k++ ) {
106
+ term = matTvec(Jz, term); // Jz^T · term
107
+ for ( let i = 0; i < n; i++ ) u[i] += term[i];
108
+ }
109
+ } else {
110
+ // direct: A = (I − Jz)^T, A[i][j] = δ_ij − Jz[j][i]
111
+ const A = [];
112
+ for ( let i = 0; i < n; i++ ) { const row = new Array(n); for ( let j = 0; j < n; j++ ) row[j] = (i === j ? 1 : 0) - Jz[j][i]; A.push(row); }
113
+ u = solveLinear(A, gradL);
114
+ }
115
+ return { grad: matTvec(Jtheta, u), u: u };
116
+ }
117
+
118
+ /**
119
+ * Central finite-difference Jacobian of fn: R^d → R^n, returned as n×d. The general way to get
120
+ * J_z / J_θ of one composite sweep F without hand-deriving per-unit VJPs — at POC scale (n,d ≲
121
+ * tens) it's cheap and exact-to-O(ε²). For an STE (hard-gate) population, feed it the SOFT sweep
122
+ * (sigmoid gates) at z* — that IS the straight-through surrogate Jacobian for the backward.
123
+ */
124
+ function numJac( fn, point, eps ) {
125
+ eps = eps || 1e-6;
126
+ const d = point.length, f0 = fn(point), n = f0.length;
127
+ const J = Array.from({ length: n }, () => new Array(d));
128
+ for ( let k = 0; k < d; k++ ) {
129
+ const a = point.slice(), b = point.slice(); a[k] += eps; b[k] -= eps;
130
+ const fa = fn(a), fb = fn(b);
131
+ for ( let i = 0; i < n; i++ ) J[i][k] = (fa[i] - fb[i]) / (2 * eps);
132
+ }
133
+ return J;
134
+ }
135
+
136
+ /**
137
+ * Spectral radius of J_z — the regime instrument (ρ→1 ⇒ slow forward, deep Neumann, large
138
+ * gradient). Estimated as the geometric-mean growth rate ρ = lim ‖J_z^k v‖^{1/k} (a normalized
139
+ * power iteration averaging the per-step log-growth). Unlike a last-step power-iteration estimate
140
+ * this is correct when the dominant eigenvalues are TIED in magnitude or complex — e.g. the cycle
141
+ * J_z=[[0,b],[c,0]] has eigenvalues ±√(b·c) (equal magnitude), where the per-step factors form a
142
+ * 2-cycle whose geometric mean is exactly √(b·c).
143
+ */
144
+ function spectralRadius( Jz, opts ) {
145
+ opts = opts || {};
146
+ const iters = opts.iters || 100, burn = opts.burn == null ? Math.floor((opts.iters || 100) / 2) : opts.burn, n = Jz.length;
147
+ const matVec = (M, v) => M.map((row) => row.reduce((s, m, j) => s + m * v[j], 0));
148
+ let v = new Array(n).fill(0).map((_, i) => 1 / Math.sqrt(n) + 1e-3 * (i % 2 ? -1 : 1)); // off-axis seed
149
+ let sumLog = 0, count = 0;
150
+ for ( let k = 0; k < iters; k++ ) {
151
+ const w = matVec(Jz, v);
152
+ const norm = Math.sqrt(w.reduce((s, x) => s + x * x, 0));
153
+ if ( norm < 1e-300 ) return 0;
154
+ if ( k >= burn ) { sumLog += Math.log(norm); count++; }
155
+ v = w.map((x) => x / norm);
156
+ }
157
+ return count ? Math.exp(sumLog / count) : 0;
158
+ }
159
+
160
+ module.exports = { solveFixpoint, implicitGrad, spectralRadius, solveLinear, matTvec, numJac };
@@ -0,0 +1,153 @@
1
+ /*
2
+ * Copyright 2026 Nathanael Braun
3
+ * @author : Nathanael BRAUN <pp9ping@gmail.com>
4
+ *
5
+ * AGPL-3.0-or-later. See <https://www.gnu.org/licenses/>.
6
+ */
7
+ 'use strict';
8
+ /**
9
+ * extract — BOUNDED SUBGRAPH EXTRACTION for fork / multi-process ship (host-side, ZERO-CORE; the 2026-07-01 fork-perf
10
+ * measure + Laurie confront, verdict SOUND-WITH-CORRECTION).
11
+ *
12
+ * WHY. `fork()` with no seed deep-copies the WHOLE working graph (`JSON.parse(serialize().graph)`) then re-mounts it.
13
+ * Measured (`doc/WIP/experiments/2026-07-01-fork-perf/`): the deep-copy is O(working-graph) and a bounded-seed fork is
14
+ * 27–245× cheaper (growing with N). The lever for the MULTI-PROCESS/worker case (`lib/runtime` ships a seed to a
15
+ * separate process — no shared memory) is shipping a bounded SLICE, not the whole graph. COW can't help cross-process.
16
+ *
17
+ * WHAT. This is PROGRAM SLICING (Weiser 1984) on the reference-dependence graph: `focus` is the slicing criterion; a
18
+ * ship-able slice is a CLOSED backward slice with its free variables FROZEN as inputs. A segment-closed **k-hop ball**
19
+ * (`opts.hops`, default 1): the interior grows by (hops-1) BFS rings from the focus + all their incident segments; the
20
+ * segments' non-interior endpoints are the FROZEN frontier (their current facts copied as constants). Sound iff the
21
+ * frozen frontier is FRAME-COMPLETE — `contract.js` G1 applied at the fork boundary: every
22
+ * cross-cut reference reads a frozen value, none is WRITTEN by the slice. Merge-back = SINGLE-WRITER (separation-logic
23
+ * `*`-disjointness; Reynolds 2002) + an ASSUMPTION-RECHECK (the frozen frontier still holds at merge time — the
24
+ * drift-defeasance moved to the fork frontier; de Kleer ATMS 1986: the frozen frontier is the assumption environment).
25
+ *
26
+ * The seed is a `serialize()`-shaped record (`{ lastRev, conceptMaps:[{...facts}], bagRefs }`) → `graph.fork(seed)`
27
+ * locally OR `spawnGraph({ seed })` to a worker. `lib/graph` is UNTOUCHED.
28
+ *
29
+ * DEFERRED / refused (Laurie): a cyclic cross-cut ref (contract G3 footprint-cycle — refuse), a bagRef spanning the cut
30
+ * (needs manager replication), a not-yet-materialized cross-cut producer (the frame-problem impossible case — refuse),
31
+ * multi-writer frontier (the ATMS multi-context, FILED §6.1); the horizon gate on the precise cross-cut-REF facts (v0
32
+ * gates all frontier facts).
33
+ */
34
+ const factsOf = ( graph, id ) => { const o = graph._objById[id]; return o && o._etty && o._etty._; };
35
+ const idOf = ( o ) => o && (o.$$_id != null ? o.$$_id : (o.$_id != null ? o.$_id : o._id));
36
+
37
+ /**
38
+ * Extract a bounded, ship-able subgraph around `focus`.
39
+ * @param graph the parent graph.
40
+ * @param focus a node id / segment id / array of ids (the slicing criterion). A focus segment contributes its
41
+ * endpoints as focus nodes.
42
+ * @param opts.tree optional concept tree → run `separatorGate` on the frozen-frontier facts (horizon check).
43
+ * @param opts.refuseAboveHorizon throw if a frozen frontier fact is above Σ_sep (default false → reported only).
44
+ * @returns { seed, interior, segments, frontier, frozen, focusNodes, horizonOk?, aboveHorizon? }
45
+ */
46
+ function extractSubgraph( graph, focus, opts ) {
47
+ opts = opts || {};
48
+ const focusIds = Array.isArray(focus) ? focus.slice() : [focus];
49
+ const focusNodes = new Set(), focusSegs = new Set();
50
+ for ( const id of focusIds ) {
51
+ const f = factsOf(graph, id);
52
+ if ( !f ) throw new Error('extractSubgraph: unknown focus id "' + id + '"');
53
+ if ( f.Segment ) { focusSegs.add(id); if ( f.originNode ) focusNodes.add(f.originNode); if ( f.targetNode ) focusNodes.add(f.targetNode); }
54
+ else focusNodes.add(id);
55
+ }
56
+ // segment-closed k-hop ball. `hops` (default 1) = the INTERIOR radius: grow the interior by (hops-1) rings of nodes
57
+ // (BFS over incident segments), then include every segment incident to an interior node; a segment's endpoint NOT in
58
+ // the interior is a FROZEN frontier node (the boundary, pinned). hops=1 → interior={focus}, frontier=1-hop neighbours
59
+ // (byte-identical to v0); hops=2 → interior=focus+1-hop, frontier=2-hop; etc.
60
+ const hops = opts.hops == null ? 1 : Math.max(1, opts.hops | 0);
61
+ const incidentOf = ( n ) => { const node = graph._objById[n]; return [].concat((node && node._outgoing) || [], (node && node._incoming) || []); };
62
+ const interiorNodes = new Set(focusNodes);
63
+ let ring = new Set(focusNodes);
64
+ for ( let d = 0; d < hops - 1; d++ ) { // grow the interior by (hops-1) BFS rings
65
+ const next = new Set();
66
+ for ( const n of ring ) for ( const sid of incidentOf(n) ) {
67
+ const sf = factsOf(graph, sid) || {};
68
+ for ( const end of [sf.originNode, sf.targetNode] ) if ( end && !interiorNodes.has(end) ) { interiorNodes.add(end); next.add(end); }
69
+ }
70
+ ring = next;
71
+ if ( !ring.size ) break; // the component is fully inside the ball
72
+ }
73
+ // include every segment incident to an interior node; non-interior endpoints become the frozen frontier.
74
+ const segs = new Set(focusSegs), frontierNodes = new Set();
75
+ for ( const n of interiorNodes ) for ( const sid of incidentOf(n) ) {
76
+ segs.add(sid);
77
+ const sf = factsOf(graph, sid) || {};
78
+ for ( const end of [sf.originNode, sf.targetNode] ) if ( end && !interiorNodes.has(end) ) frontierNodes.add(end);
79
+ }
80
+ // freeze = copy the raw facts of every slice object (interior nodes + incident segments + frozen frontier nodes).
81
+ const sliceIds = [...interiorNodes, ...segs, ...frontierNodes];
82
+ const conceptMaps = sliceIds.map(( id ) => ({ ...factsOf(graph, id) }));
83
+ const frozen = {}; for ( const n of frontierNodes ) frozen[n] = { ...factsOf(graph, n) };
84
+
85
+ const extraction = {
86
+ seed: { lastRev: graph.getCurrentRevision ? graph.getCurrentRevision() : 0, conceptMaps, bagRefs: {} },
87
+ interior: [...interiorNodes], segments: [...segs], frontier: [...frontierNodes], frozen, focusNodes: [...focusNodes], focusSegs: [...focusSegs],
88
+ };
89
+ // horizon gate (optional): every frozen frontier FACT must be on the separator horizon (Σ_sep) or the bound regresses.
90
+ if ( opts.tree ) {
91
+ const { separatorGate } = require('./decompose.js');
92
+ const facts = {};
93
+ for ( const n of frontierNodes ) for ( const k of Object.keys(frozen[n]) ) if ( k !== '_id' && k !== '_rev' ) facts[k] = true;
94
+ const gate = separatorGate(opts.tree, Object.keys(facts));
95
+ extraction.horizonOk = gate.ok; extraction.aboveHorizon = gate.above;
96
+ if ( !gate.ok && opts.refuseAboveHorizon ) throw new Error('extractSubgraph: frozen frontier facts above Σ_sep: ' + gate.above.join(','));
97
+ }
98
+ return extraction;
99
+ }
100
+
101
+ /**
102
+ * The interior-only footprint of a merge template: the ids it writes. SOUND merge writes ONLY interior/focus ids —
103
+ * NEVER a frozen frontier id (that is a write across the cut → the ATMS multi-context contradiction). `$$_id` / `$_id`
104
+ * targets an existing object; a bare `_id` creates a new one (also fine — not a frontier write).
105
+ */
106
+ function templateWrites( tpl ) {
107
+ return (Array.isArray(tpl) ? tpl : [tpl]).map(( o ) => o && (o.$$_id != null ? o.$$_id : (o.$_id != null ? o.$_id : null))).filter(( x ) => x != null);
108
+ }
109
+
110
+ /**
111
+ * Merge a bounded-slice worker/child result back into the parent — SOUNDLY.
112
+ * 1. ASSUMPTION-RECHECK: every frozen frontier fact must still equal the parent's current value (else the slice
113
+ * stabilized on a dead premise → REJECT and re-run; the drift-defeasance at the fork frontier).
114
+ * 2. SINGLE-WRITER: the project template must write ONLY interior ids, never a frozen frontier id (separation-logic
115
+ * disjointness) → else REJECT (the multi-writer/ATMS hazard).
116
+ * 3. apply via the sequenced taskflow (`pushMutation`) — never out-of-band.
117
+ * @param parent the parent graph.
118
+ * @param child the settled fork/worker child (or, cross-process, a plain object with `_objById`-like access via `factsGetter`).
119
+ * @param extraction the object returned by `extractSubgraph`.
120
+ * @param opts.project (child) -> mutationTemplate; default = write back the focus objects' produced facts onto their ids.
121
+ * @param opts.factsGetter (child, id) -> facts; default reads `child._objById[id]._etty._` (override for a cross-process JSON child).
122
+ * @returns { merged:true, template } | { merged:false, reason }
123
+ */
124
+ function mergeSlice( parent, child, extraction, opts ) {
125
+ opts = opts || {};
126
+ const getFacts = opts.factsGetter || (( c, id ) => factsOf(c, id));
127
+ // (1) assumption-recheck — frozen frontier unchanged in the parent.
128
+ for ( const n of extraction.frontier ) {
129
+ const now = factsOf(parent, n) || {}, was = extraction.frozen[n] || {};
130
+ for ( const k of Object.keys(was) ) {
131
+ if ( k === '_id' || k === '_rev' ) continue;
132
+ if ( JSON.stringify(now[k]) !== JSON.stringify(was[k]) ) return { merged: false, reason: 'frontier drift: ' + n + '.' + k + ' changed since extraction' };
133
+ }
134
+ }
135
+ // build the write-back template (default = focus objects' current child facts, minus volatile markers).
136
+ const project = opts.project || function ( c ) {
137
+ return extraction.focusNodes.concat(extraction.focusSegs).map(function ( id ) {
138
+ const f = getFacts(c, id) || {}; const o = { $$_id: id };
139
+ for ( const k of Object.keys(f) ) if ( k !== '_id' && k !== '_rev' && k !== '_origin' ) o[k] = f[k];
140
+ return o;
141
+ });
142
+ };
143
+ const tpl = project(child);
144
+ // (2) single-writer — the template must not write a frozen frontier id.
145
+ const frontierSet = new Set(extraction.frontier);
146
+ const badWrite = templateWrites(tpl).find(( id ) => frontierSet.has(id) );
147
+ if ( badWrite ) return { merged: false, reason: 'single-writer violation: template writes frontier object "' + badWrite + '"' };
148
+ // (3) apply through the sequenced taskflow.
149
+ parent.pushMutation(tpl);
150
+ return { merged: true, template: tpl };
151
+ }
152
+
153
+ module.exports = { extractSubgraph, mergeSlice, templateWrites };
@@ -0,0 +1,55 @@
1
+ /*
2
+ * Copyright 2026 Nathanael Braun
3
+ * @author : Nathanael BRAUN <pp9ping@gmail.com>
4
+ *
5
+ * AGPL-3.0-or-later. See <https://www.gnu.org/licenses/>.
6
+ */
7
+ 'use strict';
8
+ /**
9
+ * forest — THE MULTI-PATH GENERALIZATION (roadmap 2026-07-09 §5(c); ZERO-CORE). The concept-method as a LIBRARY of
10
+ * alternative sub-paths (the derivation forest): "au moins un chemin complète, un seul reste actif". Re-opened now
11
+ * that the single-path P1-P6 + the transitivity (KG-PROXY-2) hold — the single-path is the degenerate forest.
12
+ *
13
+ * `makeForestServe({ forests, candidates, pool, forge? })` → a projection `serve(leaf)`: for a leaf, try its candidate
14
+ * sub-paths IN PREFERENCE ORDER; the FIRST that dispatches + mounts + GATES sound (P1/P3/P4) is SELECTED — the rest are
15
+ * never activated. That selection IS the confluence guarantee that dodges G3 (`footprintCycles`): we SELECT one path,
16
+ * we never COMPOSE mutually-retractable coupled methods, so there is no JTMS oscillation to the apply-cap. If every
17
+ * candidate fails, fall to the §5(b) forge (last-resort learning) or refuse.
18
+ */
19
+ const { makeMethodServe } = require('./serve-leaf.js');
20
+
21
+ /**
22
+ * @param spec.candidates { <candidateKey>: <makeMethodServe method spec> } the alternative sub-paths (each a method).
23
+ * @param spec.forests { <leafKey>: [candidateKey, ...] } the forest per leaf, in PREFERENCE order.
24
+ * @param spec.keyOf (leaf) => leafKey (default: leaf.produces || leaf.id).
25
+ * @param spec.pool a shared P3 invoke-pool (default: own).
26
+ * @param spec.forge async (args) => value the §5(b) fallback when the forest is EXHAUSTED (optional).
27
+ * @returns serve async (leaf, ctx) => { value, selected, tried } (+ serve.pool, serve.close()).
28
+ */
29
+ function makeForestServe( spec ) {
30
+ spec = spec || {};
31
+ const pool = spec.pool || require('../index.js').createInvokePool();
32
+ const forests = spec.forests || {};
33
+ const keyOf = spec.keyOf || (( leaf ) => leaf.produces || leaf.id);
34
+ const serveOne = makeMethodServe({ methods: spec.candidates || {}, keyOf: ( l ) => l.__cand, pool });
35
+
36
+ async function serve( leaf, ctx ) {
37
+ const key = keyOf(leaf);
38
+ const candidates = forests[key] || [];
39
+ const tried = [];
40
+ for ( const c of candidates ) {
41
+ try {
42
+ const value = await serveOne(Object.assign({}, leaf, { __cand: c, id: leaf.id + ':' + c }), ctx); // dispatch + mount + GATE
43
+ return { value, selected: c, tried: tried.concat(c) }; // FIRST sound → SELECTED (one stays active)
44
+ } catch ( e ) { tried.push(c); } // this path didn't complete → try the next (no coupling → no G3)
45
+ }
46
+ if ( typeof spec.forge === 'function' ) // forest EXHAUSTED → §5(b) last-resort learning
47
+ return { value: await spec.forge({ leaf, stack: (ctx && ctx.stack) || [], contract: (ctx && ctx.contract) || {}, reason: 'forest-exhausted', tried }), selected: 'forged', tried };
48
+ return { value: undefined, selected: null, tried, refusal: 'forest-exhausted' };
49
+ }
50
+ serve.pool = pool;
51
+ serve.close = () => spec.pool ? Promise.resolve() : pool.close();
52
+ return serve;
53
+ }
54
+
55
+ module.exports = { makeForestServe };
@@ -0,0 +1,116 @@
1
+ /*
2
+ * Copyright 2026 Nathanael Braun
3
+ * @author : Nathanael BRAUN <pp9ping@gmail.com>
4
+ *
5
+ * AGPL-3.0-or-later. See <https://www.gnu.org/licenses/>.
6
+ */
7
+ 'use strict';
8
+ /**
9
+ * forge-fallback — THE LAST-RESORT LEARNING FALLBACK, wired (roadmap 2026-07-09 §5(b); ZERO-CORE). P2's segment-proxy
10
+ * exposes a `forge` HOOK + `reconstructStack` (the bounded problem stack, walked UP the parentSeg chain). §5(b) wires
11
+ * the pipeline behind that hook:
12
+ *
13
+ * reconstructStack → STACK-TO-PROMPT → forge (the model) → RE-ENTER the gate (assertPost) → INDEX-BACK (amortise)
14
+ *
15
+ * The LEVER (owner): the fallback's success is the SERIALIZATION of the stack into a prompt — if `stackToPrompt`
16
+ * reconstitutes "what we're doing", the model only has to answer simple-but-relevant. When it fails, you work the
17
+ * info-provisioning + the prompt, NOT the model. The forged method is PROPOSE-only: it must satisfy the contract
18
+ * (assertPost DISPOSES) before it casts — it is typed sub-graph, so determinism holds. A forged method is INDEXED so
19
+ * the next matching case is a dispatch HIT (0-call) — the amortisation. The `forge` model is injected (a real LLM in
20
+ * production; a deterministic stub in tests).
21
+ *
22
+ * BOUNDED BLAME-DRIVEN RETRY (2026-07-09, residue USE-2(d), RE-SCOPED by the 2-lens confront). `maxRounds > 1` turns
23
+ * the one-shot forge into a bounded loop: on a gate refusal, the graph's PUSHBACK (the violated post atoms + the
24
+ * rejected-history) is folded into the next prompt (`serializeBlame`, the §5b LEVER one level up) and the model
25
+ * revises, up to `maxRounds`, then a typed refusal. This is the OPEN-regime DUAL of `negotiate.js`: negotiate is the
26
+ * CLOSED-domain constructive dialogue (it ENUMERATES admissible options and refuses on empty-options); here the space
27
+ * is OPEN (a method/castTemplate, not an enum), so the pushback is the BLAME and refusal is on max-rounds — importing
28
+ * negotiate's `optionsOf` is inert here (no enumerable domain) / gold-leaks on a closed enum. The three invariants
29
+ * hold, re-earned for the open regime: 0-FALSE (the admit path is the UNCHANGED `assertPost` — a non-gated forge is
30
+ * never returned, any round), HONEST-but-OPERATIONAL REFUSAL (refuse when no gated answer was produced within the
31
+ * bound — an operational, not epistemic, refusal; the graph never fabricates one), TERMINATION (`maxRounds`).
32
+ * LIVE-PROVEN (Ministral-Q8, `WIP/experiments/2026-07-09-blame-retry-killgate/`): on an OPEN sum+mod3 target, one-shot
33
+ * 17/24 → retry 24/24 at 0-FALSE; the dominant lever is the bounded retry-with-memory (+6/7), the violated-atom
34
+ * CONTENT adds a thin-but-robust +1 (a stubborn case resampling can't reach). `maxRounds` DEFAULTS to 1 → the one-shot
35
+ * behavior is byte-identical (fully backward-compatible).
36
+ */
37
+ const { assertPost } = require('./contract.js');
38
+
39
+ /**
40
+ * defaultStackToPrompt — the tunable serialization. Reconstitutes each level's TYPED interface (statement + produces
41
+ * ⊢ / needs ⊨) + the required output (contract.write) — "what we're doing", bounded, no prose.
42
+ */
43
+ function defaultStackToPrompt( stack, contract, args ) {
44
+ const levels = (stack || []).map(( s, i ) => 'L' + i + ' ' + (s.statement || s.id)
45
+ + (s.produces ? ' ⊢' + s.produces : '') + (s.needs && s.needs.length ? ' ⊨' + s.needs.join(',') : '')).join(' | ');
46
+ const goal = 'PRODUCE ' + (((contract || {}).write) || []).join(',');
47
+ return 'STACK[' + levels + '] · ' + ((args && args.reason) || 'forge') + ' → ' + goal;
48
+ }
49
+
50
+ /**
51
+ * defaultSerializeBlame — the graph's PUSHBACK folded into the revision prompt (the §5b LEVER, one level up). The
52
+ * ONLY thing beyond a bare resample is the CONTENT: the violated post atoms + the rejected-history. Bounded, no prose.
53
+ * @param violations the `assertPost` violations of the last-rejected candidate ({kind,detail}).
54
+ * @param rejected the summaries rejected so far (so the model does NOT repeat them).
55
+ * @param round the current (0-indexed) round about to be attempted.
56
+ */
57
+ function defaultSerializeBlame( violations, rejected, round ) {
58
+ const atoms = (violations || []).filter(( v ) => v.kind === 'post-violated').map(( v ) => v.detail);
59
+ const undecl = (violations || []).filter(( v ) => v.kind === 'undeclared-write').map(( v ) => v.detail);
60
+ const why = [];
61
+ if ( atoms.length ) why.push('violated constraints: [' + atoms.join(', ') + ']');
62
+ if ( undecl.length ) why.push('undeclared writes: [' + undecl.join(', ') + ']');
63
+ return ' | REVISE (attempt #' + (round + 1) + '): the previous answer was REJECTED — ' + (why.join('; ') || 'the post did not hold')
64
+ + '. Already-rejected (do NOT repeat): [' + (rejected || []).map(( r ) => JSON.stringify(r)).join(' ') + ']. Produce a NEW answer satisfying ALL.';
65
+ }
66
+
67
+ /**
68
+ * makeForgeFallback(spec) — build a `forge` fn wired for a segment-proxy's hook.
69
+ * @param spec.forge (prompt, args) => { name, castTemplate, summary?, footprint?, method? } | null REQUIRED — the model.
70
+ * castTemplate = the cast the proxy applies (a typed sub-graph); summary/footprint = what it
71
+ * produces (re-gated); method = a method spec to index-back.
72
+ * @param spec.stackToPrompt (stack, contract, args) => prompt the LEVER (default: defaultStackToPrompt).
73
+ * @param spec.serializeBlame (violations, rejected, round) => promptSuffix the retry pushback (default: defaultSerializeBlame).
74
+ * @param spec.maxRounds the bounded-retry cap (default 1 = one-shot, byte-identical to the pre-retry behavior).
75
+ * @param spec.oracle optional G2 ground-truth probe forwarded to `assertPost` (for an effecting post).
76
+ * @param spec.index Map<libraryKey, method> the library the forged method is indexed into (amortise; default: own).
77
+ * @returns fallback async (args) => castTemplate (args = the P2 hook: { scope, graph, contract, stack, reason, blame }).
78
+ * fallback.index exposes the growing library.
79
+ */
80
+ function makeForgeFallback( spec ) {
81
+ spec = spec || {};
82
+ const stackToPrompt = spec.stackToPrompt || defaultStackToPrompt;
83
+ const serializeBlame = spec.serializeBlame || defaultSerializeBlame;
84
+ const maxRounds = spec.maxRounds || 1;
85
+ const forge = spec.forge;
86
+ const index = spec.index || new Map();
87
+ if ( typeof forge !== 'function' ) throw new Error('makeForgeFallback needs spec.forge(prompt, args)');
88
+
89
+ const fallback = async function ( args ) {
90
+ args = args || {};
91
+ const base = stackToPrompt(args.stack || [], args.contract || {}, args); // the LEVER: stack → prompt
92
+ const rejected = [];
93
+ let lastViolations = null;
94
+ for ( let round = 0; round < maxRounds; round++ ) {
95
+ const prompt = round === 0 ? base : base + serializeBlame(lastViolations, rejected, round); // fold the pushback in (round>0)
96
+ const cand = await forge(prompt, args); // the model — PROPOSE
97
+ if ( !cand || !cand.castTemplate ) throw new Error('forge-fallback: the model produced no candidate');
98
+ // RE-ENTER the gate (DISPOSE): the forged output must satisfy the contract — a bad forge cannot corrupt the caller.
99
+ if ( cand.summary ) {
100
+ const post = assertPost(args.contract || {}, cand.summary, cand.footprint || Object.keys(cand.summary), { oracle: spec.oracle });
101
+ if ( !post.ok ) {
102
+ rejected.push(cand.summary); lastViolations = post.violations;
103
+ if ( round + 1 < maxRounds ) continue; // RETRY with the graph's pushback in the prompt
104
+ throw new Error('forge-fallback: the forged method is REFUSED by the gate (' + post.violations.map(( v ) => v.kind).join(',') + ')');
105
+ }
106
+ }
107
+ if ( cand.method ) index.set(cand.name || (args.stack && args.stack[0] && args.stack[0].id) || 'forged', cand.method); // INDEX-BACK (amortise)
108
+ return cand.castTemplate;
109
+ }
110
+ throw new Error('forge-fallback: no gated candidate within ' + maxRounds + ' rounds'); // (unreachable when maxRounds>=1; a guard)
111
+ };
112
+ fallback.index = index;
113
+ return fallback;
114
+ }
115
+
116
+ module.exports = { makeForgeFallback, defaultStackToPrompt, defaultSerializeBlame };
@@ -0,0 +1,121 @@
1
+ /*
2
+ * Copyright 2026 Nathanael Braun
3
+ * @author : Nathanael BRAUN <pp9ping@gmail.com>
4
+ *
5
+ * AGPL-3.0-or-later. See <https://www.gnu.org/licenses/>.
6
+ */
7
+ 'use strict';
8
+ /**
9
+ * givens — the TYPED BASE-FACT FRONT-DOOR (ZERO-CORE, host-side). Closes the tiered-plan gap (i): a leaf served
10
+ * through the context projection saw its UPSTREAM values but never the task's own literals (the 6 and the 4 of
11
+ * "a 6×4 rectangle"), so the executor re-guessed them. This brick extracts the task's base facts DETERMINISTICALLY
12
+ * (no model), names each one with a stable typed key, and the projection seeds them into the top pool as
13
+ * `val_<given>` — a leaf that cites a given key in its `needs` gets the VALUE injected into its bounded context,
14
+ * exactly like a producer's output. Nothing here is domain reasoning: it is a DECLARED front-door — two extractors
15
+ * (numeric literals in prose; numeric cells of a table), both pure surface, both fail-closed (what is not a clean
16
+ * numeric literal is NOT a given).
17
+ *
18
+ * const { numberGivens, givensBlock, seedOf } = require('skynet-graph/lib/authoring/givens');
19
+ * const givens = numberGivens(task); // [{ key:'g1_wide', value:6, snippet:'is 6 wide' }]
20
+ * const leaves = await decompose(task + '\n' + givensBlock(givens)); // parts cite given keys in `needs`
21
+ * await createPlanLoop({ decompose, serveLeaf }).run(task, { givens: seedOf(givens) });
22
+ *
23
+ * The KEY is positional + a lexical slug (`g<i>_<word>`): stable, typed, never a sentence — the human-readable
24
+ * meaning rides the SNIPPET (presentation), never the key (dispatch), per the K1 discipline.
25
+ */
26
+
27
+ // a numeric literal in prose: optional $, digits with thousand-commas, optional decimals, optional %.
28
+ // NOTE (owner 07-10): the front-door stays DIGITS-ONLY on purpose — translating prose quantities ("ten",
29
+ // "twice") into numbers host-side is the wrong layer; the DECOMPOSER is instructed to restate any non-given
30
+ // base fact explicitly in the consuming part's instruction (DECOMPOSE_SYSTEM rule 6), which covers spelled
31
+ // numbers, fractions in words, and world knowledge alike.
32
+ const NUM_RE = /\$?\s?\d[\d,]*(?:\.\d+)?%?/g;
33
+ const WORD_RE = /[a-z][a-z'-]*/i;
34
+
35
+ // slug = the nearest content word AFTER the number (else BEFORE) — purely lexical, lowercased, bounded.
36
+ const STOP = { the: 1, a: 1, an: 1, of: 1, to: 1, and: 1, or: 1, is: 1, are: 1, was: 1, were: 1, it: 1, its: 1, in: 1, on: 1, at: 1, for: 1, with: 1, then: 1, than: 1, by: 1, per: 1, each: 1, more: 1, less: 1, had: 1, has: 1, have: 1, them: 1, they: 1, those: 1, these: 1, this: 1, that: 1, his: 1, her: 1, their: 1, he: 1, she: 1, we: 1, as: 1, so: 1, but: 1, if: 1, how: 1, many: 1, much: 1, did: 1, does: 1, do: 1 };
37
+ function slugNear( text, start, end ) {
38
+ const pick = ( s ) => {
39
+ let m;
40
+ const re = new RegExp(WORD_RE.source, 'gi');
41
+ while ( (m = re.exec(s)) ) if ( !STOP[m[0].toLowerCase()] ) return m[0].toLowerCase().replace(/[^a-z]/g, '').slice(0, 12);
42
+ return '';
43
+ };
44
+ return pick(text.slice(end, end + 24)) || pick(text.slice(Math.max(0, start - 24), start).split(/\s+/).reverse().join(' '));
45
+ }
46
+
47
+ /**
48
+ * numberGivens(text) → [{ key, value, snippet }] — every clean numeric literal of the prose, in reading order.
49
+ * Deterministic, fail-closed, DIGITS-ONLY (see the NOTE above — prose quantities are the decomposer's job,
50
+ * by prompt rule, not a host-side translation table). `value` is the bare Number ($/commas/% stripped).
51
+ */
52
+ function numberGivens( text ) {
53
+ const s = String(text == null ? '' : text);
54
+ const out = [];
55
+ let m;
56
+ NUM_RE.lastIndex = 0;
57
+ while ( (m = NUM_RE.exec(s)) ) {
58
+ const raw = m[0];
59
+ const v = Number(raw.replace(/[$,%\s]/g, ''));
60
+ if ( !isFinite(v) ) continue;
61
+ const slug = slugNear(s, m.index, m.index + raw.length);
62
+ const key = 'g' + (out.length + 1) + (slug ? '_' + slug : '');
63
+ out.push({ key: key, value: v, snippet: s.slice(Math.max(0, m.index - 18), m.index + raw.length + 18).trim().replace(/\s+/g, ' ') });
64
+ }
65
+ return out;
66
+ }
67
+
68
+ /**
69
+ * cellGivens(table) → [{ key, value, snippet, cell:{r,c} }] — every numeric cell of a row-major table (the FinQA
70
+ * front-door; row 0 = headers by convention). Key = `c<r>_<c>_<slug-of-row-label>`; snippet = "rowLabel · colHeader".
71
+ */
72
+ function cellGivens( table ) {
73
+ const rows = Array.isArray(table) ? table : [];
74
+ const headers = Array.isArray(rows[0]) ? rows[0] : [];
75
+ const out = [];
76
+ for ( let r = 1; r < rows.length; r++ ) {
77
+ const row = rows[r] || [];
78
+ const label = String(row[0] == null ? '' : row[0]);
79
+ for ( let c = 1; c < row.length; c++ ) {
80
+ let raw = String(row[c] == null ? '' : row[c]).trim();
81
+ if ( !raw ) continue;
82
+ const neg = /^\(.*\)$/.test(raw); // (1,234) = accounting negative
83
+ if ( !neg ) raw = raw.replace(/\(\s*[^)]*\)\s*$/, '').trim(); // "-17.1 ( 17.1 )" = FinQA dup-in-parens → keep the head
84
+ const v = Number(raw.replace(/[$,%()\s]/g, '')) * (neg ? -1 : 1);
85
+ if ( !isFinite(v) || !/\d/.test(raw) ) continue;
86
+ const slug = (label.toLowerCase().match(WORD_RE) || [''])[0].replace(/[^a-z]/g, '').slice(0, 12);
87
+ out.push({ key: 'c' + r + '_' + c + (slug ? '_' + slug : ''), value: v, cell: { r: r, c: c },
88
+ snippet: (label + ' · ' + String(headers[c] == null ? '' : headers[c])).trim() });
89
+ }
90
+ }
91
+ return out;
92
+ }
93
+
94
+ /** givensBlock(givens) → the prompt block appended to the decompose USER message (keys citable in `needs`). */
95
+ function givensBlock( givens ) {
96
+ if ( !givens || !givens.length ) return '';
97
+ return 'GIVENS (base facts already available — a part that uses one MUST cite its key in "needs"; do NOT restate the number): '
98
+ + givens.map(( g ) => g.key + '=' + g.value + ' ("' + g.snippet + '")' ).join(' · ');
99
+ }
100
+
101
+ /** seedOf(givens) → { key: value } — the map the projection seeds as `val_<key>` (plan-loop's `ctx.givens`). */
102
+ function seedOf( givens ) {
103
+ const map = {};
104
+ for ( const g of (givens || []) ) map[g.key] = g.value;
105
+ return map;
106
+ }
107
+
108
+ /**
109
+ * labelsOf(givens) → { key: snippet } — provenance labels for leaf prompts, STRUCTURED PROVENANCE ONLY (the
110
+ * "cells" rule, ablation-verified at N=40 and N=200): a table cell's MEANING (row · col header) is invisible in
111
+ * its bare value, so the label fixes leaf mis-localization (wrong cell/base-year/sign); a PROSE given is already
112
+ * restated self-contained by the decomposer (rule 7), so labelling it is pure prompt perturbation (net-negative),
113
+ * and upstream-producer labels help nowhere. Wire as `ctx.labels` (plan-loop → context-project).
114
+ */
115
+ function labelsOf( givens ) {
116
+ const map = {};
117
+ for ( const g of (givens || []) ) if ( g && g.cell && g.snippet ) map[g.key] = String(g.snippet).slice(0, 70);
118
+ return map;
119
+ }
120
+
121
+ module.exports = { numberGivens, cellGivens, givensBlock, seedOf, labelsOf };