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,254 @@
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
+ * concept-net — a differentiable POPULATION of concept-units, trained end-to-end across the
21
+ * composite (fixpoint) topology by implicit diff (equilibrium.js). Host-side, ZERO-CORE. Study
22
+ * 2026-06-26 dynamic-concepts; the user's target shape: each concept-unit = a NN that DECIDES
23
+ * whether to cast (the gate) × a NN that GENERATES the value it writes (the update):
24
+ *
25
+ * contribution of unit i to its target fact = gate_i(ctx_i) · update_i(ctx_i)
26
+ * gate_i = σ(W^g_i·ctx + b^g_i) (cast decision; hard = step at inference, STE backward)
27
+ * update_i= σ(W^u_i·ctx + b^u_i) (the value written when cast)
28
+ * ctx_i = z[ unit_i.inputs ] (the facts the unit reads — the WIRING / structure)
29
+ *
30
+ * One synchronous sweep F updates every computed fact from the current state; the population is
31
+ * run to a fixpoint z*=F(z*) (a learned, gated equilibrium GNN). The structure (which fact each
32
+ * unit reads, how many units) is the FORM that can evolve — add/remove units = structure search.
33
+ * Input facts are clamped (constants), so the DEQ adjoint operates only on the COMPUTED subspace
34
+ * (a clamped fact would be an identity row in J_z → I−J_z singular).
35
+ *
36
+ * This is the differentiable MIRROR of a population of engine concepts; a trained unit is baked
37
+ * back into the real engine via ste.js (STE at the cast boundary) — that bridge is the next rung.
38
+ */
39
+ const { solveFixpoint, implicitGrad, spectralRadius, numJac } = require('./equilibrium.js');
40
+
41
+ const sigmoid = (x) => 1 / (1 + Math.exp(-x));
42
+
43
+ /**
44
+ * Build a population from a STRUCTURE spec (no params — those live in the θ vector trained on top).
45
+ * @param spec.nFacts total facts z[0..nFacts-1]
46
+ * @param spec.inputs indices of CLAMPED input facts (held constant across sweeps)
47
+ * @param spec.units [{ target, inputs:[...] }] — one unit per computed fact; inputs = the wiring
48
+ * @param spec.out the readout fact (full-z index) the loss is taken on
49
+ */
50
+ function makePopulation( spec ) {
51
+ const nFacts = spec.nFacts, inputs = (spec.inputs || []).slice();
52
+ const zeroSet = new Set(spec.zero || []); // clamped facts pinned to 0 (e.g. an unroll's stage-0 init)
53
+ let off = 0;
54
+ const units = spec.units.map((u) => {
55
+ const L = u.inputs.length; // ctx size
56
+ const m = { target: u.target, inputs: u.inputs.slice(), L, oG: off, oGb: off + L, oU: off + L + 1, oUb: off + 2 * L + 1 };
57
+ off += 2 * (L + 1); // gate (L w + bias) + update (L w + bias)
58
+ return m;
59
+ });
60
+ const nParams = off;
61
+ const computed = [...new Set(units.map((u) => u.target))]; // the differentiable state coords (unique facts)
62
+ const out = spec.out == null ? computed[computed.length - 1] : spec.out;
63
+ const outC = computed.indexOf(out);
64
+
65
+ // one synchronous sweep over the FULL z (input facts preserved); hard ⇒ gate is a step (cast).
66
+ // Multiple units may target the same fact — their gate·update contributions SUM (a wide
67
+ // population / mixture); one-unit-per-fact (a ring/chain) is the special case (sum of one).
68
+ function F( z, th, opts ) {
69
+ const hard = opts && opts.hard, zn = z.slice();
70
+ for ( const u of units ) zn[u.target] = 0; // reset computed facts before accumulating
71
+ for ( const u of units ) {
72
+ let ag = th[u.oGb], au = th[u.oUb];
73
+ for ( let k = 0; k < u.L; k++ ) { const c = z[u.inputs[k]]; ag += th[u.oG + k] * c; au += th[u.oU + k] * c; }
74
+ const gate = hard ? (ag >= 0 ? 1 : 0) : sigmoid(ag);
75
+ zn[u.target] += gate * sigmoid(au);
76
+ }
77
+ return zn;
78
+ }
79
+
80
+ // scatter a computed-subvector zc into a full z with inputs clamped to `xv`; gather the reverse
81
+ const scatter = (zc, xv) => { const z = new Array(nFacts).fill(0); for ( const i of inputs ) z[i] = zeroSet.has(i) ? 0 : xv; computed.forEach((j, c) => { z[j] = zc[c]; }); return z; };
82
+ const gather = (z) => computed.map((j) => z[j]);
83
+ // reduced sweep on the computed subspace (what the DEQ differentiates)
84
+ const stepC = (zc, th, xv, opts) => gather(F(scatter(zc, xv), th, opts));
85
+
86
+ return {
87
+ nFacts, inputs, units, nParams, computed, out, outC, F, scatter, gather, stepC,
88
+ randomParams( rng, scale ) { rng = rng || Math.random; scale = scale == null ? 1 : scale; const t = new Array(nParams); for ( let i = 0; i < nParams; i++ ) t[i] = (rng() * 2 - 1) * scale; return t; },
89
+ /** Run the population to its fixpoint for input value xv. hard ⇒ cast decisions are STEP. */
90
+ settle( th, xv, opts ) { return solveFixpoint((zc) => stepC(zc, th, xv, opts), new Array(this.computed.length).fill(0), { maxIter: (opts && opts.maxIter) || 2000, tol: (opts && opts.tol) || 1e-12 }); },
91
+ };
92
+ }
93
+
94
+ /** A ring of K concept-units (a genuine cycle: fact0=input → u1 → u2 → … → uK → back to u1). */
95
+ function ringPopulation( K, opts ) {
96
+ opts = opts || {};
97
+ const units = [];
98
+ for ( let i = 1; i <= K; i++ ) units.push({ target: i, inputs: i === 1 ? [0, K] : [i - 1] });
99
+ return makePopulation({ nFacts: K + 1, inputs: [0], units, out: opts.out == null ? K : opts.out });
100
+ }
101
+
102
+ /** An ACYCLIC chain of K concept-units (a DAG: fact0=input → u1 → u2 → … → uK). The engine bridge
103
+ * starts here — a DAG cascades in topological order, no value-change re-derivation / re-cast loop. */
104
+ function chainPopulation( K, opts ) {
105
+ opts = opts || {};
106
+ const units = [];
107
+ for ( let i = 1; i <= K; i++ ) units.push({ target: i, inputs: [i - 1] });
108
+ return makePopulation({ nFacts: K + 1, inputs: [0], units, out: opts.out == null ? K : opts.out });
109
+ }
110
+
111
+ /** A WIDE population: K parallel units all reading the input fact and SUMMING into one readout
112
+ * fact (a mixture / one-hidden-layer of gate×update bumps). Unlike a chain (which collapses under
113
+ * [0,1]-squashing), capacity grows with K — the topology where the FORM genuinely needs to evolve. */
114
+ function widePopulation( K ) {
115
+ const units = [];
116
+ for ( let i = 0; i < K; i++ ) units.push({ target: 1, inputs: [0] });
117
+ return makePopulation({ nFacts: 2, inputs: [0], units, out: 1 });
118
+ }
119
+
120
+ /**
121
+ * Unroll a CYCLIC population to depth N — turn the recurrent fixpoint z*=F(z*) into an ACYCLIC DAG
122
+ * of N stages (the Picard iteration, unrolled), so it can be served on the engine. Why unroll: a
123
+ * directly-baked cycle DEADLOCKS (the `require` graph is a producer cycle with no entry point), and
124
+ * even past that the engine does not natively iterate a value-feedback loop (findings #22/#15). So
125
+ * we break the cycle into stages: stage t reads stage t-1; stage 0 reads a clamped ZERO init (= the
126
+ * mirror's z0=0). Weights are TIED across stages (the same recurrence) — `tieTheta(theta)` lays the
127
+ * original params out for the unrolled population. The depth-N readout → the true fixpoint as N grows
128
+ * (error ~ρ^N). Bake the result with bakePopulation as usual (it's now an ordinary acyclic population).
129
+ * @returns { pop, tieTheta, readout }
130
+ */
131
+ function unrollPopulation( pop, N ) {
132
+ const C = pop.computed.length, INPUT = 0, ZERO = 1;
133
+ const idx = (t, f) => 2 + t * C + pop.computed.indexOf(f); // unrolled fact index for (stage t, computed fact f)
134
+ const inputSet = new Set(pop.inputs);
135
+ const units = [];
136
+ for ( let t = 0; t < N; t++ ) for ( const u of pop.units ) {
137
+ const inputs = u.inputs.map((i) => inputSet.has(i) ? INPUT : (t === 0 ? ZERO : idx(t - 1, i)));
138
+ units.push({ target: idx(t, u.target), inputs });
139
+ }
140
+ const readout = idx(N - 1, pop.out);
141
+ const up = makePopulation({ nFacts: 2 + N * C, inputs: [INPUT, ZERO], zero: [ZERO], units, out: readout });
142
+ function tieTheta( theta ) {
143
+ const out = [];
144
+ for ( let t = 0; t < N; t++ ) for ( const u of pop.units )
145
+ for ( let k = u.oG; k < u.oG + 2 * (u.L + 1); k++ ) out.push(theta[k]); // each stage reuses u's frozen weights
146
+ return out;
147
+ }
148
+ return { pop: up, tieTheta, readout };
149
+ }
150
+
151
+ /**
152
+ * BAKE a trained population into REAL engine concepts + providers (the mirror↔engine bridge,
153
+ * the analogue of ste.js#createNet for a multi-unit population). Each unit → a concept whose
154
+ * provider reads its input facts `f<i>`, does the HARD gate × update with the frozen weights, and
155
+ * writes its target fact `f<target>` + its self-flag `U<target>` (finding #1). `require` keys on
156
+ * the PRODUCER unit's self-flag (or 'input' for a clamped input fact) — so an acyclic population
157
+ * cascades in topological order to a terminal state. Bake only an ACYCLIC population (a DAG); a
158
+ * cyclic one needs the value-change re-derivation work (#22) first.
159
+ * @returns { conceptTree: { childConcepts }, providers: { Pop } }
160
+ */
161
+ function bakePopulation( pop, theta ) {
162
+ const inputSet = new Set(pop.inputs);
163
+ const producer = {};
164
+ for ( const u of pop.units ) producer[u.target] = 'U' + u.target;
165
+ const childConcepts = {}, provFns = {};
166
+ for ( const u of pop.units ) {
167
+ const name = 'U' + u.target;
168
+ const require = [...new Set(u.inputs.map((i) => (inputSet.has(i) ? 'input' : producer[i])))];
169
+ childConcepts[name] = { _id: name, _name: name, require, provider: ['Pop::' + name] };
170
+ provFns[name] = (function ( unit, nm ) {
171
+ return function ( graph, concept, scope, argz, cb ) {
172
+ let ag = theta[unit.oGb], au = theta[unit.oUb];
173
+ for ( let k = 0; k < unit.L; k++ ) { const c = Number(scope._['f' + unit.inputs[k]]); ag += theta[unit.oG + k] * c; au += theta[unit.oU + k] * c; }
174
+ const gate = ag >= 0 ? 1 : 0, upd = sigmoid(au), facts = { $_id: '_parent' };
175
+ facts[nm] = true; facts['f' + unit.target] = gate * upd;
176
+ cb(null, facts);
177
+ };
178
+ })(u, name);
179
+ }
180
+ return { conceptTree: { childConcepts }, providers: { Pop: provFns } };
181
+ }
182
+
183
+ /** Mean-squared fixpoint-output loss over a dataset (X = input values, T = targets on the readout). */
184
+ function loss( pop, th, X, T, opts ) {
185
+ let s = 0;
186
+ for ( let i = 0; i < X.length; i++ ) s += (pop.settle(th, X[i], opts).z[pop.outC] - T[i]) ** 2;
187
+ return s / X.length;
188
+ }
189
+
190
+ /**
191
+ * Implicit-diff gradient dL/dθ over the whole population. Forward fixpoint uses the requested
192
+ * regime (hard ⇒ STE); the backward Jacobians J_z, J_θ are taken from the SOFT sweep at z*
193
+ * (the straight-through surrogate). Returns { grad, rho } (rho = mean spectral radius, instrument).
194
+ */
195
+ function grad( pop, th, X, T, opts ) {
196
+ const g = new Array(pop.nParams).fill(0); let rhoSum = 0;
197
+ for ( let i = 0; i < X.length; i++ ) {
198
+ const zc = pop.settle(th, X[i], opts).z;
199
+ const gl = new Array(pop.computed.length).fill(0);
200
+ gl[pop.outC] = 2 * (zc[pop.outC] - T[i]) / X.length;
201
+ const Jz = numJac((z) => pop.stepC(z, th, X[i], { hard: false }), zc); // soft surrogate
202
+ const Jt = numJac((t) => pop.stepC(zc, t, X[i], { hard: false }), th);
203
+ const r = implicitGrad(Jz, Jt, gl, opts && opts.mode ? { mode: opts.mode } : { mode: 'direct' });
204
+ for ( let k = 0; k < pop.nParams; k++ ) g[k] += r.grad[k];
205
+ rhoSum += spectralRadius(Jz, { iters: 60 });
206
+ }
207
+ return { grad: g, rho: rhoSum / X.length };
208
+ }
209
+
210
+ /**
211
+ * Train a population to a teacher map via implicit diff + Adam. @returns { theta, loss0, loss, rho }.
212
+ */
213
+ function train( pop, conf ) {
214
+ const X = conf.X, T = conf.T, steps = conf.steps || 1500, lr = conf.lr || 0.05, hard = !!conf.hard;
215
+ let th = (conf.theta0 || pop.randomParams(conf.rng, conf.initScale)).slice();
216
+ const loss0 = loss(pop, th, X, T, { hard });
217
+ const mm = th.map(() => 0), vv = th.map(() => 0), b1 = 0.9, b2 = 0.999;
218
+ let rho = 0;
219
+ for ( let s = 1; s <= steps; s++ ) {
220
+ const r = grad(pop, th, X, T, { hard }); rho = r.rho;
221
+ for ( let k = 0; k < th.length; k++ ) { mm[k] = b1 * mm[k] + (1 - b1) * r.grad[k]; vv[k] = b2 * vv[k] + (1 - b2) * r.grad[k] * r.grad[k]; th[k] -= lr * (mm[k] / (1 - b1 ** s)) / (Math.sqrt(vv[k] / (1 - b2 ** s)) + 1e-8); }
222
+ }
223
+ return { theta: th, loss0, loss: loss(pop, th, X, T, { hard }), rho };
224
+ }
225
+
226
+ /**
227
+ * Evolve the population FORM by success: grow the structure one unit at a time, training each
228
+ * candidate, and KEEP a larger form only while the added unit earns its keep — loss must improve by
229
+ * more than `margin` (a utility / MDL gate, the continuous cousin of abstraction.js). Growth stops
230
+ * at the parsimonious size that fits the task: too few units underfit, the right size fits, more
231
+ * don't pay for themselves. "Their form may need to evolve" + favor-by-success, made mechanical.
232
+ * @param conf.makePop (K)->population build a population of K units (e.g. K => chainPopulation(K))
233
+ * @param conf.X/T the task (inputs, targets on the readout)
234
+ * @param conf.K0/maxK size bounds (default 1 .. 6)
235
+ * @param conf.margin min loss improvement to justify one more unit (default 1e-3)
236
+ * @param conf.rngFor (K)->rng per-size seeded RNG (default Math.random)
237
+ * @returns { K, pop, theta, loss, history:[{K,loss,params}] }
238
+ */
239
+ function evolve( conf ) {
240
+ const makePop = conf.makePop, X = conf.X, T = conf.T, maxK = conf.maxK || 6;
241
+ const margin = conf.margin == null ? 1e-3 : conf.margin, steps = conf.steps || 1200, lr = conf.lr || 0.05;
242
+ const scale = conf.initScale == null ? 0.5 : conf.initScale, rngFor = conf.rngFor || (() => Math.random), hard = !!conf.hard;
243
+ const history = []; let best = null, prev = Infinity;
244
+ for ( let K = conf.K0 || 1; K <= maxK; K++ ) {
245
+ const pop = makePop(K);
246
+ const r = train(pop, { X, T, steps, lr, hard, theta0: pop.randomParams(rngFor(K), scale) });
247
+ history.push({ K, loss: r.loss, params: pop.nParams });
248
+ if ( prev - r.loss > margin ) { best = { K, pop, theta: r.theta, loss: r.loss }; prev = r.loss; }
249
+ else break; // utility gate: the new unit didn't pay for itself
250
+ }
251
+ return { K: best.K, pop: best.pop, theta: best.theta, loss: best.loss, history };
252
+ }
253
+
254
+ module.exports = { makePopulation, ringPopulation, chainPopulation, widePopulation, unrollPopulation, bakePopulation, loss, grad, train, evolve };
@@ -0,0 +1,66 @@
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
+ // Builds the nested concept tree the engine expects from concepts/<set>/.
19
+ // Engine invariants honored:
20
+ // - a child's key in parent.childConcepts MUST equal that child's _id
21
+ // - _id must be globally unique (it keys graph._conceptLib)
22
+ // - _name is the flag written on entities (defaults to the concept name)
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+ const JSON5 = require('json5');
26
+
27
+ function buildConceptTree(setDir, { exclude = [] } = {}) {
28
+ const seenIds = new Set();
29
+ const reserve = (name) => {
30
+ if (seenIds.has(name)) throw new Error(`concept id collision: ${name}`);
31
+ seenIds.add(name);
32
+ return name;
33
+ };
34
+
35
+ // Build one concept from its .json file + optional sibling directory of children.
36
+ function build(name, filePath, childDir) {
37
+ const rec = JSON5.parse(fs.readFileSync(filePath, 'utf8'));
38
+ rec._id = reserve(name);
39
+ rec._name = name;
40
+ if (fs.existsSync(childDir) && fs.statSync(childDir).isDirectory()) {
41
+ const kids = {};
42
+ for (const f of fs.readdirSync(childDir).sort()) {
43
+ if (!f.endsWith('.json')) continue;
44
+ const cname = f.slice(0, -5);
45
+ if (exclude.includes(cname)) continue;
46
+ kids[cname] = build(cname, path.join(childDir, f), path.join(childDir, cname));
47
+ }
48
+ if (Object.keys(kids).length) rec.childConcepts = kids;
49
+ }
50
+ return rec;
51
+ }
52
+
53
+ // Root = a container whose children are the top-level *.json files of the set.
54
+ const root = {};
55
+ const kids = {};
56
+ for (const f of fs.readdirSync(setDir).sort()) {
57
+ if (!f.endsWith('.json')) continue;
58
+ const cname = f.slice(0, -5);
59
+ if (exclude.includes(cname)) continue;
60
+ kids[cname] = build(cname, path.join(setDir, f), path.join(setDir, cname));
61
+ }
62
+ root.childConcepts = kids;
63
+ return root;
64
+ }
65
+
66
+ module.exports = { buildConceptTree };
@@ -0,0 +1,277 @@
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
+ * context-project — GRAPH-NATIVE CONTEXT PROJECTION (the R1 §2 "contexte par nœud", ZERO-CORE). A big roadmap
10
+ * does not fit one context window, so no node ever sees the whole task: each part reads its BOUNDED neighbourhood
11
+ * from the graph's STRUCTURE and completes its own prompt. Kill-gated on the real engine (the study
12
+ * `WIP/experiments/2026-07-08-graph-context-completion/` — reactive-pool.js the reference, recursive.js multi-level).
13
+ *
14
+ * THE MECHANISM (all by structure, no orchestrator):
15
+ * • the roadmap is seeded as SEGMENTS + a POOL ref by the parent (versionable key-values): `available` (produced
16
+ * resources), `wait_<res>` (who awaits <res>), `val_<res>` (the value). A part reads its inputs from the pool.
17
+ * • a dependency is a COUNTER GATE `ensure:'$got.length == $expected'` — a producer posts its resource to the
18
+ * pool and PUSHes its name into each waiter's `got`; the waiter casts when it has all of them. Arity-agnostic,
19
+ * one gate. The ORDER EMERGES from the data-flow (a consumer casts strictly after its producers).
20
+ * • RECURSIVE: a step with `sub` is a COMPOSITE — once its inputs are ready it seeds a SUB-pool + its sub-steps,
21
+ * DOWN-PROJECTS its resolved inputs into the sub-pool (bounded context for the children), and wires the
22
+ * sub-plan's TERMINAL leaf (the one that produces the composite's resource) to REPORT UP to the parent pool.
23
+ * • NO RUNTIME DEADLOCK: propagation is MONOTONE + deterministic, so a stuck part is a DÉCOUPAGE BUG (a resource
24
+ * awaited with no producer, or a cycle), caught OFFLINE by `guardPlan` (coverage + footprintCycles, recursive).
25
+ * A mis-split never hangs (the engine goes quiescent) and never silently builds with a missing input (a leaf
26
+ * gates on ALL its needs; only the down-projected ones are pre-satisfied) — it is REFUSED before seeding.
27
+ * • STATELESS / VERSIONABLE → COLD-REBOOTABLE: serialize → new Graph → identical state, 0 re-fire (a completed
28
+ * roadmap is a fixpoint). Follows the mutation-sequencing discipline (every change through the taskflow).
29
+ *
30
+ * USABLE À NU (domain-agnostic): the host supplies a `roadmap` of `{ id, needs, produces, sub? }` and a `serve`
31
+ * (the bounded work — a deterministic stub, or the C6 ladder / an LLM in production). `plan-loop.js` wires this as
32
+ * its REAL projection (each leaf served WITH its completed bounded context, in emergent order) in place of the
33
+ * abstract per-leaf map.
34
+ *
35
+ * DETERMINISM CAVEAT (measured, A3 monster harness): independent-ready leaves are served CONCURRENTLY, and the
36
+ * COMPLETION order of in-flight serves is a race. Values are immune (each leaf's inputs come from its own gated
37
+ * needs), but if a host's serve side-feeds OTHER leaves' prompts (e.g. a "DONE so far" summary channel), that
38
+ * prompt content becomes completion-order-dependent and replays diverge. Fix at the host: serialize the serves
39
+ * (`let q = Promise.resolve(); serve = (l) => (q = q.then(() => real(l)))`) — enqueue order is deterministic,
40
+ * and a single local model host is serial anyway, so the mutex costs nothing.
41
+ *
42
+ * const { createContextProjection, guardPlan } = require('skynet-graph/lib/authoring/context-project');
43
+ * const proj = createContextProjection({ serve: async (leaf) => callModel(leaf.prompt) });
44
+ * const { order, results, refusal } = await proj.run(roadmap, ctx);
45
+ */
46
+ const Graph = require('../graph/index.js');
47
+ const { nextStable } = require('./supervise.js');
48
+ const { footprintCycles } = require('./contract.js');
49
+
50
+ // ── guardPlan — the OFFLINE deadlock guard (recursive). A level is coherent iff every `need` has a producer at
51
+ // that level OR is inherited (down-projected from a composite's inputs), and the data-flow is acyclic. The ONLY
52
+ // place a "deadlock" can live is a bad split; this converts a silent starve into a typed refusal BEFORE seeding. ──
53
+ function guardPlan( roadmap, inherited ) {
54
+ inherited = inherited || [];
55
+ const producers = new Set(roadmap.map(( s ) => s.produces ).concat(inherited));
56
+ const uncovered = [], cycles = [];
57
+ const methodOf = ( s ) => ({ name: s.id, contract: { read: (s.needs || []).filter(( n ) => !inherited.includes(n) ), write: [s.produces], effect: 'effect' } });
58
+ cycles.push(...footprintCycles(roadmap.map(methodOf)));
59
+ for ( const s of roadmap ) {
60
+ for ( const n of (s.needs || []) ) if ( !producers.has(n) ) uncovered.push({ step: s.id, need: n });
61
+ if ( s.sub ) {
62
+ if ( !s.sub.some(( c ) => c.produces === s.produces ) ) uncovered.push({ step: s.id, need: '(no sub-terminal produces ' + s.produces + ')' });
63
+ const sub = guardPlan(s.sub, s.needs); // the composite's inputs are available DOWN in the sub-plan
64
+ uncovered.push(...sub.uncovered); cycles.push(...sub.cycles);
65
+ }
66
+ }
67
+ return { ok: uncovered.length === 0 && cycles.length === 0, uncovered, cycles };
68
+ }
69
+
70
+ // ── the concept map: a leaf STEP (counter gate → serve → post + notify [+ report up]) and a COMPOSITE DECOMPOSE
71
+ // (counter gate on inputs → seed the sub-plan). One-shot casts guarded by the `_name` marker (re-fire guard). ──
72
+ const CONCEPT_MAP = { common: { childConcepts: {
73
+ Task: { _id: 'Task', _name: 'Task', require: 'Segment', childConcepts: {
74
+ Step: { _id: 'Step', _name: 'Step', require: ['Task', 'isStep'], ensure: ['$got.length == $expected'], provider: ['CtxProj::step'] },
75
+ Decompose: { _id: 'Decompose', _name: 'Decompose', require: ['Task', 'isComposite'], ensure: ['$got.length == $expected'], provider: ['CtxProj::decompose'] },
76
+ } }
77
+ } } };
78
+
79
+ // default prompt completion — the bounded neighbourhood: the ancestor statement + the resolved inputs + the goal
80
+ // to produce. An input whose key has a `ctx.labels` entry is rendered WITH its provenance label (the "cells" rule,
81
+ // givens.js#labelsOf — structured provenance only; measured: fixes mis-localization, never label prose).
82
+ function defaultComplete( leaf ) {
83
+ const lbl = leaf.labels || {};
84
+ const inputs = Object.keys(leaf.inputs).map(( k ) => k + '=' + leaf.inputs[k] + (lbl[k] ? ' (' + lbl[k] + ')' : '') );
85
+ return 'GOAL ' + leaf.statement + (inputs.length ? ' · USE ' + inputs.join(',') : ' · (root)')
86
+ + (leaf.reportsUp ? ' · REPORT-UP ' + leaf.produces : '') + ' · PRODUCE ' + leaf.produces;
87
+ }
88
+
89
+ // deterministic prose title — the first sentence, truncated (never invented; A3 owner-spec roadmap).
90
+ function proseTitle( s, cap ) {
91
+ const first = (String(s || '').match(/^[^.!?]*[.!?]/) || [String(s || '')])[0].trim();
92
+ return first.length > cap ? first.slice(0, cap).trim() + '…' : first;
93
+ }
94
+
95
+ // ── stratComplete — the STRATIFIED leaf rendering, promoted from the A3 monster harness (opt-in via
96
+ // opts.complete; the default stays defaultComplete). Each LEVEL gets the regime measured best for it:
97
+ // • leaves inside a composite (and flat levels) = CONTEXT (the parent statement) + DONE (already-served
98
+ // level siblings, `key = value (nl…)`) + TASK — the "chainsum" regime (A3: 58/80 vs base 50/80);
99
+ // • leaves of a level that CONTAINS composites = CONTEXT + the ROADMAP view (every level step with its
100
+ // instruction and produced value `[done]`/`[todo]`, composites folded to their statement title, the
101
+ // current step marked `>>x<< [YOU ARE HERE]` + a scope guard) — the integrator USES it (A3 NEG:
102
+ // a lying roadmap breaks finals), calculation leaves ignore it, hence the stratification.
103
+ // The wording below is the CANONICAL FORM p0 — measured form-ROBUST by the K-paraphrases harness
104
+ // (`WIP/experiments/2026-07-12-k-paraphrases/`: 4 full re-wordings → sections mean 56.3/80 ± 1.5, min 55,
105
+ // vs base 50; integration mean 4.7/20 vs base 3, never below). The INFORMATION carries the gain, not the
106
+ // tokens — so the form is FROZEN (anti-butterfly: never re-phrase per task, never tune the wording).
107
+ // DONE/ROADMAP read level state at serve time → they require SERIALIZED serves (see the determinism
108
+ // caveat in the header) for replay-stable prompts.
109
+ function stratComplete( leaf ) {
110
+ const lbl = leaf.labels || {};
111
+ const inputs = Object.keys(leaf.inputs || {}).map(( k ) => k + ' = ' + leaf.inputs[k] + (lbl[k] ? ' (' + lbl[k] + ')' : '') );
112
+ const parts = [];
113
+ if ( leaf.statement ) parts.push('CONTEXT: ' + leaf.statement);
114
+ const plan = leaf.plan || [];
115
+ if ( leaf.level === 'top' && plan.some(( p ) => p.composite ) ) { // integration level → ROADMAP view
116
+ const lines = ['PLAN:'];
117
+ for ( const p of plan ) {
118
+ const head = p.composite && p.statement ? p.key + ' ("' + proseTitle(p.statement, 72) + '")'
119
+ : (p.current ? '>>' + p.key + '<<' : p.key) + (p.nl ? ' (' + String(p.nl).slice(0, 90) + ')' : '');
120
+ lines.push(' ' + head + (p.value !== undefined ? ' = ' + p.value + ' [done]' : (p.current ? ' [YOU ARE HERE]' : ' [todo]')));
121
+ }
122
+ parts.push(lines.join('\n') + '\nYou are at the marked >>step<<. Solve ONLY that step; everything else is out of your scope.');
123
+ } else if ( (leaf.done || []).length )
124
+ parts.push('DONE: ' + leaf.done.map(( d ) => d.key + ' = ' + d.value + (d.nl ? ' (' + String(d.nl).split(/\s+/).slice(0, 8).join(' ') + ')' : '') ).join(' · '));
125
+ parts.push('TASK: ' + (leaf.nl || 'produce ' + leaf.produces) + (inputs.length ? ' Given: ' + inputs.join(' ; ') + '.' : ''));
126
+ return parts.join('\n');
127
+ }
128
+
129
+ // build the seed for the TOP level (a parent énoncé + a pool ref by it, pre-populated with the wait index).
130
+ // `givens` ({ key: value }, optional) = the task's BASE FACTS (givens.js front-door): seeded as already-available
131
+ // `val_<key>` pool entries, and every step's given-needs are PRE-SATISFIED (counted in `got` like a down-projected
132
+ // input) — the same inherited semantics as a composite's down-projection, applied at the top level.
133
+ function buildSeed( roadmap, statement, givens ) {
134
+ givens = givens || {};
135
+ // _seed = the pre-satisfied entries (to tell "already available" from "produced so far" — leaf.done) ·
136
+ // _plan = the level skeleton (id/key/nl/composite/statement — leaf.plan, the roadmap-view source). Inert data.
137
+ const pool = { _id: 'POOL', _isPool: true, available: Object.keys(givens), _seed: Object.keys(givens),
138
+ _plan: roadmap.map(( s ) => ({ id: s.id, key: s.produces, nl: s.nl || '', composite: !!s.sub, statement: s.statement || '' }) ) };
139
+ Object.keys(givens).forEach(( g ) => { pool['val_' + g] = givens[g]; });
140
+ roadmap.forEach(( s ) => { pool['wait_' + s.produces] = pool['wait_' + s.produces] || []; });
141
+ roadmap.forEach(( s ) => (s.needs || []).filter(( n ) => !(n in givens) ).forEach(( n ) => { (pool['wait_' + n] = pool['wait_' + n] || []).push(s.id); }) );
142
+ const nodes = [{ _id: 'nRoot', Node: true }, pool];
143
+ const segments = [{ _id: 'PARENT', Segment: true, originNode: 'nRoot', targetNode: 'nRoot', statement: statement, pool: 'POOL' }];
144
+ roadmap.forEach(( s ) => {
145
+ nodes.push({ _id: 'a_' + s.id, Node: true }, { _id: 'b_' + s.id, Node: true });
146
+ const seg = { _id: s.id, Segment: true, originNode: 'a_' + s.id, targetNode: 'b_' + s.id, parentSeg: 'PARENT',
147
+ pool: 'POOL', needs: s.needs || [], produces: s.produces, expected: (s.needs || []).length,
148
+ got: (s.needs || []).filter(( n ) => n in givens ) };
149
+ if ( s.slot ) seg.slot = s.slot; // carry the method-slot (higher-order need) to the leaf — inert data, like `sub`
150
+ if ( s.nl != null ) seg.nl = s.nl; // step instruction + level statement (strat rendering) — inert data too
151
+ if ( s.statement != null ) seg.statement = s.statement;
152
+ if ( s.sub ) { seg.isComposite = true; seg.sub = s.sub; } else seg.isStep = true;
153
+ segments.push(seg);
154
+ });
155
+ return { lastRev: 0, nodes, segments };
156
+ }
157
+
158
+ // enumerate every id in the roadmap tree (top + all sub levels) — for collecting results incl. runtime sub-steps.
159
+ function allSteps( roadmap ) { return roadmap.flatMap(( s ) => [s].concat(s.sub ? allSteps(s.sub) : []) ); }
160
+
161
+ function makeProviders( serve, complete, ctx, order ) {
162
+ return { CtxProj: {
163
+ // LEAF — read bounded inputs from own pool, SERVE (the injected bounded work), post to own pool + notify, report up if terminal.
164
+ step: function ( graph, concept, scope, argz, cb ) {
165
+ (async () => {
166
+ const self = scope._, pool = graph.getEtty(self.pool)._;
167
+ const inputs = {}; (self.needs || []).forEach(( n ) => { inputs[n] = pool['val_' + n]; });
168
+ // the LEVEL state read on the structure (strat rendering; additive — hosts ignoring them see no change):
169
+ // done = level entries produced so far (available beyond _seed, emergent order) · plan = the level
170
+ // skeleton with current values + the own-step marker · level = top vs inside-a-composite.
171
+ const seedSet = new Set(pool._seed || pool.available || []);
172
+ const nlOf = {}; (pool._plan || []).forEach(( p ) => { nlOf[p.key] = p.nl; });
173
+ const done = (pool.available || []).filter(( k ) => !seedSet.has(k) )
174
+ .map(( k ) => ({ key: k, value: pool['val_' + k], nl: nlOf[k] || '' }) );
175
+ const plan = (pool._plan || []).map(( p ) => Object.assign({}, p, { value: pool['val_' + p.key], current: p.id === self._id }) );
176
+ const leaf = { id: self._id, statement: graph.getEtty(self.parentSeg)._.statement, produces: self.produces,
177
+ needs: self.needs || [], inputs: inputs, reportsUp: !!self.up_res, slot: self.slot,
178
+ nl: self.nl, level: self.parentSeg === 'PARENT' ? 'top' : 'sub', done: done, plan: plan,
179
+ labels: (ctx && ctx.labels) || {} };
180
+ const prompt = complete(leaf);
181
+ let value;
182
+ try { value = await serve(Object.assign({ prompt: prompt }, leaf), ctx); }
183
+ catch ( e ) { return cb(e); }
184
+ order.push(self._id);
185
+ const tpl = [ { $_id: '_parent', Step: true, prompt: prompt, out: value } ];
186
+ tpl.push({ $$_id: self.pool, available: { __push: self.produces }, ['val_' + self.produces]: value });
187
+ (pool['wait_' + self.produces] || []).forEach(( wid ) => tpl.push({ $$_id: wid, got: { __push: self.produces } }) );
188
+ if ( self.up_res ) { // REMONTÉE — deliver to the parent pool + notify parent waiters
189
+ tpl.push({ $$_id: self.up_pool, available: { __push: self.up_res }, ['val_' + self.up_res]: value });
190
+ (self.up_wait || []).forEach(( wid ) => tpl.push({ $$_id: wid, got: { __push: self.up_res } }) );
191
+ }
192
+ cb(null, tpl);
193
+ })();
194
+ },
195
+ // COMPOSITE — inputs ready → seed a SUB-pool ref by me + my sub-steps, DOWN-project my inputs, wire the terminal UP.
196
+ decompose: function ( graph, concept, scope, argz, cb ) {
197
+ const self = scope._, pool = graph.getEtty(self.pool)._, sub = self.sub, SUB = 'SUBPOOL_' + self._id;
198
+ order.push(self._id + ':decompose');
199
+ const subProducers = new Set(sub.map(( c ) => c.produces ));
200
+ const downSet = new Set(self.needs || []);
201
+ const subPool = { _id: SUB, _isPool: true, available: (self.needs || []).slice(), _seed: (self.needs || []).slice(),
202
+ _plan: sub.map(( c ) => ({ id: c.id, key: c.produces, nl: c.nl || '', composite: !!c.sub, statement: c.statement || '' }) ) };
203
+ (self.needs || []).forEach(( n ) => { subPool['val_' + n] = pool['val_' + n]; }); // ← down-projection (bounded ctx for children)
204
+ sub.forEach(( c ) => { subPool['wait_' + c.produces] = subPool['wait_' + c.produces] || []; });
205
+ sub.forEach(( c ) => (c.needs || []).filter(( n ) => subProducers.has(n) ).forEach(( n ) => subPool['wait_' + n].push(c.id) ) );
206
+ const tpl = [ { $_id: '_parent', Decompose: true, prompt: complete({ id: self._id, statement: graph.getEtty(self.parentSeg)._.statement, produces: self.produces, needs: self.needs || [], inputs: {}, reportsUp: false }) }, subPool ];
207
+ sub.forEach(( c ) => {
208
+ // a runtime-created node needs Node:true (else it lands as a generic record with no _outgoing → relink throws).
209
+ tpl.push({ _id: 'a_' + c.id, Node: true }, { _id: 'b_' + c.id, Node: true });
210
+ // gate on ALL needs; pre-fill `got` with the down-projected (already-satisfied) inputs → an uncovered
211
+ // need stays in `expected` but never enters `got` (visible famine, not a silent build).
212
+ const preGot = (c.needs || []).filter(( n ) => downSet.has(n) );
213
+ const seg = { _id: c.id, Segment: true, originNode: 'a_' + c.id, targetNode: 'b_' + c.id, parentSeg: self._id,
214
+ pool: SUB, needs: c.needs || [], produces: c.produces, expected: (c.needs || []).length, got: preGot.slice() };
215
+ if ( c.slot ) seg.slot = c.slot; // carry a nested method-slot down the recursion too
216
+ if ( c.nl != null ) seg.nl = c.nl; // strat rendering metadata, inert
217
+ if ( c.statement != null ) seg.statement = c.statement;
218
+ if ( c.sub ) { seg.isComposite = true; seg.sub = c.sub; } else seg.isStep = true; // deeper recursion supported
219
+ if ( c.produces === self.produces ) { // the TERMINAL → report UP to the parent pool
220
+ seg.up_pool = self.pool; seg.up_res = self.produces; seg.up_wait = (pool['wait_' + self.produces] || []).slice();
221
+ }
222
+ tpl.push(seg);
223
+ });
224
+ cb(null, tpl);
225
+ },
226
+ } };
227
+ }
228
+
229
+ /**
230
+ * createContextProjection(opts) — build a reusable projection.
231
+ * @param opts.serve async (leaf, ctx) => value REQUIRED. leaf = { id, statement, produces, needs, inputs, prompt,
232
+ * reportsUp, nl, level ('top'|'sub'), done ([{key,value,nl}] level results so far), plan (the level skeleton
233
+ * with values + the own-step marker) } — the structural fields feed strat rendering, hosts may ignore them.
234
+ * @param opts.complete (leaf) => promptString the bounded prompt (default: statement · USE inputs · PRODUCE;
235
+ * `stratComplete` exported = the stratified CONTEXT/DONE/ROADMAP rendering measured on the A3 monster
236
+ * harness + the K-paraphrases gate — opt-in, canonical form frozen).
237
+ * @param opts.label graph label.
238
+ * Roadmap steps may carry `nl` (the step instruction) and — on composites — `statement` (the sub-level énoncé):
239
+ * inert metadata, read by the strat rendering (a sub leaf's `statement` = its composite's, top = ctx.statement).
240
+ * ctx.givens = { key: value } base facts (givens.js#seedOf) · ctx.labels = { key: label } provenance labels
241
+ * (givens.js#labelsOf — structured provenance only), rendered by defaultComplete next to each labelled input.
242
+ * @returns { run(roadmap, ctx), guardPlan, conceptMap, buildSeed }
243
+ */
244
+ function createContextProjection( opts ) {
245
+ opts = opts || {};
246
+ if ( typeof opts.serve !== 'function' ) throw new Error('createContextProjection needs opts.serve(leaf,ctx) -> value');
247
+ const complete = opts.complete || defaultComplete;
248
+ const label = opts.label || 'context-projection';
249
+
250
+ async function run( roadmap, ctx ) {
251
+ const statement = (ctx && ctx.statement) || 'ROADMAP';
252
+ const givens = (ctx && ctx.givens) || {}; // the task's base facts (givens.js) — inherited at the top level
253
+ const guard = guardPlan(roadmap, Object.keys(givens));
254
+ if ( !guard.ok ) return { order: [], results: {}, refusal: guard.cycles.length ? 'CYCLE' : 'UNCOVERED', guard, graph: null };
255
+ const order = [];
256
+ const saved = Graph._providers; // forge idiom: set globally, restore in finally
257
+ Graph._providers = Object.assign({}, saved, makeProviders(opts.serve, complete, ctx, order));
258
+ let g;
259
+ try {
260
+ g = new Graph(buildSeed(roadmap, statement, givens), { label: label, isMaster: true, autoMount: true, conceptSets: ['common'], bagRefManagers: {} }, CONCEPT_MAP);
261
+ await nextStable(g);
262
+ } finally { Graph._providers = saved; }
263
+ // collect completed contexts (stable roadmap order — NOT the async cast order — so the result is deterministic)
264
+ const results = {};
265
+ for ( const s of allSteps(roadmap) ) {
266
+ const e = g.getEtty(s.id), f = e && e._;
267
+ if ( f && f.Step === true ) results[s.id] = { kind: 'leaf', value: f.out, prompt: f.prompt, needs: s.needs || [], produces: s.produces };
268
+ else if ( f && f.Decompose === true ) results[s.id] = { kind: 'composite', prompt: f.prompt, produces: s.produces };
269
+ else results[s.id] = { kind: 'starved', needs: s.needs || [], produces: s.produces }; // a mis-split part that never gated (guardPlan should have caught it)
270
+ }
271
+ return { order, results, refusal: null, guard, graph: g };
272
+ }
273
+
274
+ return { run: run, guardPlan: guardPlan, conceptMap: CONCEPT_MAP, buildSeed: buildSeed, complete: complete };
275
+ }
276
+
277
+ module.exports = { createContextProjection, guardPlan, defaultComplete, stratComplete, buildSeed, CONCEPT_MAP, makeProviders, allSteps };