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.
- package/CONTRIBUTING.md +22 -0
- package/LICENSE +661 -0
- package/README.md +245 -0
- package/bin/sg +20 -0
- package/concepts/README.md +23 -0
- package/concepts/_substrate/Claim/Confidence/Trusted.json +6 -0
- package/concepts/_substrate/Claim/Confidence.json +10 -0
- package/concepts/_substrate/Claim/Freshness/Stale.json +7 -0
- package/concepts/_substrate/Claim/Freshness.json +5 -0
- package/concepts/_substrate/Claim/Verification/Refuted.json +6 -0
- package/concepts/_substrate/Claim/Verification.json +6 -0
- package/concepts/_substrate/Claim.json +4 -0
- package/concepts/_substrate/Frontier/InBeam.json +6 -0
- package/concepts/_substrate/Frontier/Stuck.json +6 -0
- package/concepts/_substrate/Frontier.json +5 -0
- package/concepts/_substrate/Intake/ToTask.json +22 -0
- package/concepts/_substrate/Intake/Typed.json +10 -0
- package/concepts/_substrate/Intake.json +27 -0
- package/concepts/_substrate/Task/Answer.json +12 -0
- package/concepts/_substrate/Task/Complexity/Atomic.json +5 -0
- package/concepts/_substrate/Task/Complexity/Compound/Expansion.json +7 -0
- package/concepts/_substrate/Task/Complexity/Compound.json +6 -0
- package/concepts/_substrate/Task/Complexity.json +6 -0
- package/concepts/_substrate/Task/EvalComplexity.json +13 -0
- package/concepts/_substrate/Task/ReportUp.json +6 -0
- package/concepts/_substrate/Task/Rollup.json +10 -0
- package/concepts/_substrate/Task.json +5 -0
- package/concepts/clinical/Diagnosis/Medication.json +6 -0
- package/concepts/clinical/Diagnosis.json +15 -0
- package/concepts/clinical/Observation/LabValue/OutOfRange.json +6 -0
- package/concepts/clinical/Observation/LabValue.json +5 -0
- package/concepts/clinical/Observation.json +6 -0
- package/concepts/common/Document/pathBasket.json +6 -0
- package/concepts/common/Document.json +3 -0
- package/concepts/common/Edge/Distance.json +13 -0
- package/concepts/common/Edge/Stay/LongStay.json +8 -0
- package/concepts/common/Edge/Stay.json +8 -0
- package/concepts/common/Edge/Travel/LongTravel.json +16 -0
- package/concepts/common/Edge/Travel/ShortTravel.json +11 -0
- package/concepts/common/Edge/Travel/targetNode.json +7 -0
- package/concepts/common/Edge/Travel.json +7 -0
- package/concepts/common/Edge.json +3 -0
- package/concepts/common/Vertice.json +3 -0
- package/concepts/supply/Fulfillment.json +10 -0
- package/concepts/supply/Inventory/Reorder.json +7 -0
- package/concepts/supply/Inventory.json +5 -0
- package/concepts/supply/Procurement/SupplierConfirm.json +6 -0
- package/concepts/supply/Procurement.json +8 -0
- package/concepts/supply/Transport.json +5 -0
- package/doc/API.md +556 -0
- package/doc/CAPABILITIES.md +373 -0
- package/doc/MODELISATION.md +511 -0
- package/doc/architecture.md +267 -0
- package/doc/concept-as-graph.md +379 -0
- package/doc/concept-learning.md +168 -0
- package/doc/creative-loop-map.md +202 -0
- package/doc/original-2016-doc.md +148 -0
- package/doc/usage.md +501 -0
- package/lib/authoring/abstract.js +401 -0
- package/lib/authoring/abstraction.js +190 -0
- package/lib/authoring/adapt.js +393 -0
- package/lib/authoring/ancestry.js +216 -0
- package/lib/authoring/author.js +147 -0
- package/lib/authoring/bounded-merge.js +57 -0
- package/lib/authoring/canon.js +158 -0
- package/lib/authoring/clock.js +123 -0
- package/lib/authoring/combinator.js +92 -0
- package/lib/authoring/compete.js +76 -0
- package/lib/authoring/compose-hotspot.js +314 -0
- package/lib/authoring/compress.js +279 -0
- package/lib/authoring/concept-net.js +254 -0
- package/lib/authoring/concepts.js +66 -0
- package/lib/authoring/context-project.js +277 -0
- package/lib/authoring/contract.js +476 -0
- package/lib/authoring/corpus-pack.js +125 -0
- package/lib/authoring/cost-probe.js +86 -0
- package/lib/authoring/crystallize.js +555 -0
- package/lib/authoring/dag-decompose.js +256 -0
- package/lib/authoring/dataset-adapter.js +253 -0
- package/lib/authoring/debug-provider.js +92 -0
- package/lib/authoring/decompose.js +343 -0
- package/lib/authoring/emittability.js +236 -0
- package/lib/authoring/equilibrium.js +160 -0
- package/lib/authoring/extract.js +153 -0
- package/lib/authoring/forest.js +55 -0
- package/lib/authoring/forge-fallback.js +116 -0
- package/lib/authoring/givens.js +121 -0
- package/lib/authoring/glossary.js +177 -0
- package/lib/authoring/grammar-graph.js +191 -0
- package/lib/authoring/granularity.js +80 -0
- package/lib/authoring/graph-net.js +108 -0
- package/lib/authoring/ground.js +148 -0
- package/lib/authoring/higher-order.js +85 -0
- package/lib/authoring/hotspot.js +132 -0
- package/lib/authoring/hysteresis.js +85 -0
- package/lib/authoring/index.js +106 -0
- package/lib/authoring/lattice-morphism.js +61 -0
- package/lib/authoring/lattice-pack.js +148 -0
- package/lib/authoring/leaf-io.js +77 -0
- package/lib/authoring/library.js +156 -0
- package/lib/authoring/lifecycle.js +92 -0
- package/lib/authoring/loop.js +193 -0
- package/lib/authoring/master-loop.js +132 -0
- package/lib/authoring/mdl.js +209 -0
- package/lib/authoring/memo-stability.js +122 -0
- package/lib/authoring/method-explorer.js +138 -0
- package/lib/authoring/method-pack.js +181 -0
- package/lib/authoring/method.js +202 -0
- package/lib/authoring/mine.js +510 -0
- package/lib/authoring/mount.js +119 -0
- package/lib/authoring/negotiate.js +100 -0
- package/lib/authoring/parametric.js +139 -0
- package/lib/authoring/reaggregate.js +75 -0
- package/lib/authoring/rebalance.js +157 -0
- package/lib/authoring/recall.js +135 -0
- package/lib/authoring/registry.js +350 -0
- package/lib/authoring/relearn.js +147 -0
- package/lib/authoring/retention.js +89 -0
- package/lib/authoring/segment-proxy.js +114 -0
- package/lib/authoring/serve-leaf.js +64 -0
- package/lib/authoring/slot-aware-serve.js +77 -0
- package/lib/authoring/sound-invoke.js +59 -0
- package/lib/authoring/split-serve.js +74 -0
- package/lib/authoring/ste.js +239 -0
- package/lib/authoring/stock.js +101 -0
- package/lib/authoring/store.js +94 -0
- package/lib/authoring/supervise.js +138 -0
- package/lib/authoring/support.js +131 -0
- package/lib/authoring/task-mirror.js +51 -0
- package/lib/authoring/typed-loop.js +160 -0
- package/lib/authoring/validate.js +569 -0
- package/lib/authoring/widen.js +88 -0
- package/lib/combos/appliance.js +189 -0
- package/lib/combos/critique.js +377 -0
- package/lib/combos/defaults.js +86 -0
- package/lib/combos/durable-runner.js +115 -0
- package/lib/combos/forge.js +209 -0
- package/lib/combos/index.js +86 -0
- package/lib/combos/learning-library.js +253 -0
- package/lib/combos/mixture-serve.js +193 -0
- package/lib/combos/plan-loop.js +192 -0
- package/lib/combos/proxy-cache.js +272 -0
- package/lib/combos/self-mod.js +94 -0
- package/lib/durable/audit.js +102 -0
- package/lib/durable/checkpoint-store.js +550 -0
- package/lib/durable/fold.js +62 -0
- package/lib/durable/interpreter.js +242 -0
- package/lib/durable/xlate.js +185 -0
- package/lib/graph/Graph.js +2486 -0
- package/lib/graph/expr.js +307 -0
- package/lib/graph/index.js +26 -0
- package/lib/graph/log.js +134 -0
- package/lib/graph/objects/Concept.js +382 -0
- package/lib/graph/objects/Entity.js +503 -0
- package/lib/graph/objects/Node.js +57 -0
- package/lib/graph/objects/PathMap.js +557 -0
- package/lib/graph/objects/Segment.js +100 -0
- package/lib/graph/tasks/stabilize.js +59 -0
- package/lib/graph/tasks/taskflow.js +247 -0
- package/lib/index.js +122 -0
- package/lib/load.js +132 -0
- package/lib/providers/backends.js +117 -0
- package/lib/providers/borderline.js +129 -0
- package/lib/providers/cache.js +226 -0
- package/lib/providers/canonicalize.js +198 -0
- package/lib/providers/constat.js +107 -0
- package/lib/providers/geo.js +53 -0
- package/lib/providers/index.js +116 -0
- package/lib/providers/intake.js +263 -0
- package/lib/providers/llm-local.js +88 -0
- package/lib/providers/llm.js +236 -0
- package/lib/providers/local-host.js +197 -0
- package/lib/providers/merge-consistency.js +162 -0
- package/lib/providers/nogood.js +149 -0
- package/lib/providers/reason-loop.js +223 -0
- package/lib/providers/semiring.js +285 -0
- package/lib/providers/solver-fork.js +163 -0
- package/lib/providers/stats.js +228 -0
- package/lib/providers/verify.js +120 -0
- package/lib/runtime/index.js +135 -0
- package/lib/runtime/invoke-pool.js +92 -0
- package/lib/runtime/protocol.js +210 -0
- package/lib/runtime/transport-socket.js +77 -0
- package/lib/runtime/worker-entry.js +31 -0
- package/lib/sg/cli.js +601 -0
- package/lib/sg/log-sinks.js +214 -0
- package/lib/sg/mcp.js +395 -0
- package/lib/sg/proxy-run.js +66 -0
- package/lib/sg/serve-demo.js +185 -0
- package/lib/sg/serve.js +205 -0
- package/lib/sg/trace.js +118 -0
- package/lib/studio/protocol.js +45 -0
- package/lib/studio/public/app.js +310 -0
- package/lib/studio/public/components/ConceptEditor.js +43 -0
- package/lib/studio/public/components/ConceptTree.js +43 -0
- package/lib/studio/public/components/CorpusPanel.js +68 -0
- package/lib/studio/public/components/ForkTree.js +36 -0
- package/lib/studio/public/components/GrammarGraph.js +99 -0
- package/lib/studio/public/components/GraphCanvas.js +156 -0
- package/lib/studio/public/components/Inspector.js +48 -0
- package/lib/studio/public/components/LearningPanel.js +85 -0
- package/lib/studio/public/components/PromptConsole.js +46 -0
- package/lib/studio/public/components/ProviderTrace.js +39 -0
- package/lib/studio/public/components/SessionSplit.js +47 -0
- package/lib/studio/public/components/TilingOverlay.js +40 -0
- package/lib/studio/public/components/Timeline.js +57 -0
- package/lib/studio/public/index.html +26 -0
- package/lib/studio/public/styles.css +185 -0
- package/lib/studio/public/ws.js +58 -0
- package/lib/studio/server.js +130 -0
- package/lib/studio/session.js +397 -0
- package/lib/studio/studio.js +149 -0
- package/package.json +42 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Nathanael Braun
|
|
3
|
+
* @author : Nathanael BRAUN <pp9ping@gmail.com>
|
|
4
|
+
*
|
|
5
|
+
* This program is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
* (at your option) any later version.
|
|
9
|
+
*
|
|
10
|
+
* This program is distributed in the hope that it will be useful,
|
|
11
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
* GNU Affero General Public License for more details.
|
|
14
|
+
*
|
|
15
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
'use strict';
|
|
19
|
+
/**
|
|
20
|
+
* The "answer a (huge) prompt" loop, as a reusable concept set + helpers.
|
|
21
|
+
*
|
|
22
|
+
* DECOMPOSE (reactive concepts): a root segment (start->goal) carrying the prompt
|
|
23
|
+
* Task require Segment every segment is a Task
|
|
24
|
+
* EvalComplexity require Task -> Atomic | NeedsSplit (depth floor forces Atomic)
|
|
25
|
+
* Expand require NeedsSplit -> child sub-step segments + expandedInto
|
|
26
|
+
* Answer require Atomic -> a leaf answer
|
|
27
|
+
*
|
|
28
|
+
* SYNTHESIZE — two regimes, same bounded rollup:
|
|
29
|
+
*
|
|
30
|
+
* (a) DETERMINISTIC post-pass (`synthesize`, the one-shot default): a post-order
|
|
31
|
+
* walk leaf->root. Race-free, simple, O(V+E); reactivity buys nothing when
|
|
32
|
+
* everything is cold. Use with `loopConceptTree`.
|
|
33
|
+
*
|
|
34
|
+
* (b) REACTIVE concepts (`reactiveLoopConceptTree`, the live/standing regime): each
|
|
35
|
+
* answered segment appends its id to its parent's grow-only `answeredBy` array via
|
|
36
|
+
* the race-free `{__push}` primitive; a `Rollup` concept gated on
|
|
37
|
+
* `ensure:["$answeredBy.length == $expandedInto.length"]` (a monotone G-Set
|
|
38
|
+
* cardinality predicate — §5.2) fires EXACTLY ONCE when the last child reports,
|
|
39
|
+
* reads the children's bounded answers (ordered by `expandedInto`) and writes the
|
|
40
|
+
* parent's answer. Synthesis happens IN stabilization, bottom-up, with ZERO core
|
|
41
|
+
* change (the earlier `+=1` counter would have raced — Entity arrays REPLACE on
|
|
42
|
+
* update; `{__push}` appends at serialized apply-time). The completion gate keys on
|
|
43
|
+
* a discrete `.length`, never prose (the #1 typed-fact spine in action).
|
|
44
|
+
* KNOWN LIMIT: this is reactive on COMPLETION; re-rolling on a live leaf-answer
|
|
45
|
+
* CONTENT change needs per-child answer-following (the aggregation gap — roadmap #5).
|
|
46
|
+
*
|
|
47
|
+
* Content (eval/expand/answer/rollup) is INJECTED so the same loop runs with
|
|
48
|
+
* deterministic functions (tests) or an LLM (run-prompt.js).
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
// ---- the concept set (the "library of concept<->prompt pairs", content-agnostic) ----
|
|
52
|
+
const loopConceptTree = {
|
|
53
|
+
childConcepts: {
|
|
54
|
+
Task: {
|
|
55
|
+
_id: 'Task', _name: 'Task', require: 'Segment',
|
|
56
|
+
childConcepts: {
|
|
57
|
+
EvalComplexity: { _id: 'EvalComplexity', _name: 'EvalComplexity', require: ['Task'], provider: ['AI::evalComplexity'] },
|
|
58
|
+
Expand : { _id: 'Expand', _name: 'Expand', require: ['Task', 'NeedsSplit'], provider: ['AI::expand'] },
|
|
59
|
+
Answer : { _id: 'Answer', _name: 'Answer', require: ['Task', 'Atomic'], provider: ['AI::answer'] }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// The reactive variant: the same decompose concepts PLUS the two reactive-synthesis
|
|
66
|
+
// concepts. `ReportUp` (every answered non-root segment) appends its id to its parent's
|
|
67
|
+
// `answeredBy`; `Rollup` (every expanded segment) fires once its children all reported.
|
|
68
|
+
var reactiveLoopConceptTree = (function () {
|
|
69
|
+
var t = JSON.parse(JSON.stringify(loopConceptTree));
|
|
70
|
+
Object.assign(t.childConcepts.Task.childConcepts, {
|
|
71
|
+
ReportUp: { _id: 'ReportUp', _name: 'ReportUp', require: ['Task', 'Answered', 'parentSeg'], provider: ['AI::reportUp'] },
|
|
72
|
+
Rollup : { _id: 'Rollup', _name: 'Rollup', require: ['Task', 'expandedInto'], ensure: ['$answeredBy.length == $expandedInto.length'], provider: ['AI::rollup'] }
|
|
73
|
+
});
|
|
74
|
+
return t;
|
|
75
|
+
})();
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The decomposition providers. Inject content functions:
|
|
79
|
+
* evalFn(scope) -> { atomic: bool } (depth floor still applies)
|
|
80
|
+
* expandFn(scope) -> [{ name, description? }, ...] ordered sub-steps
|
|
81
|
+
* answerFn(scope) -> string a leaf answer
|
|
82
|
+
* rollupFn(seg, childAnswers[]) -> string bounded parent answer (reactive regime)
|
|
83
|
+
* Options: maxDepth (depth floor), maxBranch (cap sub-steps).
|
|
84
|
+
*/
|
|
85
|
+
function makeDecomposeProviders( opts ) {
|
|
86
|
+
opts = opts || {};
|
|
87
|
+
var maxDepth = opts.maxDepth == null ? 2 : opts.maxDepth,
|
|
88
|
+
maxBranch = opts.maxBranch || 4,
|
|
89
|
+
evalFn = opts.evalFn || function () { return { atomic: true }; },
|
|
90
|
+
expandFn = opts.expandFn || function () { return []; },
|
|
91
|
+
answerFn = opts.answerFn || function () { return ''; },
|
|
92
|
+
rollupFn = opts.rollupFn || function ( seg, kids ) { return kids.join(''); };
|
|
93
|
+
|
|
94
|
+
// content fns may be sync OR async (LLM) — Promise.resolve handles both
|
|
95
|
+
return {
|
|
96
|
+
AI: {
|
|
97
|
+
evalComplexity: function ( graph, concept, scope, argz, cb ) {
|
|
98
|
+
var depth = scope._.depth || 0;
|
|
99
|
+
if ( depth >= maxDepth ) return cb(null, { $_id: '_parent', EvalComplexity: true, Atomic: true });// depth floor
|
|
100
|
+
Promise.resolve(evalFn(scope)).then(function ( r ) {
|
|
101
|
+
var facts = { $_id: '_parent', EvalComplexity: true };
|
|
102
|
+
facts[(r && r.atomic) ? 'Atomic' : 'NeedsSplit'] = true;
|
|
103
|
+
cb(null, facts);
|
|
104
|
+
}).catch(function ( e ) { cb(null, { $_id: '_parent', EvalComplexity: true, Atomic: true, llmError: e.message }); });
|
|
105
|
+
},
|
|
106
|
+
expand: function ( graph, concept, scope, argz, cb ) {
|
|
107
|
+
Promise.resolve(expandFn(scope)).then(function ( raw ) {
|
|
108
|
+
var steps = (raw || []).slice(0, maxBranch);
|
|
109
|
+
if ( !steps.length ) return cb(null, { $_id: '_parent', Expand: true, Atomic: true });// nothing to split -> leaf
|
|
110
|
+
var base = scope._._id,
|
|
111
|
+
origin = scope._.originNode,
|
|
112
|
+
target = scope._.targetNode,
|
|
113
|
+
depth = (scope._.depth || 0) + 1,
|
|
114
|
+
childIds = steps.map(function ( _, i ) { return base + '_s' + i; }),
|
|
115
|
+
tpl = [{ $_id: '_parent', Expand: true, expandedInto: childIds }],
|
|
116
|
+
prev = origin;
|
|
117
|
+
steps.forEach(function ( st, i ) {
|
|
118
|
+
var last = i === steps.length - 1,
|
|
119
|
+
tnode = last ? target : base + '_m' + i;
|
|
120
|
+
if ( !last ) tpl.push({ _id: tnode, Node: true, label: st.name });
|
|
121
|
+
tpl.push({
|
|
122
|
+
_id: childIds[i], Segment: true, originNode: prev, targetNode: tnode,
|
|
123
|
+
depth: depth, parentSeg: base, label: st.name, description: st.description
|
|
124
|
+
});
|
|
125
|
+
prev = tnode;
|
|
126
|
+
});
|
|
127
|
+
cb(null, tpl);
|
|
128
|
+
}).catch(function ( e ) { cb(null, { $_id: '_parent', Expand: true, Atomic: true, llmError: e.message }); });
|
|
129
|
+
},
|
|
130
|
+
answer: function ( graph, concept, scope, argz, cb ) {
|
|
131
|
+
// MUST set its own name (Answer:true) as the self-flag, else the engine
|
|
132
|
+
// keeps seeing the concept as applicable and re-fires it forever.
|
|
133
|
+
Promise.resolve(answerFn(scope)).then(function ( a ) {
|
|
134
|
+
cb(null, { $_id: '_parent', Answer: true, Answered: true, answer: a });
|
|
135
|
+
}).catch(function ( e ) { cb(null, { $_id: '_parent', Answer: true, answer: '(error)', llmError: e.message }); });
|
|
136
|
+
},
|
|
137
|
+
// reactive synthesis: an answered child appends its OWN id to the parent's
|
|
138
|
+
// grow-only `answeredBy` (race-free `{__push}` — distinct ids, no lost update).
|
|
139
|
+
reportUp: function ( graph, concept, scope, argz, cb ) {
|
|
140
|
+
var selfId = scope._._id, parentId = scope._.parentSeg;
|
|
141
|
+
cb(null, [
|
|
142
|
+
{ $$_id: parentId, answeredBy: { __push: selfId } },
|
|
143
|
+
{ $_id: '_parent', ReportUp: true }
|
|
144
|
+
]);
|
|
145
|
+
},
|
|
146
|
+
// fires once the completion gate holds; reads children's answers in
|
|
147
|
+
// `expandedInto` order (deterministic) and writes the BOUNDED parent answer.
|
|
148
|
+
rollup: function ( graph, concept, scope, argz, cb ) {
|
|
149
|
+
var kids = (scope._.expandedInto || []).map(function ( id ) {
|
|
150
|
+
var e = graph.getEtty(id);
|
|
151
|
+
return e && e._.answer;
|
|
152
|
+
});
|
|
153
|
+
Promise.resolve(rollupFn(scope._, kids)).then(function ( ans ) {
|
|
154
|
+
cb(null, { $_id: '_parent', Rollup: true, Answered: true, answer: ans });
|
|
155
|
+
}).catch(function ( e ) { cb(null, { $_id: '_parent', Rollup: true, Answered: true, answer: '(error)', llmError: e.message }); });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Bottom-up synthesis: post-order walk from rootId; each non-leaf segment's answer
|
|
163
|
+
* is rollupFn(segmentFacts, childAnswers) (which MUST be bounded — a summary, not a
|
|
164
|
+
* concatenation, in real use). Writes the answer back onto each segment (so it's in
|
|
165
|
+
* the graph + traced) and returns the root answer. Race-free (deterministic walk).
|
|
166
|
+
*
|
|
167
|
+
* @param graph
|
|
168
|
+
* @param rootId the root segment id
|
|
169
|
+
* @param rollupFn (segFacts, childAnswers[]) -> answer string
|
|
170
|
+
*/
|
|
171
|
+
function synthesize( graph, rootId, rollupFn ) {
|
|
172
|
+
// async: rollupFn may be an LLM call. Post-order, sequential (leaves first).
|
|
173
|
+
async function answerOf( segId ) {
|
|
174
|
+
var o = graph.getEtty(segId);
|
|
175
|
+
if ( !o ) return undefined;
|
|
176
|
+
var e = o._,
|
|
177
|
+
kids = e.expandedInto || [];
|
|
178
|
+
if ( !kids.length ) return e.answer;// leaf: already answered by the Answer concept
|
|
179
|
+
var childAnswers = [];
|
|
180
|
+
for ( var i = 0; i < kids.length; i++ ) childAnswers.push(await answerOf(kids[i]));
|
|
181
|
+
var ans = await rollupFn(e, childAnswers);
|
|
182
|
+
graph.pushMutation({ $$_id: segId, answer: ans, Synthesized: true }, segId);
|
|
183
|
+
return ans;
|
|
184
|
+
}
|
|
185
|
+
return answerOf(rootId);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = {
|
|
189
|
+
loopConceptTree : loopConceptTree,
|
|
190
|
+
reactiveLoopConceptTree: reactiveLoopConceptTree,
|
|
191
|
+
makeDecomposeProviders : makeDecomposeProviders,
|
|
192
|
+
synthesize : synthesize
|
|
193
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
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
|
+
* master-loop — the ALWAYS-ON MASTER LOOP (host-side controller, ZERO-CORE). M1 of the productization
|
|
10
|
+
* campaign; the standing controller that wires the master-graph supervisor ARMS into ONE cost-ordered loop
|
|
11
|
+
* (master-graph study §2.2 — the value-of-computation ladder). Per incoming problem it climbs the ladder
|
|
12
|
+
* and takes the FIRST arm that resolves at acceptable cost:
|
|
13
|
+
*
|
|
14
|
+
* MATCH — exact cache hit on the K1 signature → 0 model calls (the warm library).
|
|
15
|
+
* RETRIEVE — fuzzy recall (U5) → typed VERIFY: full → replay (0); → 0 (full) or partial cost
|
|
16
|
+
* partial → mount the shared skeleton + RE-FORGE only the diff.
|
|
17
|
+
* FORGE — the expensive path (fork + LLM + crystallize into the → full cost; warms the library.
|
|
18
|
+
* library); the mount policy (U2) picks the regime.
|
|
19
|
+
* ESCALATE — a method pinned to the K1 floor (deopted K times) always → full cost, never cached.
|
|
20
|
+
* re-forges / stays in the LLM.
|
|
21
|
+
*
|
|
22
|
+
* DRIFT: when a premise changes, `drift(problem)` invalidates the method's cache entry and records a deopt
|
|
23
|
+
* (U2 mount-rank descends toward the ESCALATE floor → the adapt loop terminates). Re-aggregation of a
|
|
24
|
+
* derived summary is the caller's `reaggregate` arm (U3). The controller is DOMAIN-AGNOSTIC: the caller
|
|
25
|
+
* injects `signature`, `forge`, and (optionally) `reForge`; everything else is the built library machinery.
|
|
26
|
+
*
|
|
27
|
+
* const loop = createMasterLoop({ signature, forge, reForge, mount: createMountController() });
|
|
28
|
+
* const r = await loop.solve(problem); // { result, arm, regime, cost }
|
|
29
|
+
* loop.drift(problem); // a premise changed → invalidate + deopt
|
|
30
|
+
*/
|
|
31
|
+
const { digest } = require('../providers/canonicalize.js');
|
|
32
|
+
const { createRecallIndex, recallAndVerify } = require('./recall.js');
|
|
33
|
+
const { createMountController } = require('./mount.js');
|
|
34
|
+
|
|
35
|
+
function canon( x ) {
|
|
36
|
+
if ( x === undefined ) return 'null';
|
|
37
|
+
if ( x === null || typeof x !== 'object' ) return JSON.stringify(x);
|
|
38
|
+
if ( Array.isArray(x) ) return '[' + x.map(canon).join(',') + ']';
|
|
39
|
+
return '{' + Object.keys(x).sort().map(( k ) => JSON.stringify(k) + ':' + canon(x[k])).join(',') + '}';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param opts.signature (problem) => { structure, content } the typed K1 signature (required).
|
|
44
|
+
* STRUCTURE defines the method class (mount/deopt key); CONTENT is what's derived.
|
|
45
|
+
* @param opts.forge async (problem, ctx) => { result, cost, signals? } the expensive path (required).
|
|
46
|
+
* @param opts.reForge async (problem, candidate, reForgeKeys, ctx) => { result, cost } partial re-forge
|
|
47
|
+
* of only the differing content on a recalled skeleton (default: falls back to forge).
|
|
48
|
+
* @param opts.cache a Map-like { has,get,set,delete } (default: in-memory Map). Pass a disk-backed
|
|
49
|
+
* store (M2) to make the library survive restarts.
|
|
50
|
+
* @param opts.index a recall index (default: createRecallIndex()).
|
|
51
|
+
* @param opts.mount a mount controller (default: createMountController()).
|
|
52
|
+
* @param opts.recallK how many candidates recall proposes (default 3).
|
|
53
|
+
* @param opts.signals (problem, methodId) => mount signals (reliability/hitRate/depth/readOnlyFrontier).
|
|
54
|
+
*/
|
|
55
|
+
function createMasterLoop( opts ) {
|
|
56
|
+
opts = opts || {};
|
|
57
|
+
const signature = opts.signature || (( p ) => ({ structure: p, content: {} }));
|
|
58
|
+
const forge = opts.forge;
|
|
59
|
+
const reForge = opts.reForge || ((( p, c, ks, ctx ) => forge(p, ctx))); // no partial path → forge fully
|
|
60
|
+
const cache = opts.cache || new Map();
|
|
61
|
+
const index = opts.index || createRecallIndex();
|
|
62
|
+
const mount = opts.mount || createMountController();
|
|
63
|
+
const recallK = opts.recallK || 3;
|
|
64
|
+
const signalsOf = opts.signals || (() => ({}));
|
|
65
|
+
if ( typeof forge !== 'function' ) throw new Error('master-loop: opts.forge is required');
|
|
66
|
+
|
|
67
|
+
const stats = { match: 0, recallFull: 0, recallPartial: 0, forge: 0, escalate: 0, cost: 0, calls: 0 };
|
|
68
|
+
const keyOf = ( sig ) => digest({ s: canon(sig.structure), c: canon(sig.content) });
|
|
69
|
+
const idOf = ( sig ) => digest({ s: canon(sig.structure) }); // method CLASS id (mount/deopt key)
|
|
70
|
+
const hits = {}; // idOf -> {hit, total} (a cheap hit-rate signal for the mount policy)
|
|
71
|
+
|
|
72
|
+
function bump( id, hit ) { const h = hits[id] || (hits[id] = { hit: 0, total: 0 }); h.total++; if ( hit ) h.hit++; }
|
|
73
|
+
function sig2signals( problem, id ) { const h = hits[id] || { hit: 0, total: 0 }; return Object.assign({ hitRate: h.total ? h.hit / h.total : 0 }, signalsOf(problem, id)); }
|
|
74
|
+
|
|
75
|
+
async function solve( problem ) {
|
|
76
|
+
stats.calls++;
|
|
77
|
+
const sig = signature(problem), key = keyOf(sig), id = idOf(sig);
|
|
78
|
+
|
|
79
|
+
// ── ESCALATE floor: a method deopted to the floor never replays — always re-forge / stay in the LLM.
|
|
80
|
+
if ( mount.regimeOf(id) === 'escalate' ) {
|
|
81
|
+
stats.escalate++; bump(id, false);
|
|
82
|
+
const r = await forge(problem, { id, sig, regime: 'escalate' });
|
|
83
|
+
stats.cost += (r.cost || 0);
|
|
84
|
+
return { result: r.result, arm: 'escalate', regime: 'escalate', cost: r.cost || 0 };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── MATCH: exact cache hit on the K1 signature → 0 model calls.
|
|
88
|
+
if ( cache.has(key) ) {
|
|
89
|
+
stats.match++; bump(id, true);
|
|
90
|
+
const regime = mount.decide(id, sig2signals(problem, id)).regime;
|
|
91
|
+
return { result: cache.get(key), arm: 'match', regime, cost: 0 };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── RETRIEVE: fuzzy recall → typed verify.
|
|
95
|
+
const cand = recallAndVerify(index, sig, recallK);
|
|
96
|
+
if ( cand && cand.verdict.mode === 'full' ) {
|
|
97
|
+
stats.recallFull++; bump(id, true); cache.set(key, cand.method);
|
|
98
|
+
const regime = mount.decide(id, sig2signals(problem, id)).regime;
|
|
99
|
+
return { result: cand.method, arm: 'recall-full', regime, cost: 0 };
|
|
100
|
+
}
|
|
101
|
+
if ( cand && cand.verdict.mode === 'partial' ) {
|
|
102
|
+
stats.recallPartial++; bump(id, false);
|
|
103
|
+
const r = await reForge(problem, cand, cand.verdict.reForge, { id, sig });
|
|
104
|
+
stats.cost += (r.cost || 0);
|
|
105
|
+
cache.set(key, r.result); index.add(sig, r.result);
|
|
106
|
+
const regime = mount.decide(id, sig2signals(problem, id)).regime;
|
|
107
|
+
return { result: r.result, arm: 'recall-partial', regime, cost: r.cost || 0, reForged: cand.verdict.reForge };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── FORGE: the expensive path; warms the library + the recall index.
|
|
111
|
+
stats.forge++; bump(id, false);
|
|
112
|
+
const r = await forge(problem, { id, sig, regime: 'instance' });
|
|
113
|
+
stats.cost += (r.cost || 0);
|
|
114
|
+
cache.set(key, r.result); index.add(sig, r.result);
|
|
115
|
+
const regime = mount.decide(id, Object.assign(sig2signals(problem, id), r.signals)).regime;
|
|
116
|
+
return { result: r.result, arm: 'forge', regime, cost: r.cost || 0 };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** a premise drifted → invalidate this method (cache AND recall index) + record a deopt (U2 mount-rank
|
|
120
|
+
* descends toward the floor). The next solve RE-DERIVES — it must never recall the stale method. */
|
|
121
|
+
function drift( problem ) {
|
|
122
|
+
const sig = signature(problem), key = keyOf(sig), id = idOf(sig);
|
|
123
|
+
cache.delete(key);
|
|
124
|
+
if ( index.remove ) index.remove(sig); // a drifted premise → re-derive, never replay the invalidated method
|
|
125
|
+
const deopts = mount.recordDeopt(id);
|
|
126
|
+
return { id, deopts, regime: mount.regimeOf(id) };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { solve, drift, stats, cache, index, mount, keyOf, idOf };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = { createMasterLoop };
|
|
@@ -0,0 +1,209 @@
|
|
|
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
|
+
* mdl — a STATIC, corpus-level, bits-based MDL admission objective for grammar induction
|
|
21
|
+
* (#12, the §4.2-A front-end). The cheap pre-filter/ranker IN FRONT OF the empirical gate
|
|
22
|
+
* (`abstraction.evaluate`), NEVER a replacement.
|
|
23
|
+
*
|
|
24
|
+
* Where `evaluate` BOOTS the engine twice per candidate on ONE seed (grounded but expensive,
|
|
25
|
+
* single-episode, can over-fit a seed), `mdl` is O(corpus) arithmetic with NO boot: it
|
|
26
|
+
* aggregates the co-firing `count` mined across EVERY episode, so it ranks GENERALITY and
|
|
27
|
+
* prunes the many `mineChains` candidates BEFORE any boot. The one-criterion unification
|
|
28
|
+
* (Minton speedup ≡ SEQUITUR rule-utility ≡ smallest-grammar MDL ≡ Lari-Young weights).
|
|
29
|
+
*
|
|
30
|
+
* Two-part code length L = L(library) + L(corpus | library):
|
|
31
|
+
* - L(library) = Σ_c schemaBits(c) — bits to DESCRIBE the grammar
|
|
32
|
+
* - L(corpus|library) = R · log2(N) — bits to PARSE the corpus (R records,
|
|
33
|
+
* each names one of N concepts)
|
|
34
|
+
* Promote a chain A→…→B (length ℓ, co-firing `count`) iff it shrinks total L:
|
|
35
|
+
* savedBits = count·(ℓ−1)·log2(N) each co-firing now names 1 abstract, not ℓ constituents
|
|
36
|
+
* encodeBits = schemaBits(M) the new production added to the library
|
|
37
|
+
* taxBits = R·(log2(N+1) − log2(N)) the Minton/Lari-Young MATCH-COST tax: +1 symbol raises
|
|
38
|
+
* EVERY record's parse cost
|
|
39
|
+
* ΔL = encodeBits + taxBits − savedBits ; admit iff ΔL < 0 (SEQUITUR "used ≥2×" in bits)
|
|
40
|
+
*
|
|
41
|
+
* SOUNDNESS DISCIPLINE: MDL only RANKS / PRE-PRUNES. It cannot verify fixpoint-equivalence or
|
|
42
|
+
* real model-cost, so it must never be the sole admit authority (false-admit risk). Compose it
|
|
43
|
+
* with `abstraction.makeAbstractionGate` as the authority via `composeGates` (MDL rejects
|
|
44
|
+
* cheaply; survivors boot once through the empirical gate).
|
|
45
|
+
*
|
|
46
|
+
* const { rankCandidates, mdlGain, makeMdlGate, composeGates } = require('./mdl');
|
|
47
|
+
* const ranked = rankCandidates(mineChains(records, tree), { tree, records, alphabet });
|
|
48
|
+
* const gate = composeGates(makeMdlGate(ctx), makeAbstractionGate(ctx)); // cheap → authority
|
|
49
|
+
*/
|
|
50
|
+
const { eachConcept, refsOf, refKeyOf, templateKeys } = require('./validate.js');
|
|
51
|
+
|
|
52
|
+
var LOG2 = Math.log(2);
|
|
53
|
+
function log2( x ) { return Math.log(x) / LOG2; }
|
|
54
|
+
|
|
55
|
+
// The discrete SYMBOLS a concept's schema spends (the same auditable spine the validator reads:
|
|
56
|
+
// require refs + ensure/assert atoms + applyMutations template keys + the provider name).
|
|
57
|
+
function symbolsOf( schema ) {
|
|
58
|
+
schema = (schema && schema._schema) || schema || {};
|
|
59
|
+
var reqs = refsOf(schema.require, false).map(function ( r ) { return refKeyOf(r).key; });
|
|
60
|
+
var ens = refsOf(schema.ensure, true).concat(refsOf(schema.assert, true));
|
|
61
|
+
var tpl = templateKeys(schema.applyMutations);
|
|
62
|
+
var prov = schema.provider ? [Array.isArray(schema.provider) ? schema.provider[0] : schema.provider] : [];
|
|
63
|
+
return reqs.length + ens.length + tpl.length + prov.length;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// |Σ| from a number, or a {knownFacts, conceptNames, palette} alphabet (deduped union).
|
|
67
|
+
function alphabetSize( alphabet ) {
|
|
68
|
+
if ( typeof alphabet === 'number' ) return Math.max(2, alphabet);
|
|
69
|
+
var s = new Set();
|
|
70
|
+
(alphabet && alphabet.knownFacts || []).forEach(function ( x ) { s.add(x); });
|
|
71
|
+
(alphabet && alphabet.conceptNames || []).forEach(function ( x ) { s.add(x); });
|
|
72
|
+
(alphabet && alphabet.palette || []).forEach(function ( x ) { s.add(x); });
|
|
73
|
+
return Math.max(2, s.size);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Description length (bits) of ONE concept's schema: its symbol count encoded in the alphabet
|
|
78
|
+
* + a small ordinal structural constant (the fixed _id/_name/wrapper overhead). Per the plan,
|
|
79
|
+
* keep `structConst` ORDINAL — it places the rarity boundary, not an absolute fitted threshold.
|
|
80
|
+
*/
|
|
81
|
+
function schemaBits( concept, alphabet, opts ) {
|
|
82
|
+
opts = opts || {};
|
|
83
|
+
var structConst = opts.structConst != null ? opts.structConst : 4;
|
|
84
|
+
return symbolsOf(concept) * log2(alphabetSize(alphabet)) + structConst;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// L(corpus | library) = R · log2(N): R records each costing log2(N) bits to name a concept.
|
|
88
|
+
function corpusBits( records, conceptCount ) {
|
|
89
|
+
var R = (records && records.length) || 0;
|
|
90
|
+
var N = Math.max(2, conceptCount || 0);
|
|
91
|
+
return R * log2(N);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function conceptCountOf( tree ) {
|
|
95
|
+
var n = 0;
|
|
96
|
+
eachConcept(tree, function ( c ) { if ( c && c._name ) n++; });
|
|
97
|
+
return n;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function conceptByName( tree, name ) {
|
|
101
|
+
var found = null;
|
|
102
|
+
eachConcept(tree, function ( c ) { if ( c && c._name === name ) found = c; });
|
|
103
|
+
return found;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The MDL gain of promoting one mined chain to an abstract production.
|
|
108
|
+
* @param opts.chain a mineChains edge { from, to, via, count } (+ optional `length` ℓ, default 2)
|
|
109
|
+
* @param opts.tree the concept tree (for N and the constituents' schema symbols)
|
|
110
|
+
* @param opts.records the trace corpus (for R, the parse-tax base)
|
|
111
|
+
* @param opts.alphabet Σ — a number, or { knownFacts, conceptNames, palette }
|
|
112
|
+
* @param opts.abstractSchema (optional) the composed method's schema; else estimated from the
|
|
113
|
+
* constituents (sum of from+to symbols — the inlined chain's spine)
|
|
114
|
+
* @returns { deltaL, encodeBits, savedBits, taxBits, admit, count, ell, N, R }
|
|
115
|
+
*/
|
|
116
|
+
function mdlGain( opts ) {
|
|
117
|
+
opts = opts || {};
|
|
118
|
+
var chain = opts.chain || {};
|
|
119
|
+
var tree = opts.tree;
|
|
120
|
+
var count = chain.count || 0;
|
|
121
|
+
var ell = chain.length || 2;
|
|
122
|
+
var N = conceptCountOf(tree);
|
|
123
|
+
var R = (opts.records && opts.records.length) || 0;
|
|
124
|
+
// conceptNames default into the alphabet when not given explicitly.
|
|
125
|
+
var alphabet = opts.alphabet;
|
|
126
|
+
if ( alphabet && typeof alphabet === 'object' && !alphabet.conceptNames ) {
|
|
127
|
+
var names = [];
|
|
128
|
+
eachConcept(tree, function ( c ) { if ( c && c._name ) names.push(c._name); });
|
|
129
|
+
alphabet = Object.assign({}, alphabet, { conceptNames: names });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
var encodeBits;
|
|
133
|
+
if ( opts.abstractSchema ) {
|
|
134
|
+
encodeBits = schemaBits(opts.abstractSchema, alphabet, opts);
|
|
135
|
+
} else {
|
|
136
|
+
// estimate the abstract method's description from its constituents (inlined spine).
|
|
137
|
+
var fromC = conceptByName(tree, chain.from), toC = conceptByName(tree, chain.to);
|
|
138
|
+
var syms = symbolsOf(fromC) + symbolsOf(toC);
|
|
139
|
+
var structConst = opts.structConst != null ? opts.structConst : 4;
|
|
140
|
+
encodeBits = syms * log2(alphabetSize(alphabet)) + structConst;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
var savedBits = count * (ell - 1) * log2(Math.max(2, N));
|
|
144
|
+
var taxBits = R * (log2(N + 1) - log2(Math.max(2, N)));
|
|
145
|
+
var deltaL = encodeBits + taxBits - savedBits;
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
deltaL: deltaL, encodeBits: encodeBits, savedBits: savedBits, taxBits: taxBits,
|
|
149
|
+
admit: deltaL < 0, count: count, ell: ell, N: N, R: R,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Rank mined candidates by MDL gain (most-negative ΔL = best refactor first). Each is annotated
|
|
155
|
+
* with its `mdl` record. A cheap O(corpus) ordering to apply BEFORE any expensive boot.
|
|
156
|
+
* @returns the chains sorted ascending by ΔL, each `{ ...chain, mdl }`.
|
|
157
|
+
*/
|
|
158
|
+
function rankCandidates( chains, ctx ) {
|
|
159
|
+
ctx = ctx || {};
|
|
160
|
+
return (chains || [])
|
|
161
|
+
.map(function ( chain ) {
|
|
162
|
+
return Object.assign({}, chain, { mdl: mdlGain(Object.assign({}, ctx, { chain: chain })) });
|
|
163
|
+
})
|
|
164
|
+
.sort(function ( a, b ) { return a.mdl.deltaL - b.mdl.deltaL; });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The MDL gate in the shape `authorConcept`'s `spec.gate` expects. A cheap PRE-FILTER: it only
|
|
169
|
+
* judges 'add' proposals that carry their mined `chain` (so the corpus stats exist); without
|
|
170
|
+
* one it ABSTAINS (admit:true → defer to the authority gate). Reject = ΔL ≥ 0.
|
|
171
|
+
* @param ctx { tree, records, alphabet, structConst } the corpus context (as for mdlGain)
|
|
172
|
+
* @param ctx.chainOf (proposal)->chain how to read the mined chain off a proposal
|
|
173
|
+
* (default: `proposal.chain`)
|
|
174
|
+
*/
|
|
175
|
+
function makeMdlGate( ctx ) {
|
|
176
|
+
ctx = ctx || {};
|
|
177
|
+
var chainOf = ctx.chainOf || function ( proposal ) { return proposal && proposal.chain; };
|
|
178
|
+
return function gate( graph, proposal ) {
|
|
179
|
+
if ( (proposal.op || 'add') !== 'add' ) return { admit: true };
|
|
180
|
+
var chain = chainOf(proposal);
|
|
181
|
+
if ( !chain ) return { admit: true, reason: 'mdl-abstain (no mined chain on proposal)' };
|
|
182
|
+
var schema = proposal.schema;
|
|
183
|
+
var g = mdlGain(Object.assign({}, ctx, { chain: chain, abstractSchema: schema }));
|
|
184
|
+
return { admit: g.admit, reason: g.admit ? null : 'mdl-reject (ΔL=' + g.deltaL.toFixed(2) + ' ≥ 0)', mdl: g };
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Compose gates as a short-circuiting AND: run them in order, the FIRST reject wins (cheap
|
|
190
|
+
* gates first prune before the expensive authority boots). All-admit → the last gate's verdict
|
|
191
|
+
* (so the authority's `eval` rides through). Each gate is sync or async; the composite awaits.
|
|
192
|
+
*/
|
|
193
|
+
function composeGates() {
|
|
194
|
+
var gates = Array.prototype.slice.call(arguments);
|
|
195
|
+
return async function gate( graph, proposal ) {
|
|
196
|
+
var last = { admit: true };
|
|
197
|
+
for ( var i = 0; i < gates.length; i++ ) {
|
|
198
|
+
last = await gates[i](graph, proposal);
|
|
199
|
+
if ( !last || !last.admit ) return last; // short-circuit on the first reject
|
|
200
|
+
}
|
|
201
|
+
return last;
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
module.exports = {
|
|
206
|
+
symbolsOf: symbolsOf, alphabetSize: alphabetSize, schemaBits: schemaBits, corpusBits: corpusBits,
|
|
207
|
+
conceptCountOf: conceptCountOf, mdlGain: mdlGain, rankCandidates: rankCandidates,
|
|
208
|
+
makeMdlGate: makeMdlGate, composeGates: composeGates,
|
|
209
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
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
|
+
* memo-stability — the safety instrument for structure-learning (host-side, ZERO-CORE;
|
|
21
|
+
* study doc/WIP/studies/2026-06-26-…, promotes experiment F4).
|
|
22
|
+
*
|
|
23
|
+
* WHY. The engine's incrementality rides on the canonicalization memo: identical typed
|
|
24
|
+
* inputs to a concept ⇒ identical `<name>FactsDigest` ⇒ memo hit, no re-derivation. A
|
|
25
|
+
* structural change (addConcept/patchConcept, or a mined abstraction) is SAFE iff it is
|
|
26
|
+
* **memo-surface-preserving** — it must not alter the canonical facts that incumbent
|
|
27
|
+
* concepts' `require`/`ensure`/`assert` read. Violating this collapses the memo silently
|
|
28
|
+
* (the only "quiet failure" the adversarial lens identified). This module makes the F4
|
|
29
|
+
* boundary a reusable, gateable check.
|
|
30
|
+
*
|
|
31
|
+
* const { memoSnapshot, memoDiff } = require('./memo-stability');
|
|
32
|
+
* const before = memoSnapshot(graph, ['Consume']); // incumbents to protect
|
|
33
|
+
* graph.addConcept(null, candidateSchema, () => {
|
|
34
|
+
* const { stable, changed } = memoDiff(before, memoSnapshot(graph, ['Consume']));
|
|
35
|
+
* // stable === false ⇒ the candidate perturbed an incumbent's memo key ⇒ reject/rollback
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* A concept's MEMO SURFACE = the fact keys its require/ensure/assert depend on (the same
|
|
39
|
+
* ref extraction `validate.js` uses, single source of truth). A snapshot digests, per
|
|
40
|
+
* (object × concept), the projection of the object's facts onto that surface — so a drift
|
|
41
|
+
* in any depended-on key shows up as a changed digest, while an isolated new fact does not.
|
|
42
|
+
*/
|
|
43
|
+
const canon = require('../providers/canonicalize.js');
|
|
44
|
+
const { refsOf, refKeyOf } = require('./validate.js');
|
|
45
|
+
|
|
46
|
+
const asArray = (v) => (v == null ? [] : Array.isArray(v) ? v : [v]);
|
|
47
|
+
|
|
48
|
+
/** The fact keys a concept's require/ensure/assert read — its incremental memo surface. */
|
|
49
|
+
function memoSurfaceKeys( graph, conceptName ) {
|
|
50
|
+
const c = graph.getConceptByName ? graph.getConceptByName(conceptName) : null;
|
|
51
|
+
const schema = (c && (c._schema || c)) || {};
|
|
52
|
+
const keys = new Set();
|
|
53
|
+
for ( const r of refsOf(schema.require, false) ) keys.add(refKeyOf(r).key);
|
|
54
|
+
for ( const fld of ['ensure', 'assert'] )
|
|
55
|
+
for ( const e of asArray(schema[fld]) )
|
|
56
|
+
if ( typeof e === 'string' ) for ( const r of refsOf(e, true) ) keys.add(refKeyOf(r).key);
|
|
57
|
+
return [...keys];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Capture the memo keys of the named incumbents across the live graph.
|
|
62
|
+
* @param graph a live Graph
|
|
63
|
+
* @param conceptNames incumbents to protect (default: every concept in the lib)
|
|
64
|
+
* @returns {{[key:string]: string}} "<objId>|<conceptName>" -> digest of the depended-on facts
|
|
65
|
+
*/
|
|
66
|
+
function memoSnapshot( graph, conceptNames ) {
|
|
67
|
+
const out = {};
|
|
68
|
+
const names = conceptNames || Object.keys(graph._conceptLib || {});
|
|
69
|
+
const objs = graph._objById || {};
|
|
70
|
+
for ( const name of names ) {
|
|
71
|
+
const keys = memoSurfaceKeys(graph, name);
|
|
72
|
+
if ( !keys.length ) continue;
|
|
73
|
+
for ( const id of Object.keys(objs) ) {
|
|
74
|
+
const etty = objs[id] && objs[id]._etty;
|
|
75
|
+
const facts = etty && etty._;
|
|
76
|
+
if ( !facts ) continue;
|
|
77
|
+
const proj = {};
|
|
78
|
+
let has = false;
|
|
79
|
+
for ( const k of keys ) if ( k in facts ) { proj[k] = facts[k]; has = true; }
|
|
80
|
+
if ( has ) out[id + '|' + name] = canon.digest(proj);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Compare two snapshots. `stable` iff no depended-on memo key changed or disappeared.
|
|
88
|
+
* @returns {{ stable:boolean, changed:Array<{key,before,after}>, removed:string[], added:string[] }}
|
|
89
|
+
*/
|
|
90
|
+
function memoDiff( before, after ) {
|
|
91
|
+
const changed = [], removed = [];
|
|
92
|
+
for ( const k of Object.keys(before) ) {
|
|
93
|
+
if ( !(k in after) ) removed.push(k);
|
|
94
|
+
else if ( after[k] !== before[k] ) changed.push({ key: k, before: before[k], after: after[k] });
|
|
95
|
+
}
|
|
96
|
+
const added = Object.keys(after).filter((k) => !(k in before));
|
|
97
|
+
return { stable: changed.length === 0 && removed.length === 0, changed, removed, added };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Fail-closed CI gate around a structural change: snapshot the incumbents' memo keys, run
|
|
102
|
+
* `applyFn` (e.g. an addConcept/patchConcept install or a mined-abstraction adoption), and
|
|
103
|
+
* THROW if the change perturbed any of them. Wrap any structure-learning mutation in this so
|
|
104
|
+
* a memo-surface regression fails the build instead of silently collapsing the incremental memo.
|
|
105
|
+
* @returns the memoDiff on success (stable); throws an Error (with `.diff`) on instability.
|
|
106
|
+
*/
|
|
107
|
+
async function assertMemoStable( graph, incumbents, applyFn ) {
|
|
108
|
+
const before = memoSnapshot(graph, incumbents);
|
|
109
|
+
await applyFn();
|
|
110
|
+
const after = memoSnapshot(graph, incumbents);
|
|
111
|
+
const d = memoDiff(before, after);
|
|
112
|
+
if ( !d.stable ) {
|
|
113
|
+
const names = d.changed.map((c) => c.key).concat(d.removed).join(', ');
|
|
114
|
+
const e = new Error('memo-stability violation: ' + (names || 'incumbent memo keys changed') +
|
|
115
|
+
' — a structural change must be memo-surface-preserving (snapped, incumbents\' digests unchanged)');
|
|
116
|
+
e.diff = d;
|
|
117
|
+
throw e;
|
|
118
|
+
}
|
|
119
|
+
return d;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { memoSnapshot, memoDiff, memoSurfaceKeys, assertMemoStable };
|