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,66 @@
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
+ * `sg proxy` session orchestration — the local-first proxy cache run loop, EXTRACTED from cli.js so it is
10
+ * testable with an injected stub proxy (the model resolution stays GPU-bound in cli.js; this pure loop is
11
+ * where the single-shot / batch / metrics logic lives). Answers a list of queries against a `createProxyCache`
12
+ * instance, tracks per-answer provenance, and reports the ECONOMY (frontier calls saved vs a no-cache baseline).
13
+ */
14
+
15
+ /**
16
+ * Run a proxy over a list of queries, in order (so repeats/paraphrases hit the warmed stock).
17
+ * @param opts.proxy a createProxyCache instance ({ answer, metrics }).
18
+ * @param opts.queries string[] — the session's queries (one entry = one ask).
19
+ * @param opts.onAnswer optional (result, index, query) => void — a per-answer hook (CLI streams here).
20
+ * @returns { results:[{ query, answer, source, cached, cost }], metrics, saved } — `saved` = frontier calls
21
+ * avoided vs answering every query at the frontier (= local hits).
22
+ */
23
+ async function runProxySession( opts ) {
24
+ opts = opts || {};
25
+ var proxy = opts.proxy;
26
+ var queries = opts.queries || [];
27
+ if ( !proxy || typeof proxy.answer !== 'function' ) throw new Error('runProxySession needs opts.proxy (a createProxyCache instance)');
28
+ var results = [];
29
+ for ( var i = 0; i < queries.length; i++ ) {
30
+ var q = queries[i];
31
+ var r = await proxy.answer(q);
32
+ var row = { query: q, answer: r.answer, source: r.source, cached: !!r.cached, cost: r.cost };
33
+ results.push(row);
34
+ if ( typeof opts.onAnswer === 'function' ) opts.onAnswer(row, i, q);
35
+ }
36
+ var metrics = (typeof proxy.metrics === 'function') ? proxy.metrics() : null;
37
+ var saved = metrics ? metrics.local : results.filter(function ( r ) { return r.source === 'local'; }).length;
38
+ return { results: results, metrics: metrics, saved: saved };
39
+ }
40
+
41
+ /**
42
+ * A one-block human report of a proxy session's economy (→ stderr; the answers go to stdout).
43
+ * @param metrics the proxy.metrics() readout.
44
+ * @param saved frontier calls avoided (= local hits).
45
+ */
46
+ function formatProxyReport( metrics, saved ) {
47
+ if ( !metrics ) return 'proxy: (no metrics)';
48
+ var pct = Math.round((metrics.coverage || 0) * 100);
49
+ var lines = [
50
+ '── proxy economy ──────────────────────────',
51
+ ' served : ' + metrics.served + ' queries',
52
+ ' local (cache) : ' + metrics.local + ' frontier: ' + metrics.frontier,
53
+ ' coverage : ' + pct + '% (frontier calls saved: ' + saved + ')'
54
+ ];
55
+ if ( metrics.stock && metrics.stock.size != null ) {
56
+ var s = metrics.stock;
57
+ var stockLine = ' stock : ' + s.size + ' entries';
58
+ if ( s.reuseRate != null ) stockLine += ' reuse: ' + Math.round(s.reuseRate * 100) + '% deadWeight: ' + (s.deadWeight != null ? s.deadWeight : '?');
59
+ if ( s.evicted != null && s.evicted.count ) stockLine += ' evicted: ' + s.evicted.count;
60
+ lines.push(stockLine);
61
+ }
62
+ lines.push('────────────────────────────────────────────');
63
+ return lines.join('\n');
64
+ }
65
+
66
+ module.exports = { runProxySession: runProxySession, formatProxyReport: formatProxyReport };
@@ -0,0 +1,185 @@
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
+ * `sg serve` — a MINIMAL OpenAI-compatible proxy DEMO over any chat backend. Point an OpenAI client's
10
+ * `baseURL` at it (official SDKs, LangChain, Open WebUI, curl — zero integration code): a repeated query
11
+ * is answered from an exact-match SESSION cache at 0 backend calls, and every response says where it came
12
+ * from (`x-sg-served-from: cache|backend`, `x-sg-saved`).
13
+ *
14
+ * WHAT IT IS / IS NOT: this is the demo of the GESTURE — an in-memory, per-process cache keyed on the
15
+ * exact query text, gone on restart. It is NOT a knowledge store: no verification, no typed coverage, no
16
+ * persistence, no freshness. The professional appliance replaces the session cache with maintained,
17
+ * verified knowledge served under the same wire contract.
18
+ *
19
+ * THE v1 WIRE CONTRACT (same shape as the appliance):
20
+ * • POST /v1/chat/completions — the QUERY is the LAST `user` turn (string content or text parts).
21
+ * • `stream:true` is SIMULATED: the answer is computed in one block, then emitted as standard SSE
22
+ * chunks (role delta → content delta → finish → [DONE]) so streaming clients work unchanged.
23
+ * • GET /v1/models — one model row, so SDK bootstraps and model pickers work.
24
+ * • Token counts are NOT estimated: 0 means "not counted", never a made-up number.
25
+ *
26
+ * THIN split (doctrine): `createServeDemoHandler` is a PURE request handler (stub-testable, no socket),
27
+ * `startServeDemoServer` is the zero-dep node:http wrapper; backend resolution stays in cli.js.
28
+ */
29
+
30
+ /** The text of an OpenAI message `content` (a string, or an array of {type:'text',text} parts). */
31
+ function textOf( content ) {
32
+ if ( typeof content === 'string' ) return content;
33
+ if ( Array.isArray(content) )
34
+ return content.filter(function ( p ) { return p && (p.type === 'text' || typeof p.text === 'string'); })
35
+ .map(function ( p ) { return p.text; }).join('\n');
36
+ return '';
37
+ }
38
+
39
+ /** The demo QUERY of an OpenAI `messages` array = the LAST user turn's text, or null (no user turn). */
40
+ function queryOfMessages( messages ) {
41
+ if ( !Array.isArray(messages) ) return null;
42
+ for ( var i = messages.length - 1; i >= 0; i-- ) {
43
+ if ( messages[i] && messages[i].role === 'user' ) {
44
+ var t = textOf(messages[i].content).trim();
45
+ return t.length ? t : null;
46
+ }
47
+ }
48
+ return null;
49
+ }
50
+
51
+ /**
52
+ * The PURE request handler (no socket): `(req) -> res` over a chat backend.
53
+ * @param opts.ask REQUIRED async ({system,user}) -> text — the backend (a gguf, an endpoint, a stub).
54
+ * @param opts.model the model id advertised on /v1/models and echoed when the client sends none
55
+ * (default 'skynet-graph-serve-demo').
56
+ * @param opts.onAnswer optional ({query, answer, source}) => void — a per-answer hook (the CLI logs here).
57
+ * @returns async ({ method, url, body }) => { status, headers, body } — or { status, headers, sse:[frames] }
58
+ * for a simulated stream (the http wrapper writes each frame as an SSE event).
59
+ */
60
+ function createServeDemoHandler( opts ) {
61
+ opts = opts || {};
62
+ var ask = opts.ask;
63
+ if ( typeof ask !== 'function' ) throw new Error('createServeDemoHandler needs opts.ask (async ({system,user}) -> text)');
64
+ var modelId = opts.model || 'skynet-graph-serve-demo';
65
+ var cache = new Map(); // exact query text -> the backend's verbatim answer (session-lifetime)
66
+ var saved = 0; // backend calls avoided so far
67
+ var nextId = 0;
68
+
69
+ var JSON_H = { 'content-type': 'application/json' };
70
+ var oaiError = function ( status, message, type ) {
71
+ return { status: status, headers: JSON_H, body: { error: { message: message, type: type || 'invalid_request_error' } } };
72
+ };
73
+
74
+ return async function handle( req ) {
75
+ req = req || {};
76
+ var method = String(req.method || 'GET').toUpperCase();
77
+ var url = String(req.url || '/').split('?')[0];
78
+
79
+ if ( url === '/v1/models' ) {
80
+ if ( method !== 'GET' ) return oaiError(405, 'method ' + method + ' not allowed on /v1/models');
81
+ return {
82
+ status: 200, headers: JSON_H,
83
+ body: { object: 'list', data: [{ id: modelId, object: 'model', created: Math.floor(Date.now() / 1000), owned_by: 'skynet-graph' }] }
84
+ };
85
+ }
86
+
87
+ if ( url === '/v1/chat/completions' ) {
88
+ if ( method !== 'POST' ) return oaiError(405, 'method ' + method + ' not allowed on /v1/chat/completions');
89
+ var b = req.body;
90
+ if ( !b || typeof b !== 'object' ) return oaiError(400, 'request body must be a JSON object');
91
+ var q = queryOfMessages(b.messages);
92
+ if ( q == null ) return oaiError(400, 'messages must contain at least one non-empty user turn');
93
+
94
+ var source, answer;
95
+ if ( cache.has(q) ) { source = 'cache'; answer = cache.get(q); saved++; }
96
+ else {
97
+ try { answer = String(await ask({ user: q })); }
98
+ catch ( e ) { return oaiError(500, String(e && e.message || e), 'server_error'); }
99
+ cache.set(q, answer);
100
+ source = 'backend';
101
+ }
102
+ if ( typeof opts.onAnswer === 'function' ) opts.onAnswer({ query: q, answer: answer, source: source });
103
+
104
+ // PROVENANCE — on the wire for every completion (headers + usage.sg_* mirror).
105
+ var prov = { 'x-sg-served-from': source, 'x-sg-saved': String(saved) };
106
+ var usage = {
107
+ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, // 0 = not counted (never estimated)
108
+ sg_served_from: source, sg_saved: saved
109
+ };
110
+ var id = 'chatcmpl-sgdemo' + (++nextId) + '-' + Date.now().toString(36);
111
+ var created = Math.floor(Date.now() / 1000);
112
+ var modelOut = (typeof b.model === 'string' && b.model) ? b.model : modelId;
113
+
114
+ if ( b.stream === true ) {
115
+ var chunk = function ( delta, finish ) {
116
+ return 'data: ' + JSON.stringify({
117
+ id: id, object: 'chat.completion.chunk', created: created, model: modelOut,
118
+ choices: [{ index: 0, delta: delta, finish_reason: finish || null }]
119
+ });
120
+ };
121
+ return {
122
+ status: 200,
123
+ headers: Object.assign({ 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive' }, prov),
124
+ sse: [chunk({ role: 'assistant', content: '' }), chunk({ content: answer }), chunk({}, 'stop'), 'data: [DONE]']
125
+ };
126
+ }
127
+ return {
128
+ status: 200, headers: Object.assign({}, JSON_H, prov),
129
+ body: {
130
+ id: id, object: 'chat.completion', created: created, model: modelOut,
131
+ choices: [{ index: 0, message: { role: 'assistant', content: answer }, finish_reason: 'stop' }],
132
+ usage: usage
133
+ }
134
+ };
135
+ }
136
+
137
+ return oaiError(404, 'unknown route ' + method + ' ' + url);
138
+ };
139
+ }
140
+
141
+ /**
142
+ * The zero-dep node:http wrapper around a handler (JSON body in, JSON or SSE out).
143
+ * @param o.handler the createServeDemoHandler function.
144
+ * @param o.port default 4747; o.host default 127.0.0.1 (LOCAL by default, on purpose);
145
+ * @param o.maxBody request-body cap (default 1 MiB). o.onReady optional () => void once listening.
146
+ * @returns the http.Server (close it to stop).
147
+ */
148
+ function startServeDemoServer( o ) {
149
+ o = o || {};
150
+ if ( typeof o.handler !== 'function' ) throw new Error('startServeDemoServer needs o.handler (a createServeDemoHandler function)');
151
+ var http = require('http');
152
+ var LIM = o.maxBody || (1 << 20);
153
+ var srv = http.createServer(function ( req, res ) {
154
+ var chunks = [], size = 0, over = false;
155
+ req.on('data', function ( d ) {
156
+ size += d.length;
157
+ if ( size > LIM ) { over = true; req.destroy(); return; }
158
+ chunks.push(d);
159
+ });
160
+ req.on('error', function () { /* destroyed over-limit request */ });
161
+ req.on('end', function () {
162
+ if ( over ) { try { res.writeHead(413, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'request body over ' + LIM + ' bytes', type: 'invalid_request_error' } })); } catch ( e ) {} return; }
163
+ var body = null, raw = Buffer.concat(chunks).toString('utf8');
164
+ if ( raw.length ) { try { body = JSON.parse(raw); } catch ( e ) { body = undefined; } } // undefined = present but invalid JSON → 400 in the handler
165
+ Promise.resolve(o.handler({ method: req.method, url: req.url, headers: req.headers, body: body }))
166
+ .catch(function ( e ) { return { status: 500, headers: { 'content-type': 'application/json' }, body: { error: { message: String(e && e.message || e), type: 'server_error' } } }; })
167
+ .then(function ( out ) {
168
+ out = out || { status: 500, headers: {}, body: { error: { message: 'empty handler result', type: 'server_error' } } };
169
+ if ( out.sse ) {
170
+ res.writeHead(out.status || 200, out.headers || {});
171
+ (out.sse || []).forEach(function ( f ) { res.write(f + '\n\n'); });
172
+ res.end();
173
+ return;
174
+ }
175
+ var txt = JSON.stringify(out.body != null ? out.body : {});
176
+ res.writeHead(out.status || 200, Object.assign({ 'content-length': Buffer.byteLength(txt) }, out.headers || {}));
177
+ res.end(txt);
178
+ });
179
+ });
180
+ });
181
+ srv.listen(o.port != null ? o.port : 4747, o.host || '127.0.0.1', o.onReady);
182
+ return srv;
183
+ }
184
+
185
+ module.exports = { createServeDemoHandler: createServeDemoHandler, startServeDemoServer: startServeDemoServer, queryOfMessages: queryOfMessages };
@@ -0,0 +1,205 @@
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
+ * `sg serve` — the OpenAI-COMPATIBLE endpoint over the C6 proxy cache (roadmap FINIR, F1). Any client of the
10
+ * OpenAI ecosystem (official SDKs, LangChain, Open WebUI, curl) integrates by pointing `baseURL` at this
11
+ * server — zero integration code. Every completion is `createProxyCache.answer` underneath: a COVERED query is
12
+ * served from the verified local stock at 0 frontier calls, a miss escalates to the frontier and enriches the
13
+ * stock in passing (0 hallucination — verified stock or escalate; a miss always answers, no false neg).
14
+ *
15
+ * THIN assembly (doctrine): `createServeHandler` is a PURE request handler (stub-testable, no socket — the same
16
+ * split as proxy-run.js), `startServeServer` is the zero-dep node:http wrapper, and the GPU-bound model
17
+ * resolution stays in cli.js.
18
+ *
19
+ * THE v1 WIRE CONTRACT:
20
+ * • POST /v1/chat/completions — the QUERY is the LAST `user` turn (string content or text parts). Multi-turn
21
+ * history and client system prompts are NOT part of the cache identity: this is a QA proxy-cache, not a
22
+ * dialog engine (the honest v1 boundary — a context-dependent flow belongs on the frontier directly, and
23
+ * `coverageCheck` is the backstop that rejects an ill-fitting hit before it is served).
24
+ * • `stream:true` is SIMULATED (arbitrage F0-3): the answer is computed in one block, then emitted as
25
+ * standard SSE chunks (role delta → content delta → finish → [DONE]) so streaming clients work unchanged.
26
+ * A true frontier passthrough stream is a later opt-in — the local stock answers in one block anyway.
27
+ * • PROVENANCE on every completion (the debug contract): headers `x-sg-served-from: local|frontier`,
28
+ * `x-sg-arm`, `x-sg-cost`, `x-sg-coverage`, `x-sg-saved` — plus `x-sg-sgc-version` (the loaded `.sgc`
29
+ * catalog state, host-supplied via `opts.sgcVersion`, read per request so a refresh shows live) —
30
+ * mirrored as `usage.sg_*` extension fields (an OpenAI `usage` object tolerates extra keys). Token
31
+ * counts are NOT estimated: 0 means "not counted", never a made-up number.
32
+ * • GET /v1/models — one model row (the proxy), so SDK bootstraps and model pickers work.
33
+ */
34
+
35
+ /** The text of an OpenAI message `content` (a string, or an array of {type:'text',text} parts). */
36
+ function textOf( content ) {
37
+ if ( typeof content === 'string' ) return content;
38
+ if ( Array.isArray(content) )
39
+ return content.filter(function ( p ) { return p && (p.type === 'text' || typeof p.text === 'string'); })
40
+ .map(function ( p ) { return p.text; }).join('\n');
41
+ return '';
42
+ }
43
+
44
+ /** The proxy QUERY of an OpenAI `messages` array = the LAST user turn's text, or null (no user turn). */
45
+ function queryOfMessages( messages ) {
46
+ if ( !Array.isArray(messages) ) return null;
47
+ for ( var i = messages.length - 1; i >= 0; i-- ) {
48
+ if ( messages[i] && messages[i].role === 'user' ) {
49
+ var t = textOf(messages[i].content).trim();
50
+ return t.length ? t : null;
51
+ }
52
+ }
53
+ return null;
54
+ }
55
+
56
+ /**
57
+ * The PURE request handler (no socket): `(req) -> res` over a `createProxyCache` instance.
58
+ * @param opts.proxy REQUIRED — a createProxyCache instance ({ answer, metrics }).
59
+ * @param opts.model the model id advertised on /v1/models and echoed when the client sends none
60
+ * (default 'skynet-graph-proxy').
61
+ * @param opts.onAnswer optional (row) => void — a per-answer hook ({query, answer, source, cached, cost};
62
+ * the CLI logs/streams here, same seam as runProxySession).
63
+ * @param opts.sgcVersion optional string or () => string — the loaded `.sgc` catalog state (e.g.
64
+ * 'units@1.0.0,laws@2.1.0'). When non-empty it rides every completion as the
65
+ * `x-sg-sgc-version` header + `usage.sg_sgc_version` (stock-freshness provenance).
66
+ * The function form is read PER REQUEST so a catalog refresh shows up live. The lib
67
+ * has no catalog notion of its own — the host (an appliance) owns the value.
68
+ * @returns async ({ method, url, body }) => { status, headers, body } — or { status, headers, sse:[frames] }
69
+ * for a simulated stream (the http wrapper writes each frame as an SSE event).
70
+ */
71
+ function createServeHandler( opts ) {
72
+ opts = opts || {};
73
+ var proxy = opts.proxy;
74
+ if ( !proxy || typeof proxy.answer !== 'function' ) throw new Error('createServeHandler needs opts.proxy (a createProxyCache instance)');
75
+ var modelId = opts.model || 'skynet-graph-proxy';
76
+ var nextId = 0;
77
+
78
+ var JSON_H = { 'content-type': 'application/json' };
79
+ var oaiError = function ( status, message, type ) {
80
+ return { status: status, headers: JSON_H, body: { error: { message: message, type: type || 'invalid_request_error' } } };
81
+ };
82
+
83
+ return async function handle( req ) {
84
+ req = req || {};
85
+ var method = String(req.method || 'GET').toUpperCase();
86
+ var url = String(req.url || '/').split('?')[0];
87
+
88
+ if ( url === '/v1/models' ) {
89
+ if ( method !== 'GET' ) return oaiError(405, 'method ' + method + ' not allowed on /v1/models');
90
+ return {
91
+ status: 200, headers: JSON_H,
92
+ body: { object: 'list', data: [{ id: modelId, object: 'model', created: Math.floor(Date.now() / 1000), owned_by: 'skynet-graph' }] }
93
+ };
94
+ }
95
+
96
+ if ( url === '/v1/chat/completions' ) {
97
+ if ( method !== 'POST' ) return oaiError(405, 'method ' + method + ' not allowed on /v1/chat/completions');
98
+ var b = req.body;
99
+ if ( !b || typeof b !== 'object' ) return oaiError(400, 'request body must be a JSON object');
100
+ var q = queryOfMessages(b.messages);
101
+ if ( q == null ) return oaiError(400, 'messages must contain at least one non-empty user turn');
102
+
103
+ var r;
104
+ try { r = await proxy.answer(q); }
105
+ catch ( e ) { return oaiError(500, String(e && e.message || e), 'server_error'); }
106
+ var m = (typeof proxy.metrics === 'function') ? proxy.metrics() : null;
107
+ if ( typeof opts.onAnswer === 'function' )
108
+ opts.onAnswer({ query: q, answer: r.answer, source: r.source, cached: !!r.cached, cost: r.cost });
109
+
110
+ // PROVENANCE — on the wire for every completion (headers + usage.sg_* mirror).
111
+ var sgcv = (typeof opts.sgcVersion === 'function') ? opts.sgcVersion() : opts.sgcVersion;
112
+ var prov = {
113
+ 'x-sg-served-from': String(r.source || ''),
114
+ 'x-sg-arm' : String(r.arm || ''),
115
+ 'x-sg-cost' : String(r.cost != null ? r.cost : '')
116
+ };
117
+ if ( m ) {
118
+ prov['x-sg-coverage'] = String(Math.round((m.coverage || 0) * 100) / 100);
119
+ prov['x-sg-saved'] = String(m.local); // frontier calls avoided so far = local hits
120
+ }
121
+ if ( sgcv ) prov['x-sg-sgc-version'] = String(sgcv);
122
+ var usage = {
123
+ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, // 0 = not counted (never estimated)
124
+ sg_served_from: r.source, sg_cached: !!r.cached,
125
+ sg_saved: m ? m.local : undefined, sg_frontier_calls: m ? m.frontier : undefined,
126
+ sg_coverage: m ? m.coverage : undefined,
127
+ sg_sgc_version: sgcv ? String(sgcv) : undefined
128
+ };
129
+ var id = 'chatcmpl-sg' + (++nextId) + '-' + Date.now().toString(36);
130
+ var created = Math.floor(Date.now() / 1000);
131
+ var modelOut = (typeof b.model === 'string' && b.model) ? b.model : modelId;
132
+
133
+ if ( b.stream === true ) {
134
+ // SIMULATED stream: the standard SSE chunk sequence over the already-computed answer.
135
+ var chunk = function ( delta, finish ) {
136
+ return 'data: ' + JSON.stringify({
137
+ id: id, object: 'chat.completion.chunk', created: created, model: modelOut,
138
+ choices: [{ index: 0, delta: delta, finish_reason: finish || null }]
139
+ });
140
+ };
141
+ return {
142
+ status: 200,
143
+ headers: Object.assign({ 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive' }, prov),
144
+ sse: [chunk({ role: 'assistant', content: '' }), chunk({ content: String(r.answer) }), chunk({}, 'stop'), 'data: [DONE]']
145
+ };
146
+ }
147
+ return {
148
+ status: 200, headers: Object.assign({}, JSON_H, prov),
149
+ body: {
150
+ id: id, object: 'chat.completion', created: created, model: modelOut,
151
+ choices: [{ index: 0, message: { role: 'assistant', content: String(r.answer) }, finish_reason: 'stop' }],
152
+ usage: usage
153
+ }
154
+ };
155
+ }
156
+
157
+ return oaiError(404, 'unknown route ' + method + ' ' + url);
158
+ };
159
+ }
160
+
161
+ /**
162
+ * The zero-dep node:http wrapper around a handler (JSON body in, JSON or SSE out).
163
+ * @param o.handler the createServeHandler function.
164
+ * @param o.port default 4747; o.host default 127.0.0.1; o.maxBody request-body cap (default 1 MiB).
165
+ * @param o.onReady optional () => void once listening.
166
+ * @returns the http.Server (close it to stop).
167
+ */
168
+ function startServeServer( o ) {
169
+ o = o || {};
170
+ if ( typeof o.handler !== 'function' ) throw new Error('startServeServer needs o.handler (a createServeHandler function)');
171
+ var http = require('http');
172
+ var LIM = o.maxBody || (1 << 20);
173
+ var srv = http.createServer(function ( req, res ) {
174
+ var chunks = [], size = 0, over = false;
175
+ req.on('data', function ( d ) {
176
+ size += d.length;
177
+ if ( size > LIM ) { over = true; req.destroy(); return; }
178
+ chunks.push(d);
179
+ });
180
+ req.on('error', function () { /* destroyed over-limit request — response below or dropped */ });
181
+ req.on('end', function () {
182
+ if ( over ) { try { res.writeHead(413, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'request body over ' + LIM + ' bytes', type: 'invalid_request_error' } })); } catch ( e ) {} return; }
183
+ var body = null, raw = Buffer.concat(chunks).toString('utf8');
184
+ if ( raw.length ) { try { body = JSON.parse(raw); } catch ( e ) { body = undefined; } } // undefined = present but invalid JSON → 400 in the handler
185
+ Promise.resolve(o.handler({ method: req.method, url: req.url, headers: req.headers, body: body }))
186
+ .catch(function ( e ) { return { status: 500, headers: { 'content-type': 'application/json' }, body: { error: { message: String(e && e.message || e), type: 'server_error' } } }; })
187
+ .then(function ( out ) {
188
+ out = out || { status: 500, headers: {}, body: { error: { message: 'empty handler result', type: 'server_error' } } };
189
+ if ( out.sse ) {
190
+ res.writeHead(out.status || 200, out.headers || {});
191
+ (out.sse || []).forEach(function ( f ) { res.write(f + '\n\n'); });
192
+ res.end();
193
+ return;
194
+ }
195
+ var txt = JSON.stringify(out.body != null ? out.body : {});
196
+ res.writeHead(out.status || 200, Object.assign({ 'content-length': Buffer.byteLength(txt) }, out.headers || {}));
197
+ res.end(txt);
198
+ });
199
+ });
200
+ });
201
+ srv.listen(o.port != null ? o.port : 4747, o.host || '127.0.0.1', o.onReady);
202
+ return srv;
203
+ }
204
+
205
+ module.exports = { createServeHandler: createServeHandler, startServeServer: startServeServer, queryOfMessages: queryOfMessages };
@@ -0,0 +1,118 @@
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
+ * Concept-apply trace collector + render helpers — the engine behind the `sg`
21
+ * inspector CLI. Host-side (not part of the engine): wire `cfg.onConceptApply =
22
+ * trace.onConceptApply`, run the graph, then write/inspect the artifact.
23
+ *
24
+ * const trace = createTrace();
25
+ * new Graph(seed, { ..., onConceptApply: trace.onConceptApply }, conceptMap);
26
+ * // on stabilize:
27
+ * trace.write('run.json', graph, { objective });
28
+ */
29
+ const fs = require('fs');
30
+
31
+ function createTrace() {
32
+ const records = [];
33
+ const api = {
34
+ records,
35
+ onConceptApply( rec ) { records.push(rec); },
36
+ // bundle records + a final graph snapshot (serialize()) for post-hoc inspection
37
+ toArtifact( graph, meta ) {
38
+ return { meta: meta || {}, snapshot: graph ? graph.serialize() : null, records };
39
+ },
40
+ write( path, graph, meta ) {
41
+ fs.writeFileSync(path, JSON.stringify(api.toArtifact(graph, meta), null, 2));
42
+ return path;
43
+ }
44
+ };
45
+ return api;
46
+ }
47
+
48
+ // compact one-line summary of a patch (the applied mutation template)
49
+ function patchSummary( patch ) {
50
+ if ( !patch ) return '';
51
+ const arr = Array.isArray(patch) ? patch : [patch];
52
+ const segs = arr.filter(o => o && o.Segment).length;
53
+ const parts = [];
54
+ if ( segs ) parts.push('+' + segs + ' seg' + (segs > 1 ? 's' : ''));
55
+ for ( const o of arr ) {
56
+ for ( const k of Object.keys(o || {}) ) {
57
+ if ( k === '_id' || k === '$$_id' || k === '$_id' || k === '_rev' || k === 'Segment' || k === 'Node' ) continue;
58
+ let v = o[k];
59
+ v = (v && typeof v === 'object') ? (Array.isArray(v) ? '[' + v.length + ']' : JSON.stringify(v)) : String(v);
60
+ if ( v.length > 24 ) v = v.slice(0, 23) + '…';
61
+ parts.push(k + ':' + v);
62
+ }
63
+ }
64
+ const s = parts.join(' ');
65
+ return s.length > 48 ? s.slice(0, 47) + '…' : s;
66
+ }
67
+
68
+ // one summary row per record (for `sg trace`)
69
+ function summarizeTrace( records ) {
70
+ return records.map(( r, i ) => ({
71
+ n : i,
72
+ rev : r.rev,
73
+ concept: r.conceptName,
74
+ target : r.targetId,
75
+ kind : r.kind,
76
+ patch : patchSummary(r.patch),
77
+ ms : Math.round(r.ms || 0)
78
+ }));
79
+ }
80
+
81
+ // per-concept rollup (count + total ms), heaviest first (for finding expensive branches)
82
+ function perConcept( records ) {
83
+ const by = {};
84
+ for ( const r of records ) {
85
+ const c = by[r.conceptName] = by[r.conceptName] || { concept: r.conceptName, count: 0, totalMs: 0 };
86
+ c.count++;
87
+ c.totalMs += Math.round(r.ms || 0);
88
+ }
89
+ return Object.keys(by).map(k => by[k]).sort(( a, b ) => b.totalMs - a.totalMs);
90
+ }
91
+
92
+ // records whose patch flagged an llmError (for `sg errors`)
93
+ function errorRecords( records ) {
94
+ return records.filter(r => JSON.stringify(r.patch || '').includes('llmError'));
95
+ }
96
+
97
+ // full detail of one record (for `sg show <n>`)
98
+ function formatRecord( rec ) {
99
+ if ( !rec ) return '(no such record)';
100
+ const lines = [];
101
+ lines.push(`#${rec.rev} ${rec.conceptName} -> ${rec.targetId} [${rec.kind}, ${Math.round(rec.ms || 0)}ms]`);
102
+ if ( rec.why && rec.why.length ) {
103
+ lines.push(' why fired:');
104
+ for ( const w of rec.why ) {
105
+ let v = (w.value && typeof w.value === 'object') ? JSON.stringify(w.value) : String(w.value);
106
+ lines.push(` ${w.require} = ${v}` + (w.producedAtRev != null ? ` @rev ${w.producedAtRev}` : ''));
107
+ }
108
+ }
109
+ if ( rec.prompt ) {
110
+ if ( rec.prompt.system ) lines.push(' prompt.system: ' + rec.prompt.system);
111
+ if ( rec.prompt.user ) lines.push(' prompt.user: ' + rec.prompt.user);
112
+ }
113
+ if ( rec.reply != null ) lines.push(' reply: ' + (typeof rec.reply === 'string' ? rec.reply : JSON.stringify(rec.reply)));
114
+ lines.push(' patch: ' + JSON.stringify(rec.patch));
115
+ return lines.join('\n');
116
+ }
117
+
118
+ module.exports = { createTrace, summarizeTrace, perConcept, errorRecords, formatRecord, patchSummary };
@@ -0,0 +1,45 @@
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
+ * The studio wire contract (shared by server.js and the frontend ws client).
21
+ *
22
+ * Client -> server : { id, sessionId?, op, args } (op in OPS)
23
+ * Server -> client : { id, ok, result | error } (op responses)
24
+ * | { type, sessionId, payload } (unsolicited events, type in EVENTS)
25
+ */
26
+
27
+ // operations a client may invoke (one Session/Studio method each)
28
+ const OPS = Object.freeze([
29
+ 'listCorpora', 'loadCorpus', 'reset',
30
+ 'mutate', 'run', 'state',
31
+ 'conceptTree', 'getConcept', 'validateConcept', 'patchConcept', 'addConcept', 'forkPlan',
32
+ 'grammarGraph', 'corpusManifest', 'exportCorpus', 'importCorpus', 'providerTrace', 'mergePreview',
33
+ 'revisions', 'snapshot', 'rollback', 'diff',
34
+ 'fork', 'merge', 'selectSession',
35
+ 'registry', 'declareKey', 'proposeAlias', 'retractAlias', 'creditAlias', 'exportLattice', 'importLattice',
36
+ 'prompt'
37
+ ]);
38
+
39
+ // events the server pushes to subscribers
40
+ const EVENTS = Object.freeze([
41
+ 'conceptApply', 'stabilize', 'mutation', 'rollback', 'state', 'retract',
42
+ 'forks', 'promptProgress', 'promptAnswer', 'error'
43
+ ]);
44
+
45
+ module.exports = { OPS, EVENTS };