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,343 @@
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
+ * Tree-decomposition pass on the concept-dependency graph (host-side authoring tool,
21
+ * zero core change) — the Mixture-of-Reasoners "tiling" brick (doc/WIP/HANDOFF.md §7
22
+ * Tier 1; SOTA subgraph-grammars brick iii; experiment E7).
23
+ *
24
+ * From a concept tree it DERIVES — does not guess — (a) the interface alphabet (the
25
+ * SEPARATOR facts a fork frontier should cross) and (b) the hierarchical tiling (the
26
+ * candidate forks), and reports the TREEWIDTH as the inference cost bound: a low
27
+ * treewidth with thin separators means partitioning pays (cheap merge traffic, the
28
+ * #P-avoidance made operational); a dense corpus with high treewidth and no thin cut
29
+ * means it does not. It feeds fork creation (`Graph.fork`/`merge` with a snapped
30
+ * frontier — see lib/providers/merge-consistency.js for the C1 cross-boundary contract).
31
+ *
32
+ * const { treeDecomposition } = require('./decompose');
33
+ * const { treewidth, separators, tiles, partitionPays } = treeDecomposition(tree);
34
+ *
35
+ * Model (static analysis — no engine runtime):
36
+ * - PRIMAL graph: facts = vertices; each concept's referenced fact-set is a clique
37
+ * (a factor over those facts). `conceptCliques(tree)` extracts the fact-sets.
38
+ * - MIN-FILL elimination -> treewidth (exact treewidth is NP-hard, Bodlaender 1996,
39
+ * so this is the standard min-fill heuristic — an upper bound).
40
+ * - ARTICULATION points (Tarjan) -> candidate separator facts = the interface alphabet.
41
+ * - COMPONENTS after removing the separators -> the tiles (forks).
42
+ *
43
+ * Fact extraction note: this keys a cross-object walk ref (`a:b`) on its WALK BASE
44
+ * (the fact named first), which is the vertex the dependency hangs off in the primal
45
+ * graph — deliberately the same heuristic that recovered the planted structure in E7
46
+ * (synthetic -> {cost,risk}; real `common` -> [Distance,Stay]). validate.js keys the
47
+ * walk TARGET instead (it judges the resolved fact, a different question); the two are
48
+ * intentionally not unified.
49
+ */
50
+
51
+ // ---- fact extraction: the cliques (one fact-set per concept) ----
52
+
53
+ const asArray = (v) => (v == null ? [] : Array.isArray(v) ? v : [v]);
54
+ // $ref / $$ref token in an assert/ensure expression (no ':' in the class -> a cross-walk
55
+ // ref is captured at its base, e.g. `$$clock:tick` -> `clock`).
56
+ const EXPR_REF_RE = /\$\$?[A-Za-z_][\w.]*/g;
57
+
58
+ // The fact keys one concept node references — its self-flag, its require LHS facts, the
59
+ // refs inside its assert/ensure, and the keys its applyMutations template writes.
60
+ function conceptFacts( node ) {
61
+ const schema = (node && node._schema) || node || {};
62
+ const f = new Set();
63
+ if ( node && node._name ) f.add(node._name);
64
+ for ( const r of asArray(schema.require) )
65
+ if ( typeof r === 'string' ) f.add(r.split(':').pop()); // walk base of a require LHS
66
+ for ( const e of asArray(schema.assert).concat(asArray(schema.ensure)) ) {
67
+ if ( typeof e !== 'string' ) continue;
68
+ for ( const t of (e.match(EXPR_REF_RE) || []) )
69
+ f.add(t.replace(/^\$+/, '').split('.')[0].split(':').pop()); // strip $, drop .member, walk base
70
+ }
71
+ for ( const mu of asArray(schema.applyMutations) )
72
+ if ( mu && typeof mu === 'object' )
73
+ for ( const k of Object.keys(mu) ) if ( !k.startsWith('$') && k !== '_id' ) f.add(k);
74
+ return [...f];
75
+ }
76
+
77
+ // Walk the concept tree (same node semantics as validate.js#eachConcept: a node is a
78
+ // concept iff it is a child entry or carries a `_name`; the synthetic root container is
79
+ // not) and collect one non-empty clique per concept.
80
+ function conceptCliques( tree ) {
81
+ const cliques = [];
82
+ const walk = ( node, isChild ) => {
83
+ if ( !node || typeof node !== 'object' ) return;
84
+ if ( isChild || node._name ) {
85
+ const c = conceptFacts(node);
86
+ if ( c.length ) cliques.push(c);
87
+ }
88
+ const kids = node.childConcepts;
89
+ if ( kids ) for ( const k of Object.keys(kids) ) walk(kids[k], true);
90
+ };
91
+ walk(tree, false);
92
+ return cliques;
93
+ }
94
+
95
+ // ---- graph algorithms ----
96
+
97
+ function primalAdj( cliques ) {
98
+ const adj = new Map();
99
+ const add = (a, b) => { if ( !adj.has(a) ) adj.set(a, new Set()); if ( a !== b ) adj.get(a).add(b); };
100
+ for ( const cl of cliques ) for ( const a of cl ) { add(a, a); for ( const b of cl ) add(a, b); }
101
+ return adj;
102
+ }
103
+
104
+ // Min-fill elimination ordering -> a tree decomposition; bag sizes give the treewidth
105
+ // (max bag - 1). Heuristic: pick the vertex whose elimination adds the fewest fill edges.
106
+ // ALSO returns the bag-intersection SEPARATORS: when vertex v is eliminated, its higher-neighbour set
107
+ // nb(v) is exactly B_v ∩ parent(B_v) in the resulting tree decomposition (Robertson-Seymour 1986) — the
108
+ // REAL Σ_sep family (k-aware minimal separators of the chordal completion, monotone under edge addition,
109
+ // Dirac 1961), NOT the size-1 articulation cuts. `treewidth` is a SCALAR PROJECTION that loses this.
110
+ function minFillTreewidth( adj ) {
111
+ const G = new Map([...adj].map(([k, s]) => [k, new Set(s)]));
112
+ const remaining = new Set(G.keys());
113
+ const bags = [], separators = [];
114
+ while ( remaining.size ) {
115
+ let best = null, bestFill = Infinity;
116
+ for ( const v of remaining ) {
117
+ const nb = [...G.get(v)].filter((x) => remaining.has(x));
118
+ let fill = 0;
119
+ for ( let i = 0; i < nb.length; i++ )
120
+ for ( let j = i + 1; j < nb.length; j++ )
121
+ if ( !G.get(nb[i]).has(nb[j]) ) fill++;
122
+ if ( fill < bestFill ) { bestFill = fill; best = v; }
123
+ }
124
+ const nb = [...G.get(best)].filter((x) => remaining.has(x));
125
+ bags.push(new Set([best, ...nb]));
126
+ if ( nb.length ) separators.push(new Set(nb)); // B_best ∩ parent = the bag-intersection separator
127
+ for ( let i = 0; i < nb.length; i++ ) // make the neighbourhood a clique (fill in)
128
+ for ( let j = i + 1; j < nb.length; j++ ) { G.get(nb[i]).add(nb[j]); G.get(nb[j]).add(nb[i]); }
129
+ remaining.delete(best);
130
+ }
131
+ return { bags, treewidth: Math.max(0, ...bags.map((b) => b.size)) - 1, separators };
132
+ }
133
+
134
+ // A bag-intersection separator `s` SPLITS the corpus iff removing it leaves ≥2 components. It is MINIMAL
135
+ // iff no single-element-smaller subset also splits (a sufficient, conservative minimality test — Robertson-
136
+ // Seymour minimal separators). The MINIMAL splitting separators are the thin cross-tile interfaces (the
137
+ // articulation points are exactly their size-1 case); their union is the cross-tile separator alphabet Σ_sep.
138
+ function isMinimalSplit( adj, s ) {
139
+ if ( componentsWithout(adj, s).length < 2 ) return false;
140
+ for ( const v of s ) {
141
+ const sub = new Set([...s].filter(( x ) => x !== v));
142
+ if ( sub.size && componentsWithout(adj, sub).length >= 2 ) return false; // a proper subset already splits → not minimal
143
+ }
144
+ return true;
145
+ }
146
+
147
+ /**
148
+ * The bounded-context HORIZON of a primal graph (E7/Σ_sep) — the OBJECT the scalar treewidth + the size-1
149
+ * articulation `separators` both PROJECT AWAY. Reconstructs the bag-intersection separators from the min-fill
150
+ * elimination and reports:
151
+ * - `sigmaSep` the cross-tile separator alphabet = ∪ of the MINIMAL splitting separators (the thin
152
+ * interface facts; generalises articulation points to size-k cuts);
153
+ * - `minimalInterface` the THINNEST splitting separator `{ size, sep }` — the horizon WIDTH. A grammar refactor
154
+ * that grows it (a size-1 cut → a size-2 cut) regressed the bound even when the scalar
155
+ * treewidth is unchanged AND no new articulation key appeared (the killer case).
156
+ * - `indivisible` true if NO bag-separator splits (one blob); then the "width" is the whole bag (treewidth+1).
157
+ * - `treewidth` kept as a DIAGNOSTIC only (min-fill is an uncoupled upper bound → unsound as a scalar gate).
158
+ */
159
+ function bagInterface( cliques ) {
160
+ const adj = primalAdj(cliques);
161
+ const { treewidth, separators } = minFillTreewidth(adj);
162
+ const sigma = new Set();
163
+ let min = null;
164
+ for ( const s of separators ) {
165
+ if ( !isMinimalSplit(adj, s) ) continue;
166
+ for ( const f of s ) sigma.add(f);
167
+ if ( !min || s.size < min.size ) min = { size: s.size, sep: [...s].sort() };
168
+ }
169
+ return {
170
+ treewidth,
171
+ bagSeparators: separators.map(( s ) => [...s].sort()),
172
+ sigmaSep: [...sigma].sort(),
173
+ minimalInterface: min || { size: Math.max(1, treewidth + 1), sep: [...adj.keys()].sort() },
174
+ indivisible: !min,
175
+ };
176
+ }
177
+
178
+ // Tarjan articulation points — the cut vertices whose removal disconnects the primal
179
+ // graph = the candidate separator facts (the interface alphabet between tiles).
180
+ function articulationPoints( adj ) {
181
+ const disc = new Map(), low = new Map(), ap = new Set();
182
+ let timer = 0;
183
+ const dfs = (u, parent) => {
184
+ disc.set(u, ++timer); low.set(u, timer);
185
+ let children = 0;
186
+ for ( const v of adj.get(u) ) {
187
+ if ( !disc.has(v) ) {
188
+ children++; dfs(v, u);
189
+ low.set(u, Math.min(low.get(u), low.get(v)));
190
+ if ( parent !== null && low.get(v) >= disc.get(u) ) ap.add(u);
191
+ } else if ( v !== parent ) {
192
+ low.set(u, Math.min(low.get(u), disc.get(v)));
193
+ }
194
+ }
195
+ if ( parent === null && children > 1 ) ap.add(u);
196
+ };
197
+ for ( const v of adj.keys() ) if ( !disc.has(v) ) dfs(v, null);
198
+ return ap;
199
+ }
200
+
201
+ // Connected components of the primal graph with the `removed` vertices (separators) cut out.
202
+ function componentsWithout( adj, removed ) {
203
+ const seen = new Set(), comps = [];
204
+ for ( const s of adj.keys() ) {
205
+ if ( seen.has(s) || removed.has(s) ) continue;
206
+ const comp = [], stack = [s]; seen.add(s);
207
+ while ( stack.length ) {
208
+ const u = stack.pop(); comp.push(u);
209
+ for ( const v of adj.get(u) ) if ( !seen.has(v) && !removed.has(v) ) { seen.add(v); stack.push(v); }
210
+ }
211
+ comps.push(comp);
212
+ }
213
+ return comps;
214
+ }
215
+
216
+ /**
217
+ * Decompose a primal graph given directly as cliques (fact-name arrays). Use this when
218
+ * you already have the fact-sets; `treeDecomposition` extracts them from a concept tree.
219
+ * @returns { nFacts, nConcepts, treewidth, separators, tiles, nTiles, bags, partitionPays }
220
+ */
221
+ function decomposeCliques( cliques ) {
222
+ const adj = primalAdj(cliques);
223
+ const { bags, treewidth } = minFillTreewidth(adj);
224
+ const sepSet = articulationPoints(adj);
225
+ const separators = [...sepSet].sort();
226
+ const tiles = componentsWithout(adj, sepSet)
227
+ .filter((c) => c.length)
228
+ .map((c) => c.sort())
229
+ .sort((a, b) => b.length - a.length);
230
+ return {
231
+ nFacts: adj.size,
232
+ nConcepts: cliques.length,
233
+ treewidth,
234
+ separators,
235
+ tiles,
236
+ nTiles: tiles.length,
237
+ bags: bags.map((b) => [...b].sort()),
238
+ // partition pays when there is a thin cut AND it actually splits the corpus
239
+ partitionPays: separators.length > 0 && tiles.length > 1
240
+ };
241
+ }
242
+
243
+ /**
244
+ * Run the tree-decomposition pass over a concept tree.
245
+ * @param tree the nested concept tree (root container or a concept node)
246
+ * @returns the `decomposeCliques` result for the derived cliques.
247
+ */
248
+ function treeDecomposition( tree ) {
249
+ return decomposeCliques(conceptCliques(tree));
250
+ }
251
+
252
+ // Walk the tree collecting { name, facts:Set } per concept (the named form of conceptCliques).
253
+ function conceptsWithFacts( tree ) {
254
+ const out = [];
255
+ const walk = ( node, isChild ) => {
256
+ if ( !node || typeof node !== 'object' ) return;
257
+ if ( isChild || node._name ) {
258
+ const f = conceptFacts(node);
259
+ if ( f.length && node._name ) out.push({ name: node._name, facts: new Set(f) });
260
+ }
261
+ const kids = node.childConcepts;
262
+ if ( kids ) for ( const k of Object.keys(kids) ) walk(kids[k], true);
263
+ };
264
+ walk(tree, false);
265
+ return out;
266
+ }
267
+
268
+ /**
269
+ * Derive a FORK PLAN from the tree-decomposition — the "interface derivation" the SOTA synthesis
270
+ * flagged as the killer deliverable: assign each concept to a tile/fork (by where its non-separator
271
+ * facts fall) and derive each fork's FRONTIER ALPHABET (the separator facts its concepts touch =
272
+ * what crosses that fork boundary). Feeds `Graph.fork`/`merge` AND the `validateMergeProjection`
273
+ * frontier alphabet. Hub concepts (only separator facts) are reported as `interface` glue, not tiled.
274
+ * @returns { treewidth, separators, partitionPays, forks:[{ facts, concepts, frontier }], interface:[names] }
275
+ */
276
+ function forkPlan( tree ) {
277
+ const concepts = conceptsWithFacts(tree);
278
+ const decomp = decomposeCliques(concepts.map((c) => [...c.facts]));
279
+ const sepSet = new Set(decomp.separators);
280
+
281
+ // fact -> tile index (non-separator facts only; tiles are the post-cut components)
282
+ const tileOf = new Map();
283
+ decomp.tiles.forEach((t, i) => t.forEach((f) => tileOf.set(f, i)));
284
+
285
+ const forks = decomp.tiles.map((tile) => ({ facts: tile, concepts: [], frontier: new Set() }));
286
+ const iface = [];
287
+ for ( const c of concepts ) {
288
+ const votes = {};
289
+ const seps = [];
290
+ for ( const f of c.facts ) {
291
+ if ( sepSet.has(f) ) seps.push(f);
292
+ else if ( tileOf.has(f) ) votes[tileOf.get(f)] = (votes[tileOf.get(f)] || 0) + 1;
293
+ }
294
+ const best = Object.keys(votes).sort((a, b) => votes[b] - votes[a])[0];
295
+ if ( best == null ) { iface.push(c.name); continue; } // only separator facts -> interface glue
296
+ forks[best].concepts.push(c.name);
297
+ seps.forEach((s) => forks[best].frontier.add(s));
298
+ }
299
+
300
+ return {
301
+ treewidth: decomp.treewidth,
302
+ separators: decomp.separators,
303
+ partitionPays: decomp.partitionPays,
304
+ forks: forks.map((p) => ({ facts: p.facts, concepts: p.concepts.sort(), frontier: [...p.frontier].sort() })),
305
+ interface: iface.sort()
306
+ };
307
+ }
308
+
309
+ /**
310
+ * §6.3 separatorGate — the SEPARATOR HORIZON check (the §3.3 obligation `params ∪ appConditions ∪ requiredFacts
311
+ * ⊆ Σ_sep`). A proposed ancestry projection (the facts the ancestry oracle would promote to runtime params / fold
312
+ * into the memo digest KEY) is sound iff every projected fact is a CROSS-TILE SEPARATOR fact (on the horizon) —
313
+ * never a fact above the separator (in another tile's interior), which a runtime read would reach past the horizon.
314
+ * • Σ_sep = the bag-intersection minimal splitting separators (NOT the global articulation points: those are the
315
+ * size-1 cut family only, and a global cut-vertex elsewhere is NOT on THIS projection's horizon — `bagInterface`).
316
+ * • a projected fact ∉ Σ_sep is "above-separator" → REFUSED (the spec compiles it to a baked constant / forged hole).
317
+ * Pairs with the memo-key MONOTONICITY (a finer key never false-hits): key-enlargement is safe for memo correctness,
318
+ * but ONLY this gate makes it bound-safe (the digest read at dispatch is itself a `requiredFact`).
319
+ * @returns { ok, above:[facts ∉ Σ_sep], sigmaSep }
320
+ */
321
+ function separatorGate( tree, projectionFacts ) {
322
+ const iface = bagInterface(conceptCliques(tree));
323
+ const sigma = new Set(iface.sigmaSep);
324
+ const above = (projectionFacts || []).filter(( f ) => !sigma.has(f));
325
+ return { ok: above.length === 0, above, sigmaSep: iface.sigmaSep };
326
+ }
327
+
328
+ module.exports = {
329
+ treeDecomposition,
330
+ forkPlan,
331
+ decomposeCliques,
332
+ conceptCliques,
333
+ conceptFacts,
334
+ // §6.3 — the bag-intersection separator horizon (the real Σ_sep) + the gate
335
+ bagInterface,
336
+ separatorGate,
337
+ isMinimalSplit,
338
+ // lower-level graph primitives (exported for reuse / testing)
339
+ primalAdj,
340
+ minFillTreewidth,
341
+ articulationPoints,
342
+ componentsWithout
343
+ };
@@ -0,0 +1,236 @@
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
+ * emittability — the SIGNATURE-STABILITY / paraphrase-consistency profiler (roadmap STAGE-0 Grammar-P1
10
+ * instrument, 2026-07-01). Read-only measurement: given the intake `FactsDigest` a small model produces
11
+ * for many PARAPHRASES of the same latent task, does the typed signature stay STABLE? Everything
12
+ * downstream (dispatch, cache, call-elision, method reuse) keys on that digest; if paraphrases fragment
13
+ * the key, the memo never hits (K1 fragmentation) — so this is the component SCREEN feeding the
14
+ * end-to-end call-count gate (paper-dll arms). It is NOT itself the make-or-break gate (Laurie confront).
15
+ *
16
+ * TWO failure modes, not one (Laurie confront — the load-bearing correction):
17
+ * • FRAGMENTATION (cheap): paraphrases of ONE task scatter into several digests → wasted calls.
18
+ * Measured by within-task agreement (`collisionProb`, the unbiased Simpson estimator).
19
+ * • COLLISION (correctness-fatal): TWO DIFFERENT tasks land on the SAME digest → the memo returns the
20
+ * WRONG method. Measured by cross-task HOMOGENEITY of the pooled partition (V-measure).
21
+ * A scheme that maps EVERYTHING to one tuple scores perfect within-task agreement and is catastrophic;
22
+ * only the pooled two-partition view (reference=task vs induced=digest) catches that. So we report BOTH,
23
+ * with a HIGHER bar on homogeneity than on completeness.
24
+ *
25
+ * Chance correction (the vacuousness alarm): raw agreement can't tell "stable" from "always emits the same
26
+ * enum". Fleiss' κ (1971) subtracts the pooled-marginal chance agreement → κ→0 under mode collapse even as
27
+ * raw agreement→1 (the κ paradox under skew is here a FEATURE). NOTE for the memo, chance agreement is REAL
28
+ * value (a hit is a hit), so `collisionProb` is the OPERATIONAL number and κ is the VALIDITY/discrimination
29
+ * number — reported side by side, never conflated.
30
+ *
31
+ * The "by construction" split (the crux): grammar-constrained decoding guarantees FORMAT VALIDITY + VOCAB
32
+ * CLOSURE (digest drawn from a finite known set → H(D|T) bounded). It does NOT guarantee SEMANTIC
33
+ * consistency (which enum member → H(D|T)≈0, nuisance-invariance in the Information-Bottleneck sense) — and
34
+ * can DISTORT it (Park et al. 2024, Grammar-Aligned Decoding). So to measure the semantic half you must NET
35
+ * OUT format: compare arms only on inputs where BOTH emitted an in-vocab label (`crossArmAgreement`).
36
+ *
37
+ * `untyped` is NOT dropped (survivorship bias) — it is its own digest class ⊥; `typedRate` is reported
38
+ * separately from the digest agreement. Refs: Simpson 1949; Fleiss 1971; Rosenberg & Hirschberg 2007
39
+ * (V-measure); Hubert & Arabie 1985 (ARI); Errica et al. 2025 (sensitivity/consistency of LM labels).
40
+ *
41
+ * Pure + synchronous (unit-tested). No engine coupling — it consumes plain intake-result records.
42
+ */
43
+
44
+ var UNTYPED = '⊥'; // ⊥ — the digest class for a non-typed (out-of-vocab / errored) intake.
45
+
46
+ // the digest CLASS of one intake result: its FactsDigest when typed, else ⊥.
47
+ function signatureClass( r ) {
48
+ return (r && r.status === 'typed' && r.digest != null) ? String(r.digest) : UNTYPED;
49
+ }
50
+
51
+ function countBy( items, keyOf ) {
52
+ var m = new Map();
53
+ items.forEach(function ( x ) { var k = keyOf(x); m.set(k, (m.get(k) || 0) + 1); });
54
+ return m;
55
+ }
56
+
57
+ function log2( x ) { return Math.log(x) / Math.LN2; }
58
+
59
+ // Shannon entropy (bits) of a count map over N items.
60
+ function entropyOf( counts, N ) {
61
+ if ( !N ) return 0;
62
+ var h = 0;
63
+ counts.forEach(function ( n ) { if ( n > 0 ) { var p = n / N; h -= p * log2(p); } });
64
+ return h;
65
+ }
66
+
67
+ /**
68
+ * Within-task statistics for ONE task's paraphrase results.
69
+ * @param results [{status, digest}] the K paraphrase intake results for one latent task.
70
+ * @param classOf (optional) override the class projection (for per-field marginals).
71
+ * @returns { K, typedRate, collisionProb, modalCoverage, effectiveSignatures, numClasses, counts }
72
+ * collisionProb — UNBIASED Simpson estimator Σ n_i(n_i-1)/[K(K-1)] = P(two paraphrases collide on the
73
+ * same digest) = the memo-hit event. null when K<2. PRIMARY.
74
+ * modalCoverage — max n_i / K. SECONDARY (upward-biased at small K — flag, don't lead with it).
75
+ * effectiveSignatures — 1/Σ p_i^2 (plug-in Hill-2): how many memo keys one task splinters into.
76
+ */
77
+ function perTaskStats( results, classOf ) {
78
+ classOf = classOf || signatureClass;
79
+ var K = results.length;
80
+ var typed = results.filter(function ( r ) { return r && r.status === 'typed'; }).length;
81
+ var counts = countBy(results, classOf);
82
+ var sumNiNi1 = 0, sumP2 = 0, modal = 0;
83
+ counts.forEach(function ( n ) { sumNiNi1 += n * (n - 1); sumP2 += (n / K) * (n / K); if ( n > modal ) modal = n; });
84
+ return {
85
+ K: K,
86
+ typedRate: K ? typed / K : 0,
87
+ collisionProb: K > 1 ? sumNiNi1 / (K * (K - 1)) : null,
88
+ modalCoverage: K ? modal / K : 0,
89
+ effectiveSignatures: sumP2 > 0 ? 1 / sumP2 : 0,
90
+ numClasses: counts.size,
91
+ counts: counts,
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Pooled two-partition agreement over ALL tasks (reference partition = taskId, induced = digest class).
97
+ * @param groups [{ taskId, results:[{status,digest}] }]
98
+ * @returns { N, homogeneity, completeness, vMeasure, ari, miBits, hClass, hCluster }
99
+ * homogeneity — 1 - H(task|digest)/H(task): each digest holds ONE task = COLLISION-FREE (the safety
100
+ * direction; bar it HIGH).
101
+ * completeness — 1 - H(digest|task)/H(digest): each task's paraphrases share ONE digest = consistency.
102
+ * vMeasure — harmonic mean (the rate-distortion pair collapsed to one number; report H & C too).
103
+ * ari — Adjusted Rand Index (chance-corrected pooled agreement, Hubert & Arabie 1985).
104
+ */
105
+ function poolAgreement( groups, classOf ) {
106
+ classOf = classOf || signatureClass;
107
+ var points = [];
108
+ groups.forEach(function ( g ) { g.results.forEach(function ( r ) { points.push({ cls: g.taskId, clu: classOf(r) }); }); });
109
+ var N = points.length;
110
+ if ( !N ) return { N: 0, homogeneity: 1, completeness: 1, vMeasure: 1, ari: 1, miBits: 0, hClass: 0, hCluster: 0 };
111
+
112
+ var classCounts = countBy(points, function ( p ) { return p.cls; });
113
+ var cluCounts = countBy(points, function ( p ) { return p.clu; });
114
+ // joint contingency table keyed "clsclu"
115
+ var joint = countBy(points, function ( p ) { return p.cls + '' + p.clu; });
116
+
117
+ var hClass = entropyOf(classCounts, N), hCluster = entropyOf(cluCounts, N);
118
+ // H(class|cluster) = Σ_k (|k|/N) H(class within k) = H(class,cluster) - H(cluster)
119
+ var hJoint = entropyOf(joint, N);
120
+ var hClassGivenClu = hJoint - hCluster; // ≥0 (numerical clamp below)
121
+ var hCluGivenClass = hJoint - hClass;
122
+ if ( hClassGivenClu < 0 ) hClassGivenClu = 0;
123
+ if ( hCluGivenClass < 0 ) hCluGivenClass = 0;
124
+
125
+ var homogeneity = hClass === 0 ? 1 : 1 - hClassGivenClu / hClass;
126
+ var completeness = hCluster === 0 ? 1 : 1 - hCluGivenClass / hCluster;
127
+ var vMeasure = (homogeneity + completeness) === 0 ? 0 : 2 * homogeneity * completeness / (homogeneity + completeness);
128
+ var miBits = hCluster - hCluGivenClass; // I(class;cluster)
129
+
130
+ // ── Adjusted Rand Index (Hubert & Arabie 1985) ──
131
+ var choose2 = function ( n ) { return n * (n - 1) / 2; };
132
+ var sumJoint = 0; joint.forEach(function ( n ) { sumJoint += choose2(n); });
133
+ var sumA = 0; classCounts.forEach(function ( n ) { sumA += choose2(n); });
134
+ var sumB = 0; cluCounts.forEach(function ( n ) { sumB += choose2(n); });
135
+ var totPairs = choose2(N);
136
+ var expected = totPairs ? (sumA * sumB) / totPairs : 0;
137
+ var maxIndex = (sumA + sumB) / 2;
138
+ var ari = (maxIndex - expected) === 0 ? 1 : (sumJoint - expected) / (maxIndex - expected);
139
+
140
+ return { N: N, homogeneity: homogeneity, completeness: completeness, vMeasure: vMeasure, ari: ari, miBits: miBits, hClass: hClass, hCluster: hCluster };
141
+ }
142
+
143
+ /**
144
+ * Fleiss' κ (1971) over tasks: items = tasks, ratings = the K paraphrase digest-classes, categories = the
145
+ * union of digest classes seen. The vacuousness alarm: mode collapse → Pe→1 → κ→0 (or vacuous). Handles
146
+ * per-item K_i (generalized). @returns { kappa, Pbar, Pe, vacuous }
147
+ */
148
+ function fleissKappa( groups, classOf ) {
149
+ classOf = classOf || signatureClass;
150
+ var perItem = groups.map(function ( g ) {
151
+ var K = g.results.length;
152
+ var counts = countBy(g.results, classOf);
153
+ var sumSq = 0; counts.forEach(function ( n ) { sumSq += n * n; });
154
+ var Pi = K > 1 ? (sumSq - K) / (K * (K - 1)) : null; // = the unbiased Simpson for this item
155
+ return { counts: counts, K: K, Pi: Pi };
156
+ }).filter(function ( it ) { return it.Pi != null; });
157
+ if ( !perItem.length ) return { kappa: null, Pbar: null, Pe: null, vacuous: true };
158
+
159
+ var Pbar = perItem.reduce(function ( s, it ) { return s + it.Pi; }, 0) / perItem.length;
160
+ var totalRatings = perItem.reduce(function ( s, it ) { return s + it.K; }, 0);
161
+ var catTotals = new Map();
162
+ perItem.forEach(function ( it ) { it.counts.forEach(function ( n, cat ) { catTotals.set(cat, (catTotals.get(cat) || 0) + n); }); });
163
+ var Pe = 0; catTotals.forEach(function ( n ) { var p = n / totalRatings; Pe += p * p; });
164
+ var vacuous = (1 - Pe) < 1e-12;
165
+ return { kappa: vacuous ? null : (Pbar - Pe) / (1 - Pe), Pbar: Pbar, Pe: Pe, vacuous: vacuous };
166
+ }
167
+
168
+ /**
169
+ * Cross-arm SEMANTIC agreement, format NETTED OUT (the (a)/(b)-isolating comparison — Laurie + SOTA).
170
+ * Given two arms' results ALIGNED by input index, restrict to inputs where BOTH are `typed`
171
+ * (both in-vocab) and report the fraction whose digest AGREES. This is what tells whether the grammar
172
+ * constraint changes the SEMANTIC choice vs a format-guaranteed control — could be ≤0 (Park et al. 2024).
173
+ * @param a,b aligned arrays [{status,digest}] (same inputs, same order).
174
+ * @returns { nBothTyped, agree, agreeFraction, aTypedRate, bTypedRate }
175
+ */
176
+ function crossArmAgreement( a, b ) {
177
+ var n = Math.min(a.length, b.length), both = 0, agree = 0, at = 0, bt = 0;
178
+ for ( var i = 0; i < n; i++ ) {
179
+ var ta = a[i] && a[i].status === 'typed', tb = b[i] && b[i].status === 'typed';
180
+ if ( ta ) at++; if ( tb ) bt++;
181
+ if ( ta && tb ) { both++; if ( String(a[i].digest) === String(b[i].digest) ) agree++; }
182
+ }
183
+ return { nBothTyped: both, agree: agree, agreeFraction: both ? agree / both : null, aTypedRate: n ? at / n : 0, bTypedRate: n ? bt / n : 0 };
184
+ }
185
+
186
+ /**
187
+ * Aggregate profile for one arm over many paraphrase-task groups.
188
+ * @param groups [{ taskId, results:[{status,digest,facts?}] }]
189
+ * @param opts.fields optional [fieldName...] → also report per-field MARGINAL stability (a low marginal
190
+ * localizes the instability + flags a field that should be DEMOTED off the tracked digest).
191
+ * @returns aggregate (per-task array, means with the replication unit = TASK, pooled agreement, κ, marginals)
192
+ */
193
+ function profile( groups, opts ) {
194
+ opts = opts || {};
195
+ var perTask = groups.map(function ( g ) {
196
+ var s = perTaskStats(g.results);
197
+ return { taskId: g.taskId, K: s.K, typedRate: s.typedRate, collisionProb: s.collisionProb, modalCoverage: s.modalCoverage, effectiveSignatures: s.effectiveSignatures, numClasses: s.numClasses };
198
+ });
199
+ var withCollision = perTask.filter(function ( t ) { return t.collisionProb != null; });
200
+ var mean = function ( arr, f ) { return arr.length ? arr.reduce(function ( s, x ) { return s + f(x); }, 0) / arr.length : null; };
201
+
202
+ var out = {
203
+ nTasks: groups.length, // the replication unit for population claims
204
+ perTask: perTask,
205
+ meanCollisionProb: mean(withCollision, function ( t ) { return t.collisionProb; }),
206
+ meanModalCoverage: mean(perTask, function ( t ) { return t.modalCoverage; }),
207
+ meanTypedRate: mean(perTask, function ( t ) { return t.typedRate; }),
208
+ minCollisionProb: withCollision.length ? Math.min.apply(null, withCollision.map(function ( t ) { return t.collisionProb; })) : null,
209
+ pool: poolAgreement(groups),
210
+ fleiss: fleissKappa(groups),
211
+ };
212
+ if ( opts.fields ) {
213
+ out.marginals = {};
214
+ opts.fields.forEach(function ( field ) {
215
+ var proj = function ( r ) {
216
+ if ( !r || r.status !== 'typed' || !r.facts || r.facts[field] == null ) return UNTYPED;
217
+ return field + '=' + String(r.facts[field]);
218
+ };
219
+ var pt = groups.map(function ( g ) { var s = perTaskStats(g.results, proj); return { taskId: g.taskId, collisionProb: s.collisionProb, modalCoverage: s.modalCoverage }; });
220
+ var wc = pt.filter(function ( t ) { return t.collisionProb != null; });
221
+ out.marginals[field] = { perTask: pt, meanCollisionProb: mean(wc, function ( t ) { return t.collisionProb; }), pool: poolAgreement(groups, proj) };
222
+ });
223
+ }
224
+ return out;
225
+ }
226
+
227
+ module.exports = {
228
+ UNTYPED: UNTYPED,
229
+ signatureClass: signatureClass,
230
+ perTaskStats: perTaskStats,
231
+ poolAgreement: poolAgreement,
232
+ fleissKappa: fleissKappa,
233
+ crossArmAgreement: crossArmAgreement,
234
+ profile: profile,
235
+ _entropyOf: entropyOf, // exported for the unit test
236
+ };