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,510 @@
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
+ * mine — sub-forest mining for crystallization (host-side, ZERO-CORE; study
21
+ * doc/WIP/studies/2026-06-26-…, the #13 loop core).
22
+ *
23
+ * Inventing a nonterminal for a recurring sub-derivation = SUBDUE/SEQUITUR on the engine's
24
+ * derivation forest. `mineChains` reads a corpus of apply-records (the `onConceptApply`
25
+ * trace: `{concept, target}` firings) plus the concept schemas, and finds frequent
26
+ * producer->consumer edges A->B (B `require`s a fact A produces, and both fired on the same
27
+ * target). The top edges are candidate abstractions; `composeProviders` chains the
28
+ * constituents' providers into one (threading each output into the next), so the proposed
29
+ * abstract method can be measured by the MDL/utility gate (`abstraction.evaluate`).
30
+ *
31
+ * const chains = mineChains(applyRecords, conceptTree); // [{from,to,via,count}, …]
32
+ * const M = composeProviders(provA, provB); // one cast does both
33
+ * // -> evaluate({chainTree, abstractTree:{…M…}, …}) decides admission.
34
+ *
35
+ * `mineChains` keys only on STATIC produced facts (self-flag + applyMutations template keys
36
+ * — the same extraction validate.js uses); provider-written facts are not statically known,
37
+ * so the mined edge is the self-flag/template producer->consumer chain (the auditable spine).
38
+ */
39
+ const { eachConcept, refsOf, refKeyOf, templateKeys } = require('./validate.js');
40
+ const { relativize, antiUnify, canon, hasHoles, emitMethodAsSubgraph, BASE } = require('./abstract.js');
41
+ const { memoSurfaceKeys } = require('./memo-stability.js');
42
+ const { digest } = require('../providers/canonicalize.js');
43
+
44
+ /**
45
+ * Find frequent producer->consumer concept chains in a trace corpus.
46
+ * @param records [{ concept, target }] apply-records (onConceptApply firings)
47
+ * @param tree the concept tree (for static produces/requires)
48
+ * @returns [{ from, to, via, count }] sorted by co-firing count (desc)
49
+ */
50
+ function mineChains( records, tree ) {
51
+ const concepts = [];
52
+ eachConcept(tree, (c) => { if ( c._name ) concepts.push(c); });
53
+
54
+ const produces = {}, requires = {};
55
+ for ( const c of concepts ) {
56
+ const schema = c._schema || c;
57
+ produces[c._name] = new Set([c._name, ...templateKeys(schema.applyMutations)]);
58
+ requires[c._name] = refsOf(schema.require, false).map((r) => refKeyOf(r).key);
59
+ }
60
+
61
+ const firedOn = {}; // concept -> Set(target)
62
+ for ( const r of (records || []) ) {
63
+ if ( !r || !r.concept ) continue;
64
+ (firedOn[r.concept] = firedOn[r.concept] || new Set()).add(r.target);
65
+ }
66
+
67
+ const out = [];
68
+ for ( const A of concepts ) for ( const B of concepts ) {
69
+ if ( A === B ) continue;
70
+ let via = null;
71
+ for ( const f of (requires[B._name] || []) )
72
+ if ( produces[A._name] && produces[A._name].has(f) ) { via = f; break; }
73
+ if ( !via ) continue;
74
+ const tA = firedOn[A._name] || new Set(), tB = firedOn[B._name] || new Set();
75
+ let count = 0;
76
+ for ( const t of tA ) if ( tB.has(t) ) count++;
77
+ if ( count > 0 ) out.push({ from: A._name, to: B._name, via, count });
78
+ }
79
+ out.sort((x, y) => y.count - x.count);
80
+ return out;
81
+ }
82
+
83
+ /**
84
+ * Chain N cb-style providers into one. Each provider's output facts are threaded into a
85
+ * shadow scope for the next, and all output facts are merged into a single `_parent`
86
+ * template — so one cast reproduces the whole chain (the inlined abstract method).
87
+ * @param {...Function} fns providers `(graph, concept, scope, argz, cb)` -> cb(err, template)
88
+ * @returns Function a single provider with the same signature
89
+ */
90
+ function composeProviders( ...fns ) {
91
+ return function ( graph, concept, scope, argz, cb ) {
92
+ const merged = {};
93
+ const shadowFacts = Object.assign({}, (scope && scope._) || {});
94
+ let i = 0;
95
+ (function step() {
96
+ if ( i >= fns.length ) {
97
+ // the composed concept must mark ITS OWN self-flag cast (a provider concept
98
+ // writes its self-flag in its template, else the engine never sees it cast and
99
+ // re-fires it to the apply-cap). The constituents' flags are merged data facts.
100
+ if ( concept && concept._name ) merged[concept._name] = true;
101
+ return cb(null, Object.assign({ $_id: '_parent' }, merged));
102
+ }
103
+ const fn = fns[i++];
104
+ const shadowScope = { _: shadowFacts, getRef: scope && scope.getRef ? scope.getRef.bind(scope) : undefined };
105
+ fn(graph, concept, shadowScope, argz, function ( err, tpl ) {
106
+ if ( err ) return cb(err);
107
+ const objs = Array.isArray(tpl) ? tpl : (tpl ? [tpl] : []);
108
+ for ( const o of objs ) {
109
+ if ( !o || typeof o !== 'object' ) continue;
110
+ for ( const raw of Object.keys(o) ) {
111
+ if ( raw === '$_id' || raw === '$$_id' || raw === '_id' ) continue;
112
+ const key = raw.replace(/^\$+/, '');
113
+ merged[key] = o[raw];
114
+ shadowFacts[key] = o[raw];
115
+ }
116
+ }
117
+ step();
118
+ });
119
+ })();
120
+ };
121
+ }
122
+
123
+ /** Normalize engine trace records (cfg.onConceptApply `_rec`) into mineChains input. */
124
+ function recordsFromTrace( rawRecords ) {
125
+ return (rawRecords || [])
126
+ .filter((r) => r && r.conceptName)
127
+ .map((r) => ({ concept: r.conceptName, target: r.targetId }));
128
+ }
129
+
130
+ /**
131
+ * A trace collector you plug into `cfg.onConceptApply` (or `graph.on('conceptApply', …)`),
132
+ * then mine the accumulated firings. Keeps the corpus off the graph (host-side), so the
133
+ * miner runs offline/between-episodes — never mid-stabilize.
134
+ * const miner = traceMiner();
135
+ * new Graph(seed, { …, onConceptApply: miner.onConceptApply }, conceptMap);
136
+ * const chains = miner.chains(conceptTree); // [{from,to,via,count}, …]
137
+ */
138
+ function traceMiner() {
139
+ const records = [];
140
+ return {
141
+ records,
142
+ onConceptApply: function ( rec ) { if ( rec && rec.conceptName ) records.push({ concept: rec.conceptName, target: rec.targetId }); },
143
+ chains: function ( tree ) { return mineChains(records, tree); },
144
+ };
145
+ }
146
+
147
+ // ─────────────────────────────── STRUCTURAL method mining (Gap B+D — the antiUnify-in-the-loop) ───────────────
148
+ //
149
+ // `mineChains` finds frequent producer->consumer EDGES (the spine of the provider-fusion crystallizer). For a
150
+ // STRUCTURAL method — one cast that CREATES a sub-graph (intermediate node + child segments) — the recurrence and
151
+ // the soundness check live at the level of the ground mutation TEMPLATE, not the static edge: a structural concept
152
+ // fires on N call sites, each producing a ground patch; the method is sound to crystallize iff those patches all
153
+ // generalize to ONE stable skeleton (Plotkin LGG, `abstract.antiUnify`) whose content leaves are a function of the
154
+ // firing's typed K1 signature (the canonicalization barrier — a content leaf NOT determined by the signature is the
155
+ // prose case, REFUSE). `mineMethods` is that check, a pure function over the enriched trace.
156
+
157
+ // engine bookkeeping that rides in a captured `rec.patch` (the serialized `_revs[r].tpl`) but is NOT method content:
158
+ // `_rev` is a per-apply monotonic counter (would falsely read as a content-var that varies on every firing — the
159
+ // probe that built this caught it), `_origin` is the creating-apply id (base-derived, redundant). Strip both before
160
+ // relativize/antiUnify so the skeleton is the method, not the bookkeeping. (`getMutationFromPath` strips `_rev` too.)
161
+ const SYS_KEYS = new Set(['_rev', '_origin']);
162
+
163
+ // `extra` (optional Set) = DECLARED prose/untracked keys (the model's free text). They ride in the cast
164
+ // patch but, like the canonicalization barrier in `llm.js` keeps prose out of the FactsDigest, they must be
165
+ // kept OFF the mined surface — prose varies per instance, so leaving it in would falsely read as a content-var
166
+ // and break signature-determination (the K1 "prose stays in the model" line). Stripping them lets the typed
167
+ // STRUCTURE crystallize while the prose stays per-instance (never baked into the method).
168
+ function stripPatch( x, extra ) {
169
+ if ( Array.isArray(x) ) return x.map(( e ) => stripPatch(e, extra));
170
+ if ( x && typeof x === 'object' ) {
171
+ const out = {};
172
+ for ( const k of Object.keys(x) ) if ( !SYS_KEYS.has(k) && !(extra && extra.has(k)) ) out[k] = stripPatch(x[k], extra);
173
+ return out;
174
+ }
175
+ return x;
176
+ }
177
+
178
+ // every id this patch CREATES (its `$$_id`/`$_id`/`_id` values; `_parent` resolves to the base) — so the frontier
179
+ // inference never mistakes a created child for a call-site endpoint.
180
+ function createdIds( patch, base ) {
181
+ const ids = new Set([base]);
182
+ for ( const o of (Array.isArray(patch) ? patch : [patch]) ) {
183
+ if ( !o || typeof o !== 'object' ) continue;
184
+ for ( const k of ['$$_id', '$_id', '_id'] ) {
185
+ const v = o[k];
186
+ if ( typeof v === 'string' ) ids.add(v === '_parent' ? base : v.replace(/^\$+/, ''));
187
+ }
188
+ }
189
+ return ids;
190
+ }
191
+
192
+ // walk a template's string VALUES with their immediate object KEY, in a deterministic (array-index, then
193
+ // sorted-key) order. `key` is the nearest object field the value sits under (an array carries its parent key).
194
+ function eachKeyedValue( tpl, fn, key ) {
195
+ if ( Array.isArray(tpl) ) { for ( const x of tpl ) eachKeyedValue(x, fn, key); return; }
196
+ if ( tpl && typeof tpl === 'object' ) { for ( const k of Object.keys(tpl).sort() ) eachKeyedValue(tpl[k], fn, k); return; }
197
+ if ( typeof tpl === 'string' ) fn(key, tpl);
198
+ }
199
+
200
+ /**
201
+ * Read the call-site ctx ({ base, refs, fields }) off a GROUND structural patch: the base is the cast target; a
202
+ * frontier ref is an id-valued string that (a) is not the base, (b) is not created by this patch, (c) names an
203
+ * EXISTING external object (∈ knownIds). Each distinct external id gets a deterministic name `f<i>` in scan order, so
204
+ * two structurally-identical firings produce ALIGNED parameterized forms (the same endpoint → the same hole name →
205
+ * antiUnify sees a structural hole, not a content diff). `fields[name]` records WHICH object field that endpoint sat
206
+ * under (e.g. `originNode`) so the crystallized provider can re-read it off a new call site (`ctxFromScope`). A value
207
+ * that is none of these is left LITERAL (it is content payload).
208
+ */
209
+ function inferCtx( patch, base, knownIds ) {
210
+ const created = createdIds(patch, base);
211
+ const refs = {}, fields = {}, seen = new Map();
212
+ let n = 0;
213
+ eachKeyedValue(patch, ( key, v ) => {
214
+ if ( v === base || created.has(v) ) return;
215
+ if ( base && v.indexOf(base + '_') === 0 ) return; // base-derived created id
216
+ if ( knownIds && !knownIds.has(v) ) return; // not a real external object → content, leave literal
217
+ if ( !seen.has(v) ) { const name = 'f' + (n++); seen.set(v, name); refs[name] = v; fields[name] = key; }
218
+ });
219
+ return { base, refs, fields };
220
+ }
221
+
222
+ // ─────────────────────────────── DECLARED frontier (Phase 1: INFERRED → DECLARED) ─────────────────────────────
223
+ //
224
+ // `inferCtx` GUESSES the frontier by scanning the literal-id surface (a value ∈ knownIds, not base/created). That
225
+ // breaks the moment an endpoint is wired by a `$`-ref TOKEN (`$start`, `_parent:originNode`) — it isn't in knownIds
226
+ // so it reads as content, the frontier is empty, and the replay never rebinds (study 2026-06-30). `declaredCtx` reads
227
+ // each endpoint from its DECLARED field instead: the value at that field (whatever it is — a literal id OR a `$`-ref
228
+ // token) that is EXTERNAL (not the base, not created by this patch, not base-derived) IS the endpoint. So the value
229
+ // becomes a `⟦@ref⟧` hole regardless of its syntactic form, which is the whole Phase-1 fix. SAME `{base,refs,fields}`
230
+ // return shape as `inferCtx`, so every downstream consumer (relativize/antiUnify/signatureDetermined/ctxFromScope) is
231
+ // unchanged — this is a pure substitution at the ctx layer.
232
+
233
+ // Normalize a host-declared frontier into the canonical param list. Accepts an array of param specs, a plain
234
+ // `{name: field}` map, or `{name: {field, sort, role, requiredFacts}}`. `sort`/`role` default to a node-ref endpoint
235
+ // (the path-section common case) — the Laurie schema fields land here so endpoint and behavioral params are ONE shape
236
+ // differing only by `sort`/`role` (Phase 1 is the actual common substrate, not a behavioral-param bolt-on).
237
+ function normalizeFrontierParams( decl ) {
238
+ if ( !decl ) return null;
239
+ const arr = Array.isArray(decl) ? decl : Object.keys(decl).map(( name ) => {
240
+ const v = decl[name];
241
+ return typeof v === 'string' ? { name, field: v } : Object.assign({ name }, v);
242
+ });
243
+ // preserve the full param shape (a behavioral param carries its typed in/out/frontier interface) — only DEFAULT
244
+ // the universal fields; never drop the interface, else a typed behavioral param arrives at the lint untyped.
245
+ return arr.map(( p ) => Object.assign({}, p, {
246
+ name: p.name, field: p.field, sort: p.sort || 'node-ref',
247
+ role: p.role || 'endpoint', requiredFacts: p.requiredFacts || [],
248
+ }));
249
+ }
250
+
251
+ /**
252
+ * Read the call-site ctx off a GROUND structural patch from a DECLARED frontier (vs `inferCtx`'s knownIds scan).
253
+ * @param patch the ground structural patch (array of template objects)
254
+ * @param base the cast target id (= `_parent`)
255
+ * @param params normalized frontier params (`normalizeFrontierParams`); only `role:'endpoint'` params have a patch
256
+ * location (behavioral params are indexed for selection, not mined into the body ctx — Phase 1).
257
+ * @returns { base, refs:{name:value}, fields:{name:field} } — identical shape to `inferCtx`.
258
+ */
259
+ function declaredCtx( patch, base, params ) {
260
+ const created = createdIds(patch, base);
261
+ const refs = {}, fields = {}, ambiguous = [];
262
+ for ( const p of (params || []) ) {
263
+ if ( (p.role || 'endpoint') !== 'endpoint' || !p.field ) continue;
264
+ // the patch field for a "a:b" replay path is its LEAF segment (the field the value actually sits under).
265
+ const patchField = p.field.indexOf(':') >= 0 ? p.field.split(':').pop() : p.field;
266
+ const found = new Set(); // distinct EXTERNAL values at this declared field (scan order)
267
+ eachKeyedValue(patch, ( key, v ) => {
268
+ if ( key !== patchField ) return;
269
+ if ( v === base || created.has(v) ) return; // internal / the base itself → not the frontier
270
+ if ( base && v.indexOf(base + '_') === 0 ) return; // base-derived created id
271
+ found.add(v);
272
+ });
273
+ // SOUNDNESS: a declared field that resolves to >1 distinct external value is UNDER-DECLARED — relativize would
274
+ // hole only the first and leave the rest LITERAL, so a learning-episode id leaks into a fresh replay (#30, an
275
+ // orphaned segment via Segment.js:70-72). Flag it → the crystallizer REFUSES rather than mis-replaying. (Declare
276
+ // one param per endpoint to resolve — never a cap on expressivity, a disambiguation of the declaration.)
277
+ if ( found.size > 1 ) ambiguous.push({ name: p.name, field: p.field, values: [...found] });
278
+ refs[p.name] = found.size ? [...found][0] : undefined; // undefined ⇒ no hole ⇒ replay bypasses (never mis-binds)
279
+ fields[p.name] = p.field;
280
+ }
281
+ const ctx = { base, refs, fields };
282
+ if ( ambiguous.length ) ctx.ambiguous = ambiguous;
283
+ // SOUNDNESS (collapse): two DISTINCT declared endpoints resolving to the SAME value can't be told apart by
284
+ // `relativize` (it matches by value, first-ref-wins) → they merge into ONE hole and the second mis-binds at a
285
+ // replay site where they differ. Flag → the crystallizer refuses (the endpoints are structurally indistinguishable).
286
+ const byVal = {};
287
+ for ( const name of Object.keys(refs) ) { const v = refs[name]; if ( v != null ) (byVal[v] = byVal[v] || []).push(name); }
288
+ const collapsed = Object.keys(byVal).filter(( v ) => byVal[v].length > 1).map(( v ) => ({ value: v, names: byVal[v] }));
289
+ if ( collapsed.length ) ctx.collapsed = collapsed;
290
+ return ctx;
291
+ }
292
+
293
+ // SOUNDNESS net — a segment endpoint must rebind at replay, so it has to be either a BOUND frontier hole (`⟦@ref:…⟧`)
294
+ // or a base-derived id that names an object the method actually CREATES. Two ways it isn't, each ORPHANS/mis-wires at a
295
+ // fresh replay (#30) and each was adversarial-review-reproduced:
296
+ // • LITERAL — a plain external id the declaration missed (`hasHoles===false`): a baked learning id.
297
+ // • PHANTOM — a base-derived hole `⟦@base⟧<suffix>` whose suffix names NO created object: `relativizeVal` mis-holes
298
+ // an external endpoint whose id collides with `<castTarget>_…` into the base id-space (so `hasHoles` is
299
+ // true and the LITERAL check is blind); at replay it resolves to a non-existent `<newBase><suffix>`.
300
+ // `createdHoles` = the base itself (the cast target) ∪ every base-derived id the param declares as an object id. Reuses
301
+ // `abstract.hasHoles`/`BASE` (the ⟦…⟧ sentinel stays defined in ONE place).
302
+ const ENDPOINT_FIELDS = ['originNode', 'targetNode'];
303
+ const ID_KEYS = ['$$_id', '$_id', '_id'];
304
+ function leakedEndpoints( param ) {
305
+ const objs = Array.isArray(param) ? param : [param];
306
+ const createdHoles = new Set([BASE]); // the base (cast target) is a valid base-derived endpoint
307
+ for ( const o of objs ) {
308
+ if ( !o || typeof o !== 'object' ) continue;
309
+ for ( const k of ID_KEYS ) { const v = o[k]; if ( typeof v === 'string' && v.indexOf(BASE) === 0 ) createdHoles.add(v); }
310
+ }
311
+ const out = [];
312
+ for ( const o of objs ) {
313
+ if ( !o || typeof o !== 'object' ) continue;
314
+ for ( const f of ENDPOINT_FIELDS ) {
315
+ const v = o[f];
316
+ if ( typeof v !== 'string' ) continue;
317
+ if ( !hasHoles(v) ) out.push({ field: f, value: v, kind: 'literal' }); // a plain external literal
318
+ else if ( v.indexOf(BASE) === 0 && !createdHoles.has(v) ) out.push({ field: f, value: v, kind: 'phantom' }); // mis-holed external
319
+ }
320
+ }
321
+ return out;
322
+ }
323
+
324
+ // CONTENT-BLIND structural key: the parameterized form with every CONTENT scalar (anything that is not a hole
325
+ // sentinel) replaced by a placeholder, so all firings of ONE method (same structure + frontier, content varying by
326
+ // premise) group together — while a genuinely different structure (an extra intermediate, a different field set)
327
+ // gets a different key. Reuses `abstract.hasHoles` to recognise a sentinel (never re-derives the ⟦…⟧ format).
328
+ function blankContent( x ) {
329
+ if ( Array.isArray(x) ) return x.map(blankContent);
330
+ if ( x && typeof x === 'object' ) { const o = {}; for ( const k of Object.keys(x) ) o[k] = blankContent(x[k]); return o; }
331
+ if ( typeof x === 'string' && hasHoles(x) ) return x; // a base/ref hole is structural → keep
332
+ return '§'; // content scalar → blank
333
+ }
334
+ function shapeKey( param ) { return canon(blankContent(param)); }
335
+
336
+ /**
337
+ * Mine RECURRENT STRUCTURAL methods from an enriched trace.
338
+ * @param records [{ concept, target, patch, premise }] — enriched firings (see `methodTrace`)
339
+ * @param tree the concept tree (for the produced-fact set per concept)
340
+ * @param opts.minCount distinct call sites required to consider a method (default 2)
341
+ * @param opts.knownIds Set of existing object ids (frontier-vs-content disambiguation; pass graph object ids)
342
+ * @returns [{ concept, count, stable, signatureDetermined, admissible, skeleton, contentVars, frontier,
343
+ * instances:[{target,ground,ctx,premise,premiseDigest,param}], templatesByDigest, produced }]
344
+ * one entry per (concept × structural-skeleton) bucket, sorted by count desc.
345
+ */
346
+ function mineMethods( records, tree, opts ) {
347
+ opts = opts || {};
348
+ const minCount = opts.minCount || 2;
349
+ const knownIds = opts.knownIds || null;
350
+ const declaredParams = normalizeFrontierParams(opts.declaredFrontier); // Phase 1: DECLARED frontier (else inferred)
351
+ // DECLARED prose/untracked keys to strip before mining (the LLM free-text that must not fragment the method).
352
+ const proseSet = ( opts.proseKeys && opts.proseKeys.length ) ? new Set(opts.proseKeys) : null;
353
+
354
+ // static produced-fact set per concept (the contract write obligation, Gap C input).
355
+ const produced = {};
356
+ eachConcept(tree, ( c ) => { if ( c._name ) { const s = c._schema || c; produced[c._name] = [c._name, ...templateKeys(s.applyMutations)]; } });
357
+
358
+ // 1. keep only STRUCTURAL firings (a patch that creates a sub-graph: > 1 object), relativize each.
359
+ const byBucket = {}; // "concept skeletonCanon" -> instances
360
+ for ( const r of (records || []) ) {
361
+ if ( !r || !r.concept || !Array.isArray(r.patch) || r.patch.length <= 1 ) continue;
362
+ const ground = stripPatch(r.patch, proseSet);
363
+ const ctx = declaredParams ? declaredCtx(ground, r.target, declaredParams) : inferCtx(ground, r.target, knownIds);
364
+ const param = relativize(ground, ctx);
365
+ const key = r.concept + ' ' + shapeKey(param); // content-BLIND: one method, content varies by premise
366
+ (byBucket[key] = byBucket[key] || []).push({
367
+ concept: r.concept, target: r.target, ground, ctx, premise: r.premise || {},
368
+ premiseDigest: digest(r.premise || {}), param,
369
+ });
370
+ }
371
+
372
+ // 2. per bucket: distinct-site count, pairwise-antiUnify stability, K1 signatureDetermined.
373
+ const out = [];
374
+ for ( const key of Object.keys(byBucket) ) {
375
+ const inst = byBucket[key];
376
+ const concept = inst[0].concept;
377
+ const distinct = new Set(inst.map(( i ) => i.target));
378
+ const count = distinct.size;
379
+
380
+ // stable iff every pair of instances generalizes to one skeleton (no tree-shape difference).
381
+ let stable = true; const cvPaths = new Set();
382
+ for ( let i = 1; i < inst.length && stable; i++ ) {
383
+ const r = antiUnify(inst[0].ground, inst[0].ctx, inst[i].ground, inst[i].ctx);
384
+ if ( !r.stable ) stable = false;
385
+ else for ( const d of r.contentVars ) cvPaths.add(d.path);
386
+ }
387
+
388
+ // K1 ceiling (the canonicalization barrier): content must be a FUNCTION of the typed signature — i.e. all
389
+ // firings sharing a premise digest must produce the IDENTICAL parameterized method. A premise-class with two
390
+ // different parameterized forms = content depends on something outside the signature (prose/nondeterminism) →
391
+ // not signature-determined → REFUSE (it would graveyard a wrong typed fact through the cascade).
392
+ let signatureDetermined = true;
393
+ const byDigest = {};
394
+ for ( const i of inst ) {
395
+ const pc = canon(i.param);
396
+ if ( byDigest[i.premiseDigest] === undefined ) byDigest[i.premiseDigest] = pc;
397
+ else if ( byDigest[i.premiseDigest] !== pc ) signatureDetermined = false;
398
+ }
399
+ // a representative parameterized template per premise class (the crystallized provider replays these).
400
+ const templatesByDigest = {};
401
+ for ( const i of inst ) if ( !(i.premiseDigest in templatesByDigest) ) templatesByDigest[i.premiseDigest] = i.param;
402
+
403
+ // T2b: validate the engine-native emit reproduces the body (behind opts.graph); the replay param stays captured.
404
+ const emitChk = opts.graph ? emitEquivalence(opts.graph, inst[0].ground, inst[0].ctx, inst[0].target) : null;
405
+
406
+ // SOUNDNESS net: any un-holed segment endpoint across the bucket's parameterized forms = a baked external id
407
+ // (an endpoint the frontier missed). Surfaced for the crystallizer to refuse (a leak would orphan/mis-wire).
408
+ const leakSeen = {}, leak = [];
409
+ for ( const i of inst ) for ( const e of leakedEndpoints(i.param) ) { const k = e.field + '=' + e.value; if ( !leakSeen[k] ) { leakSeen[k] = 1; leak.push(e); } }
410
+
411
+ out.push({
412
+ concept, count, stable, signatureDetermined,
413
+ admissible: stable && signatureDetermined && count >= minCount,
414
+ skeleton: inst[0].param, contentVars: [...cvPaths].sort(),
415
+ frontier: Object.keys(inst[0].ctx.refs).sort(), frontierFields: inst[0].ctx.fields,
416
+ instances: inst, templatesByDigest, produced: produced[concept] || [concept],
417
+ emitEquivalent: emitChk ? emitChk.equivalent : undefined,
418
+ leak: leak.length ? leak : undefined,
419
+ });
420
+ }
421
+ out.sort(( a, b ) => b.count - a.count);
422
+ return out;
423
+ }
424
+
425
+ // ─────────────────────────────── T2b — ENGINE-NATIVE emit, behind opts.graph (validated, not the replay param) ────
426
+ //
427
+ // `emitMethodAsSubgraph` (abstract.js) closes the F6/U1a loop: serialize a derived sub-graph through the engine-native
428
+ // `getMutationFromPath` and relativize it into a re-mountable method. It was dead-but-tested; this wires it behind
429
+ // `opts.graph`. VERIFY-BEFORE-BUILD FINDING (scratchpad probe): for the modeled input the engine-native emit is NOT
430
+ // byte-identical to `relativize(capturedPatch, ctx)` — emit serializes the LIVE BODY ONLY (no parent cast object) and
431
+ // `getMutationFromPath` keeps `_origin` + emits `_id` where the captured patch carries `$$_id`. They ARE equivalent up
432
+ // to that bookkeeping (same skeleton, same holes, same content). So the crystallizer KEEPS the captured-patch param for
433
+ // replay (it also carries the parent's cast facts — Refined/alts — emit drops); emit is exercised + VALIDATED here. The
434
+ // plan's "fall back to relativize if the equivalence test fails" therefore resolves to: always relativize the captured
435
+ // patch (the kill-gate-proven path), with `emitEquivalent` recording that the F6 emit faithfully reproduces the body.
436
+
437
+ const idOfTplObj = ( o, base ) => {
438
+ if ( !o || typeof o !== 'object' ) return null;
439
+ if ( o.$_id === '_parent' ) return base;
440
+ const v = o.$$_id != null ? o.$$_id : (o.$_id != null ? o.$_id : o._id);
441
+ return typeof v === 'string' ? v.replace(/^\$+/, '') : v;
442
+ };
443
+ // normalize away the divergences the probe identified (unify the id key, drop `_origin`), then sort by id, so the two
444
+ // relativized bodies compare on STRUCTURE + CONTENT, not serialization form.
445
+ function normalizeBody( tpl ) {
446
+ return (Array.isArray(tpl) ? tpl : [tpl]).map(( o ) => {
447
+ if ( !o || typeof o !== 'object' ) return o;
448
+ const out = {};
449
+ for ( const k of Object.keys(o) ) {
450
+ if ( k === '_origin' ) continue;
451
+ out[(k === '_id' || k === '$$_id' || k === '$_id') ? 'id' : k] = o[k];
452
+ }
453
+ return out;
454
+ }).sort(( a, b ) => String(a.id).localeCompare(String(b.id)));
455
+ }
456
+
457
+ /**
458
+ * Validate the engine-native emit (`emitMethodAsSubgraph`) against the captured-patch body for ONE firing, behind a
459
+ * live `graph`. Returns { equivalent, emit, captured } — `equivalent` true iff the F6 serialize reproduces the body up
460
+ * to engine bookkeeping. Used to annotate `m.emitEquivalent`; the crystallizer's replay param stays the captured patch.
461
+ */
462
+ function emitEquivalence( graph, ground, ctx, base ) {
463
+ if ( !graph ) return { equivalent: undefined, emit: null, captured: null };
464
+ const bodyIds = [...createdIds(ground, base)].filter(( id ) => id !== base );
465
+ const emit = emitMethodAsSubgraph(graph, bodyIds, ctx);
466
+ const capturedBody = relativize((Array.isArray(ground) ? ground : [ground]).filter(( o ) => idOfTplObj(o, base) !== base), ctx);
467
+ const equivalent = emit != null && canon(normalizeBody(emit)) === canon(normalizeBody(capturedBody));
468
+ return { equivalent, emit, captured: capturedBody };
469
+ }
470
+
471
+ // the target's facts projected onto a concept's MEMO SURFACE (require/ensure/assert keys) — the typed K1 premise the
472
+ // firing was keyed on. Captured at apply time from the live graph (require keys aren't overwritten by the cast's own
473
+ // patch, so the post-apply projection is the firing premise).
474
+ function premiseOf( graph, conceptName, targetId ) {
475
+ const keys = memoSurfaceKeys(graph, conceptName);
476
+ const etty = graph.getEtty && graph.getEtty(targetId);
477
+ const facts = etty && etty._;
478
+ if ( !facts ) return {};
479
+ const proj = {};
480
+ for ( const k of keys ) if ( k in facts ) proj[k] = facts[k];
481
+ return proj;
482
+ }
483
+
484
+ /**
485
+ * An ENRICHED trace collector for structural mining — keeps the ground patch + the firing premise (which `traceMiner`
486
+ * drops). Register it on the live graph's `conceptApply` event (so it can read the premise off the graph):
487
+ * const mt = methodTrace();
488
+ * const g = new Graph(seed, cfg, conceptMap); mt.listen(g);
489
+ * await nextStable(g);
490
+ * const methods = mt.methods(tree, { knownIds: new Set(Object.keys(g._objById)) });
491
+ */
492
+ function methodTrace() {
493
+ const records = [];
494
+ function onApply( graph, rec ) {
495
+ if ( !rec ) { rec = graph; graph = null; } // tolerate the cfg.onConceptApply (rec-only) form
496
+ if ( !rec || !rec.conceptName ) return;
497
+ records.push({
498
+ concept: rec.conceptName, target: rec.targetId, patch: rec.patch, applyId: rec.applyId,
499
+ premise: graph ? premiseOf(graph, rec.conceptName, rec.targetId) : (rec.premise || {}),
500
+ });
501
+ }
502
+ return {
503
+ records, onApply, onConceptApply: onApply,
504
+ listen: function ( graph ) { graph.on('conceptApply', onApply); return this; },
505
+ methods: function ( tree, o ) { return mineMethods(records, tree, o); },
506
+ };
507
+ }
508
+
509
+ module.exports = { mineChains, composeProviders, recordsFromTrace, traceMiner,
510
+ mineMethods, methodTrace, stripPatch, inferCtx, declaredCtx, normalizeFrontierParams, emitEquivalence, premiseOf };
@@ -0,0 +1,119 @@
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
+ * mount — the 3-REGIME MOUNT POLICY (host-side meta-controller, ZERO-CORE). U2 / master-graph study §2.x.
10
+ *
11
+ * A learned method can be MOUNTED three ways; the controller picks one by (depth, efficiency, reliability),
12
+ * with HYSTERESIS (banded promote/demote so it doesn't flap) and a well-founded MOUNT-RANK whose bottom is
13
+ * the K1 "stay-in-LLM" floor (termination of the adapt loop). The regimes:
14
+ *
15
+ * - INSTANCE (fork-per-case) — the SAFE DEFAULT. Each case in its own world (single-world JTMS forces
16
+ * this for unproven / write-frontier methods). Always sound.
17
+ * - INLINE (addConcept) — mount into the live graph. Sound ONLY for a READ-ONLY frontier (confluence
18
+ * is undecidable; an inline write-frontier method can race consumers).
19
+ * Restricted to shallow, proven, read-only-frontier methods.
20
+ * - FROZEN (warm-cache replay) — a proven, "dry" method replayed from the warm cache + a deopt-guard
21
+ * (NOT a stabilization-skipping executor — §2 Correction 2). The cheapest.
22
+ * - ESCALATE (stay in the LLM) — the floor: a method that has DEOPTed K times is demoted off the library
23
+ * and always re-forged/escalated. This is the well-founded bottom (K1).
24
+ *
25
+ * MOUNT-RANK μ = (deoptBudget, …): each deopt strictly decrements deoptBudget; at 0 the method is pinned to
26
+ * ESCALATE. So the collapse→re-forge→re-mount loop cannot cycle forever — it descends to the LLM floor in
27
+ * ≤K deopts (HotSpot/PyPy deopt-pinning, Hölzle-Chambers-Ungar 1992; Bolz et al. 2009). A success replenishes
28
+ * nothing automatically (demotion is sticky until an explicit re-admit), so μ is monotone non-increasing.
29
+ *
30
+ * const ctl = createMountController();
31
+ * ctl.decide('M_AB', { reliability: 0.9, hitRate: 0.95, depth: 1, readOnlyFrontier: true }); // -> 'frozen'
32
+ * ctl.recordDeopt('M_AB'); // a guaranteeing value drifted → partial-collapse + re-forge; budget--
33
+ */
34
+
35
+ const DEFAULTS = {
36
+ maxDeopt: 3, // K: deopts before a method is pinned to ESCALATE (the floor)
37
+ freezeHi: 0.85, freezeLo: 0.70, // hysteresis band for FROZEN (promote at Hi, demote below Lo)
38
+ inlineHi: 0.65, inlineLo: 0.50, // hysteresis band for INLINE
39
+ inlineMaxDepth: 1, // INLINE only for shallow methods (deep → fork, to bound context at the join)
40
+ widenCap: 5 // §6.4: the megamorphic WIDTH cap — distinct widens before a method degrades to ESCALATE
41
+ };
42
+
43
+ // Pure classifier: given signals + the CURRENT regime (for hysteresis), pick the regime. No state.
44
+ function classify( signals, current, t ) {
45
+ t = Object.assign({}, DEFAULTS, t || {});
46
+ const s = signals || {};
47
+ const score = Math.min(s.reliability == null ? 0 : s.reliability, s.hitRate == null ? (s.reliability || 0) : s.hitRate);
48
+ const readOnly = !!s.readOnlyFrontier;
49
+ const depth = s.depth == null ? 99 : s.depth;
50
+
51
+ if ( (s.deoptCount || 0) >= t.maxDeopt ) return 'escalate'; // the K1 floor (absorbing)
52
+
53
+ // FROZEN — proven + dry + read-only frontier. Hysteresis: promote at freezeHi; once frozen, hold until <freezeLo.
54
+ const freezeThresh = (current === 'frozen') ? t.freezeLo : t.freezeHi;
55
+ if ( readOnly && score >= freezeThresh ) return 'frozen';
56
+
57
+ // INLINE — shallow + read-only frontier + moderately proven (confluence: read-only only).
58
+ const inlineThresh = (current === 'inline') ? t.inlineLo : t.inlineHi;
59
+ if ( readOnly && depth <= t.inlineMaxDepth && score >= inlineThresh ) return 'inline';
60
+
61
+ // INSTANCE — the safe default for everything else (unproven, deep, or write-frontier).
62
+ return 'instance';
63
+ }
64
+
65
+ function createMountController( opts ) {
66
+ opts = opts || {};
67
+ const t = Object.assign({}, DEFAULTS, opts.thresholds || {});
68
+ const state = new Map(); // methodId -> { regime, deoptCount }
69
+
70
+ function get( id ) { if ( !state.has(id) ) state.set(id, { regime: 'instance', deoptCount: 0, widenCount: 0 }); return state.get(id); }
71
+
72
+ return {
73
+ thresholds: t,
74
+ /** decide (and remember) the regime for a method given live signals; applies hysteresis + the deopt floor. */
75
+ decide( id, signals ) {
76
+ const st = get(id);
77
+ const sig = Object.assign({ deoptCount: st.deoptCount }, signals || {});
78
+ const regime = classify(sig, st.regime, t);
79
+ st.regime = regime;
80
+ return { regime, deoptCount: st.deoptCount, reason: reasonFor(regime, sig, t) };
81
+ },
82
+ /** a guaranteeing value drifted → the method DEOPTs (partial-collapse + re-forge). Decrements μ. */
83
+ recordDeopt( id ) {
84
+ const st = get(id);
85
+ st.deoptCount++;
86
+ if ( st.deoptCount >= t.maxDeopt ) st.regime = 'escalate'; // pinned to the floor
87
+ return st.deoptCount;
88
+ },
89
+ /**
90
+ * §6.4 — a method's pre was WIDENED (the S-boundary climbed). This is a SUCCESS, NOT a deopt:
91
+ * • it DEMOTES FROZEN→INSTANCE (G2 — the newly-admitted cases must re-hit `assertPost`; the caller MUST call
92
+ * this BEFORE issuing the gate-relax `patchConcept` so the relaxed gate never admits a case under a frozen regime);
93
+ * • it consumes its OWN megamorphic WIDTH budget (PIC degradation, Hölzle-Chambers-Ungar 1991/92), NEVER μ —
94
+ * a healthy generalizer must not be pinned to the deopt floor with zero failures. At `widenCap` distinct
95
+ * widens the method is too polymorphic to be a useful specialization → ESCALATE (degrade to generic).
96
+ */
97
+ recordWiden( id ) {
98
+ const st = get(id);
99
+ const demotedFrom = st.regime;
100
+ if ( st.regime === 'frozen' ) st.regime = 'instance'; // G2: re-guard with assertPost (never elide on widened cases)
101
+ st.widenCount++;
102
+ if ( st.widenCount >= t.widenCap ) st.regime = 'escalate'; // megamorphic width cap (NOT μ)
103
+ return { regime: st.regime, widenCount: st.widenCount, demotedFrom };
104
+ },
105
+ regimeOf( id ) { return get(id).regime; },
106
+ deoptBudget( id ) { return Math.max(0, t.maxDeopt - get(id).deoptCount); }, // the well-founded rank μ (deopts only)
107
+ widenBudget( id ) { return Math.max(0, t.widenCap - get(id).widenCount); }, // §6.4 — the megamorphic width budget
108
+ state
109
+ };
110
+ }
111
+
112
+ function reasonFor( regime, s, t ) {
113
+ if ( regime === 'escalate' ) return `deopt floor (${s.deoptCount}/${t.maxDeopt}) — stay in the LLM`;
114
+ if ( regime === 'frozen' ) return 'proven + dry + read-only frontier → warm-cache replay';
115
+ if ( regime === 'inline' ) return 'shallow + read-only frontier + proven → addConcept';
116
+ return 'unproven / deep / write-frontier → fork-per-case (safe default)';
117
+ }
118
+
119
+ module.exports = { createMountController, classify, DEFAULTS };