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,397 @@
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
+ * A studio Session wraps ONE live Graph and exposes the ops the web layer drives,
21
+ * emitting events as the graph develops. No web dependency — this is the unit the
22
+ * studio tests cover. Construct with the engine facade and an optional model `ask`:
23
+ *
24
+ * const Session = require('./session.js');
25
+ * const s = new Session('root', { Graph: require('../index.js'), ask });
26
+ * s.on('conceptApply', rec => …); s.on('state', st => …);
27
+ * s.loadCorpus({ conceptsDir, builtins: true, seed });
28
+ *
29
+ * Events: 'conceptApply'(rec) · 'stabilize'(state) · 'state'(state) · 'mutation'({targetId}) · 'rollback'({rev}).
30
+ */
31
+ const EventEmitter = require('events');
32
+ const { validateConceptTree, validateMergeProjection, eachConcept } = require('../authoring/validate.js');
33
+ const { conceptFactGraph } = require('../authoring/grammar-graph.js');
34
+ const { deriveManifest, packCorpus } = require('../authoring/corpus-pack.js');
35
+ const { mergeRingProposals, retractRingAlias, creditRingAlias } = require('../authoring/registry.js');
36
+ const { packLattice, loadLattice, ringsOf } = require('../authoring/lattice-pack.js');
37
+
38
+ class Session extends EventEmitter {
39
+ constructor( id, { Graph, ask, logger } = {} ) {
40
+ super();
41
+ this.id = id;
42
+ this.Graph = Graph;
43
+ this.ask = ask;
44
+ this.logger = logger;// shared logger so a session's graph logs surface in the host (e.g. sg studio)
45
+ this.graph = null;
46
+ this._conceptMap = null;
47
+ // the session's typed LATTICE registry (vocab enums + synonym rings) — the LearningPanel's object.
48
+ // Aliases enter ONLY through the admission gate (mergeRingProposals / loadLattice — confluence-checked,
49
+ // provenance-tagged); retraction is the recoverability guarantee. Declaring a vocab KEY is authoring
50
+ // (host-declared enums), not admission — the gate concerns ALIASES.
51
+ this._registry = { version: 'v1', keys: {} };
52
+ }
53
+
54
+ /**
55
+ * (Re)build the live graph from a corpus (dir or in-memory map) + providers + seed.
56
+ * @returns {{objects, currentRev, revCount}} the initial state
57
+ */
58
+ loadCorpus( opts = {} ) {
59
+ this._destroy();
60
+ this._conceptMap = opts.conceptMap
61
+ || (opts.conceptsDir ? this.Graph.loadConceptMap(opts.conceptsDir) : {});
62
+ const conf = {
63
+ autoMount : true,
64
+ logger : this.logger,
65
+ onConceptApply: ( rec ) => this.emit('conceptApply', rec),
66
+ onStabilize : () => this._onSettle()
67
+ };
68
+ if ( opts.sets ) conf.conceptSets = opts.sets;
69
+ this.graph = this.Graph.fromDirs({
70
+ conceptMap : this._conceptMap,
71
+ providers : opts.providersDir,
72
+ builtins : opts.builtins,
73
+ seed : opts.seed,
74
+ providerCtx: { ask: this.ask, env: process.env },
75
+ conf
76
+ });
77
+ return this.state();
78
+ }
79
+
80
+ _onSettle() {
81
+ const st = this.state();
82
+ // retraction detection (zero-core): a concept-flag present on an object last settle but
83
+ // gone now was retracted (JTMS defeasance / cascade). Emit it so the UI can flash it.
84
+ const cur = this._flagsByObj(st.objects);
85
+ if ( this._lastFlags )
86
+ for ( const [id, prev] of this._lastFlags ) {
87
+ const now = cur.get(id) || new Set();
88
+ const gone = [...prev].filter(( c ) => !now.has(c));
89
+ if ( gone.length ) this.emit('retract', { targetId: id, concepts: gone, atRev: st.currentRev });
90
+ }
91
+ this._lastFlags = cur;
92
+ this.emit('stabilize', st);
93
+ this.emit('state', st);
94
+ }
95
+
96
+ // the set of concept NAMES in the active map (cached; rebuilt when the map ref changes).
97
+ _conceptNames() {
98
+ if ( this.__cnFor !== this._conceptMap ) {
99
+ this.__cnFor = this._conceptMap;
100
+ const set = new Set();
101
+ for ( const s of Object.keys(this._conceptMap || {}) )
102
+ eachConcept(this._conceptMap[s], ( c ) => { if ( c._name ) set.add(c._name); });
103
+ this.__cnSet = set;
104
+ }
105
+ return this.__cnSet;
106
+ }
107
+ // per-object set of currently-cast concept flags (a truthy key whose name is a concept).
108
+ _flagsByObj( objects ) {
109
+ const names = this._conceptNames(), m = new Map();
110
+ for ( const o of objects ) {
111
+ const s = new Set();
112
+ for ( const k of Object.keys(o) ) if ( names.has(k) && o[k] ) s.add(k);
113
+ m.set(o._id, s);
114
+ }
115
+ return m;
116
+ }
117
+
118
+ /** Current serialized state for the canvas. */
119
+ state() {
120
+ if ( !this.graph ) return { objects: [], currentRev: 0, revCount: 0, revs: [] };
121
+ const ser = JSON.parse(this.graph.serialize().graph);
122
+ const revs = this.graph.getRevisions(); // the checkpointed (snapshotted) revs only
123
+ return {
124
+ objects : ser.conceptMaps || [],
125
+ currentRev: ser.lastRev || 0,
126
+ revCount : revs.length,
127
+ revs
128
+ };
129
+ }
130
+
131
+ /** Apply a mutation template; stabilization + a 'state' event follow on settle. */
132
+ mutate( template, targetId ) {
133
+ this.graph.pushMutation(template, targetId);
134
+ this.emit('mutation', { targetId });
135
+ return { ok: true };
136
+ }
137
+
138
+ /** Force a stabilization pass (e.g. after a no-op edit). */
139
+ run() {
140
+ this.graph.stabilize();
141
+ return { ok: true };
142
+ }
143
+
144
+ /** The active conceptMap (set tree) for the concept panel. */
145
+ conceptTree() {
146
+ return this._conceptMap || {};
147
+ }
148
+
149
+ /**
150
+ * The tree-decomposition TILING of the active concept corpus (the TilingOverlay): the
151
+ * derived separator interface + the tiles/forks + their frontier alphabets + the
152
+ * treewidth cost bound. Pure derivation off the concept-dependency graph (no engine run).
153
+ */
154
+ forkPlan() {
155
+ if ( !this._conceptMap ) return null;
156
+ const { treeDecomposition, forkPlan } = require('../authoring/decompose.js');
157
+ const root = { childConcepts: {} }; // merge the active sets into one root tree
158
+ for ( const set of Object.keys(this._conceptMap) ) {
159
+ const t = this._conceptMap[set];
160
+ if ( t && t.childConcepts ) Object.assign(root.childConcepts, t.childConcepts);
161
+ }
162
+ const decomp = treeDecomposition(root), plan = forkPlan(root);
163
+ return {
164
+ separators: decomp.separators, treewidth: decomp.treewidth,
165
+ nTiles: decomp.nTiles, partitionPays: decomp.partitionPays, forks: plan.forks
166
+ };
167
+ }
168
+
169
+ /**
170
+ * The concept↔fact GRAMMAR graph of the active corpus (the GrammarGraph view): per-concept
171
+ * produced/consumed facts WITH polarity, cross-corpus links, silent writer-collisions, the
172
+ * external entry points, and the forkPlan tiling overlay. Pure derivation (no engine run).
173
+ */
174
+ grammarGraph() {
175
+ if ( !this._conceptMap ) return { concepts: [], facts: [], edges: [], crossCorpus: [], collisions: [], entryPoints: [], tiling: null };
176
+ return conceptFactGraph(this._conceptMap);
177
+ }
178
+
179
+ /** The derived corpus manifest (produces/consumes alphabet, required providers, links). */
180
+ corpusManifest( meta ) {
181
+ return deriveManifest(this._conceptMap || {}, meta || {});
182
+ }
183
+
184
+ /**
185
+ * Pack the corpus into a portable `.sgc` bundle. Exports the LIVE concept tree (so runtime
186
+ * edits — add/patchConcept — are captured) when a graph is loaded, else the on-disk map.
187
+ */
188
+ exportCorpus( meta ) {
189
+ meta = meta || {};
190
+ const sets = Object.keys(this._conceptMap || {});
191
+ const name = meta.name || sets.join('+') || 'corpus';
192
+ let map;
193
+ if ( this.graph && this.graph.exportConcepts )
194
+ map = { [sets.length === 1 ? sets[0] : name]: this.graph.exportConcepts() };
195
+ else
196
+ map = this._conceptMap || {};
197
+ return packCorpus(map, { ...meta, name });
198
+ }
199
+
200
+ /** Apply-correlated provider/log records (the ProviderTrace view). With no graph loaded, falls back to
201
+ * the SHARED host logger — so an embedding host (`sg serve --studio`) surfaces its live request lines
202
+ * in the trace panel with zero extra wiring. @returns {Array} */
203
+ providerTrace( n, filter ) {
204
+ const log = (this.graph && this.graph.logger) || this.logger;
205
+ return log ? log.tail(n || 50, filter || {}) : [];
206
+ }
207
+
208
+ // --- the typed LATTICE registry (the LearningPanel ops — track 4) ---
209
+ /** The registry + its admitted rings, flattened for the panel. */
210
+ registry() { return { registry: this._registry, rings: ringsOf(this._registry) }; }
211
+ /** Declare/extend a vocab KEY (host-declared enum members — authoring, not admission). */
212
+ declareKey( args ) {
213
+ args = args || {};
214
+ const key = String(args.key || '').trim();
215
+ const members = (Array.isArray(args.enum) ? args.enum : String(args.enum || '').split(','))
216
+ .map(( m ) => String(m).trim()).filter(Boolean);
217
+ if ( !key || !members.length ) throw new Error('declareKey needs { key, enum: [members…] }');
218
+ const cur = this._registry.keys[key] || {};
219
+ const merged = [...new Set([...(cur.enum || []), ...members])];
220
+ this._registry = { ...this._registry, keys: { ...this._registry.keys, [key]: { ...cur, enum: merged } } };
221
+ return this.registry();
222
+ }
223
+ /** Propose an ALIAS — admitted iff member ∈ enum ∧ the ring stays confluent (THE gate, mergeRingProposals);
224
+ * a rejection carries its reason. @returns {{admitted, rejected, rings}} */
225
+ proposeAlias( args ) {
226
+ args = args || {};
227
+ const r = mergeRingProposals(this._registry, [{ key: args.key, member: args.member, alias: args.alias, via: args.via || 'studio' }]);
228
+ this._registry = r.registry;
229
+ return { admitted: r.admitted, rejected: r.rejected, rings: ringsOf(this._registry) };
230
+ }
231
+ /** Retract an alias — the un-learn verb (recoverability): removal de-locks the corrected proposal. */
232
+ retractAlias( args ) {
233
+ args = args || {};
234
+ const r = retractRingAlias(this._registry, args.key, args.alias);
235
+ this._registry = r.registry;
236
+ return { retracted: r.retracted, member: r.member, rings: ringsOf(this._registry) };
237
+ }
238
+ /** Credit an alias on a verified reuse (the support half of the defeasible envelope). */
239
+ creditAlias( args ) {
240
+ args = args || {};
241
+ const r = creditRingAlias(this._registry, args.key, args.alias);
242
+ this._registry = r.registry;
243
+ return { member: r.member, support: r.support };
244
+ }
245
+ /** Ship the registry as a `.sgc kind:'lattice'` bundle. */
246
+ exportLattice( meta ) { return packLattice(this._registry, meta || {}); }
247
+ /** Grow the registry from a shipped bundle THROUGH the gate (version-gated; conflicting rings rejected).
248
+ * An EMPTY session registry adopts the packaged canon wholesale (loadLattice's no-host path); a grown one
249
+ * merges ring-by-ring through mergeRingProposals. */
250
+ importLattice( args ) {
251
+ args = args || {};
252
+ const host = Object.keys(this._registry.keys || {}).length ? this._registry : null;
253
+ const r = loadLattice(args.bundle, host, args.opts || {});
254
+ if ( r.registry ) this._registry = r.registry;
255
+ return { adopted: r.adopted, merged: r.merged, admitted: r.admitted, rejected: r.rejected,
256
+ loadSafe: r.loadSafe, rings: ringsOf(this._registry) };
257
+ }
258
+
259
+ /**
260
+ * Preview a fork/merge projection against a frontier alphabet — what crosses the boundary and
261
+ * which keys LEAK (not in the declared alphabet). Pure check (validateMergeProjection); does
262
+ * not perform the merge. @returns {{errors, warnings}}
263
+ */
264
+ mergePreview( template, opts ) {
265
+ opts = opts || {};
266
+ return validateMergeProjection(template, { frontierAlphabet: opts.frontierAlphabet, flagContinuous: opts.flagContinuous, strict: opts.strict });
267
+ }
268
+
269
+ /** A concept's schema (the JSON the editor edits), by name or id. */
270
+ getConcept( nameOrId ) {
271
+ const c = this.graph && this.graph.getConceptByName(nameOrId);
272
+ return c ? (c._schema || c) : null;
273
+ }
274
+
275
+ // --- history (the "git for reasoning" inspection) ---
276
+ revisions() { return this.graph ? this.graph.getRevisions() : []; }
277
+ snapshot( rev ) {
278
+ const snap = this.graph && this.graph.getSnapshot(rev);
279
+ return snap ? { objects: JSON.parse(snap.graph).conceptMaps || [] } : null;
280
+ }
281
+ /** Roll data + rules back to a revision; a 'rollback' then 'state' event follow on settle. */
282
+ rollback( rev ) {
283
+ this.graph.rollbackTo(rev);
284
+ this.emit('rollback', { rev });
285
+ return { ok: true };
286
+ }
287
+ diff( a, b ) { return this.graph.diffRevisions(a, b); }
288
+
289
+ // --- live concept authoring (validate before applying) ---
290
+ /** Author-time validation of a candidate concept schema. @returns {{ok, errors, warnings}} */
291
+ validateConcept( schema ) {
292
+ const key = (schema && schema._id) || 'candidate';
293
+ const { errors, warnings } = validateConceptTree({ childConcepts: { [key]: schema } }, {});
294
+ return { ok: errors.length === 0, errors, warnings };
295
+ }
296
+ /** Hot-patch a concept; the re-eval cascade + a 'state' event follow on settle. */
297
+ patchConcept( nameOrId, updates ) { this.graph.patchConcept(nameOrId, updates); return { ok: true }; }
298
+ /** Author a new concept under a parent; opens it on live objects + re-sweeps. */
299
+ addConcept( parentNameOrId, schema ) { this.graph.addConcept(parentNameOrId, schema); return { ok: true }; }
300
+ /** Remove a concept (and its subtree); un-casts it everywhere, then re-stabilizes (CRUD completion). */
301
+ deleteConcept( nameOrId ) { this.graph.deleteConcept(nameOrId); return { ok: true }; }
302
+
303
+ /**
304
+ * Run the decompose -> synthesize answer-loop for a prompt, IN this session's
305
+ * graph (so the decomposition tree builds on the canvas). Streams 'promptProgress'
306
+ * and ends with 'promptAnswer'. Needs an LLM `ask` backend (LLM_BASE).
307
+ * Reuses lib/authoring/loop.js (same wiring as examples/run-prompt.js).
308
+ */
309
+ prompt( text, opts = {} ) {
310
+ if ( !this.ask ) throw new Error('prompt needs an LLM backend — start `sg studio` with LLM_BASE set');
311
+ const { loopConceptTree, makeDecomposeProviders, synthesize } = require('../authoring/loop.js');
312
+ const { parseJSON } = require('../providers/llm.js');
313
+ const ask = this.ask;
314
+ const emit = ( m ) => this.emit('promptProgress', m);
315
+ const ctx = ( scope ) => {
316
+ const p = scope._.ctxPath || [];
317
+ return `Objectif global: ${text}\nChemin: ${p.length ? p.join(' > ') : '(racine)'}\n`
318
+ + `Étape courante: ${scope._.label || 'RÉSOUDRE LE PROBLÈME'}\n`
319
+ + (scope._.description ? `Détail: ${scope._.description}\n` : '') + `Profondeur: ${scope._.depth || 0}`;
320
+ };
321
+ const J = async ( system, user ) => parseJSON(await ask({ system, user: user + '\n\nRéponds UNIQUEMENT le JSON.', maxTokens: 1500 }));
322
+
323
+ this.Graph._providers = makeDecomposeProviders({
324
+ maxDepth: opts.maxDepth == null ? 2 : opts.maxDepth,
325
+ evalFn: async ( scope ) => {
326
+ const r = await J('Tu juges si une étape de plan est ATOMIQUE (directement répondable) ou doit être DÉCOUPÉE. JSON: {"atomic":true|false,"reason":"court"}', ctx(scope)).catch(() => ({ atomic: true }));
327
+ emit({ kind: 'eval', depth: scope._.depth || 0, label: scope._.label, atomic: !!r.atomic });
328
+ return r;
329
+ },
330
+ expandFn: async ( scope ) => {
331
+ const r = await J('Tu découpes une étape en 2 à 3 sous-étapes ORDONNÉES et concrètes. JSON: {"steps":[{"name":"court","description":"..."}]}', ctx(scope)).catch(() => ({ steps: [] }));
332
+ const cp = [...(scope._.ctxPath || []), scope._.label];
333
+ emit({ kind: 'expand', depth: scope._.depth || 0, label: scope._.label, into: (r.steps || []).map(( s ) => s.name) });
334
+ return (r.steps || []).map(( s ) => ({ name: s.name, description: s.description, ctxPath: cp }));
335
+ },
336
+ answerFn: async ( scope ) => {
337
+ const t = await ask({ system: 'Réponds à cette étape atomique de façon concise et concrète (3-5 phrases max).', user: ctx(scope), maxTokens: 600 });
338
+ emit({ kind: 'answer', depth: scope._.depth || 0, label: scope._.label });
339
+ return String(t).trim();
340
+ }
341
+ });
342
+ const rollupFn = async ( seg, kids ) => {
343
+ const t = await ask({ system: 'Tu synthétises les réponses des sous-étapes en UNE réponse cohérente et BORNÉE (max ~6 phrases). Ne recopie pas, résume.', user: `Étape: ${seg.label}\n\nRéponses des sous-étapes:\n` + kids.map(( a, i ) => `${i + 1}. ${a}`).join('\n'), maxTokens: 700 });
344
+ emit({ kind: 'rollup', label: seg.label, children: kids.length });
345
+ return String(t).trim();
346
+ };
347
+
348
+ this._destroy();
349
+ this._conceptMap = { common: loopConceptTree };
350
+ let answered = false;
351
+ const seed = {
352
+ lastRev : 0,
353
+ nodes : [{ _id: 'start', label: 'état initial' }, { _id: 'goal', label: 'objectif' }],
354
+ segments: [{ _id: 'root', originNode: 'start', targetNode: 'goal', depth: 0, label: String(text).slice(0, 60), ctxPath: [] }]
355
+ };
356
+ this.graph = new this.Graph(seed, {
357
+ autoMount: true, conceptSets: ['common'], bagRefManagers: {}, logger: this.logger,
358
+ onConceptApply: ( rec ) => this.emit('conceptApply', rec),
359
+ onStabilize : async () => {
360
+ this._onSettle();
361
+ if ( answered ) return;
362
+ answered = true;
363
+ try { this.emit('promptAnswer', { answer: await synthesize(this.graph, 'root', rollupFn) }); }
364
+ catch ( e ) { this.emit('promptAnswer', { answer: '(synthesis failed: ' + e.message + ')' }); }
365
+ }
366
+ }, this._conceptMap);
367
+ return { ok: true };
368
+ }
369
+
370
+ /**
371
+ * REQUEST/RESPONSE bridge (Controller-P0): run the decompose→synthesize answer loop for `text`
372
+ * and RESOLVE with the answer — the event-based `prompt` lifted into a promise, so a controller
373
+ * (a CLI, an MCP tool, a small-LLM driver) can `await session.ask(q)`. Progress is still emitted
374
+ * as `promptProgress` events (subscribe before awaiting to stream). Rejects on a missing LLM
375
+ * backend or a timeout. ZERO-CORE — pure composition over the existing `prompt` + settle.
376
+ * (Named `answer`, not `ask`: `this.ask` is the injected MODEL backend — an instance field that
377
+ * would shadow a prototype `ask()`.)
378
+ * @param opts.maxDepth decomposition depth (default 2). opts.timeout ms (default 120000).
379
+ * @returns Promise<{ answer, state }>
380
+ */
381
+ answer( text, opts = {} ) {
382
+ return new Promise(( resolve, reject ) => {
383
+ const to = opts.timeout || 120000;
384
+ const timer = setTimeout(() => reject(new Error('Session.ask timed out after ' + to + 'ms')), to);
385
+ this.once('promptAnswer', ( { answer } ) => { clearTimeout(timer); resolve({ answer, state: this.state() }); });
386
+ try { this.prompt(text, opts); } catch ( e ) { clearTimeout(timer); reject(e); }
387
+ });
388
+ }
389
+
390
+ _destroy() {
391
+ if ( this.graph && this.graph.destroy ) this.graph.destroy();
392
+ this.graph = null;
393
+ this._lastFlags = null;// fresh retraction baseline for the next corpus
394
+ }
395
+ }
396
+
397
+ module.exports = Session;
@@ -0,0 +1,149 @@
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
+ * Studio — a registry of Sessions (the root + a tree of forks) plus corpus
21
+ * discovery. Routes ops to a session and relays every session's events as a
22
+ * single tagged `'event'` ({ type, sessionId, payload }) the server forwards to
23
+ * the browser. Owns the optional model `ask` backend for the prompt loop.
24
+ */
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+ const EventEmitter = require('events');
28
+ const Session = require('./session.js');
29
+
30
+ // session-level events relayed to subscribers (tagged with sessionId)
31
+ const RELAYED = ['conceptApply', 'stabilize', 'state', 'mutation', 'rollback', 'retract', 'promptProgress', 'promptAnswer'];
32
+ const { unpackCorpus } = require('../authoring/corpus-pack.js');
33
+
34
+ class Studio extends EventEmitter {
35
+ constructor( { Graph, ask, root, logger } = {} ) {
36
+ super();
37
+ this.Graph = Graph;
38
+ this.ask = ask;
39
+ this.logger = logger;// shared across all sessions/forks so every graph's logs surface
40
+ this.root = root || process.cwd();
41
+ this.sessions = new Map();
42
+ this._counter = 0;
43
+ this._activeId = 'root';// which session the status bar reflects (root, a fork, or a selected one)
44
+ this._newSession('root', null);
45
+ }
46
+
47
+ /** Set the session the host status bar should reflect (no-op if unknown). */
48
+ setActive( id ) { if ( this.sessions.has(id) ) this._activeId = id; return this._activeId; }
49
+ /** The live graph of the active session (for the host's status bar); null if none loaded. */
50
+ activeGraph() {
51
+ var s = this.sessions.get(this._activeId) || this.sessions.get('root');
52
+ return s && s.graph || null;
53
+ }
54
+
55
+ _newSession( id, parentId ) {
56
+ const s = new Session(id, { Graph: this.Graph, ask: this.ask, logger: this.logger });
57
+ s._parentId = parentId;
58
+ this.sessions.set(id, s);
59
+ RELAYED.forEach(( type ) => s.on(type, ( payload ) => this.emit('event', { type, sessionId: id, payload })));
60
+ return s;
61
+ }
62
+
63
+ getSession( id ) { return this.sessions.get(id); }
64
+
65
+ /** Load a corpus into the root session (the default active session). */
66
+ loadCorpus( opts ) { this.setActive('root'); return this.getSession('root').loadCorpus(opts); }
67
+
68
+ /**
69
+ * Import a portable `.sgc` bundle into the root session. Validates on import: hard errors block
70
+ * the load (unless `opts.force`) and are returned; warnings are surfaced but non-blocking.
71
+ * @returns {{ok, manifest, validation, errors?, warnings?}}
72
+ */
73
+ importCorpus( bundle, opts = {} ) {
74
+ let conceptMap, manifest, validation;
75
+ try { ({ conceptMap, manifest, validation } = unpackCorpus(bundle, { validate: true })); }
76
+ catch ( e ) { return { ok: false, errors: [{ kind: 'bundle', message: e.message }] }; }
77
+ const errors = (validation || []).flatMap(( v ) => v.errors);
78
+ const warnings = (validation || []).flatMap(( v ) => v.warnings);
79
+ if ( errors.length && !opts.force ) return { ok: false, manifest, validation, errors, warnings };
80
+ this.setActive('root');
81
+ this.getSession('root').loadCorpus({
82
+ conceptMap, sets: Object.keys(conceptMap),
83
+ builtins: opts.builtins !== false, seed: opts.seed !== undefined ? opts.seed : bundle.seed
84
+ });
85
+ return { ok: true, manifest, validation, warnings };
86
+ }
87
+
88
+ /** Discover corpus dirs under `root`: an immediate sub-dir that holds *.json (a set),
89
+ * or that holds set sub-dirs with *.json. */
90
+ listCorpora() {
91
+ let entries;
92
+ try { entries = fs.readdirSync(this.root, { withFileTypes: true }); } catch ( e ) { return []; }
93
+ const out = [];
94
+ for ( const e of entries ) {
95
+ if ( !e.isDirectory() || e.name === 'node_modules' || e.name.startsWith('.') ) continue;
96
+ const dir = path.join(this.root, e.name);
97
+ if ( this._hasConcepts(dir) ) out.push({ name: e.name, dir });
98
+ }
99
+ return out;
100
+ }
101
+ _hasConcepts( dir ) {
102
+ let files;
103
+ try { files = fs.readdirSync(dir, { withFileTypes: true }); } catch ( e ) { return false; }
104
+ if ( files.some(f => f.isFile() && f.name.endsWith('.json')) ) return true;
105
+ return files.some(( f ) => {
106
+ if ( !f.isDirectory() ) return false;
107
+ try { return fs.readdirSync(path.join(dir, f.name)).some(n => n.endsWith('.json')); }
108
+ catch ( e ) { return false; }
109
+ });
110
+ }
111
+
112
+ /** Fork a session into an independent child sub-graph (a sub-agent). */
113
+ fork( parentId, opts = {} ) {
114
+ const parent = this.sessions.get(parentId || 'root');
115
+ if ( !parent || !parent.graph ) throw new Error('fork: parent session not loaded');
116
+ const childId = 'fork-' + (++this._counter);
117
+ const child = this._newSession(childId, parent.id);
118
+ const conf = {
119
+ logger : this.logger,
120
+ onConceptApply: ( rec ) => child.emit('conceptApply', rec),
121
+ onStabilize : () => child._onSettle()
122
+ };
123
+ child.graph = parent.graph.fork(opts.seed, { ...(opts.conf || {}), ...conf });
124
+ child._conceptMap = parent._conceptMap;
125
+ this.setActive(childId);// the new sub-agent becomes the focused session
126
+ this.emit('event', { type: 'forks', sessionId: 'root', payload: this.forkTree() });
127
+ return { childId };
128
+ }
129
+
130
+ /** Reintegrate a fork's result into its parent, then drop the child. */
131
+ merge( childId, targetId, project ) {
132
+ const child = this.sessions.get(childId);
133
+ if ( !child ) throw new Error('merge: no such fork ' + childId);
134
+ const parent = this.sessions.get(child._parentId || 'root');
135
+ parent.graph.merge(child.graph, targetId, project);
136
+ this.sessions.delete(childId);
137
+ this.emit('event', { type: 'forks', sessionId: 'root', payload: this.forkTree() });
138
+ return { ok: true };
139
+ }
140
+
141
+ /** Flat session tree (id -> {id, parent}) for the fork panel. */
142
+ forkTree() {
143
+ const tree = {};
144
+ for ( const [id, s] of this.sessions ) tree[id] = { id, parent: s._parentId || null };
145
+ return tree;
146
+ }
147
+ }
148
+
149
+ module.exports = Studio;
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "skynet-graph",
3
+ "version": "1.2.0",
4
+ "license": "AGPL-3.0-or-later",
5
+ "main": "./lib/index.js",
6
+ "author": "Nathanael Braun <pp9ping@gmail.com>",
7
+ "bin": {
8
+ "sg": "bin/sg"
9
+ },
10
+ "files": [
11
+ "lib",
12
+ "bin",
13
+ "concepts",
14
+ "doc/*.md",
15
+ "README.md",
16
+ "CONTRIBUTING.md",
17
+ "LICENSE"
18
+ ],
19
+ "scripts": {
20
+ "test": "node --test --test-force-exit tests/unit/*.test.js tests/integration/*.test.js",
21
+ "test:watch": "node --test --watch tests/unit/*.test.js",
22
+ "test:studio": "node scripts/studio-smoke.mjs",
23
+ "local-inference:setup": "bash scripts/setup-local-inference.sh"
24
+ },
25
+ "dependencies": {
26
+ "@jsep-plugin/object": "^1.2.2",
27
+ "@jsep-plugin/ternary": "^1.1.4",
28
+ "array-differ": "^2.1.0",
29
+ "deepmerge": "^3.2.0",
30
+ "intersect": "^1.0.1",
31
+ "is": "^3.2.1",
32
+ "jsep": "^1.4.0",
33
+ "json5": "^2.2.3",
34
+ "shortid": "^2.2.16",
35
+ "ws": "^8.21.0"
36
+ },
37
+ "devDependencies": {
38
+ "esbuild": "^0.28.1",
39
+ "openai": "^6.45.0",
40
+ "puppeteer": "^25.1.0"
41
+ }
42
+ }