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,100 @@
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
+ * negotiate — THE BOUNDED LLM↔GRAPH DIALOGUE (owner 2026-07-09; ZERO-CORE, host-side). "A thinking mode for a model
10
+ * that doesn't think": instead of the model reasoning internally (CoT), the GRAPH supplies the reasoning as a
11
+ * structured pushback. The model PROPOSES a typed answer; the graph GATES it (`assertPost`); on a mismatch the graph
12
+ * returns the BLAME (why it fails) + the ADMISSIBLE OPTIONS (the domain values that PASS its own gate — enumerated by
13
+ * testing, not guessed) → serialized into a revision prompt (the §5b LEVER) → the model REVISES → repeat, BOUNDED.
14
+ *
15
+ * This turns the one-shot forge (`forge-fallback.js`: propose→gate→refuse) into a NEGOTIATION. It is CEGIS with the
16
+ * graph as the verifier and the model as the synthesizer, made CONSTRUCTIVE (the verifier enumerates admissible
17
+ * options) + defeasible + deterministic. Three invariants, all load-bearing:
18
+ * • 0-FALSE — a non-gated answer is NEVER returned (`ok` ⇒ the gate passed); converge on a gated one or REFUSE.
19
+ * • HONEST REFUSAL — no admissible option ⇒ a typed refusal (`no-admissible-option`); the graph NEVER forces a
20
+ * match (forcing would be the graph hallucinating). This is the "…t'es sûr c'est pas une halu?" with no fit.
21
+ * • TERMINATION — `maxRounds` bounds the loop (no oscillation), the dialogue-level analog of the apply-cap / G3.
22
+ *
23
+ * const solve = makeNegotiate({ propose, gate: gateFromContract(contract), optionsOf, maxRounds: 4 });
24
+ * const { ok, answer, refusal, rounds, trace } = await solve(input, ctx);
25
+ */
26
+ const { assertPost } = require('./contract.js');
27
+
28
+ /**
29
+ * gateFromContract(contract, opts) → gate(candidate) → { ok, blame, violations }
30
+ * The default graph gate: `assertPost` (P4) against a typed contract. candidate = { summary, footprint }.
31
+ * @param opts.oracle a G2 ground-truth probe for an effecting post.
32
+ */
33
+ function gateFromContract( contract, opts ) {
34
+ opts = opts || {};
35
+ return function gate( cand ) {
36
+ const summary = (cand && cand.summary) || {};
37
+ const post = assertPost(contract, summary, (cand && cand.footprint) || Object.keys(summary), { oracle: opts.oracle });
38
+ return { ok: post.ok, blame: post.blame, violations: post.violations };
39
+ };
40
+ }
41
+
42
+ /**
43
+ * admissibleOptions(gate, key, domain) → (candidate) => [values]
44
+ * THE CONSTRUCTIVE part: the graph enumerates "the other options that would match" by TESTING each domain value for
45
+ * `key` through its OWN gate — grounded (the gate decides), never a guess. Returns the domain values the gate admits.
46
+ */
47
+ function admissibleOptions( gate, key, domain ) {
48
+ return function ( cand ) {
49
+ const base = (cand && cand.summary) || {};
50
+ return (domain || []).filter(function ( val ) {
51
+ const probe = Object.assign({}, cand, { summary: Object.assign({}, base, { [key]: val }) });
52
+ return gate(probe).ok;
53
+ });
54
+ };
55
+ }
56
+
57
+ /** defaultSerialize — the LEVER: turn the graph's refusal (blame + admissible options) into a revision prompt. */
58
+ function defaultSerialize( blame, options, cand ) {
59
+ return 'REFUSED (' + ((blame && blame.kind) || 'mismatch') + '): your answer ' + JSON.stringify((cand && cand.summary) || {})
60
+ + ' does not match what is established. The ONLY admissible options are: [' + (options || []).join(', ')
61
+ + ']. Pick the one that fits; if none genuinely fits, say so rather than guessing.';
62
+ }
63
+
64
+ /**
65
+ * makeNegotiate(spec) → async solve(input, ctx) => { ok, answer?, refusal?, rounds, trace, blame? }
66
+ * @param spec.propose async (input, { round, feedback, options, prior, ctx }) => candidate|null the MODEL.
67
+ * round 0 = the initial proposal; round>0 = a revision seeing `feedback` (+ structured `options`).
68
+ * @param spec.gate (candidate, ctx) => { ok, blame } the GRAPH check (default idiom: gateFromContract).
69
+ * @param spec.optionsOf (gateResult, candidate, ctx) => [options] the admissible options (default idiom: admissibleOptions).
70
+ * @param spec.serializeFeedback (blame, options, candidate) => string the LEVER (default: defaultSerialize).
71
+ * @param spec.maxRounds the dialogue bound (default 4).
72
+ */
73
+ function makeNegotiate( spec ) {
74
+ spec = spec || {};
75
+ if ( typeof spec.propose !== 'function' ) throw new Error('makeNegotiate needs spec.propose(input, ctx) -> candidate');
76
+ if ( typeof spec.gate !== 'function' ) throw new Error('makeNegotiate needs spec.gate(candidate) -> { ok, blame }');
77
+ const optionsOf = spec.optionsOf;
78
+ const serialize = spec.serializeFeedback || defaultSerialize;
79
+ const maxRounds = spec.maxRounds || 4;
80
+
81
+ return async function solve( input, ctx ) {
82
+ let feedback = null, options = null, prior = null;
83
+ const trace = [];
84
+ for ( let round = 0; round < maxRounds; round++ ) {
85
+ const cand = await spec.propose(input, { round: round, feedback: feedback, options: options, prior: prior, ctx: ctx });
86
+ if ( cand == null ) { trace.push({ round: round, event: 'no-candidate' }); return { ok: false, refusal: 'no-candidate', rounds: round + 1, trace: trace }; }
87
+ const g = spec.gate(cand, ctx);
88
+ trace.push({ round: round, summary: cand.summary, ok: g.ok, blame: g.blame && g.blame.kind });
89
+ if ( g.ok ) return { ok: true, answer: cand, rounds: round + 1, trace: trace }; // 0-false: only a GATED answer returns ok
90
+ options = optionsOf ? (optionsOf(g, cand, ctx) || []) : [];
91
+ if ( !options.length ) // the graph has no match → HONEST refusal (never force one)
92
+ return { ok: false, refusal: 'no-admissible-option', rounds: round + 1, trace: trace, blame: g.blame };
93
+ feedback = serialize(g.blame, options, cand); // the graph pushes back (blame + options)
94
+ prior = cand;
95
+ }
96
+ return { ok: false, refusal: 'max-rounds', rounds: maxRounds, trace: trace }; // bounded → terminates
97
+ };
98
+ }
99
+
100
+ module.exports = { makeNegotiate, gateFromContract, admissibleOptions, defaultSerialize };
@@ -0,0 +1,139 @@
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
+ * parametric — role-typed SLOTS over an LGG method skeleton: bind, mount fail-closed, and blame per slot.
10
+ *
11
+ * The Probe-#1 mechanics promoted to a brick (2026-07-03; three experiment reuses: selftest, live-probe,
12
+ * live-discover-2). A method crystallized from param-DISJOINT episodes LGGs into a skeleton whose antiUnify
13
+ * HOLES are exactly the param positions — « sous-qualifié = abstrait = un slot ». This module:
14
+ *
15
+ * - `slotBindings` reads each content hole's OWNING template object and types it: role =
16
+ * `<stepKind>#<stepIndex>` (the frame's positional role id);
17
+ * - `mountParametric` fills the holes from typed params and mounts in ONE create-mode mutation (the
18
+ * ZERO-FIRE discipline — never re-decompose). Fail-closed at EVERY hole: a missing
19
+ * param → `{ status:'impracticable', hint:[{role,key,stepKind}] }` — the typed hint
20
+ * (« need sort X in role Y »), never a partial mount, never a provider re-fire;
21
+ * - `slotPostFrom` mint per-SLOT post provenance where the atoms are DECLARED (the H3 rule: build
22
+ * provenance where the knowledge exists, never reverse-engineer it at blame time);
23
+ * - `attributeSlotBlame` the blame-gate PRIMARY of restriction learning (Laurie Q2: incompetence noise is
24
+ * one-sided and self-sealing → classic candidate-elimination is UNSOUND; admitting
25
+ * ONLY blame-localized negatives restores the clean rates): a failed post atom maps
26
+ * to a slot iff its provenance names exactly ONE role; anything shared/unknown/mixed
27
+ * → `unlocalized` and the whole failure is INADMISSIBLE as negative evidence — the
28
+ * lab's C-arm (all-failures-negative) measured the price of skipping this gate
29
+ * (monotone self-sealing on rare sorts, never recovered).
30
+ *
31
+ * The provenance carrier (`postSlots`) mirrors adapt.js#composeContract's `postFrom` (atom→'host'|'donor'|
32
+ * 'both') at slot grain (atom→role|'shared'), with the same canonical atom key (adapt.js#canonAtom) so both
33
+ * provenances survive the `$x==1` ≡ `x == 1` surface variance.
34
+ */
35
+
36
+ const { fillContentHoles } = require('./abstract.js');
37
+ const { mountTemplate } = require('./typed-loop.js');
38
+ const { canonAtom } = require('./adapt.js');
39
+
40
+ /** Locate each content hole's OWNING template object and read its typed role: { path, key, stepKind,
41
+ * stepIndex, role } — role = `<stepKind>#<stepIndex>`. `gen` = adapt.js#methodContentHoles output. */
42
+ function slotBindings( gen ) {
43
+ const slots = [];
44
+ const walk = ( x, path, owner ) => {
45
+ if ( Array.isArray(x) ) { x.forEach(( v, i ) => walk(v, path + '[' + i + ']', owner)); return; }
46
+ if ( x && typeof x === 'object' ) {
47
+ if ( '§var' in x && Object.keys(x).length === 1 ) {
48
+ const key = path.split('.').pop().replace(/\[\d+\]/g, '');
49
+ slots.push({ path: x['§var'], key,
50
+ stepKind: owner && owner.stepKind, stepIndex: owner && owner.stepIndex,
51
+ role: (owner && owner.stepKind || '?') + '#' + (owner && owner.stepIndex != null ? owner.stepIndex : '?') });
52
+ return;
53
+ }
54
+ const own = (x.Segment || x.$_id === '_parent') ? x : owner;
55
+ for ( const k in x ) walk(x[k], path ? path + '.' + k : k, own);
56
+ }
57
+ };
58
+ walk(gen.skeleton, '', null);
59
+ return slots;
60
+ }
61
+
62
+ /**
63
+ * The parametric mount. `paramsByRole` keys: `<role>.<key>` (a slot's hole), or bare `<role>` when the role
64
+ * carries a single hole. Fail-closed at EVERY hole: missing → `{ status:'impracticable', hint }`, NOTHING
65
+ * mounted; complete → `{ status:'complete', mutation, filled, values }` for `graph.pushMutation`
66
+ * (create-mode: task + structure in ONE atomic mutation — the ZERO-FIRE discipline). `values` (path → value)
67
+ * is the fill-time provenance a per-slot contract minter reads.
68
+ */
69
+ function mountParametric( gen, slots, site, paramsByRole ) {
70
+ const values = {}, hint = [];
71
+ for ( const s of slots ) {
72
+ const v = paramsByRole && (s.role + '.' + s.key in paramsByRole ? paramsByRole[s.role + '.' + s.key] : paramsByRole[s.role]);
73
+ if ( v != null && v !== '' ) values[s.path] = v;
74
+ else hint.push({ role: s.role, key: s.key, stepKind: s.stepKind });
75
+ }
76
+ if ( hint.length ) return { status: 'impracticable', hint };
77
+ const filled = fillContentHoles(gen.skeleton, values);
78
+ if ( !filled ) return { status: 'impracticable', hint: slots.map(( s ) => ({ role: s.role, key: s.key, stepKind: s.stepKind })) };
79
+ const mutation = mountTemplate(filled, site);
80
+ if ( !mutation ) return { status: 'impracticable', hint: [{ role: 'frontier', key: '_id' }] };
81
+ return { status: 'complete', mutation, filled, values };
82
+ }
83
+
84
+ /** Mint per-slot post provenance from atoms DECLARED per role: `{ role: [atoms] }` →
85
+ * `{ post, postSlots }` — postSlots maps each canonical atom to its role, or 'shared' when two roles claim
86
+ * the same atom (the 'both' of postFrom: a shared atom can never localize a failure). */
87
+ function slotPostFrom( atomsByRole ) {
88
+ const post = [], postSlots = {};
89
+ for ( const role in (atomsByRole || {}) )
90
+ for ( const atom of atomsByRole[role] || [] ) {
91
+ const k = canonAtom(atom);
92
+ if ( !(k in postSlots) ) { post.push(atom); postSlots[k] = role; }
93
+ else if ( postSlots[k] !== role ) postSlots[k] = 'shared';
94
+ }
95
+ return { post, postSlots };
96
+ }
97
+
98
+ /** The per-SLOT blame rule (the H3 doctrine at slot grain). Given the post atoms `assertPost` reported
99
+ * violated on a parametric mount, attribute each to its provenance role. A failure is ADMISSIBLE as negative
100
+ * evidence for a slot iff EVERY failed atom localizes to the SAME single role — anything shared, unknown, or
101
+ * spanning roles → `{ admissible:false, role:null }` and the caller DISCARDS it (the blame-gate: an
102
+ * unlocalized failure is indistinguishable from model incompetence — Laurie Q2/8a). The same discipline
103
+ * extends to causes OUTSIDE the provenance map: when the failing episode carries a co-present UNKNOWN
104
+ * cause (e.g. an unresolved OOV token in the resolution path), the caller must treat the failure as
105
+ * unlocalized even if the failed atoms map to one role — field-proven by the G4 alias-ring probe, where
106
+ * skipping this wrong-blamed (and retracted) a CORRECT learned alias on episodes whose failure was owed
107
+ * to vocabulary attrition elsewhere.
108
+ * @param opts { contract | postSlots, failedAtoms }
109
+ * @returns { perAtom:[{atom, role}], role, admissible } — perAtom role 'unlocalized' when unmapped/shared. */
110
+ function attributeSlotBlame( opts ) {
111
+ const postSlots = (opts && (opts.postSlots || (opts.contract && opts.contract.postSlots))) || {};
112
+ const perAtom = ((opts && opts.failedAtoms) || []).map(( atom ) => {
113
+ const role = postSlots[canonAtom(atom)];
114
+ return { atom, role: role && role !== 'shared' ? role : 'unlocalized' };
115
+ });
116
+ const roles = new Set(perAtom.map(( x ) => x.role ));
117
+ const admissible = perAtom.length > 0 && roles.size === 1 && !roles.has('unlocalized');
118
+ return { perAtom, role: admissible ? [...roles][0] : null, admissible };
119
+ }
120
+
121
+ /** The per-SLOT credit rule — the blame's dual (Laurie 8d: POSITIVE credit to a composite must localize
122
+ * like blame or it OVER-GENERALIZES — a success only verifies the slots it actually EXERCISED; crediting an
123
+ * unexercised slot lifts its LGG on zero evidence). Asymmetry with blame, deliberate: a verified atom is
124
+ * DIRECT evidence for its own role (each localized atom credits its role independently — several roles may
125
+ * be credited by one success), whereas a failed atom set is a DISJUNCTION of causes (blame needs unanimity).
126
+ * Shared/unknown atoms credit NOTHING (fail-closed).
127
+ * @param opts { contract | postSlots, verifiedAtoms }
128
+ * @returns { perAtom:[{atom, role}], roles } — roles = the creditable slots, sorted, deduplicated. */
129
+ function attributeSlotCredit( opts ) {
130
+ const postSlots = (opts && (opts.postSlots || (opts.contract && opts.contract.postSlots))) || {};
131
+ const perAtom = ((opts && opts.verifiedAtoms) || []).map(( atom ) => {
132
+ const role = postSlots[canonAtom(atom)];
133
+ return { atom, role: role && role !== 'shared' ? role : 'unlocalized' };
134
+ });
135
+ const roles = [...new Set(perAtom.map(( x ) => x.role ).filter(( r ) => r !== 'unlocalized' ))].sort();
136
+ return { perAtom, roles };
137
+ }
138
+
139
+ module.exports = { slotBindings, mountParametric, slotPostFrom, attributeSlotBlame, attributeSlotCredit };
@@ -0,0 +1,75 @@
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
+ * reaggregate — DEFEASIBLE RE-AGGREGATION (host-side, ZERO-CORE). U3 / the study's A6-A7 point; closes
10
+ * finding #31 (the E4 defeasance wedge worked at the BELIEF level — a retracted premise dropped its concept
11
+ * — but the `{__push}`-aggregated SUMMARY did NOT auto-update, because push is monotonic and a cast rollup
12
+ * does not re-fire on a value change, #22). So a derived summary stayed STALE after a part retracted.
13
+ *
14
+ * THE MECHANISM (proven on the real engine). A part contributes via `{__push}` (race-free fan-in, the
15
+ * accumulation discipline). On RETRACT, the part's `cleaner` runs a SEQUENCED mutation that (a) filters its
16
+ * own contribution out of the anchor's list AND (b) re-folds the summary from what remains — in ONE
17
+ * mutation, so the summary is always consistent with the LIVE cast set without fighting #22. The summary
18
+ * concept folds on cast (after the parts push, fold-after-quiescence). Net: belief AND summary are both
19
+ * defeasible → the E4 wedge is end-to-end.
20
+ *
21
+ * const agg = defeasibleAggregate({ anchor: s => s._.partOf, valueKey: 'val', fold: xs => xs.reduce((a,x)=>a+x.val,0) });
22
+ * Graph._providers = { A: { part: agg.contribute, unpart: agg.uncontribute, summary: agg.summarize } };
23
+ * // Part concept: { require:[…], ensure:['$srcOk'], provider:['A::part'], cleaner:['A::unpart'] }
24
+ * // Summary concept: { require:['Anchor','contributions'], provider:['A::summary'] }
25
+ */
26
+
27
+ /**
28
+ * @param spec.anchor (scope) => anchorId where the aggregate lives (required)
29
+ * @param spec.fold (contributions[]) => value the aggregate (required)
30
+ * @param spec.contribution (scope) => contributionObj default { id: scope._._id, val: scope._[valueKey] }
31
+ * @param spec.same (contrib, scope) => bool identify THIS part's contribution; default by `id`
32
+ * @param spec.valueKey fact key for the default contribution value (default 'val')
33
+ * @param spec.contribKey the anchor list fact (default 'contributions')
34
+ * @param spec.summaryKey the anchor summary fact (default 'summary')
35
+ */
36
+ function defeasibleAggregate( spec ) {
37
+ spec = spec || {};
38
+ const anchorOf = spec.anchor || (( s ) => s._.partOf);
39
+ const fold = spec.fold || (( xs ) => (xs || []).length);
40
+ const valueKey = spec.valueKey || 'val';
41
+ const contribKey = spec.contribKey || 'contributions';
42
+ const summaryKey = spec.summaryKey || 'summary';
43
+ const contribution = spec.contribution || (( s ) => ({ id: s._._id, val: s._[valueKey] }));
44
+ const same = spec.same || (( x, s ) => x && x.id === s._._id);
45
+
46
+ return {
47
+ // PART cast: mark cast + push this part's contribution to the anchor (race-free fan-in via {__push}).
48
+ // If the summary was ALREADY folded once (a later re-add after a drift-recovery — a sequenced,
49
+ // single-part event, not the initial concurrent fan-in), re-fold inline too, since a cast Summary
50
+ // concept won't re-fire on the value change (#22). Initial concurrent fan-in (no summary yet) just
51
+ // pushes; the Summary concept folds the authoritative value after quiescence.
52
+ contribute: function ( graph, concept, scope, argz, cb ) {
53
+ const anchorId = anchorOf(scope), anc = graph.getEtty(anchorId);
54
+ const cur = (anc && anc._[contribKey]) || [];
55
+ const mine = contribution(scope);
56
+ const upd = { $$_id: anchorId, [contribKey]: { __push: mine } };
57
+ if ( anc && anc._[summaryKey] !== undefined ) upd[summaryKey] = fold(cur.concat([mine])); // re-add → re-fold inline
58
+ cb(null, [{ $_id: '_parent', [concept._name]: true }, upd]);
59
+ },
60
+ // PART cleaner (runs on RETRACT): filter this part's contribution out AND re-fold — one sequenced
61
+ // mutation, so the summary tracks the live cast set (the defeasible re-aggregation).
62
+ uncontribute: function ( graph, concept, scope, argz, cb ) {
63
+ const anchorId = anchorOf(scope);
64
+ const anc = graph.getEtty(anchorId);
65
+ const kept = ((anc && anc._[contribKey]) || []).filter(( x ) => !same(x, scope));
66
+ cb(null, { $$_id: anchorId, [contribKey]: kept, [summaryKey]: fold(kept) });
67
+ },
68
+ // SUMMARY cast: fold the live contributions (after the parts have pushed — fold-after-quiescence).
69
+ summarize: function ( graph, concept, scope, argz, cb ) {
70
+ cb(null, { $_id: '_parent', [concept._name]: true, [summaryKey]: fold(scope._[contribKey] || []) });
71
+ }
72
+ };
73
+ }
74
+
75
+ module.exports = { defeasibleAggregate };
@@ -0,0 +1,157 @@
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
+ * rebalance — the DEFEASIBLE REBALANCING FIXPOINT (ZERO-CORE, host-side). The R1 "réorganise les feuilles
10
+ * jusqu'à ce que l'ensemble soit équilibré" operator: a degenerate plan (redundant / over-budget / disordered
11
+ * leaves) is driven to a BALANCED fixpoint by four rules under a LEXICOGRAPHIC termination measure. Kill-gated
12
+ * GO on the real bricks (`WIP/experiments/2026-07-07-kg-r1b-fixpoint` — 4 degeneracies recover, measure
13
+ * monotone, negative-control severed refuses, checkCompose/footprintCycles with teeth).
14
+ *
15
+ * THE FOUR RULES (applied E2→E1→E3→E4 each round — see FINDING below):
16
+ * E2 scission — split any over-budget node into within-budget atoms (the typed-loop's job downstream)
17
+ * E1 fusion — collapse leaves with an IDENTICAL fusion key (redundant); distinct keys stay PARALLEL
18
+ * E3 reorder — topo by data-flow (a consumer reads its producers' writes → consumer last);
19
+ * `footprintCycles` rejects an illegal RETRACTABLE cycle → typed refusal, never oscillation
20
+ * E4 rewrite — re-fold a consumer from its LIVE producers (defeasibleAggregate semantics)
21
+ *
22
+ * FINDING (kill-gate KG-R1b): apply **E2 BEFORE E1**. E1-before-E2 is UNSOUND — when a bundle splits into
23
+ * atoms that duplicate existing leaves, E2 re-creates the redundancy E1 just removed, so the lexicographic
24
+ * measure GROWS mid-round (measured `[1]→[2,0,0,0]→[0]`). Split-then-dedupe restores monotonicity: E1 dedupes
25
+ * ALL atoms after scission, and fusing identical leaves never creates over-budget. ZERO-CORE (rule ordering).
26
+ *
27
+ * TERMINATION: measure = (redundancy, overBudget, inversions, staleness), lexicographic. Each round the tuple
28
+ * is monotone NON-INCREASING; a `cap` of rounds bounds it and non-convergence → a TYPED refusal (never a
29
+ * silent wedge). def-of-done: done is decided on the LEDGER (the measure), not a cast (#27/#44); the fusion
30
+ * key is a recursive stableStringify (#35); a severed/uncovered node is REFUSED, never silently folded.
31
+ *
32
+ * The brick is domain-agnostic: the caller supplies a `spec` adapting its plan nodes. `plan-loop.js` wires the
33
+ * spec to a decomposed report (typed requests, boundedProject digests, the C6 leaf ladder, checkCompose
34
+ * reassembly). Usable à nu with any plan whose nodes carry a fusion key, an over-budget test, a split, and a
35
+ * producer/consumer footprint.
36
+ *
37
+ * const { rebalancePlan } = require('skynet-graph/lib/authoring/rebalance');
38
+ * const out = rebalancePlan({ order: nodes }, spec); // → { plan, rounds, converged, monotone, refusal, trace }
39
+ */
40
+ const { checkCompose, footprintCycles } = require('./contract.js');
41
+ const { stableStringify } = require('../providers/cache.js');
42
+
43
+ // ── the spec the caller adapts (all optional bar isLeaf/fusionKey/overBudget/split/writes/reads/refold) ──
44
+ // spec.isLeaf(n) → is this node a produced leaf (vs a consumer/root)?
45
+ // spec.fusionKey(n) → stable identity for E1 (default: recursive stableStringify of n.request ?? n.contract)
46
+ // spec.overBudget(n) → does this node exceed the node budget (an E2 target: a bundle or an oversized digest)?
47
+ // spec.split(n) → [subnode…] within budget (E2). Must return ≥2 atoms with DISTINCT ids.
48
+ // spec.writes(n) → [key…] this node writes (its figure ids / results)
49
+ // spec.reads(n) → [key…] this node reads (a consumer reads its producers' writes)
50
+ // spec.refold(root, leaves) → the fresh consumer value from the live producers (E4). MUST be pure (no side
51
+ // effects) — the staleness check calls it too. E4 stores the result on `root.value`.
52
+ // spec.contractOf(n) → { name, contract:{read,write,pre,post,effect} } for checkCompose/footprintCycles
53
+ const CAP_DEFAULT = 8;
54
+
55
+ function defaultFusionKey( n ) { return stableStringify(n.request !== undefined ? n.request : (n.contract || n.id)); }
56
+
57
+ // E2 scission — split over-budget nodes into within-budget atoms (before E1, per the finding).
58
+ function E2( plan, spec ) {
59
+ let fired = false; const out = [];
60
+ for ( const n of plan.order ) {
61
+ if ( spec.isLeaf(n) && spec.overBudget(n) ) { const parts = spec.split(n); if ( parts && parts.length ) { fired = true; out.push(...parts); continue; } }
62
+ out.push(n);
63
+ }
64
+ plan.order = out; return fired;
65
+ }
66
+ // E1 fusion — collapse IDENTICAL leaves (redundant). checkCompose guard: identical key ⇒ unifiable; a distinct
67
+ // key is a DIFFERENT method → kept PARALLEL (never over-merged). Severed/refused leaves are never fused.
68
+ function E1( plan, spec ) {
69
+ const seen = new Set(); let fired = false; const kept = [];
70
+ for ( const n of plan.order ) {
71
+ if ( !spec.isLeaf(n) || n.severed || n.refused || spec.overBudget(n) ) { kept.push(n); continue; }
72
+ const k = spec.fusionKey(n);
73
+ if ( seen.has(k) ) { fired = true; continue; }
74
+ seen.add(k); kept.push(n);
75
+ }
76
+ plan.order = kept; return fired;
77
+ }
78
+ // E3 reorder — data-flow topo: a consumer reads its producers' writes, so it comes AFTER them. footprintCycles
79
+ // rejects an illegal retractable back-edge (a producer reading a consumer's write) BEFORE reordering.
80
+ function E3( plan, spec ) {
81
+ if ( spec.contractOf ) {
82
+ const cyc = footprintCycles(plan.order.map(spec.contractOf).filter(Boolean));
83
+ if ( cyc.length ) { plan._cycleRejected = cyc; return false; }
84
+ }
85
+ const producers = plan.order.filter(( n ) => spec.isLeaf(n) );
86
+ const consumers = plan.order.filter(( n ) => !spec.isLeaf(n) );
87
+ const want = producers.concat(consumers); // producers before consumers
88
+ const fired = plan.order.some(( n, i ) => n !== want[i] );
89
+ plan.order = want; return fired;
90
+ }
91
+ // E4 rewrite — re-fold each consumer from the LIVE producers (defeasibleAggregate semantics). Fires if stale.
92
+ function E4( plan, spec ) {
93
+ let fired = false;
94
+ const leaves = plan.order.filter(( n ) => spec.isLeaf(n) );
95
+ for ( const c of plan.order ) if ( !spec.isLeaf(c) ) {
96
+ const fresh = spec.refold(c, leaves);
97
+ if ( c.value !== fresh ) { c.value = fresh; fired = true; } // c.value is the ledger (NOT spec.valueOf — that inherits Object.prototype.valueOf)
98
+ }
99
+ return fired;
100
+ }
101
+
102
+ // ── the lexicographic termination measure (redundancy, overBudget, inversions, staleness) ──
103
+ function measure( plan, spec ) {
104
+ const singles = plan.order.filter(( n ) => spec.isLeaf(n) && !n.severed && !n.refused && !spec.overBudget(n) );
105
+ const keys = singles.map(spec.fusionKey);
106
+ const redundancy = keys.length - new Set(keys).size;
107
+ const over = plan.order.filter(( n ) => spec.isLeaf(n) && spec.overBudget(n) ).length;
108
+ const cIdx = plan.order.findIndex(( n ) => !spec.isLeaf(n) );
109
+ const inversions = cIdx === -1 ? 0 : plan.order.slice(cIdx + 1).filter(( n ) => spec.isLeaf(n) ).length;
110
+ let staleness = 0;
111
+ if ( cIdx !== -1 ) {
112
+ const leaves = plan.order.filter(( n ) => spec.isLeaf(n) );
113
+ for ( const c of plan.order ) if ( !spec.isLeaf(c) ) {
114
+ if ( c.value !== spec.refold(c, leaves) ) { staleness = 1; break; }
115
+ }
116
+ }
117
+ return [redundancy, over, inversions, staleness];
118
+ }
119
+ const lexLE = ( a, b ) => { for ( let i = 0; i < a.length; i++ ) { if ( a[i] < b[i] ) return true; if ( a[i] > b[i] ) return false; } return true; };
120
+
121
+ /**
122
+ * rebalancePlan(plan, spec, opts) — drive `plan` to a balanced fixpoint. Returns:
123
+ * { plan, rounds, converged, monotone, refusal:null|'CYCLE'|'NONCONVERGENCE', trace:[measure…] }
124
+ * `refusal` is a TYPED non-null outcome the caller surfaces (never a silent wedge). `monotone:false` flags a
125
+ * termination-measure violation (a rule grew an earlier measure — a bug to fix, not to accept).
126
+ */
127
+ function rebalancePlan( plan, spec, opts ) {
128
+ opts = opts || {};
129
+ spec = Object.assign({ fusionKey: defaultFusionKey }, spec);
130
+ const cap = opts.cap || CAP_DEFAULT;
131
+ const trace = [measure(plan, spec)];
132
+ let round = 0, monotone = true;
133
+ for ( ; round < cap; round++ ) {
134
+ const f2 = E2(plan, spec), f1 = E1(plan, spec), f3 = E3(plan, spec), f4 = E4(plan, spec);
135
+ if ( plan._cycleRejected ) { round++; break; } // illegal structure → typed refusal
136
+ const m = measure(plan, spec), prev = trace[trace.length - 1];
137
+ if ( !lexLE(m, prev) ) monotone = false;
138
+ trace.push(m);
139
+ if ( !f1 && !f2 && !f3 && !f4 ) { round++; break; } // FIXPOINT
140
+ }
141
+ const balanced = !plan._cycleRejected && measure(plan, spec).every(( x ) => x === 0 );
142
+ const converged = balanced && round <= cap;
143
+ return { plan, rounds: round, converged, monotone,
144
+ refusal: plan._cycleRejected ? 'CYCLE' : (!converged ? 'NONCONVERGENCE' : null), trace };
145
+ }
146
+
147
+ // checkReassembly(consumer, producers, opts) — is folding the producers into the consumer SOUND, and does the
148
+ // consumer read anything NO producer writes (an uncovered claim-of-absence)? Uses the real checkCompose. ──
149
+ function checkReassembly( consumer, producers, spec ) {
150
+ const cc = producers.map(( p ) => checkCompose(spec.contractOf(p), spec.contractOf(consumer)) );
151
+ const anyUnsound = cc.some(( r ) => r.verdict === 'unsound' );
152
+ const written = new Set(producers.flatMap(spec.writes));
153
+ const uncovered = spec.reads(consumer).filter(( k ) => !written.has(k) );
154
+ return { sound: !anyUnsound && uncovered.length === 0, anyUnsound, uncovered, perProducer: cc };
155
+ }
156
+
157
+ module.exports = { rebalancePlan, checkReassembly, measure, E1, E2, E3, E4, defaultFusionKey };
@@ -0,0 +1,135 @@
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
+ * recall — FUZZY-RECALL → TYPED-VERIFY retrieval (host-side, ZERO-CORE). U5 / the master-graph study §2.4.
10
+ *
11
+ * The derivation cache is an EXACT content-address (a hash hit on the K1 digest): a miss is total, so a
12
+ * related-but-different problem pays full price even when a verifiable NEAR method exists. U5 adds the
13
+ * missing arm — a RECALL front-end that PROPOSES close methods, gated by an exact TYPED VERIFY that ADMITS.
14
+ *
15
+ * THE CONTRACT (the load-bearing soundness line):
16
+ * - RECALL is FUZZY (embedding / similarity over the whole method signature). It may be lossy and may
17
+ * surface false neighbours. It only ORDERS + BUDGETS candidates — it NEVER admits.
18
+ * - VERIFY is EXACT + TYPED. It gates on the STRUCTURE (the K1-canonical fields that DEFINE the method),
19
+ * never on the similarity score. Three verdicts:
20
+ * · full — structure matches AND content matches → replay at 0 model calls (= the cache hit).
21
+ * · partial — structure matches, content differs → mount the shared skeleton (U1 instantiate),
22
+ * RE-FORGE only the differing content holes.
23
+ * · reject — structure mismatch → forge fresh. NO false replay, whatever the score.
24
+ *
25
+ * So a candidate that is similar by embedding (overlapping CONTENT tokens) but has a DIFFERENT typed
26
+ * STRUCTURE is high-similarity yet REJECTED — recall's fuzziness can never write an unsound derivation
27
+ * (the canonicalize.js line; worst case of bad recall = a wasted lookup + a fresh call). The win compounds
28
+ * with concept-as-graph reuse: more crystallized structural methods → more partial hits → fewer model calls.
29
+ *
30
+ * const idx = createRecallIndex();
31
+ * idx.add({ structure:{oKind:'A',tKind:'B'}, content:{mid:'m1'} }, methodA);
32
+ * const cands = idx.recall({ structure:{oKind:'A',tKind:'B'}, content:{mid:'m9'} }, 3); // fuzzy, ranked
33
+ * const v = verify(querySig, cands[0].sig); // { mode:'partial', reForge:['mid'], reuse:['oKind','tKind'] }
34
+ */
35
+
36
+ // ---- default embedding: a token multiset over the signature's typed values (deterministic, dep-free) ----
37
+ function defaultEmbed( sig ) {
38
+ const toks = new Map();
39
+ const add = ( t ) => toks.set(t, (toks.get(t) || 0) + 1);
40
+ const walk = ( o ) => {
41
+ if ( o == null ) return;
42
+ if ( typeof o !== 'object' ) { add(String(o)); return; }
43
+ for ( const k of Object.keys(o) ) { add('k:' + k); walk(o[k]); }
44
+ };
45
+ walk(sig);
46
+ return toks;
47
+ }
48
+
49
+ // cosine over token multisets (the default similarity — pluggable via opts.sim / a real embedder via opts.embed).
50
+ function cosine( a, b ) {
51
+ let dot = 0, na = 0, nb = 0;
52
+ for ( const [, v] of a ) na += v * v;
53
+ for ( const [, v] of b ) nb += v * v;
54
+ for ( const [t, v] of a ) if ( b.has(t) ) dot += v * b.get(t);
55
+ return (na && nb) ? dot / Math.sqrt(na * nb) : 0;
56
+ }
57
+
58
+ function createRecallIndex( opts ) {
59
+ opts = opts || {};
60
+ const embed = opts.embed || defaultEmbed;
61
+ const sim = opts.sim || cosine;
62
+ const entries = [];
63
+ return {
64
+ /** index a method under its typed signature { structure, content }. */
65
+ add( sig, method ) { entries.push({ sig: sig, method: method, vec: embed(sig) }); return this; },
66
+ /** FUZZY recall: top-k candidates by similarity over the WHOLE signature. ORDERS only — never admits. */
67
+ recall( sig, k ) {
68
+ const v = embed(sig);
69
+ return entries
70
+ .map(( e ) => ({ method: e.method, sig: e.sig, score: sim(v, e.vec) }))
71
+ .sort(( a, b ) => b.score - a.score)
72
+ .slice(0, k || 3);
73
+ },
74
+ /** remove every entry whose signature canonicalizes to `sig` — used on DRIFT (a stale method must not
75
+ * be recalled; a drifted premise means RE-DERIVE, never replay the invalidated method). */
76
+ remove( sig ) {
77
+ const c = canon(sig);
78
+ let n = 0;
79
+ for ( let i = entries.length - 1; i >= 0; i-- ) if ( canon(entries[i].sig) === c ) { entries.splice(i, 1); n++; }
80
+ return n;
81
+ },
82
+ entries,
83
+ size() { return entries.length; }
84
+ };
85
+ }
86
+
87
+ // stable, key-sorted stringify (digest-safe; used by the exact typed VERIFY).
88
+ function canon( x ) {
89
+ if ( x === undefined ) return 'null';
90
+ if ( x === null || typeof x !== 'object' ) return JSON.stringify(x);
91
+ if ( Array.isArray(x) ) return '[' + x.map(canon).join(',') + ']';
92
+ return '{' + Object.keys(x).sort().map(( k ) => JSON.stringify(k) + ':' + canon(x[k])).join(',') + '}';
93
+ }
94
+
95
+ /**
96
+ * The EXACT TYPED VERIFY gate. Decides reuse on the STRUCTURE (the K1 fields that DEFINE the method),
97
+ * independent of the recall similarity score.
98
+ * @param query { structure, content } the new problem's typed signature
99
+ * @param candidate { structure, content } a recalled method's signature
100
+ * @returns { mode:'full'|'partial'|'reject', reuse:[fields], reForge:[fields] }
101
+ * - 'reject' : structure mismatch (or empty shared structure) — forge fresh; no false replay.
102
+ * - 'full' : structure AND content match — replay at 0 calls.
103
+ * - 'partial' : structure matches, content differs — mount the skeleton (reuse), re-forge the diff (reForge).
104
+ */
105
+ function verify( query, candidate ) {
106
+ const qs = (query && query.structure) || {}, cs = (candidate && candidate.structure) || {};
107
+ const qStructKeys = Object.keys(qs).sort();
108
+ // STRUCTURE must match exactly (same typed shape) — the K1 barrier. Any structural difference ⇒ reject.
109
+ if ( !qStructKeys.length || canon(qs) !== canon(cs) ) return { mode: 'reject', reuse: [], reForge: [] };
110
+ const qc = (query && query.content) || {}, cc = (candidate && candidate.content) || {};
111
+ const reForge = [];
112
+ for ( const k of new Set([...Object.keys(qc), ...Object.keys(cc)]) )
113
+ if ( canon(qc[k]) !== canon(cc[k]) ) reForge.push(k);
114
+ return reForge.length
115
+ ? { mode: 'partial', reuse: qStructKeys, reForge: reForge.sort() } // shared skeleton + re-forge the diff
116
+ : { mode: 'full', reuse: qStructKeys.concat(Object.keys(qc).sort()), reForge: [] };
117
+ }
118
+
119
+ /**
120
+ * Drive recall→verify for a query: recall top-k (fuzzy), then return the FIRST candidate the typed verify
121
+ * admits (full or partial), preferring 'full'. Returns null (→ forge fresh) if none verifies. The chosen
122
+ * candidate is NOT necessarily the highest-similarity one — verify, not the score, decides.
123
+ */
124
+ function recallAndVerify( index, query, k ) {
125
+ const cands = index.recall(query, k || 3);
126
+ let partial = null;
127
+ for ( const c of cands ) {
128
+ const v = verify(query, c.sig);
129
+ if ( v.mode === 'full' ) return { method: c.method, sig: c.sig, score: c.score, verdict: v };
130
+ if ( v.mode === 'partial' && !partial ) partial = { method: c.method, sig: c.sig, score: c.score, verdict: v };
131
+ }
132
+ return partial; // null if every recalled candidate was rejected by the typed verify
133
+ }
134
+
135
+ module.exports = { createRecallIndex, verify, recallAndVerify, defaultEmbed, cosine };