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,256 @@
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
+ * dag-decompose — the model-driven TYPED DAG DECOMPOSE (ZERO-CORE, host-side). The upstream half of the context
10
+ * projection: it turns a task into the `needs`/`produces` DAG that `context-project.js` + `plan-loop.js` execute.
11
+ * Realizes the study `WIP/2026-07-08-etude-decoupage-de-tache.md` §6.1: the decompose PROMPT + GRAMMAR-CONSTRAINED
12
+ * decoding force a well-formed typed dataflow graph, and the engine's offline guards (guardPlan cycle/coverage,
13
+ * rebalance) validate the CONTENT — "propose freely, validate hard, refuse cleanly".
14
+ *
15
+ * Where the sibling reactive `typed-loop` chains its steps SEQUENTIALLY (originNode = the previous step's target),
16
+ * this emits an explicit producer→consumer DAG: each part declares WHAT IT PRODUCES (a typed key) and WHAT IT NEEDS
17
+ * (other parts' produced keys). That is the class matching our projection (LLMCompiler `$1,$2` / ReWOO `#E` /
18
+ * HuggingGPT `<resource-N>`), plus bounded per-node context.
19
+ *
20
+ * THE GRAMMAR IS THE K1 BARRIER (the crown jewel, per typed-loop): the output is decoded under a JSON Schema, so a
21
+ * small model CANNOT emit a malformed part — `stepKind` snaps onto a CLOSED enum (fail-closed via `canonValue`), the
22
+ * shape is guaranteed. The grammar guarantees FORMAT, not correct dependency CONTENT — guardPlan/rebalance do that.
23
+ *
24
+ * const { makeDagDecompose } = require('skynet-graph/lib/authoring/dag-decompose');
25
+ * const decompose = makeDagDecompose({ ask: makeLocalAsk({ modelPath }), stepKinds: ['retrieve','compute','compare','summarize'] });
26
+ * const loop = createPlanLoop({ decompose, serveLeaf }); // the projection consumes it directly
27
+ * // à nu: const leaves = await decompose(task); const roadmap = leavesToRoadmap(leaves);
28
+ *
29
+ * @param opts.ask async ({ system, user, maxTokens, temperature, grammar }) => string — a grammar-capable
30
+ * chat backend (makeLocalAsk supports `grammar:{jsonSchema}`). REQUIRED.
31
+ * @param opts.stepKinds closed enum of step TYPES (the K1-discriminating class) — canon-snapped, fail-closed.
32
+ * @param opts.maxSteps cap on the number of parts (default 8).
33
+ * @param opts.minSteps granularity floor: a split below it triggers ONE bounded re-ask with a coarseness blame
34
+ * (gap iii — anti 1-step collapse); still under → the larger split is kept (honest degrade).
35
+ * @param opts.givenKeys the task's given keys (givens.js): arms the cited-never-produced validation — a "needs"
36
+ * key that no part produces and that is not a given joins the same bounded re-ask blame.
37
+ * @param opts.onReask observer ({task,firstCount,minSteps,uncovered}) fired when the validation re-ask triggers.
38
+ * @param opts.system override the decompose system prompt.
39
+ * @param opts.hint an optional one-line orientation appended to the system prompt (the §6.2 per-archetype steer,
40
+ * e.g. "The parts are INDEPENDENT — emit no needs." for a fan-out / extraction prompt).
41
+ */
42
+ const { canonValue } = require('../providers/canonicalize.js');
43
+
44
+ // the study §6.1 contract, as the system prompt (imperative, the 5 hard rules that encode invariants 1-3,5,8).
45
+ const DECOMPOSE_SYSTEM =
46
+ 'You are a TASK DECOMPOSER for a bounded-context execution engine. Split the task into parts and output ONLY a ' +
47
+ 'JSON array. Each part = {"produces": a short snake_case typed key naming the ARTIFACT this part outputs, ' +
48
+ '"stepKind": the step TYPE, "instruction": one imperative sentence, "needs": the list of OTHER parts\' "produces" ' +
49
+ 'keys whose output this part consumes}. HARD RULES (the engine rejects a split that breaks them): ' +
50
+ '(1) ACYCLIC — no part may need a part that (transitively) needs it. ' +
51
+ '(2) COVERED — every key in "needs" must be some other part\'s "produces", or a GIVEN key listed in the task. ' +
52
+ '(3) THIN — need as FEW parts as possible; produce exactly ONE key. ' +
53
+ '(4) INDEPENDENT — if two parts do not depend on each other, do NOT invent a dependency (they run in parallel). ' +
54
+ '(5) TYPED — "produces"/"needs" are keys, never sentences. Decompose only as far as needed; a directly-answerable part is a leaf. ' +
55
+ '(6) GIVENS — when the task lists GIVENS (typed keys, e.g. g1_price=5), a part that uses a base fact MUST cite its ' +
56
+ 'given key in "needs" and MUST NOT restate that number in the instruction (the engine injects the value). ' +
57
+ '(7) SELF-CONTAINED — a base fact NOT listed in GIVENS (a quantity spelled in words, a fraction, common knowledge ' +
58
+ 'like 52 weeks per year) MUST be written explicitly AS A NUMBER in the instruction of every part that uses it; ' +
59
+ 'never reference it as a key and never assume the executor saw the original task.';
60
+
61
+ // the METHOD-SLOT / higher-order need (roadmap §5(a)): a part may declare it is a LOOP whose behavioural hole (the
62
+ // body) is filled by a DISPATCHED sub-method applied over items. The combinator vocabulary is CLOSED to the three the
63
+ // LLM can legitimately emit; `fold` is EXCLUDED on purpose — a reducer/init is arbitrary host code, never LLM-emittable
64
+ // (roadmap §5(a) confront: a `combinator:'fold'` from the grammar would crash `results.reduce(undefined,undefined)`).
65
+ const SLOT_COMBINATORS = ['map', 'all', 'any'];
66
+
67
+ // the JSON-Schema grammar: a bounded array of typed parts; `stepKind` on the CLOSED enum (the decode-level K1 barrier).
68
+ // A part may OPTIONALLY carry a `slot` (the method-slot): `over` = a produced-key whose value yields the items, `body`
69
+ // = the dispatched slot-filler on the CLOSED `bodyKinds` enum (same K1 barrier as stepKind), `combinator` closed.
70
+ function decomposeSchema( stepKinds, maxSteps, bodyKinds ) {
71
+ const kind = (stepKinds && stepKinds.length) ? { type: 'string', enum: stepKinds } : { type: 'string' };
72
+ const body = (bodyKinds && bodyKinds.length) ? { type: 'string', enum: bodyKinds } : { type: 'string' };
73
+ return {
74
+ type: 'array', minItems: 1, maxItems: maxSteps,
75
+ items: {
76
+ type: 'object',
77
+ properties: {
78
+ produces: { type: 'string' },
79
+ stepKind: kind,
80
+ instruction: { type: 'string' },
81
+ needs: { type: 'array', items: { type: 'string' } },
82
+ slot: { type: 'object', properties: { // OPTIONAL — present ⇒ this part is a loop
83
+ over: { type: 'string' },
84
+ body: body,
85
+ combinator: { type: 'string', enum: SLOT_COMBINATORS }
86
+ }, required: ['over', 'body'] }
87
+ },
88
+ required: ['produces', 'stepKind', 'instruction', 'needs'] // slot NOT required → plain parts unchanged
89
+ }
90
+ };
91
+ }
92
+
93
+ // parse a raw `slot` fail-closed: `over` cleaned to a key; `body` snapped onto bodyKinds (a MISS drops the whole slot —
94
+ // never mount an unconstrained hole); `combinator` snapped onto SLOT_COMBINATORS (a MISS → the safe default `map`).
95
+ // Returns null (⇒ degrade to a PLAIN leaf) when the slot is malformed / the body is out-of-vocab.
96
+ function parseSlot( raw, bodyKinds ) {
97
+ if ( !raw || typeof raw !== 'object' ) return null;
98
+ const over = keyOf(raw.over);
99
+ if ( !over ) return null; // a loop with no items source is malformed
100
+ const bsnap = (bodyKinds && bodyKinds.length) ? canonValue(raw.body, { enum: bodyKinds }) : { value: raw.body };
101
+ if ( bsnap.miss || raw.body == null || raw.body === '' ) return null; // body∉vocab → fail-closed DROP (plain leaf)
102
+ const csnap = canonValue(raw.combinator, { enum: SLOT_COMBINATORS, default: 'map' });
103
+ const slot = { over: over, body: bsnap.value, combinator: csnap.miss ? 'map' : csnap.value };
104
+ if ( csnap.miss && raw.combinator != null && raw.combinator !== '' ) { slot.combinatorMiss = true; slot.combinatorRaw = String(raw.combinator); }
105
+ return slot;
106
+ }
107
+
108
+ // clean a produced key to a stable typed id (snake_case-ish, the projection's fusion/write key).
109
+ function keyOf( raw ) { return String(raw == null ? '' : raw).trim().toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '') || 'part'; }
110
+
111
+ function parseSteps( out ) {
112
+ if ( Array.isArray(out) ) return out;
113
+ try { const v = JSON.parse(out); return Array.isArray(v) ? v : (v && Array.isArray(v.parts) ? v.parts : []); }
114
+ catch ( _e ) { // defensive (grammar-constrained output IS valid JSON; this covers a raw/ungrammared backend)
115
+ const m = String(out).match(/\[[\s\S]*\]/); if ( m ) { try { return JSON.parse(m[0]); } catch ( _e2 ) { /* fall through */ } }
116
+ return [];
117
+ }
118
+ }
119
+
120
+ /**
121
+ * makeDagDecompose(opts) → async decompose(task, ctx) -> [{ id, request:{id,kind}, nl, readsExtra:[keys] }]
122
+ * The returned leaf list is the plan-loop DECOMPOSE contract: `request.id` is the produced key (leafWrites), and
123
+ * `readsExtra` are the produced keys it consumes (the projection derives needs = readsExtra ∩ produced). Fail-closed:
124
+ * an out-of-vocab `stepKind` rides `kindMiss:true` + the raw surface; a `needs` self-reference is dropped.
125
+ */
126
+ function makeDagDecompose( opts ) {
127
+ opts = opts || {};
128
+ if ( typeof opts.ask !== 'function' ) throw new Error('makeDagDecompose needs opts.ask({system,user,grammar}) -> string');
129
+ const stepKinds = opts.stepKinds || [];
130
+ const bodyKinds = opts.bodyKinds || [];
131
+ const maxSteps = opts.maxSteps || 8;
132
+ const minSteps = opts.minSteps || 0;
133
+ const system = (opts.system || DECOMPOSE_SYSTEM) + (opts.hint ? ' ' + opts.hint : '');
134
+ const schema = decomposeSchema(stepKinds, maxSteps, bodyKinds);
135
+
136
+ const givenKeys = (opts.givenKeys || []).map(keyOf);
137
+
138
+ // the OFFLINE plan validation (gap iii + the P0-critique symmetric): granularity floor + cited-never-produced
139
+ // keys (a need that is neither some part's produces nor a given = a phantom the projection would drop → the
140
+ // consuming leaf computes blind). Both feed ONE bounded re-ask with an explicit blame.
141
+ function problemsOf( raw ) {
142
+ const produced = new Set(raw.map(( s ) => keyOf(s.produces) ));
143
+ const uncovered = [];
144
+ for ( const s of raw ) for ( const n of (Array.isArray(s.needs) ? s.needs : []).map(keyOf) )
145
+ if ( n && !produced.has(n) && givenKeys.indexOf(n) < 0 && n !== keyOf(s.produces) ) uncovered.push(n);
146
+ return { tooCoarse: !!(minSteps && raw.length < minSteps), uncovered: Array.from(new Set(uncovered)) };
147
+ }
148
+
149
+ return async function decompose( task, ctx ) {
150
+ const user = typeof task === 'string' ? task : ((ctx && ctx.statement) || JSON.stringify(task));
151
+ const askOnce = ( sys ) => opts.ask({ system: sys, user: user, grammar: { jsonSchema: schema }, maxTokens: opts.maxTokens || 700, temperature: 0 });
152
+ let raw = parseSteps(await askOnce(system)).slice(0, maxSteps);
153
+ let probs = problemsOf(raw);
154
+ // ONE bounded re-ask on a validation miss; keep the attempt with fewer uncovered keys (then finer). The check
155
+ // only has teeth when the host declares its vocabulary: no givenKeys ⇒ only the granularity floor fires.
156
+ if ( probs.tooCoarse || (givenKeys.length && probs.uncovered.length) ) {
157
+ if ( opts.onReask ) opts.onReask({ task: user, firstCount: raw.length, minSteps: minSteps, uncovered: probs.uncovered });
158
+ const blame = ' PREVIOUS ATTEMPT REJECTED:'
159
+ + (probs.tooCoarse ? ' only ' + raw.length + ' part(s) — TOO COARSE, emit AT LEAST ' + minSteps + ' parts, each ONE small operation, chained via "needs".' : '')
160
+ + (givenKeys.length && probs.uncovered.length ? ' these "needs" keys are produced by NO part and are NOT givens: ['
161
+ + probs.uncovered.join(', ') + '] — either add a part that produces each, cite a listed given key, or restate the fact as a number in the instruction (rule 7).' : '');
162
+ const raw2 = parseSteps(await askOnce(system + blame)).slice(0, maxSteps);
163
+ const probs2 = problemsOf(raw2);
164
+ const better = probs2.uncovered.length < probs.uncovered.length
165
+ || (probs2.uncovered.length === probs.uncovered.length && (probs.tooCoarse && !probs2.tooCoarse))
166
+ || (probs2.uncovered.length === probs.uncovered.length && probs2.tooCoarse === probs.tooCoarse && raw2.length > raw.length);
167
+ if ( better ) { raw = raw2; probs = probs2; }
168
+ }
169
+ const seen = Object.create(null);
170
+ const leaves = [];
171
+ for ( const s of raw ) {
172
+ let key = keyOf(s.produces);
173
+ while ( seen[key] ) key = key + '_2'; // uniquify a duplicated produced key (rebalance would dedupe identical, but a distinct write key keeps needs unambiguous)
174
+ seen[key] = true;
175
+ const snap = stepKinds.length ? canonValue(s.stepKind, { enum: stepKinds }) : { value: s.stepKind };
176
+ const request = { id: key, kind: snap.miss ? null : snap.value };
177
+ if ( snap.miss ) { request.kindMiss = true; request.kindRaw = String(s.stepKind); }
178
+ const slot = parseSlot(s.slot, bodyKinds); // the method-slot (higher-order need), fail-closed
179
+ if ( slot ) request.slot = slot;
180
+ const needs = (Array.isArray(s.needs) ? s.needs : []).map(keyOf).filter(( n ) => n !== key );
181
+ if ( slot && slot.over !== key && !needs.includes(slot.over) ) needs.push(slot.over); // `over` rides the needs channel (resolution+gate+coverage)
182
+ leaves.push({ id: 'n_' + key, request: request, nl: String(s.instruction == null ? '' : s.instruction), readsExtra: needs });
183
+ }
184
+ if ( !leaves.length ) // never crash the loop: degrade to a single atomic leaf (the whole task)
185
+ leaves.push({ id: 'n_task', request: { id: 'task', kind: null }, nl: user, readsExtra: [] });
186
+ return leaves;
187
+ };
188
+ }
189
+
190
+ // leaves → a context-project roadmap (à-nu use of the projection directly, without plan-loop). produces = the write
191
+ // key; needs = readsExtra restricted to keys some leaf produces OR a given key (givens = { key: value } — the
192
+ // projection pre-satisfies those; any other external readsExtra is context, not a gate). ──
193
+ function leavesToRoadmap( leaves, givens ) {
194
+ givens = givens || {};
195
+ const produced = new Set(leaves.map(( l ) => l.request && l.request.id ).filter(Boolean));
196
+ return leaves.map(( l ) => {
197
+ const step = { id: l.request.id, produces: l.request.id, nl: l.nl,
198
+ needs: (l.readsExtra || []).filter(( n ) => produced.has(n) || n in givens ) };
199
+ if ( l.request.slot ) step.slot = l.request.slot; // carry the method-slot through to the projection
200
+ return step;
201
+ });
202
+ }
203
+
204
+ // ── the ARCHETYPE ROUTER (study §6.2/§6.3): type-of-prompt → best decomposition organization. A prompt is first
205
+ // classified into a decomposition ARCHETYPE (grammar-constrained, closed enum, fail-closed), then the matching
206
+ // per-archetype ORIENTATION (`hint`) steers `makeDagDecompose` toward the structure that archetype justifies
207
+ // (chain / fan-out / layered DAG / fan-out+merge / general DAG). The type→scheme map is a well-motivated PRIOR
208
+ // (the study's caveat: not a proven law — same-model inflation, thin cross-model evidence); this wires the DECISION
209
+ // mechanism, not a claim of optimality. ──
210
+ const ARCHETYPES = ['sequential', 'extraction', 'multihop', 'aggregate', 'planning'];
211
+ const ARCHETYPE_HINTS = {
212
+ sequential: 'This is SEQUENTIAL reasoning (each step feeds the next): emit a CHAIN — each part needs AT MOST the single previous part.',
213
+ extraction: 'This is INDEPENDENT extraction/transformation of many items: emit parts with EMPTY "needs" (a pure fan-out); add one final aggregating part only if a single combined output is required.',
214
+ multihop: 'This is MULTI-HOP gathering: emit the independent gather parts in ONE parallel layer (no "needs" among them), then a SINGLE combine part that needs them.',
215
+ aggregate: 'This is DECOMPOSE-then-AGGREGATE: emit the independent producer parts (empty "needs"), then EXACTLY ONE final part whose "needs" lists ALL of them and merges them.',
216
+ planning: 'Emit the dependency DAG directly: each part references by key the outputs it consumes; keep independent parts free of needs so they run in parallel.',
217
+ };
218
+ function detectSystem( archetypes ) {
219
+ return 'Classify the task into EXACTLY ONE decomposition archetype and reply with ONLY the label. Labels: ' +
220
+ archetypes.join(' | ') + '. Guidance — sequential: each step strictly feeds the next; extraction: many ' +
221
+ 'independent items of the same type; multihop: gather from several sources then combine; aggregate: produce ' +
222
+ 'independent pieces then merge into one; planning: a general step/tool plan with mixed dependencies.';
223
+ }
224
+
225
+ /**
226
+ * makeArchetypeRouter(opts) → { detect(task), route(task) -> {archetype,hint,leaves}, decompose(task) -> leaves }
227
+ * `decompose` is the plan-loop seam (detect → steer → emit the DAG). `route` also surfaces the detected archetype.
228
+ * @param opts.ask grammar-capable ask (REQUIRED). @param opts.stepKinds closed step-kind enum (passed through).
229
+ * @param opts.archetypes / opts.hints override the archetype vocabulary / their orientation lines (usable à nu).
230
+ * @param opts.fallback archetype used when detection misses (default 'planning' — the general DAG, always safe).
231
+ */
232
+ function makeArchetypeRouter( opts ) {
233
+ opts = opts || {};
234
+ if ( typeof opts.ask !== 'function' ) throw new Error('makeArchetypeRouter needs opts.ask({system,user,grammar}) -> string');
235
+ const archetypes = opts.archetypes || ARCHETYPES;
236
+ const hints = opts.hints || ARCHETYPE_HINTS;
237
+ const fallback = opts.fallback || (archetypes.indexOf('planning') !== -1 ? 'planning' : archetypes[0]);
238
+
239
+ async function detect( task, ctx ) {
240
+ const user = typeof task === 'string' ? task : ((ctx && ctx.statement) || JSON.stringify(task));
241
+ const out = await opts.ask({ system: detectSystem(archetypes), user: user, grammar: { jsonSchema: { type: 'string', enum: archetypes } }, maxTokens: 8, temperature: 0 });
242
+ let raw; try { raw = JSON.parse(out); } catch ( _e ) { raw = String(out).trim().replace(/^["']|["']$/g, ''); }
243
+ const snap = canonValue(raw, { enum: archetypes, default: fallback }); // fail-closed to the safe default
244
+ return snap.miss ? fallback : snap.value;
245
+ }
246
+ async function route( task, ctx ) {
247
+ const archetype = await detect(task, ctx);
248
+ const hint = hints[archetype] || '';
249
+ const leaves = await makeDagDecompose({ ask: opts.ask, stepKinds: opts.stepKinds, maxSteps: opts.maxSteps, hint: hint })(task, ctx);
250
+ return { archetype: archetype, hint: hint, leaves: leaves };
251
+ }
252
+ async function decompose( task, ctx ) { return (await route(task, ctx)).leaves; }
253
+ return { detect: detect, route: route, decompose: decompose, archetypes: archetypes, hints: hints };
254
+ }
255
+
256
+ module.exports = { makeDagDecompose, makeArchetypeRouter, leavesToRoadmap, decomposeSchema, parseSlot, SLOT_COMBINATORS, DECOMPOSE_SYSTEM, ARCHETYPES, ARCHETYPE_HINTS, keyOf };
@@ -0,0 +1,253 @@
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
+ * dataset-adapter — the PROMOTED dataset-adapter brick (roadmap-use-cases: "promouvoir la brique dataset-adapter
10
+ * en lib"). Turns a LABELLED query dataset (WikiSQL, a semantic-parsing set, a client corpus) into the neutral
11
+ * shape the typed-grammar STOCK pipeline consumes, so "plusieurs datasets" is one registry away and their
12
+ * distilled `.sgc` grammars are COMPARABLE (same explorer metrics on each).
13
+ *
14
+ * PURE + engine-free (an optional `fs` read for jsonl; no Graph, no model). An adapter maps one record →
15
+ * {
16
+ * query: string — the NL query (→ the model extractor / the frontier answer)
17
+ * context: object|null — typed context for the extractor prompt (e.g. {columns:[...]}), or null
18
+ * klass: string — the COVERAGE CLASS key = signature.structure (ONE method distilled per class)
19
+ * goldShape: string[] — the gold typed decomposition = the ORACLE (goldGate admits iff model==gold)
20
+ * }
21
+ * The class is the typed signature the proxy dispatches on (one frontier answer covers the class); the goldShape
22
+ * is the gold-gate oracle (0-false stock). Same two fields ⇒ any dataset flows the SAME pipeline + SAME metrics.
23
+ *
24
+ * const { getAdapter, loadDataset } = require('skynet-graph/lib/authoring/dataset-adapter');
25
+ * const byClass = loadDataset(getAdapter('wikisql'), { file, aux:{tablesFile}, perClass:4 });
26
+ *
27
+ * An adapter: { name, stepEnum:[kinds], adapt(record, aux) -> mapped|null }. Register more with registerAdapter.
28
+ */
29
+
30
+ var _adapters = {};
31
+
32
+ /** register a dataset adapter (name → {name, stepEnum, adapt}). Later registration overrides. */
33
+ function registerAdapter( adapter ) {
34
+ if ( !adapter || !adapter.name || typeof adapter.adapt !== 'function' ) throw new Error('registerAdapter needs { name, adapt(record, aux) }');
35
+ _adapters[adapter.name] = adapter;
36
+ return adapter;
37
+ }
38
+ /** fetch a registered adapter by name (throws if unknown). */
39
+ function getAdapter( name ) {
40
+ if ( !_adapters[name] ) throw new Error('no dataset adapter "' + name + '" (registered: ' + Object.keys(_adapters).join(', ') + ')');
41
+ return _adapters[name];
42
+ }
43
+ /** list registered adapter names. */
44
+ function listAdapters() { return Object.keys(_adapters); }
45
+
46
+ /**
47
+ * Group a dataset's records by their coverage CLASS (the typed signature). Reads a jsonl file (or takes an
48
+ * in-memory `records` array), maps each with the adapter, drops nulls, and buckets by `klass`.
49
+ * @param adapter a registered/inline adapter { adapt(record, aux) }.
50
+ * @param opts.file jsonl path (one JSON record per line) — OR opts.records (an array, no fs).
51
+ * @param opts.aux adapter-specific side data (e.g. {tables} / {tablesFile}); passed to adapt(record, aux).
52
+ * @param opts.classes optional string[] whitelist of class keys to keep (else all).
53
+ * @param opts.perClass optional cap on kept instances PER class (the extra are dropped; the caller slices held-out).
54
+ * @returns { [klass]: [ {query,context,klass,goldShape}, … ] }
55
+ */
56
+ function loadDataset( adapter, opts ) {
57
+ opts = opts || {};
58
+ var aux = resolveAux(adapter, opts.aux);
59
+ var records = opts.records || readJsonl(opts.file);
60
+ var byClass = {};
61
+ for ( var i = 0; i < records.length; i++ ) {
62
+ var m = adapter.adapt(records[i], aux);
63
+ if ( !m || !m.klass || !Array.isArray(m.goldShape) ) continue;
64
+ if ( opts.classes && opts.classes.indexOf(m.klass) < 0 ) continue;
65
+ var b = (byClass[m.klass] = byClass[m.klass] || []);
66
+ if ( opts.perClass != null && b.length >= opts.perClass ) continue;
67
+ b.push(m);
68
+ }
69
+ return byClass;
70
+ }
71
+
72
+ // an adapter may declare aux it needs loaded from a file (e.g. WikiSQL's tables). Resolve a *File → parsed.
73
+ function resolveAux( adapter, aux ) {
74
+ aux = aux || {};
75
+ if ( typeof adapter.resolveAux === 'function' ) return adapter.resolveAux(aux);
76
+ return aux;
77
+ }
78
+
79
+ function readJsonl( file ) {
80
+ if ( !file ) return [];
81
+ var fs = require('fs');
82
+ var out = [], lines = fs.readFileSync(file, 'utf8').trim().split('\n');
83
+ for ( var i = 0; i < lines.length; i++ ) { var l = lines[i].trim(); if ( l ) try { out.push(JSON.parse(l)); } catch ( e ) {} }
84
+ return out;
85
+ }
86
+
87
+ // ── built-in: WikiSQL (github salesforce/WikiSQL). Its `sql:{sel,agg,conds}` is a PRE-TYPED decomposition →
88
+ // the gold oracle with NO SQL parsing. class = {agg}|{nConds}; method shape = nConds×filter [+aggregate] +select.
89
+ var WIKISQL_AGG = ['none', 'max', 'min', 'count', 'sum', 'avg'];
90
+ function wikisqlGoldShape( sql ) {
91
+ var steps = [];
92
+ for ( var i = 0; i < (sql.conds || []).length; i++ ) steps.push('filter');
93
+ if ( sql.agg !== 0 ) steps.push('aggregate');
94
+ steps.push('select');
95
+ return steps;
96
+ }
97
+ registerAdapter({
98
+ name: 'wikisql',
99
+ stepEnum: ['filter', 'aggregate', 'select'],
100
+ // aux: { tables } (id→table) OR { tablesFile } (a jsonl of {id,header}); resolveAux loads the file once.
101
+ resolveAux: function ( aux ) {
102
+ if ( aux.tables ) return aux;
103
+ if ( aux.tablesFile ) { var t = {}; readJsonl(aux.tablesFile).forEach(function ( x ) { t[x.id] = x; }); return { tables: t }; }
104
+ return { tables: {} };
105
+ },
106
+ adapt: function ( r, aux ) {
107
+ if ( !r || !r.sql || !Array.isArray(r.sql.conds) ) return null;
108
+ var agg = WIKISQL_AGG[r.sql.agg] || 'none';
109
+ return {
110
+ query: r.question,
111
+ context: { columns: ((aux.tables[r.table_id] || {}).header) || [] },
112
+ klass: agg + '|' + r.sql.conds.length, // the coverage class = {agg, nConds}
113
+ goldShape: wikisqlGoldShape(r.sql)
114
+ };
115
+ }
116
+ });
117
+
118
+ // ── built-in: Spider (Yale text-to-SQL). Unlike WikiSQL, the HF release ships only the raw gold SQL string
119
+ // (no pre-parsed AST), so this adapter is a SMALL structural SQL analyzer over the gold `query` — the typed
120
+ // decomposition is a deterministic function of the gold SQL (parsing the gold IS reading the oracle, the same
121
+ // way Spider's own evaluation parses gold). Its POINT vs WikiSQL: Spider is RECURSIVE — a WHERE operand can be
122
+ // a nested `(SELECT …)` subquery, and two full queries combine via INTERSECT/UNION/EXCEPT. The subquery case is
123
+ // exactly a graft-into-slot (blendMethods) — a nested query's method = a BLEND of the outer skeleton + the
124
+ // subquery grammar (the "concepts combine" payoff). Set-ops are a SECOND, binary top-level composition (noted,
125
+ // but out of blendMethods' scope). stepEnum = the typed step kinds; nesting is depth (not a step token).
126
+ var SPIDER_STEPS = ['join', 'filter', 'group', 'having', 'aggregate', 'order', 'select'];
127
+
128
+ // scan a lowercased SQL string tracking paren depth + single-quoted strings; call `at(i, ch, depth)` per char.
129
+ function scanSQL( s, at ) {
130
+ var depth = 0, q = false;
131
+ for ( var i = 0; i < s.length; i++ ) {
132
+ var ch = s[i];
133
+ if ( ch === "'" ) { q = !q; continue; }
134
+ if ( q ) continue;
135
+ if ( ch === '(' ) { at(i, ch, depth); depth++; continue; }
136
+ if ( ch === ')' ) { depth--; at(i, ch, depth); continue; }
137
+ at(i, ch, depth);
138
+ }
139
+ }
140
+ // first index of any needle at paren-depth 0 (outside quotes); returns { index, token } or null.
141
+ function findTop( s, needles ) {
142
+ var hit = null;
143
+ scanSQL(s, function ( i, ch, depth ) {
144
+ if ( hit || depth !== 0 ) return;
145
+ for ( var k = 0; k < needles.length; k++ ) if ( s.startsWith(needles[k], i) ) { hit = { index: i, token: needles[k].trim() }; return; }
146
+ });
147
+ return hit;
148
+ }
149
+ // count occurrences of a needle at paren-depth 0.
150
+ function countTop( s, needle ) {
151
+ var n = 0;
152
+ scanSQL(s, function ( i, ch, depth ) { if ( depth === 0 && s.startsWith(needle, i) ) n++; });
153
+ return n;
154
+ }
155
+ // the balanced-paren substring of the FIRST `(select …)` (original case), or null. Used to pull the subquery
156
+ // for the blend cell (the donor grammar). Scans the lowercased copy for the marker, cuts from the original.
157
+ function firstSubquery( orig, s ) {
158
+ var start = -1;
159
+ scanSQL(s, function ( i, ch, depth ) {
160
+ if ( start >= 0 || ch !== '(' ) return;
161
+ if ( /^\(\s*select\b/.test(s.slice(i, i + 12)) ) start = i;
162
+ });
163
+ if ( start < 0 ) return null;
164
+ var d = 0, end = -1;
165
+ for ( var i = start; i < orig.length; i++ ) { if ( orig[i] === '(' ) d++; else if ( orig[i] === ')' ) { d--; if ( d === 0 ) { end = i; break; } } }
166
+ return end > start ? orig.slice(start + 1, end).trim() : null;
167
+ }
168
+ // MASK each balanced `(select …)` subquery to `(?)` so the OUTER structure is analyzed with the subquery as an
169
+ // opaque operand (else the subquery's own agg/join/group would pollute the outer skeleton — the blend host must be
170
+ // the clean outer shape, the subquery its own donor grammar). Operates on a lowercased string.
171
+ function maskSubqueries( s ) {
172
+ var out = '', i = 0;
173
+ while ( i < s.length ) {
174
+ if ( s[i] === '(' && /^\(\s*select\b/.test(s.slice(i, i + 12)) ) {
175
+ var d = 0, j = i;
176
+ for ( ; j < s.length; j++ ) { if ( s[j] === '(' ) d++; else if ( s[j] === ')' ) { d--; if ( d === 0 ) { j++; break; } } }
177
+ out += '(?)'; i = j;
178
+ } else { out += s[i]; i++; }
179
+ }
180
+ return out;
181
+ }
182
+
183
+ // analyze ONE select block (no top-level set-op): counts + flags of the OUTER query (subqueries masked out).
184
+ function analyzeSelect( orig ) {
185
+ var raw = orig.toLowerCase();
186
+ var s = maskSubqueries(raw); // outer structure sees the subquery as an opaque operand
187
+ var joins = countTop(s, ' join ');
188
+ var whereAt = findTop(s, [' where ']);
189
+ var filters = 0, nested = false;
190
+ if ( whereAt ) {
191
+ // the WHERE clause runs to the first top-level group by / order by / having / end.
192
+ var rest = s.slice(whereAt.index + 7);
193
+ var endAt = findTop(rest, [' group by ', ' order by ', ' having ']);
194
+ var where = endAt ? rest.slice(0, endAt.index) : rest;
195
+ // conditions = top-level ` and `/` or `-separated fragments (a subquery is masked → its ANDs don't count).
196
+ var conds = 1;
197
+ scanSQL(where, function ( i, ch, depth ) { if ( depth === 0 && (where.startsWith(' and ', i) || where.startsWith(' or ', i)) ) conds++; });
198
+ filters = conds;
199
+ nested = where.indexOf('(?)') >= 0; // a subquery operand appeared in a WHERE condition
200
+ }
201
+ return {
202
+ joins: joins, filters: filters,
203
+ group: findTop(s, [' group by ']) != null,
204
+ having: findTop(s, [' having ']) != null,
205
+ agg: /\b(count|sum|avg|min|max)\s*\(/.test(s), // masked → outer aggregates only
206
+ order: findTop(s, [' order by ']) != null,
207
+ nested: nested, subquery: nested ? firstSubquery(orig, raw) : null
208
+ };
209
+ }
210
+
211
+ // the ordered typed decomposition of the OUTER query (subquery stays inside its filter operand → depth, not a step).
212
+ function spiderGoldShape( a ) {
213
+ var steps = [];
214
+ for ( var i = 0; i < a.joins; i++ ) steps.push('join');
215
+ for ( var j = 0; j < a.filters; j++ ) steps.push('filter');
216
+ if ( a.group ) steps.push('group');
217
+ if ( a.having ) steps.push('having');
218
+ if ( a.agg ) steps.push('aggregate');
219
+ if ( a.order ) steps.push('order');
220
+ steps.push('select');
221
+ return steps;
222
+ }
223
+
224
+ // analyze a full gold SQL: detect a top-level set-op, else the single select. Returns { …counts, setop, subquery }.
225
+ function analyzeSpiderSQL( query ) {
226
+ var orig = String(query || '').replace(/\s+/g, ' ').trim();
227
+ var s = orig.toLowerCase();
228
+ var so = findTop(s, [' intersect ', ' union ', ' except ']);
229
+ if ( so ) { var left = analyzeSelect(orig.slice(0, so.index)); left.setop = so.token; return left; }
230
+ var a = analyzeSelect(orig); a.setop = null; return a;
231
+ }
232
+
233
+ registerAdapter({
234
+ name: 'spider',
235
+ stepEnum: SPIDER_STEPS,
236
+ analyze: analyzeSpiderSQL, // exposed for the blend cell (outer skeleton + subquery)
237
+ goldShapeOf: spiderGoldShape,
238
+ adapt: function ( r ) {
239
+ if ( !r || !r.query ) return null;
240
+ var a = analyzeSpiderSQL(r.query);
241
+ var shape = spiderGoldShape(a);
242
+ // the coverage class = the outer shape + a nesting flag + a set-op tag (both make a distinct, VISIBLE class).
243
+ var klass = shape.join('>') + (a.nested ? '|n' : '') + (a.setop ? '|' + a.setop : '');
244
+ return {
245
+ query: r.question, context: { db_id: r.db_id },
246
+ klass: klass, goldShape: shape,
247
+ sql: r.query, nested: a.nested, subquery: a.subquery, setop: a.setop // extras for the blend cell (kept by loadDataset)
248
+ };
249
+ }
250
+ });
251
+
252
+ module.exports = { registerAdapter: registerAdapter, getAdapter: getAdapter, listAdapters: listAdapters, loadDataset: loadDataset,
253
+ analyzeSpiderSQL: analyzeSpiderSQL, spiderGoldShape: spiderGoldShape };
@@ -0,0 +1,92 @@
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
+ * debug-provider.js — a STRUCTURAL debugging provider for the distillation kill-gate (B-thin;
21
+ * `doc/WIP/specs/2026-06-30-distill-killgate-construct-method-design.md`).
22
+ *
23
+ * It snaps a bug to typed facts (the canonicalization barrier, `canonicalize.canonFacts`) and emits a
24
+ * MULTI-OBJECT decomposition sub-graph `Bug → Hypothesis → Localize → Fix(target)` whose typed content
25
+ * (`fixKind`, the intermediate `role`s) is a FUNCTION of the typed `bugClass` — so the cast is
26
+ * signature-determined and therefore crystallizable into a re-mountable `Method` (`crystallizeStructural`).
27
+ * The free text rides an UNTRACKED `debugProse` key (it stays in the model, off the dependency edges).
28
+ *
29
+ * Two non-negotiables it obeys (CLAUDE.md):
30
+ * • a flat length-1 fact patch is SKIPPED by the structural miner (`mine.js`), so the cast emits a
31
+ * multi-object sub-graph (intermediate nodes + child segments);
32
+ * • a wired provider does NOT auto-flag its cast, so the head template sets BOTH the cast marker
33
+ * `{ DebugStep:true }` and a DISTINCT durable re-fire guard `{ Decomposed:true }` (the gotcha).
34
+ *
35
+ * `ask` is INJECTABLE: a deterministic stub in tests, the real model in the live harness — so the
36
+ * deterministic backbone and the live measurement share ONE code path. `classify(rawJSON)` decides the
37
+ * typed class (a deterministic table in the stub; the canon barrier on the model's JSON, live).
38
+ *
39
+ * @param opts.ask async ({system,user,maxTokens}) -> string (the model, or a stub).
40
+ * @param opts.parseJSON JSON-salvage fn. Default JSON.parse.
41
+ * @param opts.classify (rawJSON) -> { bugClass, fixKind }. The typed-class decision.
42
+ * @param opts.factsSchema optional override of the closed enum vocab for { bugClass, fixKind }.
43
+ * @returns { AI: { debugStep } } a provider-map fragment (concept provider ref `AI::debugStep`).
44
+ */
45
+ var canonicalize = require('../providers/canonicalize.js');
46
+
47
+ var DEFAULT_FACTS = {
48
+ bugClass: { enum: ['off-by-one', 'null-deref', 'wrong-branch', 'type-mismatch', 'resource-leak'] },
49
+ fixKind: { enum: ['adjust-bound', 'guard-null', 'fix-cond', 'cast', 'release'] },
50
+ };
51
+
52
+ function makeDebugProvider( opts ) {
53
+ opts = opts || {};
54
+ var ask = opts.ask;
55
+ var parseJSON = opts.parseJSON || JSON.parse;
56
+ var classify = opts.classify;
57
+ var factsSchema = opts.factsSchema || DEFAULT_FACTS;
58
+
59
+ return { AI: {
60
+ debugStep: function ( graph, concept, scope, argz, cb ) {
61
+ var base = scope._._id, origin = scope._.originNode, target = scope._.targetNode;
62
+ var sys = 'You debug code. Decompose the bug into hypothesis / localize / fix. ' +
63
+ 'Respond ONLY JSON: {"bugClass":"...","hypothesis":"...","fix":"..."}';
64
+ var usr = 'Failing test: ' + (scope._.failingTest || '?') + '\n' +
65
+ 'Bug: ' + (scope._.bugText || scope._.label || base);
66
+ Promise.resolve(ask({ system: sys, user: usr, maxTokens: 600 }))
67
+ .then(function ( txt ) {
68
+ graph.traceProvider && graph.traceProvider(concept, scope, { prompt: { system: sys, user: usr }, reply: txt });
69
+ var raw = parseJSON(txt);
70
+ var cls = classify(raw); // host/stub picks the typed class
71
+ var cf = canonicalize.canonFacts({ bugClass: cls.bugClass, fixKind: cls.fixKind }, factsSchema);
72
+ var fixKind = cf.facts.fixKind;
73
+ var h = base + '_h', l = base + '_l';
74
+ var head = { $_id: '_parent', DebugStep: true, Decomposed: true,
75
+ debugProse: (raw && ((raw.hypothesis || '') + ' | ' + (raw.fix || ''))) };
76
+ if ( cf.misses.length ) head.DebugStepCanonMiss = cf.misses; // visible + fail-closed (un-cacheable)
77
+ Object.assign(head, cf.facts); // ONLY declared, snapped keys -> TRACKED
78
+ cb(null, [
79
+ head,
80
+ { _id: h, Node: true, role: 'hypothesis', fixKind: fixKind },
81
+ { _id: l, Node: true, role: 'localize' },
82
+ { _id: base + '_a0', Segment: true, originNode: origin, targetNode: h, parentSeg: base },
83
+ { _id: base + '_a1', Segment: true, originNode: h, targetNode: l, parentSeg: base },
84
+ { _id: base + '_a2', Segment: true, originNode: l, targetNode: target, parentSeg: base },
85
+ ]);
86
+ })
87
+ .catch(function ( e ) { cb(null, { $_id: '_parent', DebugStep: true, Decomposed: true, llmError: e.message }); });
88
+ },
89
+ } };
90
+ }
91
+
92
+ module.exports = { makeDebugProvider: makeDebugProvider };