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,53 @@
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
+ * Geo provider — great-circle distance between two node Positions.
21
+ *
22
+ * Concept wiring (see concepts/common/Edge/Distance.json):
23
+ * { "require": ["originNode:Position", "targetNode:Position"],
24
+ * "provider": ["CommonGeo::Distance"] }
25
+ *
26
+ * The provider signature is the engine's: (graph, concept, scope, argz, cb),
27
+ * cb(err, mutationTemplate). It emits `{ $_id:'_parent', Distance:{ inKm } }`.
28
+ */
29
+
30
+ /**
31
+ * Haversine great-circle distance in kilometres.
32
+ * @param a {lat,lng} in degrees
33
+ * @param b {lat,lng} in degrees
34
+ * @returns {number} km
35
+ */
36
+ function haversineKm( a, b ) {
37
+ var R = 6371, toR = function ( x ) { return (x * Math.PI) / 180; };
38
+ var dLat = toR(b.lat - a.lat), dLng = toR(b.lng - a.lng);
39
+ var s = Math.sin(dLat / 2) ** 2 +
40
+ Math.cos(toR(a.lat)) * Math.cos(toR(b.lat)) * Math.sin(dLng / 2) ** 2;
41
+ return 2 * R * Math.asin(Math.sqrt(s));
42
+ }
43
+
44
+ var CommonGeo = {
45
+ Distance: function ( graph, concept, scope, argz, cb ) {
46
+ var p1 = graph.getRef('originNode:Position', scope),
47
+ p2 = graph.getRef('targetNode:Position', scope);
48
+ if ( !p1 || !p2 ) return cb(null, null);// wait until both positions exist
49
+ cb(null, { $_id: '_parent', Distance: { inKm: Math.round(haversineKm(p1, p2)) } });
50
+ }
51
+ };
52
+
53
+ module.exports = { CommonGeo: CommonGeo, haversineKm: haversineKm };
@@ -0,0 +1,116 @@
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
+ * Base providers, packaged for host opt-in.
21
+ *
22
+ * The engine deliberately does NOT auto-wire providers (Graph._providers is left
23
+ * to the host — see Graph.js). This package gives a host a one-liner instead of
24
+ * hand-rolling the glue:
25
+ *
26
+ * const Graph = require('skynet-graph');
27
+ * const { register, CommonGeo, createLLMProvider } = require('skynet-graph/providers');
28
+ * register(Graph, [ { CommonGeo }, createLLMProvider({ ask: myBackend }) ]);
29
+ *
30
+ * register(Graph) with no selection wires the defaults (Geo + a default LLM client).
31
+ */
32
+ var geo = require('./geo');
33
+ var llm = require('./llm');
34
+ var intake = require('./intake');
35
+ var reasonLoop = require('./reason-loop');
36
+ var canonicalize = require('./canonicalize');
37
+ var verify = require('./verify');
38
+ var mergeConsistency = require('./merge-consistency');
39
+ var solverFork = require('./solver-fork');
40
+ var stats = require('./stats');
41
+ var nogood = require('./nogood');
42
+ var semiring = require('./semiring');
43
+ var constat = require('./constat');
44
+ var cache = require('./cache');
45
+
46
+ /**
47
+ * Merge provider-map fragments onto Graph._providers (preserving any already set).
48
+ * @param Graph the engine constructor (providers live on graph.static._providers)
49
+ * @param fragments array of `{ NamespaceName: { fn, ... } }` maps;
50
+ * omitted -> defaults: [ { CommonGeo }, createLLMProvider() ]
51
+ * @returns the Graph (for chaining)
52
+ */
53
+ function register( Graph, fragments ) {
54
+ if ( !fragments ) fragments = [{ CommonGeo: geo.CommonGeo }, llm.createLLMProvider()];
55
+ Graph._providers = Object.assign.apply(Object, [{}, Graph._providers || {}].concat(fragments));
56
+ return Graph;
57
+ }
58
+
59
+ module.exports = {
60
+ CommonGeo : geo.CommonGeo,
61
+ haversineKm : geo.haversineKm,
62
+ createLLMProvider: llm.createLLMProvider,
63
+ createIntake : intake.createIntake,
64
+ createReasonLoop : reasonLoop.createReasonLoop, // the packaged AI:: answer-loop set that drives concepts/_substrate end-to-end (+ the Intake→Task bridge AI::seedTask)
65
+ makeAsk : llm.makeAsk,
66
+ makeLocalAsk : require('./llm-local').makeLocalAsk, // in-process GGUF `ask` (node-llama-cpp; native dep loaded lazily, opt-in)
67
+ createLocalModelHost : require('./local-host').createLocalModelHost, // centralized embedded-inference host (load-once + prompt cache + VRAM/LRU)
68
+ sharedLocalModelHost : require('./local-host').sharedLocalModelHost, // the process-wide shared host makeLocalAsk delegates to
69
+ makeBackend : require('./backends').makeBackend, // R0: preset/config layer over the ask-makers (a NAME + key -> chat ask)
70
+ resolveBackend : require('./backends').resolveBackend, // normalize a spec -> descriptor (precedence spec > env > preset; carries the egress flag)
71
+ listBackends : require('./backends').listBackends, // secret-free preset catalogue (for a dropdown / help)
72
+ parseJSON : llm.parseJSON,
73
+ canonFacts : canonicalize.canonFacts,
74
+ canonValue : canonicalize.canonValue,
75
+ compileEnumMap : canonicalize.compileEnumMap, // G-1: the confluent enum+synonym-ring map (curated-thesaurus core)
76
+ digest : canonicalize.digest,
77
+ makeBorderlineSnap: require('./borderline').makeBorderlineSnap, // G-1: the borderline-only LLM gate (LAST resort, re-canonicalized, propose-only)
78
+ borderlineFacts : require('./borderline').borderlineFacts,
79
+ enumGbnf : require('./borderline').enumGbnf,
80
+ createVerifier : verify.createVerifier,
81
+ checks : verify.checks,
82
+ majority : verify.majority,
83
+ mergeConsistency : mergeConsistency.mergeConsistency,
84
+ consistencyBandOf: mergeConsistency.bandOf,
85
+ createConsistency: mergeConsistency.createConsistency,
86
+ consistencyConceptTree: mergeConsistency.consistencyConceptTree,
87
+ createSolver : solverFork.createSolver,
88
+ snappedFrontier : solverFork.snappedFrontier,
89
+ solverConceptTree: solverFork.solverConceptTree,
90
+ backtrackColoring: solverFork.backtrackColoring,
91
+ createStats : stats.createStats,
92
+ shrink : stats.shrink,
93
+ empiricalBayesKappa: stats.empiricalBayesKappa,
94
+ reliabilityBandOf: stats.bandOf,
95
+ shrinkageConceptTree: stats.shrinkageConceptTree,
96
+ createNogood : nogood.createNogood,
97
+ recordNogood : nogood.recordNogood,
98
+ guardTrial : nogood.guardTrial,
99
+ nogoodGuardConcept: nogood.nogoodGuardConcept,
100
+ reduceSemiring : semiring.reduceSemiring,
101
+ createSemiring : semiring.createSemiring,
102
+ semiringConceptTree: semiring.semiringConceptTree,
103
+ SEMIRINGS : semiring.SEMIRINGS,
104
+ selectConceptTree: semiring.selectConceptTree,
105
+ paretoFront : semiring.paretoFront,
106
+ paretoSelect : semiring.paretoSelect,
107
+ makePareto : semiring.makePareto,
108
+ dominates : semiring.dominates,
109
+ createConstat : constat.createConstat,
110
+ recordConstat : constat.recordConstat,
111
+ buildConstat : constat.buildConstat,
112
+ CONSTAT_FIELDS : constat.CONSTAT_FIELDS,
113
+ createProviderCache: cache.createProviderCache,
114
+ keyFromScope : cache.keyFromScope,
115
+ register : register
116
+ };
@@ -0,0 +1,263 @@
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
+ * Intake — C0, the prose→typed FRONT DOOR (roadmap §3.2; the runtime realization of the
21
+ * published P4 boundary, `artifact/paper-dll/p4-coverage.js`).
22
+ *
23
+ * It is the ONE place free-text prose crosses into the typed world, and the system's
24
+ * declared SOUNDNESS BOUNDARY. `Intake::type` is a specialization of `LLM::complete`
25
+ * (it reuses the SAME canonicalization barrier, `canonicalize.js`) plus three additions
26
+ * that make the boundary *gated and visible* rather than silently crossed:
27
+ *
28
+ * 1. a DISCRETE gate fact `IntakeStatus ∈ typed | partial | untyped` — downstream
29
+ * concepts MUST `ensure:["$IntakeStatus=='typed'"]`. A `required` key that came back
30
+ * out-of-vocabulary makes the intake `untyped` (decision-bearing miss); an optional-
31
+ * only miss makes it `partial`; a clean snap makes it `typed`.
32
+ * 2. a MISS-AWARE `<name>FactsDigest`: emitted ONLY when `typed`. This closes a latent
33
+ * false-memo-hit (see "Invariant 2" below).
34
+ * 3. an optional independent BACK-CHECK (verify.js discipline): a failed back-check
35
+ * downgrades to `untyped` even with a clean snap.
36
+ *
37
+ * Two non-negotiables (CLAUDE.md "Typed-fact discipline"; doc/MODELISATION.md §4.2):
38
+ * • never let prose silently RE-KEY the memo (K1 fragmentation), and
39
+ * • never FALSE-ADMIT a prose-derived value as if typed.
40
+ *
41
+ * ── Invariant 2 (the load-bearing soundness fix) ────────────────────────────────────────
42
+ * `LLM::complete` digests `cf.facts` unconditionally (llm.js). When a key came back
43
+ * out-of-vocabulary, `canonicalize.canonValue` returns its *default* (e.g. `null`), so two
44
+ * textually-distinct, semantically-DIFFERENT out-of-vocab inputs both produce `{k:null}` →
45
+ * the SAME `digest` → a latent false memo hit (the next same-default record reuses the wrong
46
+ * typing). C0 refuses to mint a reusable digest unless the projection is faithful (`typed`).
47
+ *
48
+ * Note (a critique that mattered): folding the miss-key list into the digest —
49
+ * `digest({...facts, __miss: misses})` — is NOT a sufficient fix. Two distinct inputs that
50
+ * miss the SAME required key produce identical `{k:null, __miss:[k]}` and STILL collide. The
51
+ * only sound horn is "no reusable digest when not typed": out-of-vocabulary genuinely is not
52
+ * K1-canonicalizable, so the typed projection is not a valid key for it. The CanonMiss marker
53
+ * additionally makes such a result un-cacheable (cache.js#defaultCanonMiss), so a non-typed
54
+ * intake is never stored and never collides in the provider cache either.
55
+ *
56
+ * Degradation (= P4's "non-K1 degrades COST not CORRECTNESS"):
57
+ * fully in-vocab → typed / digest emitted / memoized / 0 repeat-cost
58
+ * optional miss only → partial / no digest / CanonMiss→un-cacheable / 1 call + escalate
59
+ * required miss / bad → untyped / no digest / CanonMiss→un-cacheable / 1 call + escalate
60
+ * + raw prose preserved
61
+ *
62
+ * Escalation (widen vocab / micro-LLM / human / vocab-extension) is HOST-OWNED by design:
63
+ * the boundary is made visible (`IntakeStatus`, `<name>CanonMiss`, the preserved prose); what
64
+ * to do about a miss is a host policy, routed off the discrete status fact.
65
+ *
66
+ * Concept wiring:
67
+ * { "require": ["rawText"], "provider": ["Intake::type"],
68
+ * "prompt": { "system": "...", "user": "${rawText}",
69
+ * "facts": { "severity": { "enum": ["low","high"] } }, // CLOSED vocab
70
+ * "prose": "intakeNarrative" }, // untracked remainder
71
+ * "intake": { "required": ["severity"] } } // decision-bearing keys
72
+ * downstream: { "require": ["Intake"], "ensure": ["$IntakeStatus=='typed'"], ... }
73
+ */
74
+
75
+ var canonicalize = require('./canonicalize');
76
+ var llm = require('./llm');
77
+
78
+ /**
79
+ * Build the Intake provider-map fragment.
80
+ * @param opts.ask async ({system,user,maxTokens}) -> string. Defaults to the bundled client.
81
+ * @param opts.parseJSON JSON-salvage fn. Defaults to llm.parseJSON.
82
+ * @param opts.backCheck optional independent verifier (graph,concept,scope,facts,raw) ->
83
+ * bool | 'pass' | 'fail' | Promise<…>. A falsy/'fail' verdict downgrades
84
+ * the intake to `untyped` (the typed projection is not trustworthy).
85
+ * Apply verify.js independence discipline (don't grade with the same call).
86
+ * @param opts.namespace provider namespace key. Default 'Intake'.
87
+ * @returns { Intake: { type } }
88
+ */
89
+ function createIntake( opts ) {
90
+ opts = opts || {};
91
+ var ask = opts.ask || llm.makeAsk(opts),
92
+ _parseJSON = opts.parseJSON || llm.parseJSON,
93
+ backCheck = opts.backCheck,
94
+ // G-1: an optional (factsSchema)->resolvedSchema seam so a concept can REFERENCE registry keys (`{ref:'severity'}`)
95
+ // instead of inlining the enum+ring — the host wires it (`resolveFacts: fs => registry.resolveFactsSchema(fs,reg).facts`)
96
+ // WITHOUT intake (a provider) depending on the authoring registry (no layer inversion). Default: identity.
97
+ resolveFacts = opts.resolveFacts || function ( fs ) { return fs; },
98
+ // G-1 rung-2 CONNECTIVE INGEST (host opt-in, default absent → the front door behaves exactly as before): the
99
+ // borderline-only LLM gate (`makeBorderlineSnap(...)`). On a barrier CanonMiss it (a) yields a PROVISIONAL member
100
+ // for THIS run's dispatch — surfaced on the untracked `<name>Borderline` audit fact, kept OUT of the typed spine /
101
+ // digest so it stays un-cacheable (the miss is unchanged for status/digest) — and (b) emits a propose-only ring
102
+ // proposal that we DEPOSIT into the autonomous convergence loop (a fresh proxy node) so the exogenous vocabulary
103
+ // grows. SOUNDNESS (Laurie confront): the load is on model-INDEPENDENT floors (provisional-un-cacheable here +
104
+ // retractRingAlias de-lock + provenance in the registry), not on trusting the oracle; the model is a tunable.
105
+ borderlineSnap = opts.borderlineSnap,
106
+ // the proxy-node deposit template. Default inline (intake stays standalone — no authoring dep); a host may wire
107
+ // `proposalTemplate: registry.proposalTemplate` for a single source of truth. Fields = the RegistryMerge contract.
108
+ proposalTemplate = opts.proposalTemplate || function ( p, id ) {
109
+ return { $$_id: id, proposalKey: p.key, proposalAlias: p.alias, proposalMember: p.member, proposalVia: p.via };
110
+ },
111
+ namespace = opts.namespace || 'Intake';
112
+
113
+ var ns = {};
114
+ ns[namespace] = {
115
+ type: function ( graph, concept, scope, argz, cb ) {
116
+ // schema lives under `prompt` (system/user/facts/prose — so validate.js recognizes the
117
+ // prose contract) + the NEW bits under `intake` (required/statusKey); argz overrides both.
118
+ var cfg = Object.assign({}, concept._schema && concept._schema.prompt,
119
+ concept._schema && concept._schema.intake,
120
+ argz && argz[0]),
121
+ name = concept._name,
122
+ sys = llm.interpolate(cfg.system, graph, scope),
123
+ usr = llm.interpolate(cfg.user, graph, scope),
124
+ required = cfg.required || [],
125
+ statusKey = cfg.statusKey || 'IntakeStatus';
126
+
127
+ Promise.resolve()
128
+ .then(function () { return ask({ system: sys, user: usr, maxTokens: cfg.maxTokens }); })
129
+ .then(function ( txt ) {
130
+ graph.traceProvider && graph.traceProvider(concept, scope, { prompt: { system: sys, user: usr }, reply: txt });
131
+
132
+ var facts = { $_id: '_parent' };
133
+ facts[name] = true; // self-flag (provider-cast-marker gotcha)
134
+
135
+ // --- the canonicalization barrier (REUSED, not re-implemented) ---
136
+ // resolve any `{ref:'key'}` fact specs against the registry (host-wired seam) → the ring lives in the registry.
137
+ var raw = _parseJSON(txt),
138
+ rfacts = resolveFacts(cfg.facts || {}),
139
+ cf = canonicalize.canonFacts(raw, rfacts);
140
+ Object.assign(facts, cf.facts); // ONLY declared discrete keys, snapped -> TRACKED
141
+ var misses = cf.misses;
142
+ if ( misses.length ) facts[name + 'CanonMiss'] = misses; // visible + fail-closed (untracked, un-cacheable)
143
+
144
+ // the terminal, UNTRACKED free text (stays in the model)
145
+ var proseKey = cfg.prose || (name + 'Prose'),
146
+ proseVal = cfg.proseFrom != null ? raw[cfg.proseFrom]
147
+ : (raw && raw.prose != null ? raw.prose : txt);
148
+ facts[proseKey] = proseVal;
149
+
150
+ // a required key that came back out-of-vocab = a DECISION-BEARING miss
151
+ var reqMiss = required.filter(function ( k ) { return misses.indexOf(k) !== -1; });
152
+ // surface the required subset explicitly (untracked, un-cacheable): `<name>CanonMiss` carries
153
+ // ALL misses (required+optional); `<name>Missing` is the decision-bearing subset a typed REFUSAL
154
+ // names ("missing required: [...]"). Reading it downstream is a pure fact-read, no re-derivation.
155
+ if ( reqMiss.length ) facts[name + 'Missing'] = reqMiss;
156
+
157
+ // --- G-1 rung-2 CONNECTIVE INGEST: consult the borderline gate on the barrier MISSES only (LAST resort) ---
158
+ // The provisional resolutions ride on `<name>Borderline` (audit + dispatch) but the keys STAY misses, so
159
+ // the typed-spine status/digest below are UNCHANGED (un-cacheable — the guess never mints typed truth on
160
+ // THIS run; the ring-grown convergence, if any, lands on a LATER run through the deterministic barrier).
161
+ // Each proposal is deposited on a fresh proxy node → the autonomous RegistryMerge loop admits it.
162
+ var targetId = (scope && scope._ && scope._._id) || (concept && concept._id),
163
+ proposalNodes = [];
164
+ var borderStep = (!borderlineSnap || !misses.length) ? Promise.resolve() : (function () {
165
+ var provisional = [];
166
+ return misses.reduce(function ( chain, key ) {
167
+ return chain.then(function () {
168
+ var spec = rfacts[key] || {}, src = spec.from != null ? spec.from : key;
169
+ return Promise.resolve(borderlineSnap(raw[src], spec)).then(function ( r ) {
170
+ if ( !r || r.miss || !r.provisional ) return; // still out-of-vocab → the existing CanonMiss escalation
171
+ provisional.push({ key: key, member: r.value }); // best-effort dispatch value (un-cacheable)
172
+ if ( r.proposal ) proposalNodes.push(proposalTemplate(
173
+ { key: key, alias: r.proposal.alias, member: r.proposal.member, via: r.via || 'llm-borderline' },
174
+ name + '-prop-' + targetId + '-' + key)); // stable id per (node,concept,key) → idempotent, one fire
175
+ });
176
+ });
177
+ }, Promise.resolve()).then(function () {
178
+ if ( provisional.length ) facts[name + 'Borderline'] = provisional; // UNTRACKED audit fact (keys remain misses)
179
+ });
180
+ })();
181
+
182
+ // --- optional independent back-check (verify.js discipline) ---
183
+ return borderStep
184
+ .then(function () { return backCheck ? backCheck(graph, concept, scope, cf.facts, raw) : undefined; })
185
+ .then(function ( bc ) {
186
+ var verified = (bc === undefined) ? undefined : (bc === true || bc === 'pass');
187
+ if ( verified !== undefined ) facts[name + 'Verified'] = verified ? 'pass' : 'fail';
188
+
189
+ // --- the discrete gate: typed | partial | untyped (borderline provisionals do NOT clear a miss) ---
190
+ var status = (reqMiss.length || verified === false) ? 'untyped'
191
+ : (misses.length ? 'partial' : 'typed');
192
+ facts[statusKey] = status;
193
+
194
+ // --- Invariant 2: a REUSABLE digest is minted ONLY for a faithful (typed) projection ---
195
+ if ( cfg.digest !== false && status === 'typed' ) facts[name + 'FactsDigest'] = canonicalize.digest(cf.facts);
196
+
197
+ // deposit the propose-only ring proposals alongside the _parent mutation (array template)
198
+ cb(null, proposalNodes.length ? [facts].concat(proposalNodes) : facts);
199
+ });
200
+ })
201
+ .catch(function ( e ) {
202
+ var facts = { $_id: '_parent', llmError: e.message };
203
+ facts[name] = true;
204
+ facts[cfg && cfg.statusKey || 'IntakeStatus'] = 'untyped'; // an errored intake is never 'typed'
205
+ cb(null, facts);
206
+ });
207
+ }
208
+ };
209
+ return ns;
210
+ }
211
+
212
+ /**
213
+ * The DEPTH back-check (closes C0 "back-check depth", roadmap FINIR F5) — a ready-made INDEPENDENT verifier
214
+ * of the typed projection against its source PROSE, pluggable straight into `createIntake({ backCheck })`.
215
+ * CEGIS-style re-check: a SEPARATE model call (verify.js independence discipline — never grade with the call
216
+ * that produced the extraction; ideally a different model) is shown the prose and the canonicalized facts and
217
+ * must judge faithfulness per key. A 'fail' downgrades the intake to `untyped` (Invariant 2: a doubted
218
+ * projection never mints a reusable digest); the judged-wrong keys land on the scope via `opts.onBlame` for
219
+ * localized blame (which key to re-extract), not just a global veto.
220
+ *
221
+ * PROSE SOURCE: `opts.proseOf(scope, concept)` — default reads the common inbound keys off the target object
222
+ * (`prose`, `text`, `message`, `q`). When no prose is found the check ABSTAINS (returns undefined → the hook
223
+ * records no verdict) rather than vetoing blind — wire proseOf explicitly on a non-standard shape.
224
+ * A checker-call REJECTION propagates (the intake's catch → llmError + `untyped`): fail-closed, never certify
225
+ * on a broken verifier. An unparseable verdict is a 'fail' (can't verify → don't trust).
226
+ *
227
+ * @param opts.ask REQUIRED async ({system,user,maxTokens}) -> text — the independent checker.
228
+ * @param opts.proseOf (scope, concept) -> string — where the source prose lives (see default above).
229
+ * @param opts.system override the strict-verifier prompt.
230
+ * @param opts.onBlame optional (wrongKeys, scope, concept) => void — receives the judged-wrong keys.
231
+ * @param opts.maxTokens verdict budget (default 96).
232
+ * @returns a `backCheck(graph, concept, scope, facts, raw)` -> Promise<'pass'|'fail'|undefined>
233
+ */
234
+ function makeProseBackCheck( opts ) {
235
+ opts = opts || {};
236
+ var ask = opts.ask;
237
+ if ( typeof ask !== 'function' ) throw new Error('makeProseBackCheck needs opts.ask (an INDEPENDENT checker call)');
238
+ var _parseJSON = opts.parseJSON || llm.parseJSON;
239
+ var maxTokens = opts.maxTokens || 96;
240
+ var system = opts.system || 'You are a strict verifier. Given a PROSE statement and TYPED FACTS extracted from it, '
241
+ + 'judge whether EVERY fact value is supported by the prose. Reply ONLY the JSON '
242
+ + '{"faithful": true|false, "wrong": ["<factKey>", ...]} — wrong lists the unsupported keys. '
243
+ + 'Judge only; never correct the facts.';
244
+ var proseOf = opts.proseOf || function ( scope ) {
245
+ var o = scope && scope._ || {};
246
+ return String(o.prose || o.text || o.message || o.q || '').trim();
247
+ };
248
+ return function backCheck( graph, concept, scope, facts /*, raw */ ) {
249
+ var prose = proseOf(scope, concept);
250
+ if ( !prose ) return Promise.resolve(undefined); // nothing to check against → abstain, never veto blind
251
+ return ask({ system: system, user: 'PROSE: ' + prose + '\nTYPED FACTS: ' + JSON.stringify(facts), maxTokens: maxTokens })
252
+ .then(function ( reply ) {
253
+ var v;
254
+ try { v = _parseJSON(reply); } catch ( e ) { v = null; }
255
+ if ( !v || typeof v.faithful !== 'boolean' ) return 'fail'; // unverifiable → don't trust
256
+ var wrong = Array.isArray(v.wrong) ? v.wrong : [];
257
+ if ( (!v.faithful || wrong.length) && typeof opts.onBlame === 'function' ) opts.onBlame(wrong, scope, concept);
258
+ return (v.faithful && !wrong.length) ? 'pass' : 'fail';
259
+ });
260
+ };
261
+ }
262
+
263
+ module.exports = { createIntake: createIntake, makeProseBackCheck: makeProseBackCheck };
@@ -0,0 +1,88 @@
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
+ * llm-local — an IN-PROCESS `ask` backend (node-llama-cpp / GGUF) for the LLM provider seam, so the library runs its
10
+ * small functional model(s) itself — a self-contained reasoning appliance, no external HTTP endpoint (2026-07-01
11
+ * embedded-inference study). ZERO-CORE: the ONLY model coupling is `ask({system,user,maxTokens,temperature}) -> string`
12
+ * (mirrors `llm.js#makeOpenAIAsk`); this is a drop-in `ask` for `createLLMProvider({ ask })`.
13
+ *
14
+ * node-llama-cpp is an OPTIONAL native dep (precedent: the durable layer's `node:sqlite`) — declared in
15
+ * `optionalDependencies`, loaded via dynamic import (v3 is ESM-only), so a host that never wires a local model never
16
+ * needs it. GPU auto-detected (CUDA/Metal/Vulkan) with a CPU fallback; the no-build/browser sibling is `wllama` (same
17
+ * GGUF, WASM) — a future `makeWasmAsk`.
18
+ *
19
+ * THE CROWN JEWEL — CONSTRAINED DECODING. `opts.jsonSchema` (or `opts.gbnf`) forces the model to emit a grammar-valid
20
+ * token at every step: a small (noisy) model then CANNOT produce a malformed typed fact → the canonicalization barrier
21
+ * (`prompt.facts`) is enforced at the DECODE level, so the K1 memo keys stay stable (the signature-stability lever).
22
+ * Grammar guarantees valid FORMAT, not correct CONTENT — keep the C-contract / verify pass as the content check.
23
+ *
24
+ * Per-concept MULTI-MODEL: register several namespaces, each its own `makeLocalAsk` (a specialist GGUF, or one base
25
+ * model + a per-context LoRA adapter) — `createLLMProvider({ namespace:'CLASSIFY', ask: makeLocalAsk({...}) })`. Small
26
+ * local models at the leaves; reserve a bigger/remote model for the META/supervisor tier (its judgment is weak at 3B).
27
+ *
28
+ * CENTRALIZED (see `local-host.js`): every `makeLocalAsk` is a thin handle over a process-wide shared host that owns
29
+ * the GPU-resident model(s) + an in-memory prompt cache. So N handles / namespaces of the SAME gguf share ONE VRAM
30
+ * load, several constrained grammars share that load (grammar is applied per-call), identical deterministic (temp-0)
31
+ * prompts are served from cache, and VRAM is bounded by LRU eviction (env `SG_LOCAL_VRAM_BUDGET_GB` / `SG_LOCAL_MAX_MODELS`,
32
+ * or pass `opts.host` for a dedicated budget / test isolation).
33
+ *
34
+ * const { createLLMProvider } = require('skynet-graph/lib/providers/llm');
35
+ * const { makeLocalAsk } = require('skynet-graph/lib/providers/llm-local');
36
+ * register(Graph, [ createLLMProvider({ ask: makeLocalAsk({ modelPath: '/models/small.gguf' }) }) ]);
37
+ *
38
+ * @param opts.modelPath path to a GGUF file (or env LOCAL_MODEL).
39
+ * @param opts.gpu 'auto'(default) | 'cuda' | 'metal' | 'vulkan' | false (CPU).
40
+ * @param opts.contextSize context window (default 4096).
41
+ * @param opts.lora a LoRA adapter path | { adapters:[{ filePath, scale }] } (per-specialist, one base in VRAM).
42
+ * @param opts.jsonSchema a JSON Schema → grammar-constrained decoding (the typed-fact enforcer).
43
+ * @param opts.gbnf a raw GBNF grammar string (alternative to jsonSchema).
44
+ * @param opts.seed decode seed (default 0 → deterministic with temperature 0).
45
+ * @param opts.reasoningBudget native thinking budget in tokens (node-llama-cpp `budgets.thoughtTokens`): 0 disables
46
+ * thinking (the adapted lever for a reasoning model at a one-token typed touchpoint, where CoT only
47
+ * truncates the answer); undefined = the model default. Overridable per call.
48
+ * @returns async ask({ system, user, maxTokens, temperature, grammar, reasoningBudget }) -> string (JSON string when a grammar is set).
49
+ * A PER-CALL `grammar` ({jsonSchema}|{gbnf}) OVERRIDES the construction-time one — so a caller whose grammar
50
+ * depends on the call (e.g. the borderline gate's `enumGbnf(spec)`, one grammar per enum) can request it per
51
+ * call; `local-host.ask` already applies grammar per call, sharing one model load. Omit it → construction default.
52
+ */
53
+ // resolve the GGUF path: explicit opt → env → the single .gguf in the gitignored models/ dir (a DX convenience).
54
+ function resolveModelPath( opts ) {
55
+ if ( opts.modelPath ) return opts.modelPath;
56
+ if ( process.env.LOCAL_MODEL ) return process.env.LOCAL_MODEL;
57
+ try {
58
+ const fs = require('fs'), path = require('path');
59
+ const dir = opts.modelsDir || path.join(process.cwd(), 'models');
60
+ const ggufs = fs.readdirSync(dir).filter(( f ) => f.endsWith('.gguf'));
61
+ if ( ggufs.length === 1 ) return path.join(dir, ggufs[0]);
62
+ } catch ( _e ) { /* no models/ dir → fall through to the guidance error */ }
63
+ return null;
64
+ }
65
+
66
+ const { sharedLocalModelHost } = require('./local-host');
67
+
68
+ function makeLocalAsk( opts ) {
69
+ opts = opts || {};
70
+ const modelPath = resolveModelPath(opts);
71
+ if ( !modelPath ) throw new Error('makeLocalAsk: no model — pass opts.modelPath, set env LOCAL_MODEL, or drop a single .gguf into models/ (npm run local-inference:setup)');
72
+ // Delegate model ownership + the prompt cache to a CENTRALIZED host (local-host.js): so N handles /
73
+ // namespaces of the SAME gguf share ONE VRAM load, several grammars share that load (grammar is applied
74
+ // per-call), identical deterministic prompts are cached, and VRAM is managed under a budget with LRU
75
+ // eviction. `opts.host` overrides the process-wide shared host (test isolation / a dedicated budget).
76
+ const host = opts.host || sharedLocalModelHost();
77
+ const grammar = opts.jsonSchema ? { jsonSchema: opts.jsonSchema } : opts.gbnf ? { gbnf: opts.gbnf } : null;
78
+
79
+ return async function ask( { system, user, maxTokens = 1024, temperature = 0, grammar: callGrammar, reasoningBudget: callRB } ) {
80
+ return host.ask({
81
+ modelPath, contextSize: opts.contextSize, gpu: opts.gpu, lora: opts.lora,
82
+ grammar: callGrammar || grammar, system, user, maxTokens, temperature,
83
+ reasoningBudget: callRB != null ? callRB : opts.reasoningBudget, seed: opts.seed == null ? 0 : opts.seed
84
+ });
85
+ };
86
+ }
87
+
88
+ module.exports = { makeLocalAsk };