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,253 @@
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
+ * C3 — the LEARNING METHOD LIBRARY (roadmap P2 SKELETON → P3 complete). A thin assembly over the
21
+ * always-on master loop (the cost-ladder MATCH→RETRIEVE→FORGE→ESCALATE), a persistent method store,
22
+ * and the O(1) dispatch catalog — so a recurrent typed workload:
23
+ * • AMORTIZES — a repeat problem elides the forge (MATCH → 0 model calls) once the library warms;
24
+ * • SURVIVES a restart — the library is disk-backed (a fresh process replays at 0 calls);
25
+ * • SHIPS — `pack()` → a portable `.sgc` a different deployment `load()`s and replays (version-gated);
26
+ * `packAll()`/`loadAll()` ship the EXERCISED library = methods AND the grown typed lattice (the
27
+ * registry: isa vocab + admitted synonym rings), each a version-gated single-kind `.sgc` bundle;
28
+ * • RE-DERIVES on drift — `drift(problem)` invalidates the method (cache + recall) and deopts its
29
+ * mount rank toward the ESCALATE floor, so the next solve re-forges only the violated entry.
30
+ *
31
+ * P2 laid the SKELETON: master-loop + store + dispatch catalog + `.sgc` portability, the §4 posture
32
+ * (durable store ON, validator/audit available; effectful forge opt-in). P3 COMPLETES it (all additive,
33
+ * `learning:false` default = the skeleton unchanged):
34
+ * • `crystallizeFrom(records, ctx)` — distill methods from captured trace records
35
+ * (crystallizeFromRecords pass-through; admitted candidates are indexed into the catalog);
36
+ * • `learning:true` — the FORGE arm becomes the creative loop (adaptOrForgeAsync:
37
+ * dispatch→hit(0 call)→antiUnify-adapt(1 call)→forge, verifier-gated, auto-indexed); the host
38
+ * supplies the projections `target(problem)`/`dispatchFacts(problem)` and a forge that returns
39
+ * `{candidate, calls}` (the method IS the amortized result). A `reject` THROWS a typed error
40
+ * (fail-closed — a rejection must never be cached as a 0-call result). `drift()` then ALSO
41
+ * invalidates the catalog-side template (library.invalidateTemplate) so the stale method cannot
42
+ * re-`hit` at 0 calls right after the exact cache was evicted (correct-on-drift, both layers);
43
+ * • `dispatch`/`blame`/`credit` — pure pass-throughs (library.dispatch, attributeSlot{Blame,Credit})
44
+ * exposing the O(1) catalog lookup and the LOCALIZED blame/credit rules on mounted frames. The
45
+ * AUTONOMOUS blame→revise loop is C5 (`relearn.js`), deliberately NOT here.
46
+ * The bricks stay usable "à nu" — this is an optional convenience over `Graph.authoring.*`.
47
+ *
48
+ * @param opts.signature (problem) => {structure, content} the K1 signature; STRUCTURE = method class
49
+ * (mount/deopt key), CONTENT = what's derived. Default: identity on the problem.
50
+ * @param opts.forge async (problem, ctx) => {result, cost, signals?} the expensive path (REQUIRED).
51
+ * With `learning:true`: async (problem, ctx) => {candidate, calls} — ctx carries
52
+ * {scopeFacts, neighbours, donors} from the dispatch (reuse them = adapt).
53
+ * @param opts.reForge optional partial re-forge of the differing content on a recalled skeleton.
54
+ * @param opts.store a file PATH (→ disk-backed, cross-restart) or a Map-like store; default in-memory.
55
+ * NOTE with `learning:true` the exact cache holds METHOD candidates (they carry a
56
+ * provider fn) — use `pack()`/`load()` for cross-restart method portability; a
57
+ * JSON file store is for opaque (JSON-safe) results.
58
+ * @param opts.mount a mount controller (default createMountController; opts.maxDeopt → its threshold).
59
+ * @param opts.recallK fuzzy-recall breadth (default 3).
60
+ * @param opts.learning OPT-IN (§4 default OFF). true → REQUIRES opts.target(problem) => {frontier,
61
+ * signatureKeys} and opts.dispatchFacts(problem) => scopeFacts (host projections;
62
+ * the combo never computes a signature/target — watch-point §11.1). Optional:
63
+ * opts.adaptContent(contentVars, scopeFacts) => {path:value} (the content-forger,
64
+ * may be async), opts.verify(candidate, scopeFacts) => bool (may be async),
65
+ * opts.interfaceRecall (§6.2 donor skeletons).
66
+ * @param opts.registry optional grown typed lattice (a registry: isa vocab + synonym rings) the combo HOLDS
67
+ * so pack/load ship it with the methods. Host-supplied + host-grown (the combo never
68
+ * derives/grows it — loadLattice grows it through the registry's own admission gate).
69
+ * @param opts.* the §4 knobs via resolveComboDefaults.
70
+ * @returns {{ solve, drift, stats, pack, load, packLattice, loadLattice, packAll, loadAll, registry,
71
+ * library, loop, store, crystallizeFrom, dispatch, blame, credit }}
72
+ */
73
+
74
+ var defaults = require('./defaults.js');
75
+ var master = require('../authoring/master-loop.js');
76
+ var mountMod = require('../authoring/mount.js');
77
+ var libMod = require('../authoring/library.js');
78
+ var storeMod = require('../authoring/store.js');
79
+ var packMod = require('../authoring/method-pack.js');
80
+ var latticeMod = require('../authoring/lattice-pack.js');
81
+ var adaptMod = require('../authoring/adapt.js');
82
+ var crystalMod = require('../authoring/crystallize.js');
83
+ var parametric = require('../authoring/parametric.js');
84
+
85
+ function createLearningLibrary( opts ) {
86
+ opts = opts || {};
87
+ var d = defaults.resolveComboDefaults(opts);
88
+ if ( typeof opts.forge !== 'function' ) throw new Error('createLearningLibrary needs opts.forge (async (problem, ctx) -> {result, cost})');
89
+ if ( d.learning && (typeof opts.target !== 'function' || typeof opts.dispatchFacts !== 'function') )
90
+ throw new Error('createLearningLibrary learning:true needs opts.target(problem) -> {frontier, signatureKeys} and opts.dispatchFacts(problem) -> scopeFacts');
91
+
92
+ // ── the persistent method store (the "library"): a file path → disk-backed (survives restart),
93
+ // a Map-like → used as-is, else in-memory. This is the master-loop's exact-match cache. ────────
94
+ var store = (typeof opts.store === 'string') ? storeMod.createFileStore(opts.store)
95
+ : (opts.store || new Map());
96
+
97
+ // ── the O(1) dispatch catalog (for the P3 adapt-or-forge creative loop; exposed now for wiring) ──
98
+ var library = opts.library || libMod.makeLibrary();
99
+
100
+ // ── the grown typed LATTICE (the registry: isa vocab + synonym rings). HOST-supplied + host-grown
101
+ // (like target/dispatchFacts — the combo never derives/grows it; it only HOLDS the ref so pack/load
102
+ // can ship it alongside the methods). null until a host passes one or loadLattice adopts one. ──────
103
+ var registry = opts.registry || null;
104
+
105
+ // ── the mount controller (regime + deopt-to-ESCALATE ladder) ────────────────────────────────────
106
+ var mount = opts.mount || mountMod.createMountController(opts.maxDeopt != null ? { thresholds: { maxDeopt: opts.maxDeopt } } : undefined);
107
+
108
+ // ── the FORGE arm. Default: the host's forge, raw (the skeleton). With `learning:true`: the
109
+ // creative loop — adaptOrForgeAsync CHAINS dispatch(0-call hit)→antiUnify-adapt(1 call)→host
110
+ // forge, gated + auto-indexed by the brick; the combo only CALLS the host projections and
111
+ // renames candidate→result (the method IS the amortized unit). No invented signals (a policy
112
+ // verdict would be combo logic — mount already gets hitRate from the loop's own bookkeeping);
113
+ // the host keeps the opts.signals seam. A `reject` THROWS (fail-closed): the master loop would
114
+ // otherwise cache it as a permanent 0-call result. ─────────────────────────────────────────────
115
+ var masterForge = opts.forge;
116
+ if ( d.learning ) masterForge = async function ( problem, ctx ) {
117
+ var r = await adaptMod.adaptOrForgeAsync({
118
+ lib : library,
119
+ target : opts.target(problem),
120
+ scopeFacts : opts.dispatchFacts(problem),
121
+ adaptContent : opts.adaptContent,
122
+ verify : opts.verify,
123
+ interfaceRecall: opts.interfaceRecall,
124
+ recallK : opts.recallK,
125
+ requireContract: d.failClosed,
126
+ blend : opts.blend, // B.4 — opt-in compositional-reuse rung (graft a donor before a fresh forge)
127
+ donor : opts.donor,
128
+ combine : opts.combine, // the 2nd composition operator (set-op) — opt-in, sibling to blend; the host
129
+ // resolves {left, right, op, outerClass?} for a set-op task (0 calls). The
130
+ // rung's outerClass defaults to the `target` above (re-keys the combined method).
131
+ forge : function ( sf, neighbours, donors ) {
132
+ return opts.forge(problem, Object.assign({}, ctx, { scopeFacts: sf, neighbours: neighbours, donors: donors }));
133
+ }
134
+ });
135
+ if ( r.outcome === 'reject' ) {
136
+ var err = new Error('learning forge rejected: ' + r.reason);
137
+ err.outcome = 'reject'; err.reason = r.reason; err.calls = r.calls;
138
+ throw err;
139
+ }
140
+ return { result: r.candidate, cost: r.calls, outcome: r.outcome };
141
+ };
142
+
143
+ // ── the always-on master loop: MATCH→RETRIEVE→FORGE→ESCALATE ────────────────────────────────────
144
+ var loop = master.createMasterLoop({
145
+ signature: opts.signature,
146
+ forge : masterForge,
147
+ reForge : opts.reForge,
148
+ cache : store,
149
+ mount : mount,
150
+ recallK : opts.recallK,
151
+ signals : opts.signals
152
+ });
153
+
154
+ return {
155
+ loop : loop,
156
+ store : store,
157
+ library: library,
158
+
159
+ /** solve a problem through the cost ladder → {result, arm, regime, cost}. */
160
+ solve: function ( problem ) { return loop.solve(problem); },
161
+
162
+ /** a premise drifted → invalidate the method (cache + recall) + deopt its mount rank. With
163
+ * `learning:true` ALSO drop the catalog-side template for this site (invalidateTemplate) —
164
+ * else the next FORGE would re-`hit` the stale method at 0 calls (correct-on-drift). */
165
+ drift: function ( problem ) {
166
+ var r = loop.drift(problem);
167
+ if ( d.learning ) r.invalidated = libMod.invalidateTemplate(library, opts.target(problem), opts.dispatchFacts(problem)).invalidated;
168
+ return r;
169
+ },
170
+
171
+ /** P3 — distill methods from captured trace records (`methodTrace().records` off a live run):
172
+ * a PURE pass-through to crystallizeFromRecords (`ctx` carries {episodeTree, schemaGraph,
173
+ * declaredFrontier, equivKeys, proseKeys, minCount, all, idFor} — the combo is NOT tree-bound;
174
+ * traces from different trees may feed the same library). Every ADMITTED candidate is indexed
175
+ * into the dispatch catalog (in `all` mode, from res.candidates — indexMethod does not dedup). */
176
+ crystallizeFrom: function ( records, ctx ) {
177
+ var res = crystalMod.crystallizeFromRecords(Object.assign({ records: records }, ctx || {}));
178
+ var all = res.candidates || [res];
179
+ for ( var i = 0; i < all.length; i++ )
180
+ if ( all[i].admitted && all[i].candidate ) libMod.indexMethod(library, all[i].candidate);
181
+ return res;
182
+ },
183
+
184
+ /** P3 — the O(1) catalog lookup, exposed: → {key, candidates, scanned, total}. */
185
+ dispatch: function ( target, scopeFacts ) { return libMod.dispatch(library, target, scopeFacts); },
186
+
187
+ /** P3 — the LOCALIZED blame rule (per-slot): → {perAtom, role, admissible}. Admissible iff
188
+ * every failed atom maps to ONE role (else discard — the blame-gate). Pass-through. */
189
+ blame: function ( o ) { return parametric.attributeSlotBlame(o); },
190
+
191
+ /** P3 — the localized credit rule (blame's dual): → {perAtom, roles}. Pass-through. */
192
+ credit: function ( o ) { return parametric.attributeSlotCredit(o); },
193
+
194
+ /** the ladder counters {match, recallFull, recallPartial, forge, escalate, cost, calls}. */
195
+ stats: function () { return loop.stats; },
196
+
197
+ /** pack the warm library as a portable, version-stamped `.sgc` bundle (ship it). */
198
+ pack: function ( packOpts ) { return packMod.packMethods(loop, packOpts || {}); },
199
+
200
+ /** load a `.sgc` bundle into this library (version-gated — a stale package never silently
201
+ * replays). With `learning:true` (and the version gate passed) every loaded method that IS a
202
+ * candidate (schema + templatesBySig — the catalog's own unit, a shape read) is ALSO re-indexed
203
+ * into the dispatch catalog, so a fresh deployment 0-call `hit`s on dispatch (G-P3-5). The pack
204
+ * ships the EXERCISED library (recall-index entries); a crystallized-but-never-solved method
205
+ * re-crystallizes from its records (the durable source) — the honest portability boundary. */
206
+ load: function ( bundle, loadOpts ) {
207
+ var r = packMod.loadMethods(bundle, loop, loadOpts || {});
208
+ if ( d.learning && r.exactReplaySafe ) {
209
+ var ms = packMod.unpackMethods(bundle).methods, n = 0;
210
+ for ( var i = 0; i < ms.length; i++ )
211
+ if ( ms[i].method && ms[i].method.schema && ms[i].method.templatesBySig ) { libMod.indexMethod(library, ms[i].method); n++; }
212
+ r.catalogued = n;
213
+ }
214
+ return r;
215
+ },
216
+
217
+ /** the current grown typed lattice (the registry: isa vocab + synonym rings), or null. */
218
+ registry: function () { return registry; },
219
+
220
+ /** pack the grown typed LATTICE as a portable, version-stamped `.sgc kind:'lattice'` bundle — the
221
+ * runtime-grown canon (isa vocab + admitted synonym rings) ships alongside the methods. Packs the
222
+ * held registry (empty if the host never supplied one). Pass-through to lattice-pack. */
223
+ packLattice: function ( packOpts ) { return latticeMod.packLattice(registry, packOpts || {}); },
224
+
225
+ /** load a `.sgc kind:'lattice'` bundle into the held canon (version-gated). With a held registry it
226
+ * GROWS it through the SAME admission gate (mergeRingProposals — confluence-checked, conflicting
227
+ * aliases rejected); with none it ADOPTS the packaged canon. The grown registry becomes the held
228
+ * one (so a later packLattice ships it). Pass-through to lattice-pack. */
229
+ loadLattice: function ( bundle, loadOpts ) {
230
+ var r = latticeMod.loadLattice(bundle, registry, loadOpts || {});
231
+ if ( r.loadSafe && r.registry ) registry = r.registry;
232
+ return r;
233
+ },
234
+
235
+ /** SHIP BOTH in one call: the exercised library = the methods AND the grown lattice. Returns a plain
236
+ * `{ methods, lattice }` envelope of two single-kind `.sgc` bundles (NOT a new `.sgc` kind — each
237
+ * part stays a valid corpus/method/lattice bundle). `lattice` is null when no registry is held. */
238
+ packAll: function ( packOpts ) {
239
+ return { methods: packMod.packMethods(loop, packOpts || {}), lattice: registry ? latticeMod.packLattice(registry, packOpts || {}) : null };
240
+ },
241
+
242
+ /** load a `{ methods, lattice }` envelope (the packAll output) — methods then lattice, each
243
+ * version-gated by its own pack. Returns `{ methods, lattice }` load results (lattice null if absent). */
244
+ loadAll: function ( bundle, loadOpts ) {
245
+ bundle = bundle || {};
246
+ var m = bundle.methods ? this.load(bundle.methods, loadOpts) : null;
247
+ var l = bundle.lattice ? this.loadLattice(bundle.lattice, loadOpts) : null;
248
+ return { methods: m, lattice: l };
249
+ }
250
+ };
251
+ }
252
+
253
+ module.exports = { createLearningLibrary: createLearningLibrary };
@@ -0,0 +1,193 @@
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
+ * C8 — the MIXTURE-RUNTIME server (owner "cerveau gauche × LLM intuitif", 2026-07-09). A cheap local model,
10
+ * ORIENTED by a forged certified-method stock, serves what a trust-gate can vouch for at 0 big-model cost, and
11
+ * ESCALATES the rest to a bigger/frontier tier (the tiered corrector, proven 10/16). A thin assembly over the
12
+ * proven arc — orientation (exact 5→8/36 gold-free, `WIP/experiments/2026-07-09-orientation` + `…-mixture-runtime`)
13
+ * + tiered correction (`…-tiered-correction`) — adding only the runtime trust-gate.
14
+ *
15
+ * ⟐ THE HONEST GUARANTEE (kill-gated live on Spider, Ministral-8B; the confront de-leaked the numbers):
16
+ * 1. 0-FALSE lives on the LEARNED CERTIFIED LAYER, never on the decomposition PICK. The stock's shapes are
17
+ * gold-certified at forge time (`stock.js#goldGate`); at runtime a shape is only ever SERVED-AS-CERTIFIED if
18
+ * it is in that vocabulary. We NEVER claim the small's chosen shape is correct — that is "pas gagné" (owner).
19
+ * 2. ORIENTATION improves the SCORE, not the soundness: the certified menu lifts the small's exact-match
20
+ * (+3/36 gold-free, measured) — a quality lever, cost-free, no guarantee attached.
21
+ * 3. The TRUST-GATE is a CONFIDENCE gate, not a truth oracle. The only gate that reached 0-false live is
22
+ * CROSS-AGREEMENT — the small's oriented shape EQUALS an INDEPENDENT predictor's shape (surface k-NN). Two
23
+ * independent signals concurring = trustable (100% precision live, ~38% recall). Self-consistency and a
24
+ * self-fit check did NOT separate correct from a confidently-wrong misread (a systematic misread is stable).
25
+ * ⚠ SCALE ERRATUM (2026-07-10, `WIP/experiments/2026-07-10-mixture-scale`, N=201 stratified over the FULL
26
+ * Spider dev distribution, 12% in-stock coverage): the "100%" was the n≤8 small-sample bound coming home —
27
+ * at scale surface cross-agreement admits 6/8 WRONG (25% precision): on an out-of-stock query surface-CLOSE
28
+ * to a certified class, the menu-steered small and the surface k-NN agree on the same wrong certified shape
29
+ * (independence of signals does not make the QUERY separable). CONFIRMED AT HIGH COVERAGE TOO (FinQA N=120,
30
+ * 85% traffic-covered stock: precision 42% — `2026-07-10-finqa-mixture-scale`): the failure is not the
31
+ * uncovered mass, surface agreement is simply not a truth signal. DO NOT ship cross-agreement as a 0-false
32
+ * trust tier; the fail-closed default (nothing auto-trusted) stays the safe wiring, and forge-time admission
33
+ * (oracle-gated) is a DIFFERENT, untouched layer. A trustable runtime tier needs a typed/grounded signal
34
+ * (per-query executable verification, deterministic slot-checker) — open work, tracked in the ledger.
35
+ * ⚠ INDEPENDENCE IS LOAD-BEARING (live finding): the cross-agreement precision COLLAPSES (100%→60% live) if
36
+ * `proposeMenu` and `predict` share the SAME surface signal — the menu then STEERS the small toward the very
37
+ * shape the predictor will name, so "agreement" is circular. Wire the FLAT certified menu (default) for
38
+ * orientation and keep `predict` as the independent signal; do NOT surface-narrow the menu AND predict from
39
+ * the same k-NN (narrowing is a large-stock optimization, but then use a DIFFERENT independent predictor).
40
+ * 4. ESCALATION carries the residual: whatever the trust-gate does not vouch for goes to the big tier (no false
41
+ * negative — the caller always gets an answer). The big model, not the gate, is what corrects a misread.
42
+ * 5. PRE-ROUTING is where the amortization actually lives (Fable-review correction, verified on the memoized
43
+ * traces `WIP/experiments/2026-07-09-amortization-e2e/prerouting.js`): gate-AFTER-small makes every query pay
44
+ * the small pass, so a high escalation rate costs MORE than big-alone (+28% live). A TEXT-ONLY router that
45
+ * sends predicted-uncovered queries STRAIGHT to the big tier (skipping the wasted small pass) turns the same
46
+ * k-NN predictor into +2%, and a perfect covered/uncovered router into −18%. `preRoute` wires that router
47
+ * BEFORE the small call; it never touches the trust decision (a pre-routed serve is a plain escalation).
48
+ * So: closed-vocabulary soundness + a small MEASURED 0-false trusted tier + a raw-score lift + escalation of the
49
+ * rest. The amortization is `1 − escalation-rate`; grow it by growing the stock / a tighter independent predictor,
50
+ * and CAPTURE it by pre-routing (route-then-gate, never pay-small-first).
51
+ *
52
+ * Bricks stay usable "à nu": `small`/`big`/`proposeMenu`/`predict` are all INJECTED (the combo owns no model and
53
+ * no corpus). `makeSurfaceDispatch` is an optional helper that builds `proposeMenu`+`predict` from a labelled
54
+ * anchor corpus (TF-IDF k-NN) — the surface signal is real (+20 pts over permuted-labels) but modest over the
55
+ * frequency prior, so it is an OPTIMIZATION (narrow a large stock + feed the cross-agreement gate), never load-bearing.
56
+ *
57
+ * const sd = makeSurfaceDispatch({ anchors: [{text, shape}, …], k: 5 });
58
+ * const mx = createMixtureServe({ certifiedShapes, small, big, proposeMenu: sd.proposeMenu, predict: sd.predict });
59
+ * const r = await mx.serve(query); // { shape, tier:'local-trusted'|'escalated'|'local-untrusted', certified, … }
60
+ *
61
+ * @param opts.certifiedShapes REQUIRED array<string> — the learned 0-false vocabulary (from the forged `.sgc` stock).
62
+ * @param opts.small REQUIRED async (query, menu) => shape — the cheap tier-1 model, oriented by `menu`.
63
+ * @param opts.big optional async (query, menu) => shape — the tier-2 escalation (frontier corrector). Absent
64
+ * → an untrusted result is returned tagged `local-untrusted` (the host decides), never forced.
65
+ * @param opts.proposeMenu optional (query) => array<string> — the oriented menu (surface-narrowed); default = the
66
+ * flat certified vocabulary (already gold-free; narrowing is the optional optimization).
67
+ * @param opts.predict optional (query) => shape — an INDEPENDENT shape predictor (surface k-NN). Enables the
68
+ * default CROSS-AGREEMENT trust-gate. Without it (and without `trust`) nothing is auto-trusted
69
+ * (fail-closed: everything escalates) — we never auto-trust a bare certified shape (47% live).
70
+ * @param opts.trust optional (query, shape, ctx{menu,predicted}) => bool — override the trust predicate.
71
+ * @param opts.preRoute optional true | (query, ctx{predicted,menu}) => bool — route-then-gate (the amortization
72
+ * capture): decided BEFORE the small call; `true` routes a query whose predicted shape is
73
+ * OUTSIDE the certified vocabulary straight to `big` (needs `predict`); a function is a
74
+ * custom router (return true = go direct). Inert without a `big` tier (fail-safe: the small
75
+ * path still serves). A pre-routed record is `tier:'escalated', preRouted:true`.
76
+ * @param opts.onServe optional (record) => void — a per-serve hook (telemetry).
77
+ * @returns {{ serve, stats, certifiedShapes }}
78
+ */
79
+ function createMixtureServe( opts ) {
80
+ opts = opts || {};
81
+ var shapes = opts.certifiedShapes;
82
+ if ( !Array.isArray(shapes) || !shapes.length ) throw new Error('createMixtureServe needs opts.certifiedShapes (the learned certified vocabulary from the forged stock)');
83
+ if ( typeof opts.small !== 'function' ) throw new Error('createMixtureServe needs opts.small (async (query, menu) -> shape) — the cheap tier-1 model');
84
+ var certified = Object.create(null); shapes.forEach(function ( s ) { certified[s] = true; });
85
+ var big = (typeof opts.big === 'function') ? opts.big : null;
86
+ var proposeMenu = (typeof opts.proposeMenu === 'function') ? opts.proposeMenu : function () { return shapes; };
87
+ var predict = (typeof opts.predict === 'function') ? opts.predict : null;
88
+ // default trust = CROSS-AGREEMENT when an independent predictor is given; else fail-closed (never auto-trust).
89
+ var trust = (typeof opts.trust === 'function') ? opts.trust
90
+ : (predict ? function ( q, shape, ctx ) { return shape === ctx.predicted; } : function () { return false; });
91
+ var onServe = (typeof opts.onServe === 'function') ? opts.onServe : null;
92
+ // pre-router (route-then-gate): default = predicted shape outside the certified vocabulary → go direct to big.
93
+ var router = (typeof opts.preRoute === 'function') ? opts.preRoute
94
+ : (opts.preRoute ? function ( q, ctx ) { return !certified[ctx.predicted]; } : null);
95
+ if ( router && !predict && typeof opts.preRoute !== 'function' ) throw new Error('createMixtureServe preRoute:true needs opts.predict (the text-only covered/uncovered signal) — or pass a custom router function');
96
+ var stats = { served: 0, localTrusted: 0, escalated: 0, localUntrusted: 0, preRouted: 0 };
97
+
98
+ async function serve( query ) {
99
+ stats.served++;
100
+ var menu = proposeMenu(query) || shapes;
101
+ var predicted = predict ? predict(query) : undefined; // ONE predict call: shared by the router and the gate
102
+ if ( router && big && router(query, { predicted: predicted, menu: menu }) ) {
103
+ var dshape = await big(query, menu);
104
+ stats.preRouted++; stats.escalated++;
105
+ var drec = { shape: dshape, tier: 'escalated', certified: !!certified[dshape], trusted: false, preRouted: true, predicted: predicted };
106
+ if ( onServe ) onServe(Object.assign({ query: query }, drec));
107
+ return drec;
108
+ }
109
+ var shape = await opts.small(query, menu);
110
+ var isCert = !!certified[shape];
111
+ var trusted = isCert && !!trust(query, shape, { menu: menu, predicted: predicted });
112
+ var rec;
113
+ if ( trusted ) {
114
+ stats.localTrusted++;
115
+ rec = { shape: shape, tier: 'local-trusted', certified: true, trusted: true, predicted: predicted };
116
+ } else if ( big ) {
117
+ var bshape = await big(query, menu);
118
+ stats.escalated++;
119
+ rec = { shape: bshape, tier: 'escalated', certified: !!certified[bshape], trusted: false, local: shape, predicted: predicted };
120
+ } else {
121
+ stats.localUntrusted++;
122
+ rec = { shape: shape, tier: 'local-untrusted', certified: isCert, trusted: false, predicted: predicted };
123
+ }
124
+ if ( onServe ) onServe(Object.assign({ query: query }, rec));
125
+ return rec;
126
+ }
127
+
128
+ return { serve: serve, stats: stats, certifiedShapes: shapes.slice() };
129
+ }
130
+
131
+ // --- makeSurfaceDispatch: the KG-1 surface signal promoted to a reusable brick (TF-IDF k-NN over labelled anchors).
132
+
133
+ var STOP = { 'the': 1, 'a': 1, 'an': 1, 'of': 1, 'to': 1, 'in': 1, 'on': 1, 'for': 1, 'and': 1, 'or': 1, 'is': 1, 'are': 1, 'was': 1, 'were': 1, 'be': 1, 'with': 1, 'by': 1, 'at': 1, 'as': 1, 'from': 1, 'that': 1, 'this': 1, 'these': 1, 'those': 1, 'what': 1, 'which': 1, 'who': 1, 'how': 1, 'many': 1, 'much': 1, 'list': 1, 'show': 1, 'give': 1, 'find': 1, 'name': 1, 'number': 1, 'count': 1, 'all': 1, 'each': 1, 'every': 1, 'their': 1, 'its': 1, 'there': 1 };
134
+ function tokenize( s ) { return String(s || '').toLowerCase().split(/[^a-z0-9]+/).filter(function ( w ) { return w && w.length > 1 && !STOP[w]; }); }
135
+ function vectorize( toks, idf ) {
136
+ var tf = {}, i; for ( i = 0; i < toks.length; i++ ) tf[toks[i]] = (tf[toks[i]] || 0) + 1;
137
+ var v = {}, n = 0, w; for ( w in tf ) { var x = tf[w] * (idf[w] || 0); v[w] = x; n += x * x; }
138
+ n = Math.sqrt(n) || 1; for ( w in v ) v[w] /= n; return v;
139
+ }
140
+ function cosine( a, b ) { var s = 0, x = a, y = b, w; if ( Object.keys(a).length > Object.keys(b).length ) { x = b; y = a; } for ( w in x ) if ( y[w] ) s += x[w] * y[w]; return s; }
141
+
142
+ /**
143
+ * Build the surface-dispatch pair (`proposeMenu` + `predict`) from a labelled anchor corpus — the same signal the
144
+ * KG-1 kill-gate measured (real geometry, +20 pts over permuted-labels; modest over the frequency prior, so an
145
+ * optimization). Deterministic (pure TF-IDF, index-stable tie-breaks). The anchors are TRAINING queries of the
146
+ * certified classes (their `shape` is the training label) — no runtime gold.
147
+ * @param opts.anchors REQUIRED array<{ text, shape }> — labelled training queries.
148
+ * @param opts.k proposal menu size (default 5).
149
+ * @returns {{ proposeMenu, predict }} — proposeMenu(query) => top-k distinct shapes (cosine-vote); predict(query)
150
+ * => the single top-1 shape (for the cross-agreement trust-gate).
151
+ */
152
+ function makeSurfaceDispatch( opts ) {
153
+ opts = opts || {};
154
+ var anchors = opts.anchors;
155
+ if ( !Array.isArray(anchors) || !anchors.length ) throw new Error('makeSurfaceDispatch needs opts.anchors ([{text, shape}])');
156
+ var K = opts.k == null ? 5 : opts.k;
157
+ var toks = anchors.map(function ( a ) { return tokenize(a.text); });
158
+ var df = {}; toks.forEach(function ( t ) { var seen = {}, i; for ( i = 0; i < t.length; i++ ) if ( !seen[t[i]] ) { seen[t[i]] = 1; df[t[i]] = (df[t[i]] || 0) + 1; } });
159
+ var idf = {}, w; for ( w in df ) idf[w] = Math.log((anchors.length + 1) / (df[w] + 1)) + 1;
160
+ var vecs = toks.map(function ( t ) { return vectorize(t, idf); });
161
+
162
+ // ranked (shape -> summed cosine) over ALL anchors, ties broken by lexical shape then first-seen index.
163
+ function rankShapes( query ) {
164
+ var qv = vectorize(tokenize(query), idf), byShape = {}, i;
165
+ for ( i = 0; i < anchors.length; i++ ) { var sh = anchors[i].shape, s = cosine(qv, vecs[i]); byShape[sh] = (byShape[sh] || 0) + s; }
166
+ return Object.keys(byShape).sort(function ( a, b ) { return (byShape[b] - byShape[a]) || (a < b ? -1 : 1); });
167
+ }
168
+ return {
169
+ proposeMenu: function ( query ) { return rankShapes(query).slice(0, K); },
170
+ predict: function ( query ) { return rankShapes(query)[0] || null; }
171
+ };
172
+ }
173
+
174
+ /**
175
+ * qualifyMenu — present the certified shapes with their SLOT QUALIFICATIONS so the small conforms to the reference
176
+ * vocabulary instead of deviating off-menu (the owner's "meilleure qualification des slots"). Kill-gated live: a
177
+ * slot-glossed menu lifted in-stock exact-match +3/18 (8→11) over the opaque shape strings, because the loss is
178
+ * CONFORMANCE (the small emits an off-reference shape), not selection-among-options (KG-3). Pure/deterministic; the
179
+ * host supplies the domain gloss (a step→meaning map — the "role" the lattice types each slot with).
180
+ * @param shapes array<string> — the certified shapes (e.g. "join>filter>select").
181
+ * @param glossMap { <stepKind>: "human meaning" } — the per-slot role. Missing kinds echo the kind.
182
+ * @param sep the step separator inside a shape (default '>').
183
+ * @returns array<string> — each shape annotated "shape (kind=meaning, …)" for the orientation prompt.
184
+ */
185
+ function qualifyMenu( shapes, glossMap, sep ) {
186
+ var s = sep || '>', g = glossMap || {};
187
+ return (shapes || []).map(function ( shape ) {
188
+ var parts = String(shape).split(s).map(function ( k ) { return k + '=' + (g[k] || k); });
189
+ return shape + ' (' + parts.join(', ') + ')';
190
+ });
191
+ }
192
+
193
+ module.exports = { createMixtureServe: createMixtureServe, makeSurfaceDispatch: makeSurfaceDispatch, qualifyMenu: qualifyMenu };
@@ -0,0 +1,192 @@
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
+ * plan-loop — C7, the HIERARCHICAL PLAN LOOP (design `WIP/2026-07-07-design-r1-plan-loop.md`; kill-gated R1:
10
+ * KG-R1a the channel + KG-R1b the fixpoint). A task LONGER THAN THE CONTEXT is decomposed into typed leaves,
11
+ * each leaf sees only a PROJECTED digest (never the whole task), leaves are served by the cost ladder, the plan
12
+ * is driven to a BALANCED fixpoint (rebalance brick), and reassembly is VERIFIED — a leaf whose required facts
13
+ * are missing is REFUSED (never a silent wrong), and reassembly reads nothing uncovered (never a claim-of-absence).
14
+ *
15
+ * THIN assembly (the combo doctrine — no new logic; a missing piece goes into a brick):
16
+ * decompose (injected: typed-loop) → project + leaf pre-contract → serveLeaf (injected: the C6 ladder,
17
+ * match→retrieve→forge→ESCALADE frontier — kill-gate finding: escalation is LOAD-BEARING on hard/novel leaves,
18
+ * C-local alone is model-capability-bound) → rebalancePlan (E2∘E1∘E3∘E4) → checkReassembly (checkCompose).
19
+ *
20
+ * Both heavy stages are INJECTED so the combo stays usable "à nu" and testable without a model: production wires
21
+ * decompose = typed-loop's recursive typed decompose, serveLeaf = createProxyCache(...).solve (the C6 ladder).
22
+ *
23
+ * const { createPlanLoop } = require('skynet-graph').combos; // or Graph.combos.createPlanLoop
24
+ * const loop = createPlanLoop({ decompose, serveLeaf });
25
+ * const { answer, converged, refused, reassembly } = await loop.run(task, ctx);
26
+ * // ctx.givens = { key: value } (givens.js#seedOf): the task's BASE FACTS, seeded into the projection pool as
27
+ * // val_<key> — a leaf citing a given key in readsExtra gets the value injected into its bounded inputs (gap i).
28
+ */
29
+ const { rebalancePlan, checkReassembly } = require('../authoring/rebalance.js');
30
+ const { createContextProjection } = require('../authoring/context-project.js');
31
+ const { stableStringify } = require('../providers/cache.js');
32
+
33
+ const tok = ( s ) => Math.ceil(String(s == null ? '' : s).length / 4);
34
+
35
+ // leaf pre-contract (default): a typed request is COMPLETE iff no required field is null/'' (a severed digest
36
+ // amputates one → refused AT PROJECTION, before any serve). Shallow + one level into a `filters` array. ──
37
+ function defaultComplete( request ) {
38
+ if ( !request || typeof request !== 'object' ) return false;
39
+ for ( const k of Object.keys(request) ) {
40
+ const v = request[k];
41
+ if ( v === null || v === '' ) return false;
42
+ if ( Array.isArray(v) ) for ( const f of v ) if ( f && typeof f === 'object' )
43
+ for ( const kk of Object.keys(f) ) if ( f[kk] === null || f[kk] === '' ) return false;
44
+ }
45
+ return true;
46
+ }
47
+
48
+ // default answer fold — canonical {id: value | REFUSED}, sorted (deterministic; E4/refold semantics). ──
49
+ function defaultFold( leaves ) {
50
+ const parts = [];
51
+ for ( const lf of leaves ) for ( const id of leafWrites(lf) )
52
+ parts.push([id, lf.refused ? 'REFUSED' : (lf.severed ? 'REFUSED' : String(lf.value == null ? '' : lf.value))]);
53
+ parts.sort(( a, b ) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
54
+ return parts.map(( [id, v] ) => id + '=' + v ).join(';');
55
+ }
56
+ const leafWrites = ( n ) => n.bundle ? n.bundle.map(( r ) => r.id ) : [n.request && n.request.id];
57
+
58
+ // sink fold — the answer is the LAST SINK's value (a leaf nobody reads; "the LAST part yields the final answer"
59
+ // plans). Promoted from the KG-ZOOM harness (P1/P1.5, measured on 560 tasks). Wire as opts.fold: sinkFold. ──
60
+ function sinkFold( leaves ) {
61
+ const needed = new Set(leaves.flatMap(( l ) => l.readsExtra || [] ));
62
+ const sinks = leaves.filter(( l ) => l.request && !needed.has(l.request.id) && !l.refused );
63
+ const last = sinks.length ? sinks[sinks.length - 1] : leaves[leaves.length - 1];
64
+ return last ? String(last.value == null ? '' : last.value) : '';
65
+ }
66
+
67
+ // degenerate decompose — the split FAILED (no leaves, or a single untyped task-echo leaf). With opts.fallback
68
+ // the product answer is "don't split then": serve the WHOLE task, FLAGGED (never a silent wrong/undefined). ──
69
+ function defaultIsDegenerate( leaves ) {
70
+ return leaves.length === 0 || (leaves.length === 1 && !(leaves[0] && leaves[0].request && leaves[0].request.kind));
71
+ }
72
+
73
+ /**
74
+ * createPlanLoop(opts)
75
+ * @param opts.decompose REQUIRED async (task, ctx) => [{ id, request, nl }] — the typed leaves (typed-loop in
76
+ * production). May be DEGENERATE (redundant / disordered / an over-budget `{ bundle:[request,…] }`) — the
77
+ * fixpoint repairs it. `request` is the leaf's typed request (the fusion key + contract are derived from it).
78
+ * @param opts.serveLeaf REQUIRED async (leaf, ctx) => value — the cost ladder (createProxyCache(...).solve).
79
+ * @param opts.isComplete (request) => bool — the leaf pre-contract (default: no null/'' required field).
80
+ * @param opts.complete (leaf) => promptString — the projection's prompt renderer, passed through to
81
+ * createContextProjection (e.g. context-project's `stratComplete`). Caveat: the projected roadmap keys
82
+ * are internal step ids (L0, L1…) — a host wanting figure-keyed prompts renders in serveLeaf instead.
83
+ * @param opts.fold (servedLeaves) => answer — the report fold (default: canonical {id:value|REFUSED};
84
+ * `sinkFold` exported for "the LAST part yields the answer" plans).
85
+ * @param opts.fallback async (task, ctx) => value — OPT-IN whole-task fallback when the decompose is DEGENERATE
86
+ * (opts.isDegenerate, default: no leaves / single untyped echo): a failed split means "don't split", the
87
+ * answer is served whole and FLAGGED `fallback:true` in the result (report it apart from pure-decomposed).
88
+ * @param opts.budget node token budget (default 512) — a served digest over budget is an E2 scission target.
89
+ * @param opts.cap fixpoint round cap (default from the brick).
90
+ * ctx.givens = base facts seeded as val_<key> (givens.js#seedOf) · ctx.labels = provenance labels
91
+ * (givens.js#labelsOf, the cells rule) — both ride into the context projection.
92
+ */
93
+ function createPlanLoop( opts ) {
94
+ opts = opts || {};
95
+ if ( typeof opts.decompose !== 'function' ) throw new Error('createPlanLoop needs opts.decompose(task,ctx) -> [{id,request,nl}]');
96
+ if ( typeof opts.serveLeaf !== 'function' ) throw new Error('createPlanLoop needs opts.serveLeaf(leaf,ctx) -> value');
97
+ const budget = opts.budget || 512;
98
+ const isComplete = opts.isComplete || defaultComplete;
99
+ const fold = opts.fold || defaultFold;
100
+ const isDegenerate = opts.isDegenerate || defaultIsDegenerate;
101
+
102
+ // the rebalance spec — derived entirely from the leaves' TYPED requests (no new logic) ──
103
+ const spec = {
104
+ isLeaf: ( n ) => n.kind === 'leaf',
105
+ fusionKey: ( n ) => stableStringify(n.bundle ? n.bundle : n.request),
106
+ overBudget: ( n ) => n.kind === 'leaf' && (!!n.bundle || tok(n.digest) > budget),
107
+ split: ( n ) => (n.bundle || []).map(( r, i ) => ({ id: n.id + '_' + i, kind: 'leaf', request: r, nl: r.nl, refused: !isComplete(r), value: n.served && n.served[i] }) ),
108
+ writes: leafWrites,
109
+ reads: ( n ) => n.kind === 'root' ? (n.reads || []) : (n.readsExtra || []),
110
+ refold: ( root, leaves ) => fold(leaves),
111
+ contractOf: ( n ) => n.kind === 'root'
112
+ ? { name: n.id, contract: { read: (n.reads || []), write: ['__answer'], effect: 'pure' } }
113
+ : { name: n.id, contract: { read: (n.readsExtra || []), write: leafWrites(n), effect: n.retract ? 'effect' : 'pure' } },
114
+ };
115
+
116
+ // serve the atomic leaves THROUGH the graph-native projection: build a roadmap from the leaves' footprint
117
+ // (a leaf's produces = a unique per-leaf key; its needs = the readsExtra it shares with a producing sibling),
118
+ // serve each leaf WITH its completed bounded context (upstream values relabelled to the figure key), in the
119
+ // emergent dependency order. A cyclic footprint → the projection's `guardPlan` returns a TYPED refusal (the
120
+ // intra-plan cycle detector) which run() surfaces — never a silent wedge, never a serve-despite-impossible-deps.
121
+ // Mutates lf.value / lf.projectedPrompt in place; returns the refusal string or null. ──
122
+ async function projectServe( atomic, producedKeys, task, ctx ) {
123
+ const givens = (ctx && ctx.givens) || {}; // the task's base facts (givens.js) — ride as pre-satisfied needs
124
+ const writeKeyOf = ( l ) => leafWrites(l)[0];
125
+ const firstIdxOf = {}; atomic.forEach(( l, i ) => { const k = writeKeyOf(l); if ( k != null && !(k in firstIdxOf) ) firstIdxOf[k] = i; });
126
+ const stepId = ( i ) => 'L' + i, byStep = {}, writeKeyByStep = {};
127
+ atomic.forEach(( l, i ) => { byStep[stepId(i)] = l; writeKeyByStep[stepId(i)] = writeKeyOf(l); });
128
+ const roadmap = atomic.map(( l, i ) => ({ id: stepId(i), produces: stepId(i),
129
+ needs: (l.readsExtra || []).filter(( k ) => producedKeys.has(k) && firstIdxOf[k] !== i ).map(( k ) => stepId(firstIdxOf[k]) )
130
+ .concat((l.readsExtra || []).filter(( k ) => !producedKeys.has(k) && k in givens )) }) );
131
+ const serve = async ( s, c ) => {
132
+ const inputs = {}; for ( const k of Object.keys(s.inputs) ) inputs[writeKeyByStep[k] || k] = s.inputs[k]; // relabel producer-step id → figure key
133
+ // the projected prompt + provenance labels ride along (a host serveLeaf may use leaf.prompt as-is, or
134
+ // render its own from inputs + labels — the cells rule, givens.js#labelsOf).
135
+ return await opts.serveLeaf(Object.assign({}, byStep[s.id], { inputs: inputs, prompt: s.prompt, labels: s.labels }), c);
136
+ };
137
+ const statement = typeof task === 'string' ? task : (( ctx && ctx.statement ) || JSON.stringify(task));
138
+ const pr = await createContextProjection({ serve: serve, complete: opts.complete }).run(roadmap, Object.assign({ statement: statement }, ctx));
139
+ if ( pr.refusal ) return pr.refusal; // unresolvable structure (cycle) → surfaced, leaves unserved
140
+ for ( const s of roadmap ) { const l = byStep[s.id]; l.value = pr.results[s.id].value; l.projectedPrompt = pr.results[s.id].prompt; }
141
+ return null;
142
+ }
143
+
144
+ async function run( task, ctx ) {
145
+ // 1. DECOMPOSE (typed) → leaves (possibly degenerate)
146
+ const raw = await opts.decompose(task, ctx) || [];
147
+ // 1b. DEGENERATE + opts.fallback → the whole-task answer, FLAGGED (a failed split is "don't split",
148
+ // never a silent String(undefined) nor an unexplained refusal when the host opted into the fallback).
149
+ if ( opts.fallback && isDegenerate(raw) ) {
150
+ const v = await opts.fallback(task, ctx);
151
+ return { answer: v, fallback: true, converged: true, refusal: null, refused: [],
152
+ reassembly: null, rounds: 0, monotone: true, trace: [], leaves: raw.length, projected: false };
153
+ }
154
+ // 2. PROJECT + leaf PRE-CONTRACT (a severed/incomplete request → refused AT PROJECTION, never served-then-guessed)
155
+ const leaves = raw.map(( n ) => Object.assign({ kind: 'leaf' }, n, { refused: !!n.bundle ? false : !isComplete(n.request) }) );
156
+ // 3. SERVE. Bundles are pre-served per atom (a scission concern). Admissible ATOMIC leaves are served through
157
+ // the REAL context projection (lib/authoring/context-project): a leaf that reads another leaf's write is
158
+ // served AFTER it, with the upstream value completed into its bounded context (the R1 §2 "contexte par
159
+ // nœud" — no leaf sees the whole task). A decompose with NO intra-plan dependency degenerates to the
160
+ // direct serve (all leaves are independent roots) — that IS the no-dependency case of the same projection,
161
+ // kept as a fast path (redundant serves are cheap — the C6 cache dedupes).
162
+ for ( const lf of leaves ) if ( lf.bundle ) {
163
+ lf.served = []; for ( const r of lf.bundle ) lf.served.push(await opts.serveLeaf({ kind: 'leaf', request: r, nl: r.nl }, ctx));
164
+ }
165
+ const atomic = leaves.filter(( l ) => !l.bundle && !l.refused );
166
+ const producedKeys = new Set(atomic.map(( l ) => leafWrites(l)[0] ).filter(( k ) => k != null ));
167
+ const givens = (ctx && ctx.givens) || {};
168
+ const projected = atomic.some(( l ) => (l.readsExtra || []).some(( k ) => producedKeys.has(k) || k in givens ) );
169
+ let projRefusal = null;
170
+ if ( !projected ) { for ( const lf of atomic ) lf.value = await opts.serveLeaf(lf, ctx); }
171
+ else projRefusal = await projectServe(atomic, producedKeys, task, ctx);
172
+ // 4. BUILD the plan + REBALANCE to the fixpoint (E2 split → E1 dedupe → E3 reorder → E4 refold)
173
+ const producedIds = leaves.filter(( l ) => !l.refused ).flatMap(spec.writes).filter(Boolean);
174
+ const root = { id: '__root', kind: 'root', reads: producedIds, value: null };
175
+ const plan = { order: leaves.concat([root]) };
176
+ const rb = rebalancePlan(plan, spec, { cap: opts.cap });
177
+ // 5. VERIFIED REASSEMBLY (checkCompose sound + nothing uncovered)
178
+ const producers = rb.plan.order.filter(spec.isLeaf);
179
+ const reassembly = checkReassembly(root, producers, spec);
180
+ return {
181
+ answer: root.value, converged: rb.converged, refusal: projRefusal || rb.refusal,
182
+ refused: producers.filter(( p ) => p.refused ).flatMap(spec.writes).filter(Boolean),
183
+ reassembly, rounds: rb.rounds, monotone: rb.monotone, trace: rb.trace,
184
+ leaves: producers.length, projected: projected,
185
+ };
186
+ }
187
+
188
+ return { run: run, rebalance: rebalancePlan, spec: spec };
189
+ }
190
+
191
+ module.exports = { createPlanLoop: createPlanLoop, defaultComplete: defaultComplete, defaultFold: defaultFold,
192
+ sinkFold: sinkFold, defaultIsDegenerate: defaultIsDegenerate };