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
package/lib/sg/cli.js ADDED
@@ -0,0 +1,601 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Copyright 2026 Nathanael Braun
4
+ * @author : Nathanael BRAUN <pp9ping@gmail.com>
5
+ *
6
+ * This program is free software: you can redistribute it and/or modify
7
+ * it under the terms of the GNU Affero General Public License as published by
8
+ * the Free Software Foundation, either version 3 of the License, or
9
+ * (at your option) any later version.
10
+ *
11
+ * This program is distributed in the hope that it will be useful,
12
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ * GNU Affero General Public License for more details.
15
+ *
16
+ * You should have received a copy of the GNU Affero General Public License
17
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
18
+ */
19
+ 'use strict';
20
+ /**
21
+ * sg — the Skynet-Graph CLI.
22
+ *
23
+ * Boot a graph standalone from plain directories:
24
+ * sg run --concepts <dir> [--providers <dir>] [--builtins] [--sets a,b]
25
+ * [--seed <file.json>] [--trace <out.json>] [--json] [--timeout <ms>]
26
+ *
27
+ * Inspect a trace artifact (produced by --trace, or lib/sg/trace.js write()):
28
+ * sg trace <file> list every concept-apply (rev, concept, target, patch, ms)
29
+ * sg show <file> <n> full detail of record n (prompt / reply / patch / why)
30
+ * sg concepts <file> per-concept rollup (count + total ms), heaviest first
31
+ * sg errors <file> applies whose patch flagged an llmError
32
+ */
33
+ const fs = require('fs');
34
+ const { summarizeTrace, perConcept, errorRecords, formatRecord } = require('./trace.js');
35
+ const { createLogger } = require('../graph/log.js');
36
+ const { createPlainSink, createDashboardSink, createFileSink, mostPermissive, banner, startStatsLogger } = require('./log-sinks.js');
37
+ const pkg = require('../../package.json');
38
+
39
+ function die( msg ) { process.stderr.write('sg: ' + msg + '\n'); process.exit(1); }
40
+ function out( s ) { process.stdout.write(s + '\n'); }
41
+
42
+ function loadArtifact( file ) {
43
+ if ( !file ) die('missing <file>');
44
+ let raw;
45
+ try { raw = fs.readFileSync(file, 'utf8'); } catch ( e ) { die('cannot read ' + file + ': ' + e.message); }
46
+ try { return JSON.parse(raw); } catch ( e ) { die('not valid JSON: ' + file); }
47
+ }
48
+
49
+ // minimal fixed-width table printer
50
+ function table( rows, cols ) {
51
+ if ( !rows.length ) return '(no records)';
52
+ const w = {};
53
+ cols.forEach(c => { w[c] = Math.max(c.length, ...rows.map(r => String(r[c] == null ? '' : r[c]).length)); });
54
+ const line = r => cols.map(c => String(r[c] == null ? '' : r[c]).padEnd(w[c])).join(' ');
55
+ const head = {}; cols.forEach(c => head[c] = c);
56
+ return [line(head), cols.map(c => '-'.repeat(w[c])).join(' '), ...rows.map(line)].join('\n');
57
+ }
58
+
59
+ // --key value | --flag(boolean) parser
60
+ function parseFlags( args ) {
61
+ const o = {};
62
+ for ( let i = 0 ; i < args.length ; i++ ) {
63
+ const a = args[i];
64
+ if ( !a.startsWith('--') ) continue;
65
+ const key = a.slice(2);
66
+ const next = args[i + 1];
67
+ if ( next === undefined || next.startsWith('--') ) o[key] = true;
68
+ else { o[key] = next; i++; }
69
+ }
70
+ return o;
71
+ }
72
+
73
+ // concise post-stabilize report (or full object dump with --json)
74
+ function runSummary( graph, asJson ) {
75
+ const ser = JSON.parse(graph.serialize().graph);
76
+ const objs = ser.conceptMaps || [];
77
+ if ( asJson ) return JSON.stringify(objs, null, 2);
78
+ const lines = ['stabilized: ' + objs.length + ' object(s), rev ' + ser.lastRev];
79
+ for ( const o of objs ) {
80
+ const facts = Object.keys(o).filter(k => k[0] !== '_').slice(0, 14);
81
+ lines.push(' ' + (o._id || '(no id)') + ' {' + facts.join(', ') + '}');
82
+ }
83
+ return lines.join('\n');
84
+ }
85
+
86
+ function openBrowser( url ) {
87
+ const { spawn } = require('child_process');
88
+ const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
89
+ try { spawn(cmd, [url], { stdio: 'ignore', detached: true }).unref(); } catch ( e ) {}
90
+ }
91
+
92
+ // Build the shared graph logger from the common --log-* flags (used by every
93
+ // command that boots a graph). Console sink is attached by the caller (dashboard
94
+ // for `run`, plain for `studio`). The logger threshold is the most permissive any
95
+ // sink needs; each sink then filters to its own level.
96
+ function buildLogger( opts ) {
97
+ const consoleLevel = opts['log-level'] || 'info';
98
+ const fileLevel = opts['log-file'] ? (opts['log-file-level'] || 'verbose') : null;
99
+ const logger = createLogger({ label: 'sg', level: mostPermissive([consoleLevel, fileLevel].filter(Boolean)), console: false });
100
+ if ( opts['log-file'] ) logger.addSink(createFileSink({ path: String(opts['log-file']), level: fileLevel }));
101
+ return { logger, consoleLevel };
102
+ }
103
+
104
+ const argv = process.argv.slice(2);
105
+ const cmd = argv[0];
106
+
107
+ switch ( cmd ) {
108
+ case 'studio': {
109
+ const opts = parseFlags(argv.slice(1));
110
+ const Graph = require('../index.js');
111
+ const { createServer } = require('../studio/server.js');
112
+ const port = Number(opts.port) || 4848;
113
+ const root = opts.root ? require('path').resolve(String(opts.root)) : process.cwd();
114
+ // optional LLM backend for the prompt console (decompose -> synthesize)
115
+ let ask;
116
+ if ( process.env.LLM_BASE ) {
117
+ const { makeAsk } = require('../providers/llm.js');
118
+ ask = makeAsk({ base: process.env.LLM_BASE, model: process.env.LLM_MODEL || 'claude-3-5-sonnet-20241022' });
119
+ }
120
+ // terminal logging for the graphs the studio boots, shared across all sessions/forks
121
+ const { logger, consoleLevel } = buildLogger(opts);
122
+ const studioMode = opts['log-plain'] ? 'plain' : (opts['log-mode'] || (process.stdout.isTTY ? 'dashboard' : 'plain'));
123
+ const srv = createServer({ Graph, root, ask, logger });
124
+ // dashboard: fixed bottom status bar (reflects the ACTIVE session's graph) + scrolling
125
+ // logs; stats live ONLY in the bar. plain (log-only): no bar, stats emitted as periodic
126
+ // log lines instead. Banner to the console stream.
127
+ let studioDash = null, studioStats = null;
128
+ if ( studioMode === 'dashboard' ) {
129
+ process.stdout.write(banner(pkg.version, !!process.stdout.isTTY) + '\n');
130
+ try { studioDash = createDashboardSink({ getGraph: () => srv.studio.activeGraph(), stream: process.stdout, level: consoleLevel }); logger.addSink(studioDash); }
131
+ catch ( e ) { logger.addSink(createPlainSink({ stream: process.stderr, level: consoleLevel })); }
132
+ } else {
133
+ process.stderr.write(banner(pkg.version, !!process.stderr.isTTY) + '\n');
134
+ logger.addSink(createPlainSink({ stream: process.stderr, level: consoleLevel }));
135
+ studioStats = startStatsLogger(logger, () => srv.studio.activeGraph(), 2000);// stats in the log stream (no bar)
136
+ }
137
+ process.on('SIGINT', () => { if ( studioDash && studioDash.close ) studioDash.close(); if ( studioStats ) clearInterval(studioStats); process.exit(130); });
138
+ srv.listen(port, () => {
139
+ const url = 'http://localhost:' + port;
140
+ out('sg studio → ' + url + ' (corpora root: ' + root + ', logs: ' + consoleLevel + ', mode: ' + studioMode + ')');
141
+ if ( opts.open ) openBrowser(url);
142
+ });
143
+ break;
144
+ }
145
+ case 'run': {
146
+ const opts = parseFlags(argv.slice(1));
147
+ if ( !opts.concepts ) die('run needs --concepts <dir>');
148
+ const Graph = require('../index.js');
149
+ const { createTrace } = require('./trace.js');
150
+ const trace = opts.trace ? createTrace() : null;
151
+ let done = false;
152
+ let dash = null;
153
+ let statsTimer = null;
154
+ const finish = ( graph ) => {
155
+ if ( done ) return; done = true;
156
+ if ( statsTimer ) clearInterval(statsTimer);
157
+ if ( dash && dash.close ) dash.close(); // restore the terminal before printing the summary
158
+ out(runSummary(graph, opts.json));
159
+ if ( trace ) out('trace -> ' + trace.write(String(opts.trace), graph, { concepts: opts.concepts }));
160
+ process.exit(0);
161
+ };
162
+
163
+ // logging: shared logger + display mode (dashboard on a TTY, else plain)
164
+ const { logger, consoleLevel } = buildLogger(opts);
165
+ const mode = opts['log-mode'] || (opts['log-plain'] ? 'plain' : (process.stdout.isTTY ? 'dashboard' : 'plain'));
166
+ // styled boot banner (to the same stream the console logs use)
167
+ const bannerStream = mode === 'dashboard' ? process.stdout : process.stderr;
168
+ bannerStream.write(banner(pkg.version, !!bannerStream.isTTY) + '\n');
169
+
170
+ const conf = { autoMount: true, onStabilize: ( g ) => finish(g), logger };
171
+ if ( opts.sets ) conf.conceptSets = String(opts.sets).split(',');
172
+ if ( trace ) conf.onConceptApply = trace.onConceptApply;
173
+ const g = Graph.fromDirs({
174
+ concepts : opts.concepts,
175
+ providers : opts.providers,
176
+ builtins : opts.builtins ? true : undefined,
177
+ seed : opts.seed ? String(opts.seed) : undefined,
178
+ providerCtx: { env: process.env },
179
+ conf
180
+ });
181
+ // console sink — dashboard: fixed bottom status bar + scrolling logs, stats ONLY in the
182
+ // bar. plain (log-only): logs to stderr (stdout summary stays pipeable) + stats emitted as
183
+ // periodic log lines. Dashboard degrades to plain off a TTY.
184
+ if ( mode === 'dashboard' ) {
185
+ try { dash = createDashboardSink({ graph: g, stream: process.stdout, level: consoleLevel }); logger.addSink(dash); }
186
+ catch ( e ) { logger.addSink(createPlainSink({ stream: process.stderr, level: consoleLevel })); }
187
+ } else {
188
+ logger.addSink(createPlainSink({ stream: process.stderr, level: consoleLevel }));
189
+ statsTimer = startStatsLogger(logger, () => g, 2000);// log-only: stats as periodic log lines
190
+ }
191
+ process.on('SIGINT', () => { if ( dash && dash.close ) dash.close(); if ( statsTimer ) clearInterval(statsTimer); process.exit(130); });
192
+ // settle-hook only fires after a write; an empty/no-op seed never settles -> bound it
193
+ setTimeout(() => finish(g), Number(opts.timeout) || 8000);
194
+ break;
195
+ }
196
+ case 'ask': {
197
+ // sg ask "<question>" — the request/response reasoning endpoint (Controller-P0).
198
+ // --concepts <dir> → the TYPED APPLIANCE (lib/combos/appliance.js): intake→reason-loop→typed
199
+ // refusal→memo, the §4 posture ON. A refusal NAMES the missing requirement
200
+ // (never a wrong answer). This is the differentiator.
201
+ // (no --concepts) → the LEGACY best-effort decompose→synthesize loop (Session), kept for compat.
202
+ // Backend: --local-model <gguf> / env LOCAL_MODEL (embedded), or an HTTP endpoint via LLM_BASE.
203
+ const opts = parseFlags(argv.slice(1));
204
+ const text = (argv[1] && !argv[1].startsWith('--')) ? argv[1] : (opts.q || opts.prompt);
205
+ if ( !text || text === true ) die('ask needs a question: sg ask "<question>" [--concepts <dir>] [--local-model <gguf> | (env LLM_BASE)] [--depth N] [--json]');
206
+
207
+ const localModel = opts['local-model'] ? String(opts['local-model']) : process.env.LOCAL_MODEL;
208
+ let backend; // { localModel } | an ask function | (die)
209
+ if ( localModel ) backend = { localModel: localModel };
210
+ else if ( process.env.LLM_BASE ) backend = require('../providers/llm.js').makeAsk({ base: process.env.LLM_BASE, model: process.env.LLM_MODEL || 'claude-3-5-sonnet-20241022' });
211
+ else die('ask needs an LLM backend: pass --local-model <path.gguf> (or env LOCAL_MODEL), or set LLM_BASE');
212
+
213
+ const { logger, consoleLevel } = buildLogger({ ...opts, 'log-level': opts['log-level'] || 'warn' });
214
+ logger.addSink(createPlainSink({ stream: process.stderr, level: consoleLevel })); // logs → stderr; answer → stdout
215
+ if ( !opts.json ) process.stderr.write(banner(pkg.version, !!process.stderr.isTTY) + '\n');
216
+ const depth = opts.depth != null ? Number(opts.depth) : undefined, timeout = Number(opts.timeout) || 180000;
217
+
218
+ if ( opts.concepts ) {
219
+ // the TYPED appliance — a domain concept set + the reasoning substrate; typed answer or typed refusal.
220
+ const { createAppliance } = require('../index.js').combos;
221
+ const app = createAppliance({ concepts: opts.concepts, ask: backend, logger: logger, maxDepth: depth });
222
+ app.answer(String(text), { timeout: timeout }).then(( r ) => {
223
+ if ( opts.json ) out(JSON.stringify(r, null, 2));
224
+ else if ( r.status === 'answered' ) out(r.answer + (r.confBand ? '\n [confidence: ' + r.confBand + ']' : ''));
225
+ else out('REFUSED — ' + r.reason + (r.missing && r.missing.length ? ' (missing: ' + r.missing.join(', ') + ')' : ''));
226
+ app.close(); process.exit(r.status === 'answered' ? 0 : 2);
227
+ }).catch(( e ) => die(e.message));
228
+ } else {
229
+ // LEGACY best-effort loop (no typed barrier) — kept for compat; the appliance is the typed path.
230
+ if ( !opts.json ) process.stderr.write(' (note: bare `sg ask` is the legacy best-effort loop; pass --concepts <dir> for the typed appliance)\n');
231
+ const Graph = require('../index.js');
232
+ const Session = require('../studio/session.js');
233
+ const ask = (backend && backend.localModel)
234
+ ? require('../providers/llm-local.js').makeLocalAsk({ modelPath: backend.localModel })
235
+ : backend; // an LLM_BASE function stays as-is
236
+ const s = new Session('ask', { Graph, ask, logger });
237
+ if ( !opts.json ) s.on('promptProgress', ( m ) => process.stderr.write(' … ' + m.kind + (m.label ? ' ' + m.label : '') + '\n'));
238
+ s.answer(String(text), { maxDepth: depth, timeout: timeout })
239
+ .then(( { answer, state } ) => { out(opts.json ? JSON.stringify({ answer, objects: state.objects }, null, 2) : answer); process.exit(0); })
240
+ .catch(( e ) => die(e.message));
241
+ }
242
+ break;
243
+ }
244
+ case 'proxy': {
245
+ // sg proxy "<question>" — the LOCAL-FIRST PROXY CACHE (C6). Serve a COVERED query from the local
246
+ // stock at 0 frontier calls; escalate a MISS to the frontier and enrich the stock in passing. The
247
+ // local side NEVER fabricates (0 hallucination — verified stock or escalate); a miss always answers
248
+ // (no false neg). The economy (frontier calls saved) is reported to stderr.
249
+ // --frontier-model <gguf> | env FRONTIER_MODEL | env LLM_BASE the ground-truth generator (REQUIRED)
250
+ // --local-model <gguf> | env LOCAL_MODEL small model → semantic coverage + coverage-check (opt-in)
251
+ // --store <file.json> durable stock across runs
252
+ // --json machine output (answers + metrics)
253
+ // No positional question → read queries from stdin (one per line) = a real recurring session.
254
+ const opts = parseFlags(argv.slice(1));
255
+ const positional = (argv[1] && !argv[1].startsWith('--')) ? argv[1] : (opts.q || opts.prompt);
256
+ const { createProxyCache, makeFrontierAsk, makeLocalCoverage } = require('../index.js').combos;
257
+ const { makeLocalAsk } = require('../providers/llm-local.js');
258
+
259
+ // FRONTIER = the ground truth (embedded gguf or an HTTP endpoint) — never invented on the local side.
260
+ const frontierModel = opts['frontier-model'] ? String(opts['frontier-model']) : process.env.FRONTIER_MODEL;
261
+ let frontierChat;
262
+ if ( frontierModel ) frontierChat = makeLocalAsk({ modelPath: frontierModel, reasoningBudget: 0 });
263
+ else if ( process.env.LLM_BASE ) frontierChat = require('../providers/llm.js').makeAsk({ base: process.env.LLM_BASE, model: process.env.LLM_MODEL || 'claude-3-5-sonnet-20241022' });
264
+ else die('proxy needs a FRONTIER backend: --frontier-model <gguf> (or env FRONTIER_MODEL), or set LLM_BASE');
265
+ const frontierAsk = makeFrontierAsk(frontierChat);
266
+
267
+ // LOCAL (opt-in) = the semantic-key + coverage-check judge → a paraphrase hits the stock, a stale hit is rejected.
268
+ let semantic = {};
269
+ const localModel = opts['local-model'] ? String(opts['local-model']) : process.env.LOCAL_MODEL;
270
+ if ( localModel ) semantic = makeLocalCoverage({ localAsk: makeLocalAsk({ modelPath: localModel, reasoningBudget: 0 }) });
271
+
272
+ const px = createProxyCache(Object.assign({ frontierAsk, store: opts.store ? String(opts.store) : undefined, retention: true }, semantic));
273
+ const { runProxySession, formatProxyReport } = require('./proxy-run.js');
274
+
275
+ const readStdin = () => new Promise(( res ) => { let buf = ''; process.stdin.setEncoding('utf8'); process.stdin.on('data', ( d ) => buf += d); process.stdin.on('end', () => res(buf)); });
276
+ (async () => {
277
+ let queries;
278
+ if ( positional && positional !== true ) queries = [String(positional)];
279
+ else {
280
+ if ( process.stdin.isTTY ) die('proxy needs a question: sg proxy "<question>" (or pipe questions on stdin, one per line)');
281
+ queries = (await readStdin()).split('\n').map(( s ) => s.trim()).filter(Boolean);
282
+ }
283
+ if ( !queries.length ) die('proxy: no questions given');
284
+ const onAnswer = ( row ) => {
285
+ if ( opts.json ) return;
286
+ if ( queries.length === 1 ) out(String(row.answer)); // single-shot: answer → stdout
287
+ else out('[' + (row.source === 'local' ? 'cache' : 'frontier') + '] ' + row.query + '\n → ' + String(row.answer));
288
+ };
289
+ const { results, metrics, saved } = await runProxySession({ proxy: px, queries, onAnswer });
290
+ if ( opts.json ) out(JSON.stringify({ results, metrics, saved }, null, 2));
291
+ else process.stderr.write(formatProxyReport(metrics, saved) + '\n');
292
+ process.exit(0);
293
+ })().catch(( e ) => die(e.message));
294
+ break;
295
+ }
296
+ case 'serve': {
297
+ // sg serve — the OpenAI-COMPATIBLE endpoint over the C6 proxy cache (roadmap FINIR, F1). Point any
298
+ // OpenAI client's baseURL at http://host:port/v1: a covered query is served from the local stock at
299
+ // 0 frontier calls, a miss escalates to the frontier + enriches the stock. Provenance rides EVERY
300
+ // completion (headers x-sg-served-from/-arm/-cost/-coverage/-saved + usage.sg_*); per-request lines
301
+ // → stderr; Ctrl-C prints the economy report. The handler/server logic lives in serve.js (stub-tested);
302
+ // only the GPU-bound model resolution lives here — same resolution as `sg proxy`.
303
+ // --frontier-model <gguf> | env FRONTIER_MODEL | env LLM_BASE the ground-truth generator (REQUIRED)
304
+ // --local-model <gguf> | env LOCAL_MODEL semantic coverage + coverage-check (opt-in)
305
+ // --store <file.json> durable stock across restarts
306
+ // --port N (default 4747) --host <addr> (default 127.0.0.1) --model <id> (advertised model id)
307
+ const opts = parseFlags(argv.slice(1));
308
+ const { createProxyCache, makeFrontierAsk, makeLocalCoverage } = require('../index.js').combos;
309
+ const { makeLocalAsk } = require('../providers/llm-local.js');
310
+
311
+ const frontierModel = opts['frontier-model'] ? String(opts['frontier-model']) : process.env.FRONTIER_MODEL;
312
+ let frontierChat;
313
+ if ( frontierModel ) frontierChat = makeLocalAsk({ modelPath: frontierModel, reasoningBudget: 0 });
314
+ else if ( process.env.LLM_BASE ) frontierChat = require('../providers/llm.js').makeAsk({ base: process.env.LLM_BASE, model: process.env.LLM_MODEL || 'claude-3-5-sonnet-20241022' });
315
+ else die('serve needs a FRONTIER backend: --frontier-model <gguf> (or env FRONTIER_MODEL), or set LLM_BASE');
316
+ const frontierAsk = makeFrontierAsk(frontierChat);
317
+
318
+ let semantic = {};
319
+ const localModel = opts['local-model'] ? String(opts['local-model']) : process.env.LOCAL_MODEL;
320
+ if ( localModel ) semantic = makeLocalCoverage({ localAsk: makeLocalAsk({ modelPath: localModel, reasoningBudget: 0 }) });
321
+
322
+ const px = createProxyCache(Object.assign({ frontierAsk, store: opts.store ? String(opts.store) : undefined, retention: true }, semantic));
323
+
324
+ // a long-running server defaults to VISIBLE request lines (--log-level warn silences them)
325
+ const { logger, consoleLevel } = buildLogger({ ...opts, 'log-level': opts['log-level'] || 'log' });
326
+ logger.addSink(createPlainSink({ stream: process.stderr, level: consoleLevel }));
327
+ process.stderr.write(banner(pkg.version, !!process.stderr.isTTY) + '\n');
328
+
329
+ const { createServeHandler, startServeServer } = require('./serve.js');
330
+ const { formatProxyReport } = require('./proxy-run.js');
331
+ const handler = createServeHandler({
332
+ proxy: px, model: opts.model ? String(opts.model) : undefined,
333
+ onAnswer: ( row ) => logger.log('[' + (row.source === 'local' ? 'cache' : 'frontier') + '] ' + row.query)
334
+ });
335
+ const port = Number(opts.port) || 4747;
336
+ const host = opts.host ? String(opts.host) : '127.0.0.1';
337
+ const srv = startServeServer({ handler, port, host, onReady: () => {
338
+ out('sg serve → http://' + host + ':' + port + '/v1 (OpenAI-compatible; point a client\'s baseURL here)');
339
+ } });
340
+ // --studio: the visual debugger in the SAME process. The studio server SHARES this logger, and a
341
+ // session's providerTrace falls back to the shared ring buffer when no graph is loaded — so the live
342
+ // request lines ([cache]/[frontier] per query) surface in the studio's provider-trace panel as-is.
343
+ if ( opts.studio ) {
344
+ const { createServer } = require('../studio/server.js');
345
+ const studioPort = Number(opts['studio-port']) || port + 1;
346
+ const ssrv = createServer({ Graph: require('../index.js'), root: process.cwd(), logger });
347
+ ssrv.listen(studioPort, () => out('sg studio (debug) → http://127.0.0.1:' + studioPort));
348
+ }
349
+ process.on('SIGINT', () => {
350
+ const m = px.metrics();
351
+ process.stderr.write('\n' + formatProxyReport(m, m.local) + '\n');
352
+ srv.close(() => process.exit(0));
353
+ setTimeout(() => process.exit(0), 500).unref();
354
+ });
355
+ break;
356
+ }
357
+ case 'mcp': {
358
+ // sg mcp — the MCP TOOLS server (stdio JSON-RPC; roadmap FINIR F2): the agentic surface, where the
359
+ // TYPED capabilities are tools (ask → answer OR a STRUCTURED typed refusal; lattice_load = learning
360
+ // through the version-gated admission; trace_tail = the debug contract). stdout is the PROTOCOL
361
+ // channel — banner/logs go to stderr ONLY. Two modes (same model resolution as ask/proxy):
362
+ // --concepts <dir> (+ --local-model <gguf> | env LOCAL_MODEL | LLM_BASE) TYPED appliance
363
+ // --frontier-model <gguf> | env FRONTIER_MODEL | env LLM_BASE C6 proxy
364
+ // [--local-model <gguf>] (proxy: semantic coverage) [--store <file.json>] durable stock
365
+ // [--stock <f.sgc>] ASSISTANT lanes over a forged methods stock: `hint` (certified menu, advisory)
366
+ // + `propose` (gated against the frozen referential, options enumerated).
367
+ // Register with a host: claude mcp add sg -- node bin/sg mcp --frontier-model <gguf> --store ./stock.json
368
+ const opts = parseFlags(argv.slice(1));
369
+ const { logger, consoleLevel } = buildLogger(opts);
370
+ logger.addSink(createPlainSink({ stream: process.stderr, level: consoleLevel }));
371
+ process.stderr.write(banner(pkg.version, !!process.stderr.isTTY) + '\n');
372
+
373
+ const { createMcpServer, defaultTools, startMcpStdio, stockWiring } = require('./mcp.js');
374
+ const wiring = { logger };
375
+ if ( opts.stock ) Object.assign(wiring, stockWiring(JSON.parse(require('fs').readFileSync(String(opts.stock), 'utf8'))));
376
+ const localModel = opts['local-model'] ? String(opts['local-model']) : process.env.LOCAL_MODEL;
377
+ if ( opts.concepts ) {
378
+ let backend;
379
+ if ( localModel ) backend = { localModel };
380
+ else if ( process.env.LLM_BASE ) backend = require('../providers/llm.js').makeAsk({ base: process.env.LLM_BASE, model: process.env.LLM_MODEL || 'claude-3-5-sonnet-20241022' });
381
+ else die('mcp --concepts needs an LLM backend: --local-model <gguf> (or env LOCAL_MODEL), or set LLM_BASE');
382
+ wiring.appliance = require('../index.js').combos.createAppliance({ concepts: opts.concepts, ask: backend, logger });
383
+ wiring.critiqueAsk = localModel // the C9 critical-mind tool runs on the same backend
384
+ ? require('../providers/llm-local.js').makeLocalAsk({ modelPath: localModel, reasoningBudget: 0 })
385
+ : backend;
386
+ } else {
387
+ const { createProxyCache, makeFrontierAsk, makeLocalCoverage } = require('../index.js').combos;
388
+ const { makeLocalAsk } = require('../providers/llm-local.js');
389
+ const frontierModel = opts['frontier-model'] ? String(opts['frontier-model']) : process.env.FRONTIER_MODEL;
390
+ let frontierChat;
391
+ if ( frontierModel ) frontierChat = makeLocalAsk({ modelPath: frontierModel, reasoningBudget: 0 });
392
+ else if ( process.env.LLM_BASE ) frontierChat = require('../providers/llm.js').makeAsk({ base: process.env.LLM_BASE, model: process.env.LLM_MODEL || 'claude-3-5-sonnet-20241022' });
393
+ else die('mcp needs a backend: --concepts <dir> (typed appliance) or --frontier-model <gguf> / env FRONTIER_MODEL / LLM_BASE (proxy)');
394
+ let semantic = {};
395
+ if ( localModel ) semantic = makeLocalCoverage({ localAsk: makeLocalAsk({ modelPath: localModel, reasoningBudget: 0 }) });
396
+ wiring.proxy = createProxyCache(Object.assign({ frontierAsk: makeFrontierAsk(frontierChat), store: opts.store ? String(opts.store) : undefined, retention: true }, semantic));
397
+ wiring.critiqueAsk = frontierChat; // the C9 critical-mind tool runs on the same backend
398
+ }
399
+ const server = createMcpServer({ tools: defaultTools(wiring), serverInfo: { name: 'skynet-graph', version: pkg.version } });
400
+ startMcpStdio({ server });
401
+ process.stderr.write('sg mcp ready — tools: ' + server.tools.map(( t ) => t.name).join(', ') + '\n');
402
+ break;
403
+ }
404
+ case 'flow': {
405
+ // sg flow run <module.js> — the C2 durable runner as a CLI (roadmap FINIR F5; the P4 «éventuel»).
406
+ // The MODULE owns the workflow (JS, not JSON — tasks are functions). It exports
407
+ // { spec, runTask | makeRunTask(), keyOf?, STREAM|stream? } — see examples/poc/durable-flow.js.
408
+ // --store <file.sqlite> crash-safe durable store (node:sqlite) --flow <id> (default 'flow')
409
+ // --resume reclaim in-flight tokens then drain (exactly-once) --json
410
+ const sub = argv[1];
411
+ const modPath = (argv[2] && !argv[2].startsWith('--')) ? argv[2] : null;
412
+ if ( sub !== 'run' || !modPath ) die('usage: sg flow run <module.js> [--store <file.sqlite>] [--flow <id>] [--resume] [--json]');
413
+ const opts = parseFlags(argv.slice(3));
414
+ let mod;
415
+ try { mod = require(require('path').resolve(modPath)); } catch ( e ) { die('cannot load ' + modPath + ': ' + e.message); }
416
+ const runTask = mod.runTask || (typeof mod.makeRunTask === 'function' ? mod.makeRunTask().runTask : null);
417
+ if ( !mod.spec || typeof runTask !== 'function' ) die('the module must export { spec, runTask | makeRunTask() } (see examples/poc/durable-flow.js)');
418
+ const stream = mod.STREAM || mod.stream || [];
419
+ const { createDurableRunner } = require('../index.js').combos;
420
+ const runner = createDurableRunner({ runTask, keyOf: mod.keyOf, store: opts.store ? String(opts.store) : undefined });
421
+ const flowId = opts.flow ? String(opts.flow) : 'flow';
422
+ (async () => {
423
+ const r = opts.resume ? await runner.resume(flowId, mod.spec) : await runner.run(flowId, mod.spec, stream);
424
+ const stats = runner.stats(flowId);
425
+ const a = runner.audit(flowId);
426
+ if ( opts.json ) out(JSON.stringify({ result: r, stats: stats, summary: a.summary }, null, 2));
427
+ else {
428
+ out('flow ' + flowId + (opts.resume ? ' (resumed)' : '') + ' — routed=' + (r.routed != null ? r.routed : '-')
429
+ + ' memoHits=' + (r.memoHits || 0) + ' done=' + (stats.done || 0));
430
+ out(a.summary);
431
+ }
432
+ runner.close();
433
+ process.exit(0);
434
+ })().catch(( e ) => die(e.message));
435
+ break;
436
+ }
437
+ case 'forge': {
438
+ // sg forge --adapter <file.js> [--data <p>] [--model <gguf>] [--vote N] [--classes a,b] [--per N]
439
+ // [--room <dir>] [--out <f.sgc>] [--dossier <f.md>] [--name <n>] [--version <v>] [--json]
440
+ // The M3 fabrication command: build a gold-verified .sgc stock + a validation dossier from a dataset,
441
+ // written into a ROOM — a durable SGC library dir (default SG-Rooms/lib1; --room picks another).
442
+ // The ADAPTER (JS) exports { stepEnum, loadClasses({data,classes,per}) -> {sig:[recs]}, decompose(ask,rec,o) }.
443
+ // No --model → the deterministic gold-forge (a dry run / dossier preview); --model <gguf> → the embedded
444
+ // model as the forge (add --vote N for consistencyVote). Exit 0 iff the dossier verdict passes.
445
+ const opts = parseFlags(argv.slice(1));
446
+ if ( !opts.adapter ) die('usage: sg forge --adapter <file.js> [--data <p>] [--model <gguf>] [--vote N] [--classes a,b] [--per N] [--room <dir>] [--out <f.sgc>] [--dossier <f.md>]');
447
+ const path = require('path'), fs = require('fs');
448
+ let adapter;
449
+ try { adapter = require(path.resolve(String(opts.adapter))); } catch ( e ) { die('cannot load adapter ' + opts.adapter + ': ' + e.message); }
450
+ if ( typeof adapter.loadClasses !== 'function' ) die('the adapter must export loadClasses({data,classes,per}) -> {sig:[recs{problem,goldSteps}]}');
451
+ const { forgeStock, dossierMarkdown } = require('../combos/forge.js');
452
+ const classesList = opts.classes ? String(opts.classes).split(',') : undefined;
453
+ const classes = adapter.loadClasses({ data: opts.data, classes: classesList, per: opts.per ? Number(opts.per) : undefined });
454
+ let ask = null, voters = null, modelName = 'gold-forge (deterministic)';
455
+ if ( opts.model ) {
456
+ const { makeLocalAsk } = require('../providers/llm-local.js');
457
+ const { createLocalModelHost } = require('../providers/local-host.js');
458
+ const host = createLocalModelHost({});
459
+ modelName = path.basename(String(opts.model));
460
+ if ( opts.vote ) voters = Array.from({ length: Number(opts.vote) }, ( _, i ) => makeLocalAsk({ modelPath: String(opts.model), reasoningBudget: 0, seed: i, host: host }));
461
+ else ask = makeLocalAsk({ modelPath: String(opts.model), reasoningBudget: 0, seed: 0, host: host });
462
+ }
463
+ (async () => {
464
+ const r = await forgeStock({ classes: classes, decompose: adapter.decompose, stepEnum: adapter.stepEnum,
465
+ ask: ask, voters: voters, name: opts.name || 'stock', version: opts.version || 'v1',
466
+ dataset: opts.dataset || opts.name || adapter.name || 'corpus', modelName: modelName, now: new Date().toISOString() });
467
+ // write the stock + dossier into the ROOM — a durable SGC library dir. Default SG-Rooms/lib1 (used
468
+ // by every forge); --room picks another dir; --out/--dossier override the individual file paths.
469
+ const room = opts.room != null ? String(opts.room) : 'SG-Rooms/lib1';
470
+ const base = String(opts.name || 'stock').replace(/[^A-Za-z0-9._-]/g, '_');
471
+ const outPath = opts.out ? String(opts.out) : path.join(room, base + '.sgc');
472
+ const dossierPath = opts.dossier ? String(opts.dossier) : path.join(room, base + '.dossier.md');
473
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
474
+ fs.writeFileSync(outPath, JSON.stringify(r.bundle, null, 1));
475
+ fs.mkdirSync(path.dirname(dossierPath), { recursive: true });
476
+ fs.writeFileSync(dossierPath, dossierMarkdown(r.dossier));
477
+ if ( opts.json ) out(JSON.stringify(r.dossier, null, 2));
478
+ else {
479
+ out('forge (' + r.dossier.model.forge + ') — admitted ' + r.verdict.admitted + '/' + r.verdict.attempted
480
+ + ' · FALSE admitted ' + r.verdict.falseAdmitted + ' · neg-control rejected=' + r.dossier.soundness.negControl.rejected);
481
+ out(' .sgc → ' + outPath + ' · dossier → ' + dossierPath);
482
+ out('verdict: ' + (r.verdict.pass ? 'PASS' : 'FAIL') + ' · sha ' + r.dossier.bundle.sha256.slice(0, 12));
483
+ }
484
+ process.exit(r.verdict.pass ? 0 : 1);
485
+ })().catch(( e ) => die(e.message));
486
+ break;
487
+ }
488
+ case 'trace': {
489
+ const a = loadArtifact(argv[1]);
490
+ out(table(summarizeTrace(a.records || []), ['n', 'rev', 'concept', 'target', 'kind', 'patch', 'ms']));
491
+ break;
492
+ }
493
+ case 'show': {
494
+ const a = loadArtifact(argv[1]);
495
+ const n = Number(argv[2]);
496
+ if ( !Number.isInteger(n) ) die('show needs a record index: sg show <file> <n>');
497
+ out(formatRecord((a.records || [])[n]));
498
+ break;
499
+ }
500
+ case 'concepts': {
501
+ const a = loadArtifact(argv[1]);
502
+ out(table(perConcept(a.records || []), ['concept', 'count', 'totalMs']));
503
+ break;
504
+ }
505
+ case 'errors': {
506
+ const a = loadArtifact(argv[1]);
507
+ const errs = errorRecords(a.records || []);
508
+ out(errs.length ? errs.map(( r ) => formatRecord(r)).join('\n\n') : '(no errored applies)');
509
+ break;
510
+ }
511
+ case 'validate': {
512
+ // sg validate <conceptsDir> — the grammar author's pre-flight (the sandbox CLI companion):
513
+ // author-time validation of every concept set under <dir> (unknown refs, prose on dependency
514
+ // edges, unparseable exprs, missing _name…). Exit 0 = no errors; --strict also fails on warnings.
515
+ const dir = argv[1];
516
+ if ( !dir || String(dir).startsWith('--') ) die('usage: sg validate <conceptsDir> [--strict] [--verbose]');
517
+ const vopts = parseFlags(argv.slice(2));
518
+ const { loadConceptMap } = require('../load.js');
519
+ const { validateConceptTree } = require('../authoring/validate.js');
520
+ let map;
521
+ try { map = loadConceptMap(String(dir)); } catch ( e ) { die('cannot load ' + dir + ': ' + e.message); }
522
+ const sets = Object.keys(map);
523
+ if ( !sets.length ) die('no concept sets under ' + dir);
524
+ let nErr = 0, nWarn = 0;
525
+ for ( const set of sets ) {
526
+ const v = validateConceptTree(map[set], {});
527
+ nErr += v.errors.length; nWarn += v.warnings.length;
528
+ out(set + ': ' + (v.errors.length ? v.errors.length + ' error(s)' : 'OK')
529
+ + (v.warnings.length ? ' · ' + v.warnings.length + ' warning(s)' : ''));
530
+ for ( const e of v.errors ) out(' ✖ [' + e.concept + '] ' + e.kind + ' — ' + e.message);
531
+ if ( vopts.verbose || vopts.strict )
532
+ for ( const w of v.warnings ) out(' ⚠ [' + w.concept + '] ' + w.kind + ' — ' + w.message);
533
+ }
534
+ process.exit(nErr || (vopts.strict && nWarn) ? 1 : 0);
535
+ break;
536
+ }
537
+ case 'methods': {
538
+ const a = loadArtifact(argv[1]);
539
+ const { describeLibrary, formatLibrary } = require('../authoring/method-explorer.js');
540
+ const source = (a && a.format === 'sgc' && a.kind === 'methods') ? (a.methods || []) : a; // a .sgc methods bundle or a bare library
541
+ let registry = null;
542
+ const ri = argv.indexOf('--registry');
543
+ if ( ri !== -1 && argv[ri + 1] ) {
544
+ try { const rf = JSON.parse(fs.readFileSync(argv[ri + 1], 'utf8')); registry = rf && rf.registry ? rf.registry : rf; } // a .sgc lattice bundle or a bare registry
545
+ catch ( e ) { die('cannot read registry ' + argv[ri + 1] + ': ' + e.message); }
546
+ }
547
+ out(formatLibrary(describeLibrary(source, registry ? { registry } : {})));
548
+ break;
549
+ }
550
+ default:
551
+ out([
552
+ 'sg— Skynet-Graph CLI',
553
+ ' sg studio [--root <dir>] [--port N] [--open] [--log-level <lvl>] [--log-plain] [--log-file <path>]',
554
+ ' visual debug/run UI; graph logs stream to the terminal (bottom status bar on a TTY,',
555
+ ' or --log-plain for log-only with periodic stats lines)',
556
+ ' sg run --concepts <dir> [--providers <dir>] [--builtins] [--sets a,b]',
557
+ ' [--seed <file>] [--trace <out>] [--json] [--timeout <ms>]',
558
+ ' [--log-level error|warn|log|info|verbose] [--log-mode dashboard|plain]',
559
+ ' [--log-plain] [--log-file <path>] [--log-file-level <level>]',
560
+ ' sg ask "<question>" [--concepts <dir>] [--local-model <gguf> | (env LLM_BASE)] [--depth N] [--json]',
561
+ ' typed QA appliance with --concepts (intake→reason→typed refusal→memo; a refusal',
562
+ ' names the missing requirement); legacy best-effort loop without it. Embedded',
563
+ ' local model or LLM_BASE; answer → stdout, logs → stderr',
564
+ ' sg validate <conceptsDir> [--strict] [--verbose]',
565
+ ' author-time validation of your concept grammar(s) — unknown refs, prose on',
566
+ ' dependency edges, unparseable exprs. Exit 0 = clean (the sandbox pre-flight)',
567
+ ' sg proxy ["<question>"] --frontier-model <gguf> | (env FRONTIER_MODEL | LLM_BASE)',
568
+ ' [--local-model <gguf>] [--store <file.json>] [--json]',
569
+ ' local-first proxy cache (C6): a covered query is served from the local stock at',
570
+ ' 0 frontier calls, a miss escalates + enriches. 0 hallucination (verified stock or',
571
+ ' escalate), no false neg. No question → reads a session from stdin (one/line).',
572
+ ' --local-model adds semantic coverage (a paraphrase hits the stock). Economy → stderr',
573
+ ' sg serve --frontier-model <gguf> | (env FRONTIER_MODEL | LLM_BASE)',
574
+ ' [--local-model <gguf>] [--store <file.json>] [--port N] [--host <addr>] [--model <id>]',
575
+ ' [--studio [--studio-port N]] (the visual debugger in the same process: live request',
576
+ ' lines in its provider-trace panel — default port+1)',
577
+ ' OpenAI-COMPATIBLE endpoint over the C6 proxy: point any OpenAI client baseURL at',
578
+ ' http://host:port/v1 — a covered query is served from the local stock at 0 frontier',
579
+ ' calls, a miss escalates + enriches. Provenance headers x-sg-* + usage.sg_* on every',
580
+ ' completion; per-request lines → stderr; Ctrl-C prints the economy report',
581
+ ' sg mcp --concepts <dir> | --frontier-model <gguf> | (env FRONTIER_MODEL | LLM_BASE)',
582
+ ' [--local-model <gguf>] [--store <file.json>]',
583
+ ' MCP tools server (stdio JSON-RPC) for agent hosts: ask (answer OR a STRUCTURED',
584
+ ' typed refusal naming the missing requirement), drift/metrics, lattice_load (learning',
585
+ ' through the version-gated admission — no direct-write tool), methods_describe,',
586
+ ' lattice_rings, trace_tail. Register: claude mcp add sg -- node bin/sg mcp …',
587
+ ' sg flow run <module.js> [--store <file.sqlite>] [--flow <id>] [--resume] [--json]',
588
+ ' run a durable workflow (C2): the module exports { spec, runTask|makeRunTask(),',
589
+ ' keyOf?, STREAM? }. --store = crash-safe SQLite; --resume reclaims in-flight',
590
+ ' tokens then drains (exactly-once). Prints economy + the audit summary',
591
+ ' sg trace <file> list concept-applies',
592
+ ' sg show <file> <n> detail of record n (prompt/reply/patch/why)',
593
+ ' sg concepts <file> per-concept rollup (count + total ms)',
594
+ ' sg errors <file> applies that flagged an llmError',
595
+ ' sg methods <file.sgc> [--registry <lattice.sgc>]',
596
+ ' list a concept-method population (title/category/description) + judge it:',
597
+ ' class distribution, OPENNESS (distinct classes, singletons, entropy) and',
598
+ ' COVERAGE vs a declared vocabulary (which task-classes have a method / the GAPS)'
599
+ ].join('\n'));
600
+ process.exit(cmd ? 1 : 0);
601
+ }