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,138 @@
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
+ * method-explorer — LIST + JUDGE a concept-method population (host-side, ZERO-CORE, fs-free). The owner's
10
+ * "outil/explorer" ask (2026-07-05): give every crystallized method a TITLE / mini-DESCRIPTION / CATEGORY, and
11
+ * summarize the population so its QUALITY, its OPENNESS (diversity), and its COVERAGE (which declared task-
12
+ * classes have a method — and which are GAPS) are legible. Reads any library SOURCE method-pack normalizes
13
+ * (a master loop / recall index / { entries } / array / a `.sgc methods` bundle's `.methods`).
14
+ *
15
+ * A concept-method is a TYPED decomposition: a class SIGNATURE (the K1 `structure` discriminant keys) → a
16
+ * derivation over `content` holes → reusable templates (`templatesBySig`). So:
17
+ * - CATEGORY = the canonical structure signature (the class the method serves).
18
+ * - TITLE = the structure values, human-legible.
19
+ * - COVERAGE = per structure-key, which VALUES have a method; against a declared vocabulary (a registry enum
20
+ * or an explicit `expected`), the MISSING values = the population's gaps (the actionable metric).
21
+ * - OPENNESS = distinct classes, singleton fraction (one-off vs reused), templates/method (reuse depth),
22
+ * and the Shannon ENTROPY of the class distribution (spread vs concentration).
23
+ *
24
+ * const { describeLibrary, formatLibrary } = require('skynet-graph/lib/authoring/method-explorer');
25
+ * const report = describeLibrary(masterLoop, { registry }); // registry → coverage gaps vs the declared canon
26
+ * console.log(formatLibrary(report)); // a text listing + population summary
27
+ */
28
+
29
+ const { toEntries } = require('./method-pack.js');
30
+
31
+ function canon( x ) {
32
+ if ( x === undefined ) return 'null';
33
+ if ( x === null || typeof x !== 'object' ) return JSON.stringify(x);
34
+ if ( Array.isArray(x) ) return '[' + x.map(canon).join(',') + ']';
35
+ return '{' + Object.keys(x).sort().map(( k ) => JSON.stringify(k) + ':' + canon(x[k])).join(',') + '}';
36
+ }
37
+ function templateCountOf( method ) {
38
+ if ( !method || typeof method !== 'object' ) return 0;
39
+ if ( method.templatesBySig && typeof method.templatesBySig === 'object' ) return Object.keys(method.templatesBySig).length;
40
+ if ( Array.isArray(method.templates) ) return method.templates.length;
41
+ return method.template || method.derivation ? 1 : 0;
42
+ }
43
+
44
+ /**
45
+ * A single method's descriptor: id, title, category, mini-description + the typed keys and template count.
46
+ * @param entry a normalized `{ structure, content, method }`
47
+ */
48
+ function describeMethod( entry ) {
49
+ const s = entry.structure || {}, c = entry.content || {};
50
+ const sKeys = Object.keys(s).sort(), cKeys = Object.keys(c).sort();
51
+ const category = sKeys.length ? sKeys.map(( k ) => k + '=' + s[k]).join(',') : '(untyped)';
52
+ const title = sKeys.length ? sKeys.map(( k ) => String(s[k])).join(' · ') : '(untyped)';
53
+ const templates = templateCountOf(entry.method);
54
+ const id = (entry.method && entry.method.id) || 'm:' + canon(s);
55
+ return {
56
+ id, title, category,
57
+ description: `keys on {${sKeys.join(', ') || '∅'}} → derives {${cKeys.join(', ') || '∅'}} · ${templates} template${templates === 1 ? '' : 's'}`,
58
+ structureKeys: sKeys, contentKeys: cKeys, templateCount: templates
59
+ };
60
+ }
61
+
62
+ function entropyBits( counts ) {
63
+ const total = counts.reduce(( a, b ) => a + b, 0);
64
+ if ( total <= 0 ) return 0;
65
+ let h = 0;
66
+ for ( const n of counts ) if ( n > 0 ) { const p = n / total; h -= p * Math.log2(p); }
67
+ return h;
68
+ }
69
+
70
+ /**
71
+ * Describe + judge a whole method population.
72
+ * @param source any library source (see `toEntries`) — a master loop / recall index / { entries } / array /
73
+ * a `.sgc methods` bundle's `.methods`.
74
+ * @param opts { registry } to compute coverage GAPS against the declared enum vocab, or { expected:{ <key>:
75
+ * [values] } } for an explicit declared value-space per structure key.
76
+ * @returns {{ methods, population:{ count, categories, coverage, openness } }}
77
+ */
78
+ function describeLibrary( source, opts ) {
79
+ opts = opts || {};
80
+ const entries = toEntries(source);
81
+ const methods = entries.map(describeMethod);
82
+
83
+ // class distribution (the CATEGORY = the served signature)
84
+ const catCount = {};
85
+ for ( const m of methods ) catCount[m.category] = (catCount[m.category] || 0) + 1;
86
+ const categories = Object.keys(catCount).map(( category ) => ({ category, count: catCount[category] }))
87
+ .sort(( a, b ) => b.count - a.count || (a.category < b.category ? -1 : 1));
88
+
89
+ // COVERAGE — per structure-key present values, and the declared-vocab GAPS (missing values with no method)
90
+ const keyVals = {};
91
+ for ( const e of entries ) for ( const k of Object.keys(e.structure || {}) ) (keyVals[k] = keyVals[k] || new Set()).add(String(e.structure[k]));
92
+ const expectedFor = ( key ) => {
93
+ if ( opts.expected && opts.expected[key] ) return opts.expected[key];
94
+ const entry = opts.registry && opts.registry.keys && opts.registry.keys[key];
95
+ return entry && entry.enum ? entry.enum : null;
96
+ };
97
+ const coverage = Object.keys(keyVals).sort().map(( key ) => {
98
+ const present = [...keyVals[key]].sort();
99
+ const c = { key, present, covered: present.length };
100
+ const exp = expectedFor(key);
101
+ if ( exp ) { const has = keyVals[key]; c.expected = exp.length; c.missing = exp.filter(( v ) => !has.has(String(v))).sort(); c.fraction = exp.length ? present.filter(( v ) => exp.map(String).includes(v)).length / exp.length : 0; }
102
+ return c;
103
+ });
104
+
105
+ // OPENNESS — diversity of the population
106
+ const singleton = categories.filter(( c ) => c.count === 1 ).length;
107
+ const totalTemplates = methods.reduce(( s, m ) => s + m.templateCount, 0);
108
+ const openness = {
109
+ distinctClasses: categories.length,
110
+ singletonFraction: categories.length ? singleton / categories.length : 0,
111
+ avgTemplatesPerMethod: methods.length ? totalTemplates / methods.length : 0,
112
+ entropyBits: entropyBits(categories.map(( c ) => c.count )),
113
+ maxEntropyBits: categories.length > 1 ? Math.log2(categories.length) : 0
114
+ };
115
+
116
+ return { methods, population: { count: methods.length, categories, coverage, openness } };
117
+ }
118
+
119
+ /** Render a `describeLibrary` report as a compact text listing + a population summary (the CLI/explorer view). */
120
+ function formatLibrary( report ) {
121
+ const L = [];
122
+ const p = report.population;
123
+ L.push(`CONCEPT-METHODS — ${p.count} method(s), ${p.openness.distinctClasses} distinct class(es)`);
124
+ L.push('');
125
+ for ( const m of report.methods ) L.push(` • ${m.title.padEnd(24)} [${m.category}] ${m.description}`);
126
+ L.push('');
127
+ L.push(`POPULATION`);
128
+ L.push(` classes (top): ${p.categories.slice(0, 8).map(( c ) => c.category + '×' + c.count).join(' · ')}`);
129
+ L.push(` openness: ${p.openness.distinctClasses} classes · singletons ${(100 * p.openness.singletonFraction).toFixed(0)}% · templates/method ${p.openness.avgTemplatesPerMethod.toFixed(2)} · entropy ${p.openness.entropyBits.toFixed(2)}/${p.openness.maxEntropyBits.toFixed(2)} bits`);
130
+ L.push(` coverage:`);
131
+ for ( const c of p.coverage ) {
132
+ if ( c.expected != null ) L.push(` ${c.key}: ${c.covered}/${c.expected} (${(100 * c.fraction).toFixed(0)}%)${c.missing.length ? ' · GAPS: ' + c.missing.join(', ') : ' · complete'}`);
133
+ else L.push(` ${c.key}: ${c.covered} value(s) present [${c.present.join(', ')}]`);
134
+ }
135
+ return L.join('\n');
136
+ }
137
+
138
+ module.exports = { describeMethod, describeLibrary, formatLibrary, templateCountOf };
@@ -0,0 +1,181 @@
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
+ * method-pack — the `.sgc` CRYSTALLIZED-METHOD PACKAGE (host-side, ZERO-CORE, fs-free). M3 of the
10
+ * productization campaign / master-graph study persistence rung. The SIBLING of `corpus-pack.js`: that one
11
+ * packages the AUTHORED grammar (a concept tree → a portable bundle with a derived manifest); THIS one
12
+ * packages the LEARNED method library (the forged + crystallized methods the master loop accumulates) so a
13
+ * warm library MOVES between deployments — not just survives a local restart (that is M2 / `store.js`).
14
+ *
15
+ * It reuses the SAME `.sgc` envelope (`format:'sgc'` + `sgcVersion` + `manifest`), discriminated by a
16
+ * `kind` field: `'corpus'` (the existing grammar bundle) vs `'methods'` (this one). One exchange format,
17
+ * two payload kinds; `corpus-pack.js` is untouched and a reader dispatches on `kind` (default `'corpus'`).
18
+ *
19
+ * const bundle = packMethods(masterLoop, { name: 'travel', version: 'v3' }); // .sgc JSON
20
+ * // ... ship the bundle to another deployment, write it to disk via store.saveSgc ...
21
+ * const r = loadMethods(bundle, freshLoop, { version: 'v3' }); // re-hydrate the library
22
+ * // r.added entries recalled; r.exactReplayed entries warm the 0-call cache (iff versions match)
23
+ *
24
+ * THE PORTABLE UNIT is the recall-index entry `{ structure, content, method }` — signature-addressed, so a
25
+ * receiver can RECALL→VERIFY it (U5). The exact content-address cache is a DERIVED acceleration: its key is
26
+ * recomputable from each entry's signature via the host loop's own `keyOf`, so packing the index is enough.
27
+ *
28
+ * THE LOAD-BEARING SOUNDNESS LINE (B8 — method-version pinning, made portable):
29
+ * A method is `(typed signature) → derivation`; that FUNCTION is defined by the grammar + the providers,
30
+ * whose identity IS the version token. Replaying a packaged method verbatim is sound in a receiving host
31
+ * IFF the host's version matches the package's — otherwise the same typed input may now map to a DIFFERENT
32
+ * derivation, so NO verbatim replay (neither the exact 0-call cache NOR a recall→verify `'full'` hit, which
33
+ * also replays the stored method at 0 cost) is trustworthy. So the version gate covers BOTH replay paths:
34
+ * - versions AGREE → hydrate the recall index AND the exact cache (full 0-call replay, sound).
35
+ * - versions DIFFER → hydrate NEITHER: the host re-forges (sound). The entries stay readable via
36
+ * `unpackMethods` for a host that wants to re-forge FROM the skeletons (a future version-aware-recall
37
+ * refinement could downgrade a cross-version `'full'` to a partial re-forge; not built — refuse beats
38
+ * a stale replay: "fuzziness in recall, exactness in truth" — a cross-version method is not TRUTH here).
39
+ * Versions are opt-in (like `cache.js` `version()`): the gate enforces iff BOTH sides declare a version
40
+ * (absent ⇒ permissive — the host explicitly opted out of pinning). Independently, the typed VERIFY
41
+ * (`recall.js#verify`) ALSO runs on the receiver, so even a same-version package never replays a method
42
+ * whose typed STRUCTURE mismatches a local query (the canonicalize.js line holds across hosts).
43
+ *
44
+ * A typed SCHEMA is DERIVED from the entries (mirrors `corpus-pack.js#deriveManifest`): the method-CLASS
45
+ * discriminants (the K1 `structure` keys) and the derived `content` keys — self-describing, no hand-upkeep,
46
+ * so a receiving host knows what typed inputs key each method class without reading the bodies.
47
+ */
48
+
49
+ // stable, key-sorted, recursive stringify (digest-safe; the same canon the recall index + master loop use).
50
+ function canon( x ) {
51
+ if ( x === undefined ) return 'null';
52
+ if ( x === null || typeof x !== 'object' ) return JSON.stringify(x);
53
+ if ( Array.isArray(x) ) return '[' + x.map(canon).join(',') + ']';
54
+ return '{' + Object.keys(x).sort().map(( k ) => JSON.stringify(k) + ':' + canon(x[k])).join(',') + '}';
55
+ }
56
+
57
+ function clone( x ) { return x == null ? x : JSON.parse(JSON.stringify(x)); }
58
+
59
+ /**
60
+ * Normalize any reasonable library SOURCE into a flat array of `{ structure, content, method }` entries:
61
+ * - a master loop (has `.index` recall index) → its index entries
62
+ * - a recall index (has `.entries` + `.recall`) → its entries
63
+ * - `{ entries:[...] }` → those entries
64
+ * - a bare array → itself
65
+ * Each raw entry may be `{ sig:{structure,content}, method }` (the index shape) or already
66
+ * `{ structure, content, method }`. Missing structure/content default to `{}`.
67
+ */
68
+ function toEntries( source ) {
69
+ let raw;
70
+ if ( Array.isArray(source) ) raw = source;
71
+ else if ( source && source.index && Array.isArray(source.index.entries) ) raw = source.index.entries;
72
+ else if ( source && Array.isArray(source.entries) ) raw = source.entries;
73
+ else raw = [];
74
+ return raw.map(( e ) => {
75
+ const structure = (e.sig ? e.sig.structure : e.structure) || {};
76
+ const content = (e.sig ? e.sig.content : e.content) || {};
77
+ return { structure: clone(structure), content: clone(content), method: clone(e.method) };
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Derive the typed SCHEMA off the method entries — the self-describing contract a receiver needs.
83
+ * @returns {{ structureKeys, contentKeys, classes:[{structure,count}] }}
84
+ * structureKeys the union of K1 method-CLASS discriminant keys (what defines a method class).
85
+ * contentKeys the union of derived `content` keys (the per-instance holes).
86
+ * classes the distinct typed structures present, with how many entries each covers.
87
+ */
88
+ function deriveMethodSchema( entries ) {
89
+ const sKeys = new Set(), cKeys = new Set(), classes = new Map();
90
+ for ( const e of entries ) {
91
+ for ( const k of Object.keys(e.structure || {}) ) sKeys.add(k);
92
+ for ( const k of Object.keys(e.content || {}) ) cKeys.add(k);
93
+ const key = canon(e.structure || {});
94
+ const c = classes.get(key) || { structure: clone(e.structure || {}), count: 0 };
95
+ c.count++; classes.set(key, c);
96
+ }
97
+ return {
98
+ structureKeys: [...sKeys].sort(),
99
+ contentKeys : [...cKeys].sort(),
100
+ classes : [...classes.values()].sort(( a, b ) => canon(a.structure) < canon(b.structure) ? -1 : 1)
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Pack a learned method library into a portable `.sgc` methods bundle (plain JSON).
106
+ * @param source a master loop / recall index / { entries } / array (see `toEntries`)
107
+ * @param opts { name, version, description }
108
+ */
109
+ function packMethods( source, opts ) {
110
+ opts = opts || {};
111
+ const entries = toEntries(source);
112
+ return {
113
+ format : 'sgc',
114
+ sgcVersion: 1,
115
+ kind : 'methods',
116
+ manifest : {
117
+ name : opts.name || 'methods',
118
+ version : opts.version || '0.0.0',
119
+ description: opts.description || '',
120
+ methodCount: entries.length,
121
+ schema : deriveMethodSchema(entries)
122
+ },
123
+ methods : entries
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Unpack a `.sgc` methods bundle. Pure read — returns the entries + manifest + derived schema and a
129
+ * version-gate verdict (does NOT mutate any host). Throws on a non-methods bundle.
130
+ * @param bundle a packMethods() output
131
+ * @param opts { hostVersion } — if given, compared against the package version for `exactReplaySafe`.
132
+ * @returns {{ methods, manifest, schema, versionPackage, exactReplaySafe }}
133
+ */
134
+ function unpackMethods( bundle, opts ) {
135
+ opts = opts || {};
136
+ if ( !bundle || bundle.format !== 'sgc' ) throw new Error('not an .sgc bundle');
137
+ if ( bundle.kind !== 'methods' ) throw new Error('not a .sgc methods bundle (kind=' + bundle.kind + ')');
138
+ const methods = bundle.methods || [];
139
+ const manifest = bundle.manifest || {};
140
+ return {
141
+ methods, manifest, schema: manifest.schema || deriveMethodSchema(methods),
142
+ versionPackage : manifest.version,
143
+ exactReplaySafe: versionsAgree(opts.hostVersion, manifest.version)
144
+ };
145
+ }
146
+
147
+ // B8 gating: active iff BOTH sides declare a version (opt-in, like cache.js#version). Absent ⇒ permissive.
148
+ function versionsAgree( vHost, vPkg ) {
149
+ if ( vHost == null || vPkg == null ) return true; // no pinning requested → permissive
150
+ return vHost === vPkg;
151
+ }
152
+
153
+ /**
154
+ * Re-hydrate a `.sgc` methods bundle INTO a host library, gated by the version (B8). On a version MATCH it
155
+ * feeds BOTH replay paths — the recall index AND the exact 0-call cache; on a MISMATCH it hydrates NEITHER
156
+ * (the host re-forges; the stale bodies never write a derivation here). See the soundness line in the header.
157
+ *
158
+ * @param bundle a packMethods() output
159
+ * @param host { index, cache, keyOf } — e.g. a master loop (it exposes exactly these). `cache`/`keyOf`
160
+ * optional: without them only the recall index is hydrated.
161
+ * @param opts { version } — the host's method-lib version (B8). Omit to opt out of version pinning.
162
+ * @returns {{ added, exactReplayed, exactReplaySafe, skipped, versionHost, versionPackage }}
163
+ */
164
+ function loadMethods( bundle, host, opts ) {
165
+ opts = opts || {};
166
+ const { methods, versionPackage } = unpackMethods(bundle, { hostVersion: opts.version });
167
+ const safe = versionsAgree(opts.version, versionPackage);
168
+ let added = 0, exactReplayed = 0;
169
+ if ( !safe ) // version mismatch → refuse to inject stale methods into the live replay paths (re-forge instead).
170
+ return { added: 0, exactReplayed: 0, exactReplaySafe: false, skipped: methods.length, versionHost: opts.version, versionPackage };
171
+ for ( const e of methods ) {
172
+ const sig = { structure: e.structure, content: e.content };
173
+ if ( host && host.index && host.index.add ) { host.index.add(sig, clone(e.method)); added++; } // recall path
174
+ if ( host && host.cache && host.cache.set && typeof host.keyOf === 'function' ) {
175
+ host.cache.set(host.keyOf(sig), clone(e.method)); exactReplayed++; // exact 0-call path
176
+ }
177
+ }
178
+ return { added, exactReplayed, exactReplaySafe: true, skipped: 0, versionHost: opts.version, versionPackage };
179
+ }
180
+
181
+ module.exports = { packMethods, unpackMethods, deriveMethodSchema, loadMethods, toEntries };
@@ -0,0 +1,202 @@
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
+ * method — HIGHER-ORDER methods-as-graphs: a method receives a SUB-GRAPH as a typed named-slot parameter
10
+ * and applies it (host-side, builds on `abstract.js`; engine-native splice via `pushMutation`). BRICK 1 of
11
+ * the conception build (doc/WIP/studies/2026-06-28-concept-as-graph-conception-assembled.md §3 / C2).
12
+ *
13
+ * The parameter is FIRST-ORDER in mechanics: a body is a PARAMETERIZED template (holes from
14
+ * `abstract.js#relativize`) bound BY NAME at a call site (`instantiate`) — never inferred. So "method takes
15
+ * a sub-graph" is decidable substitution, not higher-order unification.
16
+ *
17
+ * Landmines respected: each application gets a FRESH id-base (finding #30 — else N applications collapse
18
+ * onto one body via the existing-object merge path), the splice goes through the SEQUENCED `pushMutation`
19
+ * (determinism), and an unbound frontier ref THROWS (a leak) rather than splicing an unsound partial.
20
+ */
21
+ const { instantiate, refHolesOf } = require('./abstract.js');
22
+
23
+ /**
24
+ * Bind a parameterized body sub-graph to a NEW call site → a ground template. Throws on an unbound frontier
25
+ * ref (a leak) — never returns a partially-bound template.
26
+ * @param paramBody parameterized template (holes via `relativize`)
27
+ * @param ctx { base, refs:{name:id} } the call site (fresh base + the slot's frontier ids)
28
+ * @returns the ground (bound) template
29
+ */
30
+ function bindSubgraphArg( paramBody, ctx ) {
31
+ const bound = instantiate(paramBody, ctx);
32
+ if ( bound == null )
33
+ throw new Error('applySubgraphArg: unbound frontier ref (leak) for refs=' + JSON.stringify((ctx && ctx.refs) || {}));
34
+ return bound;
35
+ }
36
+
37
+ /**
38
+ * Bind a body sub-graph into a call-site SLOT and splice it SEQUENCED under the slot (parented via
39
+ * `_origin = targetId`).
40
+ * @param graph
41
+ * @param paramBody the parameterized body
42
+ * @param ctx { base, refs } the call site (fresh base + frontier ids)
43
+ * @param targetId the slot object the splice mounts under (the parent)
44
+ * @param cb called once the splice settles
45
+ * @returns the bound template
46
+ */
47
+ function applySubgraphArg( graph, paramBody, ctx, targetId, cb ) {
48
+ const bound = bindSubgraphArg(paramBody, ctx);
49
+ graph.pushMutation(bound, targetId, false, undefined, undefined, cb);
50
+ return bound;
51
+ }
52
+
53
+ /**
54
+ * BUILD the MAP combinator's template (PURE — no graph mutation): apply a body sub-graph to EACH element of
55
+ * a collection, each instance with its OWN fresh id-base (so N elements never collide onto one body, #30).
56
+ * Returned as ONE combined template — a provider returns this via `cb` (the ENGINE applies it, parented under
57
+ * the cast slot), or `mapSubgraph` pushes it directly.
58
+ * @param opts.elements [id] the collection element node ids (the CASES)
59
+ * @param opts.body the parameterized body (the PARAM); frontier `elem` is bound to each element
60
+ * @param opts.basePrefix id-base prefix per element (default 'map' → map0, map1, …)
61
+ * @param opts.refsOf optional (elem,i) => extra frontier refs to bind besides `elem`
62
+ * @returns the combined ground template (array)
63
+ */
64
+ function mapTemplate( opts ) {
65
+ opts = opts || {};
66
+ const elements = opts.elements || [];
67
+ const prefix = opts.basePrefix || 'map';
68
+ const tpl = [];
69
+ elements.forEach(function ( elem, i ) {
70
+ const refs = Object.assign({ elem: elem }, opts.refsOf ? opts.refsOf(elem, i) : {});
71
+ tpl.push.apply(tpl, bindSubgraphArg(opts.body, { base: prefix + i, refs: refs }));
72
+ });
73
+ return tpl;
74
+ }
75
+
76
+ /**
77
+ * The MAP combinator, HOST-driven: build the fan-out template and splice it SEQUENCED under the slot.
78
+ * (The ENGINE-driven form is a provider returning `mapTemplate(...)` — see method-subgraph.test.js.)
79
+ * @param graph
80
+ * @param opts as `mapTemplate` + `opts.slotId` (the map segment the bodies mount under)
81
+ * @param cb called once the map settles
82
+ * @returns the combined ground template
83
+ */
84
+ function mapSubgraph( graph, opts, cb ) {
85
+ const tpl = mapTemplate(opts);
86
+ if ( !tpl.length ) { if ( cb ) setTimeout(cb); return tpl; }
87
+ graph.pushMutation(tpl, opts.slotId, false, undefined, undefined, cb);
88
+ return tpl;
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------- the method LINT (Brick 2)
92
+
93
+ // kinds we treat as K1-typed slot containers; anything else (incl. these prose markers) is a memo footgun.
94
+ const TYPED_KINDS = new Set(['enum', 'id', 'number', 'bool', 'list', 'subgraph']);
95
+ const PROSE_TYPES = new Set(['prose', 'text', 'string', 'str', 'freetext', 'free-text']);
96
+
97
+ // the fact keys a pre/post entry touches: the $refs it mentions, or — if it is a bare key (no $) — itself.
98
+ function frameKeys( entries ) {
99
+ const keys = new Set();
100
+ for ( const e of (entries || []) ) {
101
+ if ( typeof e !== 'string' ) continue;
102
+ const refs = e.match(/\$\$?([A-Za-z_][\w.:]*)/g);
103
+ if ( refs ) refs.forEach(( r ) => keys.add(r.replace(/^\$\$?/, '')));
104
+ else keys.add(e.trim());
105
+ }
106
+ return keys;
107
+ }
108
+
109
+ const isTypedName = ( t, opts ) =>
110
+ typeof t === 'string' && t.length > 0 && !PROSE_TYPES.has(t.toLowerCase()) &&
111
+ (!opts || !opts.types || opts.types.indexOf(t) >= 0);
112
+
113
+ /**
114
+ * lintMethod — enforce the decidability invariants + the footprint/frame check on a METHOD DEFINITION, so the
115
+ * "decidable line" is CHECKED, not emergent (design doc §3 / C4). Mirrors `validate.js`: `{ errors, warnings }`
116
+ * with records `{ method, kind, message, slot? }`. Errors = a method that breaks an invariant; an uncontracted
117
+ * method is a WARNING (the cost gradient — a runtime micro-LLM fallback, §0.1), not an error.
118
+ *
119
+ * @param def { name, slots:{ <name>:{ role:'param'|'case', kind, frontier?, in?, out?, elem? } }, body?, contract? }
120
+ * @param opts { types? } optional host type-alphabet — if given, slot type names must be in it.
121
+ */
122
+ function lintMethod( def, opts ) {
123
+ opts = opts || {};
124
+ const errors = [], warnings = [];
125
+ const method = (def && def.name) || '?';
126
+ const err = ( kind, message, slot ) => errors.push({ method, kind, message, slot });
127
+ const slots = (def && def.slots) || {};
128
+ const used = def && def.body != null ? refHolesOf(def.body) : null;
129
+
130
+ for ( const name of Object.keys(slots) ) {
131
+ const s = slots[name] || {};
132
+ if ( !name ) { err('unnamed-slot', 'a slot has an empty name'); continue; } // (a)
133
+ if ( s.infer ) err('inference-slot', `slot "${name}" is infer:true — a body is SUPPLIED, never solved-for (undecidable)`, name); // (c)
134
+ if ( !TYPED_KINDS.has(s.kind) ) // (b) K1
135
+ err('prose-slot', `slot "${name}" kind "${s.kind}" is not K1-typed — a prose/untyped key re-keys every run (memo death)`, name);
136
+ if ( s.kind === 'subgraph' ) {
137
+ if ( !isTypedName(s.in, opts) || !isTypedName(s.out, opts) ) // (b) typed interface
138
+ err('prose-interface', `sub-graph slot "${name}" has a non-typed in/out interface (in=${s.in}, out=${s.out})`, name);
139
+ if ( s.role === 'param' && !('frontier' in s) ) // (c) supplied
140
+ err('unbound-param', `param sub-graph slot "${name}" has no frontier — it must be supplied by binding, not inferred`, name);
141
+ if ( 'frontier' in s && !Array.isArray(s.frontier) ) // (d) fixed tentacles
142
+ err('variable-tentacles', `slot "${name}" frontier must be a FIXED array, got ${JSON.stringify(s.frontier)}`, name);
143
+ if ( Array.isArray(s.frontier) && used ) // (c2) decl ↔ impl
144
+ lintFrontierMatch(s.frontier, used, name, err);
145
+ }
146
+ if ( s.kind === 'list' && !isTypedName(s.elem, opts) ) // (b) typed elem
147
+ err('untyped-collection', `collection slot "${name}" has no typed elem type (got ${JSON.stringify(s.elem)})`, name);
148
+ }
149
+
150
+ const c = def && def.contract; // (e) frame
151
+ if ( !c ) {
152
+ warnings.push({ method, kind: 'uncontracted', message: `method "${method}" declares no contract — composition falls back to a runtime micro-LLM (cost gradient, §0.1)` });
153
+ } else {
154
+ const read = new Set(c.read || []), write = new Set(c.write || []);
155
+ for ( const k of frameKeys(c.post) ) if ( !write.has(k) )
156
+ err('frame-violation', `postcondition key "${k}" is outside the declared write-footprint {${[...write].join(', ')}}`);
157
+ for ( const k of frameKeys(c.pre) ) if ( !read.has(k) )
158
+ err('frame-violation', `precondition key "${k}" is outside the declared read-footprint {${[...read].join(', ')}}`);
159
+ }
160
+ return { errors, warnings };
161
+ }
162
+
163
+ function lintFrontierMatch( frontier, used, name, err ) {
164
+ const declared = new Set(frontier);
165
+ for ( const d of declared ) if ( !used.has(d) )
166
+ err('frontier-mismatch', `slot "${name}" declares frontier ref "${d}" the body never uses`, name);
167
+ for ( const u of used ) if ( !declared.has(u) )
168
+ err('frontier-mismatch', `the body uses ref "${u}" not declared in slot "${name}" frontier`, name);
169
+ }
170
+
171
+ // ------------------------------------------------------------------ case-parameterized SELECTION (Brick 3)
172
+
173
+ /**
174
+ * selectCluster — generate a concept-tree fragment of MUTUALLY-EXCLUSIVE, typed-gated SELECTORS that pick
175
+ * WHICH method applies to a problem from the CASE's structural (typed) facts, step by step (design doc §4A;
176
+ * "la bonne boucle est définie par les conditions initiales"). Selection lives in the CONCEPT layer (gated
177
+ * on the case facts already on the segment via the param/case role cut), NOT in a blind getPaths enumeration.
178
+ *
179
+ * Mutual exclusion is the confluence guarantee: at most one selector casts per segment. A `fallback` selector
180
+ * fires iff NO typed rule matched — that is the cost-gradient hand-off to a runtime micro-LLM (§0.1), not a
181
+ * hard failure. Each rule's provider must set the rule's `_name` cast marker (finding #33).
182
+ *
183
+ * @param rules [{ name, when:"<assert expr over the case's typed facts>", provider:"Ns::fn" }]
184
+ * @param opts { trigger:'toSelect', on:[factKeys that must be PRESENT to consider selection],
185
+ * fallback?:name, fallbackProvider?:"Ns::fn" }
186
+ * @returns a `childConcepts` map (drop into a concept tree under `common`).
187
+ */
188
+ function selectCluster( rules, opts ) {
189
+ opts = opts || {};
190
+ const require = ['Segment', opts.trigger || 'toSelect'].concat(opts.on || []);
191
+ const childConcepts = {}, conds = [];
192
+ for ( const r of (rules || []) ) {
193
+ childConcepts[r.name] = { _id: r.name, _name: r.name, require: require.slice(), assert: [r.when], provider: [r.provider] };
194
+ conds.push('(' + r.when + ')');
195
+ }
196
+ if ( opts.fallback ) // fires iff NO typed rule matched → micro-LLM
197
+ childConcepts[opts.fallback] = { _id: opts.fallback, _name: opts.fallback, require: require.slice(),
198
+ assert: [conds.length ? '!(' + conds.join(' || ') + ')' : 'true'], provider: [opts.fallbackProvider] };
199
+ return childConcepts;
200
+ }
201
+
202
+ module.exports = { applySubgraphArg, mapSubgraph, mapTemplate, bindSubgraphArg, lintMethod, selectCluster };