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,401 @@
|
|
|
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
|
+
* abstract — ABSTRACTIVATION of a STRUCTURAL method (host-side, ZERO-CORE). The F6 slice the
|
|
10
|
+
* decisive-experiment gate green-lit (study `doc/WIP/studies/2026-06-27-concept-as-graph-adaptive-
|
|
11
|
+
* mount-abstraction.md` §8 / U1; finding #30).
|
|
12
|
+
*
|
|
13
|
+
* THE PROBLEM (#30). The derivation cache (`providers/cache.js`) replays a stored mutation TEMPLATE
|
|
14
|
+
* verbatim. A LEAF template is id-RELATIVE (`$_id:'_parent'`) so a different RECORD of the same canonical
|
|
15
|
+
* class replays soundly (method-instance.js: cost→0). But a STRUCTURAL template — one that CREATES a
|
|
16
|
+
* sub-graph (an intermediate node + child segments) — bakes ABSOLUTE object ids at derivation time
|
|
17
|
+
* (`base+'_m0'`, `originNode:'S'`, where `base = seg._id`). Replaying THAT verbatim in a DIFFERENT problem
|
|
18
|
+
* injects the wrong id-space → unsound / a crash in `Segment._onOriginChange`. So flat caching transfers
|
|
19
|
+
* structural decisions only across EXACT-structure repeats (same id-space), never related-but-different
|
|
20
|
+
* problems. Cross-problem STRUCTURAL transfer needs PARAMETERIZED / relative templates.
|
|
21
|
+
*
|
|
22
|
+
* THE FIX (anti-unification + splice-time binding). A structural template is the LEAST GENERAL
|
|
23
|
+
* GENERALIZATION (Plotkin 1970 LGG) of its ground instances: the differing parts are HOLES, the shared
|
|
24
|
+
* skeleton is kept. Two kinds of hole:
|
|
25
|
+
* - STRUCTURAL holes — created ids derived from the cast object's id (`base+suffix`) and the FRONTIER
|
|
26
|
+
* refs the template consumes (the call-site endpoints, e.g. `originNode`/`targetNode`). These are
|
|
27
|
+
* bound DETERMINISTICALLY from the new call site at replay → free, sound, no model call.
|
|
28
|
+
* - CONTENT holes — the typed payload the model derived (the intermediate state/kind/role). These are
|
|
29
|
+
* the CACHED PAYLOAD, replayed verbatim. They are sound to replay IFF the cache KEY is the K1-canonical
|
|
30
|
+
* TYPED signature that determines them (the canonicalization barrier) — never raw prose.
|
|
31
|
+
*
|
|
32
|
+
* So: `relativize(tpl, ctx)` rewrites a ground structural template into a PARAMETERIZED one (store time);
|
|
33
|
+
* `instantiate(paramTpl, ctx')` binds the holes to a NEW call site (replay time); `antiUnify(A, B)` checks
|
|
34
|
+
* that two ground derivations of the same signature generalize to the SAME skeleton (the soundness /
|
|
35
|
+
* crystallization check — the study's "≥2 ground derivations → one schema"). `methodTransform(spec)`
|
|
36
|
+
* packages these as the generic `{onStore,onReplay}` hooks that `providers/cache.js#wrap` accepts, so a
|
|
37
|
+
* STRUCTURAL provider gets cross-problem transfer with no change to the cache or the grammar.
|
|
38
|
+
*
|
|
39
|
+
* const { methodTransform } = require('./abstract');
|
|
40
|
+
* const T = methodTransform({ frontier: { origin: 'originNode', target: 'targetNode' } });
|
|
41
|
+
* Graph._providers = cache.wrapFragment({ P: { plan } }, { 'P::plan': planSigKey }, { 'P::plan': T });
|
|
42
|
+
* // key on the TYPED signature ^^^^^^^^^^ transform ^^^
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
// sentinels for the two hole kinds, embedded INSIDE id-valued strings. The ⟦ ⟧ brackets are OUT of the id
|
|
46
|
+
// alphabet ([A-Za-z0-9_-]: shortid + authored ids), so a sentinel can never collide with a real id token —
|
|
47
|
+
// and they are HUMAN-READABLE: these holes appear in mutation templates, so prefer legible over opaque (an
|
|
48
|
+
// opaque/byte sentinel would only be justified for a numeric payload — weights/float vectors — not here).
|
|
49
|
+
const BASE = '⟦@base⟧'; // created-id prefix: BASE + suffix (suffix '' = the base id itself)
|
|
50
|
+
const REF = ( name ) => '⟦@ref:' + name + '⟧'; // a frontier ref hole, bound to a call-site id
|
|
51
|
+
const REF_RE = /^⟦@ref:(.+)⟧$/;
|
|
52
|
+
|
|
53
|
+
function isStr( x ) { return typeof x === 'string'; }
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Rewrite ONE id-valued string into its parameterized form, given the call-site `ctx`.
|
|
57
|
+
* ctx.base the cast object's id (= scope._._id); created ids are `base` or `base+suffix`.
|
|
58
|
+
* ctx.refs { name: idValue } the frontier ids the template consumes (endpoints, parent, …).
|
|
59
|
+
* A value that is neither base-derived nor a known frontier ref is left LITERAL (it is content payload).
|
|
60
|
+
* NB: refs are matched FIRST (exact) so a frontier id is never mistaken for a base-derived id; a frontier
|
|
61
|
+
* ref never equals `base` (the cast object is not its own endpoint), so the order is unambiguous.
|
|
62
|
+
*/
|
|
63
|
+
function relativizeVal( v, ctx ) {
|
|
64
|
+
if ( !isStr(v) ) return v;
|
|
65
|
+
for ( const name in ctx.refs ) if ( ctx.refs[name] === v ) return REF(name);
|
|
66
|
+
if ( v === ctx.base ) return BASE;
|
|
67
|
+
if ( ctx.base && v.indexOf(ctx.base + '_') === 0 ) return BASE + v.slice(ctx.base.length);
|
|
68
|
+
return v;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// the inverse: bind a parameterized id-valued string to a NEW call site `ctx`. Returns `undefined` if a
|
|
72
|
+
// required frontier ref is unbound in the new ctx (caller treats that as a BYPASS — never a wrong replay).
|
|
73
|
+
function instantiateVal( v, ctx, miss ) {
|
|
74
|
+
if ( !isStr(v) ) return v;
|
|
75
|
+
const m = REF_RE.exec(v);
|
|
76
|
+
if ( m ) { const name = m[1]; if ( !(name in ctx.refs) || ctx.refs[name] == null ) { miss.bad = true; return v; } return ctx.refs[name]; }
|
|
77
|
+
if ( v.indexOf(BASE) === 0 ) return ctx.base + v.slice(BASE.length);
|
|
78
|
+
return v;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// walk a template (array | object | scalar) applying a per-string-value transform. Object KEYS are
|
|
82
|
+
// structural (`_id`, `$_id`, `originNode`, …), never data ids, so only VALUES are transformed. `'_parent'`
|
|
83
|
+
// (the cast-target alias) is already relative and matches nothing → passes through untouched.
|
|
84
|
+
function mapValues( tpl, fn ) {
|
|
85
|
+
if ( Array.isArray(tpl) ) return tpl.map(( x ) => mapValues(x, fn));
|
|
86
|
+
if ( tpl && typeof tpl === 'object' ) {
|
|
87
|
+
const out = {};
|
|
88
|
+
for ( const k in tpl ) out[k] = mapValues(tpl[k], fn);
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
return fn(tpl);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Ground structural template + call-site ctx → PARAMETERIZED template (holes for ids/refs). */
|
|
95
|
+
function relativize( tpl, ctx ) {
|
|
96
|
+
ctx = ctx || {}; ctx.refs = ctx.refs || {};
|
|
97
|
+
return mapValues(tpl, ( v ) => relativizeVal(v, ctx));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* PARAMETERIZED template + a NEW call-site ctx → GROUND template (ids rebased, frontier refs rebound).
|
|
102
|
+
* Returns `null` if any frontier ref hole is unbound in the new ctx (the caller bypasses to a fresh call).
|
|
103
|
+
*/
|
|
104
|
+
function instantiate( paramTpl, ctx ) {
|
|
105
|
+
ctx = ctx || {}; ctx.refs = ctx.refs || {};
|
|
106
|
+
const miss = { bad: false };
|
|
107
|
+
const out = mapValues(paramTpl, ( v ) => instantiateVal(v, ctx, miss));
|
|
108
|
+
return miss.bad ? null : out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** True iff a template still carries hole sentinels (i.e. it is a parameterized, not-yet-bound form). */
|
|
112
|
+
function hasHoles( tpl ) {
|
|
113
|
+
let found = false;
|
|
114
|
+
mapValues(tpl, ( v ) => { if ( isStr(v) && (v.indexOf(BASE) === 0 || REF_RE.test(v)) ) found = true; return v; });
|
|
115
|
+
return found;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Deterministic, key-sorted, recursive stringify (for skeleton equality). */
|
|
119
|
+
function canon( x ) {
|
|
120
|
+
if ( x === undefined ) return 'null';
|
|
121
|
+
if ( x === null || typeof x !== 'object' ) return JSON.stringify(x);
|
|
122
|
+
if ( Array.isArray(x) ) return '[' + x.map(canon).join(',') + ']';
|
|
123
|
+
return '{' + Object.keys(x).sort().map(( k ) => JSON.stringify(k) + ':' + canon(x[k])).join(',') + '}';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Plotkin LGG (1970) over the engine's typed templates — the study's "≥2 ground derivations → one schema"
|
|
128
|
+
* SOUNDNESS / crystallization check. Relativize TWO ground derivations (each with its own call-site ctx —
|
|
129
|
+
* so ids/refs already align as STRUCTURAL holes), then compute their least general generalization: where
|
|
130
|
+
* the two agree, keep the literal; where they DIFFER at a leaf, introduce a CONTENT variable; where the
|
|
131
|
+
* TREE SHAPE differs (different keys / array length / object-vs-scalar), they do not generalize.
|
|
132
|
+
*
|
|
133
|
+
* `stable` = the two are SHAPE-COMPATIBLE (one structural skeleton; all differences are content leaves).
|
|
134
|
+
* That justifies treating them as ONE parameterized method: the structural holes bind from the call site
|
|
135
|
+
* (`instantiate`), the content variables are what the cache KEY must determine (the K1 signature — a
|
|
136
|
+
* content var that is NOT a function of the typed key is a false-hit risk, the canonicalize.js line).
|
|
137
|
+
* A shape mismatch ⇒ NOT one abstraction (don't crystallize them together).
|
|
138
|
+
*
|
|
139
|
+
* @returns { stable, skeleton, contentVars, shapeDiffs }
|
|
140
|
+
* skeleton the LGG: structural holes + `{ '§var': path }` at each content leaf (documentation / gate input)
|
|
141
|
+
* contentVars [{ path, a, b }] the per-instance-varying leaves (cache-key obligations)
|
|
142
|
+
* shapeDiffs [path] positions where the tree shape itself differs (empty ⇔ stable)
|
|
143
|
+
*/
|
|
144
|
+
function antiUnify( groundA, ctxA, groundB, ctxB ) {
|
|
145
|
+
const sa = relativize(groundA, ctxA), sb = relativize(groundB, ctxB);
|
|
146
|
+
const acc = { contentVars: [], shapeDiffs: [] };
|
|
147
|
+
const skeleton = lgg(sa, sb, '', acc);
|
|
148
|
+
return { stable: acc.shapeDiffs.length === 0, skeleton: acc.shapeDiffs.length === 0 ? skeleton : null,
|
|
149
|
+
contentVars: acc.contentVars, shapeDiffs: acc.shapeDiffs };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// least general generalization of two (already structurally-relativized) values.
|
|
153
|
+
function lgg( a, b, path, acc ) {
|
|
154
|
+
if ( canon(a) === canon(b) ) return a; // agree → keep the literal/skeleton
|
|
155
|
+
const oa = a && typeof a === 'object', ob = b && typeof b === 'object';
|
|
156
|
+
if ( !oa || !ob || Array.isArray(a) !== Array.isArray(b) ) { // leaf disagreement OR object-vs-scalar
|
|
157
|
+
if ( oa || ob ) { acc.shapeDiffs.push(path); return null; } // shape mismatch (one side has structure)
|
|
158
|
+
acc.contentVars.push({ path, a, b }); return { '§var': path }; // both scalars, differ → content variable
|
|
159
|
+
}
|
|
160
|
+
if ( Array.isArray(a) ) {
|
|
161
|
+
if ( a.length !== b.length ) { acc.shapeDiffs.push(path); return null; }
|
|
162
|
+
return a.map(( x, i ) => lgg(x, b[i], path + '[' + i + ']', acc));
|
|
163
|
+
}
|
|
164
|
+
const ka = Object.keys(a).sort(), kb = Object.keys(b).sort();
|
|
165
|
+
if ( canon(ka) !== canon(kb) ) { acc.shapeDiffs.push(path); return null; } // different field sets → shape mismatch
|
|
166
|
+
const out = {};
|
|
167
|
+
for ( const k of ka ) out[k] = lgg(a[k], b[k], path ? path + '.' + k : k, acc);
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Read the call-site ctx (base + frontier refs) off a provider `scope`, per a declarative `spec`.
|
|
173
|
+
* spec.frontier { name: factKey | "ref:path" } — where each frontier id lives on the cast object.
|
|
174
|
+
* A bare factKey reads `scope._[factKey]`; a "a:b" path resolves via `scope.getRef`.
|
|
175
|
+
* spec.base optional fact key for the base id (default the cast object's own `_id`).
|
|
176
|
+
* Returns null if the base id is unknown (→ the transform bypasses, never an unsound store/replay).
|
|
177
|
+
*/
|
|
178
|
+
function ctxFromScope( scope, spec ) {
|
|
179
|
+
spec = spec || {};
|
|
180
|
+
const f = (scope && scope._) || {};
|
|
181
|
+
const base = spec.base ? f[spec.base] : f._id;
|
|
182
|
+
if ( base == null ) return null;
|
|
183
|
+
const refs = {};
|
|
184
|
+
const frontier = spec.frontier || {};
|
|
185
|
+
for ( const name in frontier ) {
|
|
186
|
+
const where = frontier[name];
|
|
187
|
+
let v;
|
|
188
|
+
if ( isStr(where) && where.indexOf(':') >= 0 ) { // a cross-object ref path
|
|
189
|
+
try { const r = scope.getRef ? scope.getRef(where) : undefined; v = (r && r._ && r._._id !== undefined) ? r._._id : (r && r._id !== undefined ? r._id : r); } catch ( e ) { v = undefined; }
|
|
190
|
+
} else {
|
|
191
|
+
v = f[where];
|
|
192
|
+
}
|
|
193
|
+
refs[name] = v;
|
|
194
|
+
}
|
|
195
|
+
return { base, refs };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Package relativize/instantiate as the generic `{onStore,onReplay}` transform that
|
|
200
|
+
* `providers/cache.js#wrap(fn, keyFn, transform)` accepts:
|
|
201
|
+
* - onStore(groundTpl, {scope}) → PARAMETERIZED form to store (or null = don't store: not transfer-safe).
|
|
202
|
+
* - onReplay(paramTpl, {scope}) → GROUND form bound to the new call site (or null = bypass to a fresh call).
|
|
203
|
+
* The cold call still returns its own ground template unchanged; only the STORED form is parameterized,
|
|
204
|
+
* and only a HIT is re-bound — so the cold derivation is never perturbed.
|
|
205
|
+
*/
|
|
206
|
+
function methodTransform( spec ) {
|
|
207
|
+
spec = spec || {};
|
|
208
|
+
return {
|
|
209
|
+
onStore: function ( tpl, info ) {
|
|
210
|
+
const ctx = ctxFromScope(info && info.scope, spec);
|
|
211
|
+
if ( !ctx ) return null; // unknown call site → don't store (no unsound future replay)
|
|
212
|
+
const param = relativize(tpl, ctx);
|
|
213
|
+
// only worth storing parameterized if it actually generalized (else it's a flat template under a
|
|
214
|
+
// shared signature key → would leak on replay). Require at least one hole.
|
|
215
|
+
return hasHoles(param) ? param : null;
|
|
216
|
+
},
|
|
217
|
+
onReplay: function ( paramTpl, info ) {
|
|
218
|
+
const ctx = ctxFromScope(info && info.scope, spec);
|
|
219
|
+
if ( !ctx ) return null; // unknown call site → bypass (fresh call)
|
|
220
|
+
return instantiate(paramTpl, ctx); // null if a frontier ref is unbound → bypass
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* EMIT-METHOD-AS-SUBGRAPH (study §8 / U1a), built on the ENGINE-NATIVE serializer `Graph#getMutationFromPath`.
|
|
227
|
+
*
|
|
228
|
+
* `relativize`/`instantiate` are the domain-agnostic GENERALIZATION of a pattern the engine already proved
|
|
229
|
+
* in production (the epikeo/aetheris travel-path mounting: `MapPaths` normalizes a discovered sub-graph's
|
|
230
|
+
* endpoints to the FORMAL ids 'start'/'target' and serializes it into `OpenPaths`; `pushPath`/`MountPaths`
|
|
231
|
+
* re-mount it, BINDING the formal frontier back to the call site — `tpl[0].originNode = scope._.originNode`,
|
|
232
|
+
* `relatedTpl.map(m => ({...m, $_id:'$'+m._id}))`). `getMutationFromPath` is the serializer that turns a
|
|
233
|
+
* graph fragment into a mutation TEMPLATE (bagRef→`$$` refs, strips `_rev`).
|
|
234
|
+
*
|
|
235
|
+
* This helper closes the loop: take a DERIVED method's sub-graph (the objects a structural cast created),
|
|
236
|
+
* serialize it through the engine primitive, and relativize it into a re-mountable PARAMETERIZED method —
|
|
237
|
+
* so a method is a re-mountable graph (the study's U1a), not only a cached opaque provider output. Bind it
|
|
238
|
+
* at a new call site with `instantiate(...)` and apply via `graph.pushMutation(bound, targetId, …)`.
|
|
239
|
+
*
|
|
240
|
+
* @param graph the graph holding the derived sub-graph
|
|
241
|
+
* @param subIds the ids of the method's emitted objects (the created intermediate nodes + child segments)
|
|
242
|
+
* @param ctx { base, refs:{name:idValue} } — the derivation call site (as for `relativize`)
|
|
243
|
+
* @returns the PARAMETERIZED method template (holes for ids/frontier refs), or null if a sub-id is missing.
|
|
244
|
+
*/
|
|
245
|
+
function emitMethodAsSubgraph( graph, subIds, ctx ) {
|
|
246
|
+
const path = [];
|
|
247
|
+
for ( const id of subIds || [] ) {
|
|
248
|
+
const e = graph.getEtty ? graph.getEtty(id) : (graph._objById && graph._objById[id] && graph._objById[id]._etty);
|
|
249
|
+
if ( !e || !e._ ) return null;
|
|
250
|
+
path.push(Object.assign({}, e._));
|
|
251
|
+
}
|
|
252
|
+
if ( !path.length ) return null;
|
|
253
|
+
const tpl = graph.getMutationFromPath ? graph.getMutationFromPath(path, [], null) : path; // engine-native serialize
|
|
254
|
+
return relativize(tpl, ctx);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* CONTENT-HOLE GENERALIZATION (the adapt operator — "conceptual blending" / Fauconnier-Turner via Plotkin LGG).
|
|
259
|
+
* Given ≥2 parameterized templates of ONE method (its `templatesBySig` values — same structural skeleton, content
|
|
260
|
+
* differing per signature class), discover WHICH leaves are CONTENT (vary across the instances) vs the SKELETON
|
|
261
|
+
* (shared, kept verbatim — both the structural `⟦@…⟧` holes and the agreeing literals). It is `antiUnify` folded
|
|
262
|
+
* over the set: the union of every pairwise content-var path. The result is a skeleton with a `{'§var': path}`
|
|
263
|
+
* CONTENT HOLE at each varying leaf — so a controller can forge ONLY those holes for a new signature and reuse the
|
|
264
|
+
* skeleton, instead of re-forging the whole method (the v0 demo's hard-coded "swap the `state` field" → principled).
|
|
265
|
+
*
|
|
266
|
+
* @param templates array of ≥2 parameterized templates (already relativized — structural holes are literal here).
|
|
267
|
+
* @returns { stable, skeleton, contentVars:[{path}] } stable=false if a template's SHAPE differs from the first
|
|
268
|
+
* (then it is excluded from the union) or there are <2 templates; contentVars empty ⇒ nothing to forge.
|
|
269
|
+
*/
|
|
270
|
+
function generalizeContent( templates ) {
|
|
271
|
+
const ts = (templates || []).filter(Boolean);
|
|
272
|
+
if ( ts.length < 2 ) return { stable: false, skeleton: ts[0] || null, contentVars: [] };
|
|
273
|
+
const paths = new Set();
|
|
274
|
+
let stable = true;
|
|
275
|
+
for ( let i = 1; i < ts.length; i++ ) {
|
|
276
|
+
const au = antiUnify(ts[0], {}, ts[i], {}); // ctx={} → no further relativization; compare the parameterized forms
|
|
277
|
+
if ( !au.stable ) { stable = false; continue; } // a shape-incompatible template can't share this skeleton
|
|
278
|
+
for ( const cv of au.contentVars ) paths.add(cv.path);
|
|
279
|
+
}
|
|
280
|
+
return { stable, skeleton: holePaths(ts[0], paths, ''), contentVars: [...paths].map(( p ) => ({ path: p })) };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// rewrite the leaves at `paths` into `{'§var': path}` content holes; path format MIRRORS lgg's (array `[i]`,
|
|
284
|
+
// object `k` / `parent.k`) so the holes line up with `generalizeContent`'s contentVars.
|
|
285
|
+
function holePaths( tpl, paths, path ) {
|
|
286
|
+
if ( paths.has(path) ) return { '§var': path };
|
|
287
|
+
if ( Array.isArray(tpl) ) return tpl.map(( x, i ) => holePaths(x, paths, path + '[' + i + ']'));
|
|
288
|
+
if ( tpl && typeof tpl === 'object' ) { const o = {}; for ( const k in tpl ) o[k] = holePaths(tpl[k], paths, path ? path + '.' + k : k); return o; }
|
|
289
|
+
return tpl;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Fill a content-hole skeleton (from `generalizeContent`) with forged values keyed by hole path → a parameterized
|
|
293
|
+
* template (structural holes still present, to be `instantiate`d at the call site). Returns null if any content
|
|
294
|
+
* hole was NOT forged (a partial forge must never silently bake an `undefined` leaf). */
|
|
295
|
+
function fillContentHoles( skel, valuesByPath ) {
|
|
296
|
+
let missing = false;
|
|
297
|
+
const walk = ( x ) => {
|
|
298
|
+
if ( Array.isArray(x) ) return x.map(walk);
|
|
299
|
+
if ( x && typeof x === 'object' ) {
|
|
300
|
+
if ( '§var' in x && Object.keys(x).length === 1 ) { const p = x['§var']; if ( !valuesByPath || !(p in valuesByPath) ) { missing = true; return x; } return valuesByPath[p]; }
|
|
301
|
+
const o = {}; for ( const k in x ) o[k] = walk(x[k]); return o;
|
|
302
|
+
}
|
|
303
|
+
return x;
|
|
304
|
+
};
|
|
305
|
+
const out = walk(skel);
|
|
306
|
+
return missing ? null : out;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* CONCEPTUAL BLENDING (Boden's COMBINATIONAL creativity / Fauconnier-Turner): graft a DONOR method's body into a
|
|
311
|
+
* structural SLOT of a HOST method — producing a NEW method that is neither, made by recombining their parts. The
|
|
312
|
+
* slot is a host SEGMENT hole (the canonical decompose shape origin→mid→target has child-segment slots); the donor
|
|
313
|
+
* decomposes THAT segment, so the host's edge is replaced by the donor's sub-graph (a deeper, blended method).
|
|
314
|
+
*
|
|
315
|
+
* SOUNDNESS (the snapped-separator interface = the study's open frontier, made concrete):
|
|
316
|
+
* - INTERFACE: the donor's frontier (its `⟦@ref:…⟧` endpoints) binds to the host slot's endpoints (the segment's
|
|
317
|
+
* `originNode`/`targetNode`). v0: a segment slot ↔ a donor whose frontier is {origin,target} (a 2-endpoint method).
|
|
318
|
+
* - NO ID COLLISION: the donor is RE-BASED under the host slot's id (`⟦@base⟧_a0` → the donor's `⟦@base⟧`), so its
|
|
319
|
+
* created ids nest as `⟦@base⟧_a0_m0` etc. — disjoint from the host's `⟦@base⟧_*` and any sibling slot. The donor's
|
|
320
|
+
* own parent element MERGES its decomposition markers onto the host segment (which becomes a decomposed parent).
|
|
321
|
+
* The result is a parameterized template (the OUTER frontier unchanged) → `instantiate` mounts it at a fresh site.
|
|
322
|
+
*
|
|
323
|
+
* @param hostTpl a parameterized method template (array of mutation objects with `⟦@…⟧` holes).
|
|
324
|
+
* @param hostSegId the `$$_id`/`$_id` hole of the host SEGMENT slot to graft into (e.g. `⟦@base⟧_a0`).
|
|
325
|
+
* @param donorTpl a parameterized method template whose frontier is {origin,target} (it decomposes a segment).
|
|
326
|
+
* @returns the blended parameterized template, or null (slot not found / not a segment / nothing to graft).
|
|
327
|
+
*/
|
|
328
|
+
function blendAtSegment( hostTpl, hostSegId, donorTpl ) {
|
|
329
|
+
const idOf = ( o ) => o && (o.$$_id != null ? o.$$_id : (o.$_id != null ? o.$_id : o._id));
|
|
330
|
+
const host = (Array.isArray(hostTpl) ? hostTpl : [hostTpl]).slice();
|
|
331
|
+
const si = host.findIndex(( o ) => idOf(o) === hostSegId );
|
|
332
|
+
if ( si < 0 ) return null; // no such slot
|
|
333
|
+
const seg = host[si];
|
|
334
|
+
if ( seg.originNode == null || seg.targetNode == null ) return null; // the slot must be a SEGMENT (2-endpoint interface)
|
|
335
|
+
const refMap = { origin: seg.originNode, target: seg.targetNode }; // donor frontier ← host slot endpoints (the snapped separator)
|
|
336
|
+
const donor = rebaseDonor(Array.isArray(donorTpl) ? donorTpl : [donorTpl], hostSegId, refMap);
|
|
337
|
+
const dParent = donor.find(( o ) => idOf(o) === hostSegId ) || {}; // donor's parent re-based onto the slot
|
|
338
|
+
const dRest = donor.filter(( o ) => idOf(o) !== hostSegId ); // the donor's body (mid + children), nested under the slot
|
|
339
|
+
const merged = Object.assign({}, seg, dParent); // the slot keeps its endpoints + gains donor's decomposition markers
|
|
340
|
+
const out = host.slice(); out[si] = merged;
|
|
341
|
+
return out.concat(dRest);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* BINARY COMBINATION at a FORK — the SECOND composition operator (sibling to blendAtSegment's NESTING). Where a
|
|
346
|
+
* blend grafts a donor INTO a host slot (series → a deeper single query), a combine runs TWO complete methods in
|
|
347
|
+
* PARALLEL from the same origin and joins their results at a `combine` node with a set operation (∩/∪/−) → a NEW
|
|
348
|
+
* method whose result = op(left, right). This is the shape of a SQL set-op query (`A INTERSECT B`): two independent
|
|
349
|
+
* sub-queries over the same source, combined. Both branches are 2-endpoint methods (origin→target frontier).
|
|
350
|
+
*
|
|
351
|
+
* STRUCTURE (a diamond, all under ONE outer parent — no fabricated sub-parents):
|
|
352
|
+
* origin →[left body, re-based ⟦@base⟧_L_*]→ ⟦@base⟧_c ←[right body, re-based ⟦@base⟧_R_*]← origin
|
|
353
|
+
* ⟦@base⟧_c →[set-op segment ⟦@base⟧_out]→ target
|
|
354
|
+
* NO ID COLLISION: each branch re-based under a distinct namespace (⟦@base⟧_L / ⟦@base⟧_R); its frontier binds
|
|
355
|
+
* origin→the outer origin, target→the shared combine node; its parent object is DROPPED and its segments re-parent
|
|
356
|
+
* to the outer base. The combine node legitimately has two incoming edges (a node may be many segments' target).
|
|
357
|
+
* @param leftTpl/rightTpl parameterized 2-endpoint method templates.
|
|
358
|
+
* @param op the set operation tag ('intersect' | 'union' | 'except').
|
|
359
|
+
* @returns the combined parameterized template (OUTER frontier origin→target), or null if a branch is empty.
|
|
360
|
+
*/
|
|
361
|
+
function combineAtFork( leftTpl, rightTpl, op ) {
|
|
362
|
+
const idOf = ( o ) => o && (o.$$_id != null ? o.$$_id : (o.$_id != null ? o.$_id : o._id));
|
|
363
|
+
const L = Array.isArray(leftTpl) ? leftTpl : [leftTpl], R = Array.isArray(rightTpl) ? rightTpl : [rightTpl];
|
|
364
|
+
if ( !L.length || !R.length ) return null;
|
|
365
|
+
const combineId = BASE + '_c';
|
|
366
|
+
const refMap = { origin: REF('origin'), target: combineId }; // branch frontier: origin→outer origin, target→combine node
|
|
367
|
+
// re-base a branch, DROP its parent object, re-parent its segments to the OUTER base.
|
|
368
|
+
const branch = ( tpl, ns ) => rebaseDonor(tpl, BASE + ns, refMap).filter(( o ) => idOf(o) !== BASE + ns )
|
|
369
|
+
.map(( o ) => ('parentSeg' in o) ? Object.assign({}, o, { parentSeg: BASE }) : o);
|
|
370
|
+
const parent = { $$_id: BASE, Combine: true, Combined: true, setop: op };
|
|
371
|
+
const combineNode = { $$_id: combineId, Node: true, state: 'combine' };
|
|
372
|
+
const setopSeg = { $$_id: BASE + '_out', Segment: true, originNode: combineId, targetNode: REF('target'), parentSeg: BASE, state: 'setop', setop: op };
|
|
373
|
+
return [parent].concat(branch(L, '_L'), branch(R, '_R'), [combineNode, setopSeg]);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// re-base a donor template: its `⟦@base⟧` (+ `⟦@base⟧_suffix`) holes nest under `newBase`; its frontier refs are
|
|
377
|
+
// re-bound via `refMap` (donorRefName → the host slot's endpoint value). REF matched FIRST (as in relativizeVal).
|
|
378
|
+
function rebaseDonor( donorTpl, newBase, refMap ) {
|
|
379
|
+
return mapValues(donorTpl, ( v ) => {
|
|
380
|
+
if ( !isStr(v) ) return v;
|
|
381
|
+
const m = REF_RE.exec(v);
|
|
382
|
+
if ( m ) return (m[1] in refMap) ? refMap[m[1]] : v; // a frontier hole → the host slot's endpoint
|
|
383
|
+
if ( v === BASE ) return newBase; // the donor base → the slot id
|
|
384
|
+
if ( v.indexOf(BASE + '_') === 0 ) return newBase + v.slice(BASE.length); // ⟦@base⟧_m0 → newBase_m0 (nested)
|
|
385
|
+
return v;
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** Collect the frontier-ref hole NAMES a parameterized template uses (reuses THIS module's REF_RE, so the
|
|
390
|
+
* sentinel format stays in ONE place — callers never re-derive it). Returns a Set of names. */
|
|
391
|
+
function refHolesOf( tpl, out ) {
|
|
392
|
+
out = out || new Set();
|
|
393
|
+
mapValues(tpl, function ( v ) { var m = isStr(v) && REF_RE.exec(v); if ( m ) out.add(m[1]); return v; });
|
|
394
|
+
return out;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
module.exports = {
|
|
398
|
+
relativize, instantiate, antiUnify, hasHoles, methodTransform, ctxFromScope,
|
|
399
|
+
emitMethodAsSubgraph, refHolesOf, BASE, REF, canon,
|
|
400
|
+
generalizeContent, fillContentHoles, blendAtSegment, combineAtFork
|
|
401
|
+
};
|
|
@@ -0,0 +1,190 @@
|
|
|
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
|
+
* abstraction — the MDL/utility admission gate for "abstract methods" (host-side, ZERO-CORE;
|
|
21
|
+
* study doc/WIP/studies/2026-06-26-…, promotes experiment F2).
|
|
22
|
+
*
|
|
23
|
+
* Hybridizing concepts into a reusable abstract method = inventing a nonterminal for a
|
|
24
|
+
* recurring sub-derivation. Grammar theory gives ONE admission criterion (Minton speedup-
|
|
25
|
+
* utility ≡ SEQUITUR rule-utility ≡ smallest-grammar MDL ≡ Lari-Young nonterminal control):
|
|
26
|
+
* **admit a learned production iff its net utility is positive**. Here, "utility" is measured
|
|
27
|
+
* empirically on the real engine — an abstract grammar is admitted iff it is
|
|
28
|
+
* (a) VALID (passes the author-time validator),
|
|
29
|
+
* (b) fixpoint-EQUIVALENT to the chain it replaces (same external facts), and
|
|
30
|
+
* (c) net-CHEAPER (fewer cumulative applies — the refactor win).
|
|
31
|
+
* Keeping the macro *alongside* its constituents (no refactor) costs more applies for the
|
|
32
|
+
* same result -> a net tax (Minton 1990) -> rejected. This is the offline, author-time form
|
|
33
|
+
* the adversarial lens endorsed ("library curation with a gate"); it never mints mid-stabilize.
|
|
34
|
+
*
|
|
35
|
+
* const { evaluate } = require('./abstraction');
|
|
36
|
+
* const r = await evaluate({ seed, providers, chainTree, abstractTree, equivKeys: ['amplified'] });
|
|
37
|
+
* if (r.admit) // adopt abstractTree as the grammar
|
|
38
|
+
*/
|
|
39
|
+
const Graph = require('../graph/index.js');
|
|
40
|
+
const { nextStable } = require('./supervise.js');
|
|
41
|
+
const { validateConceptTree } = require('./validate.js');
|
|
42
|
+
const { forkPlan, bagInterface, conceptCliques } = require('./decompose.js');
|
|
43
|
+
|
|
44
|
+
const clone = (x) => JSON.parse(JSON.stringify(x));
|
|
45
|
+
|
|
46
|
+
// Collect the names of concepts that INVOKE a provider — the casts that cost an (LLM-ish) model call.
|
|
47
|
+
// The U4 currency fix: the utility gate must score MODEL CALLS, not all `applies` (a provider-less
|
|
48
|
+
// applyMutations cast is ~free; fusing it away saves nothing — Minton 1990, the utility problem).
|
|
49
|
+
function providerNames( tree, out ) {
|
|
50
|
+
out = out || new Set();
|
|
51
|
+
const walk = ( node, isChild ) => {
|
|
52
|
+
if ( !node || typeof node !== 'object' ) return;
|
|
53
|
+
const schema = node._schema || node;
|
|
54
|
+
if ( (isChild || node._name) && node._name && schema.provider ) out.add(node._name);
|
|
55
|
+
if ( node.childConcepts ) for ( const k of Object.keys(node.childConcepts) ) walk(node.childConcepts[k], true);
|
|
56
|
+
};
|
|
57
|
+
walk(tree, false);
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Boot a tree on the seed, count cumulative applies AND estimated model-cost, and collect the values of
|
|
62
|
+
// `equivKeys` per object — the external contract by which two grammars are compared for equivalence.
|
|
63
|
+
async function bootMeasure( tree, seed, providers, opts ) {
|
|
64
|
+
Graph._providers = Object.assign({}, Graph._providers || {}, providers || {});
|
|
65
|
+
const provSet = providerNames(tree);
|
|
66
|
+
// cost currency (U4): opts.cost(name)->estimate, else 1 per provider-bearing cast, 0 for provider-less.
|
|
67
|
+
const costOf = opts.cost || (( name ) => (provSet.has(name) ? 1 : 0));
|
|
68
|
+
let applies = 0, cost = 0;
|
|
69
|
+
const cfg = {
|
|
70
|
+
label: opts.label || 'abstraction', isMaster: true, autoMount: true,
|
|
71
|
+
conceptSets: opts.conceptSets || ['common'], bagRefManagers: {}, logLevel: 'error',
|
|
72
|
+
onConceptApply: function ( rec ) { applies++; cost += (costOf(rec && rec.conceptName) || 0); },
|
|
73
|
+
};
|
|
74
|
+
const g = new Graph(clone(seed || { lastRev: 0, nodes: [], segments: [] }), cfg, { common: clone(tree) });
|
|
75
|
+
await nextStable(g);
|
|
76
|
+
const keys = opts.equivKeys || [];
|
|
77
|
+
const facts = {};
|
|
78
|
+
for ( const id of Object.keys(g._objById || {}) ) {
|
|
79
|
+
const etty = g._objById[id] && g._objById[id]._etty;
|
|
80
|
+
const f = etty && etty._;
|
|
81
|
+
if ( !f ) continue;
|
|
82
|
+
const proj = {};
|
|
83
|
+
let has = false;
|
|
84
|
+
for ( const k of keys ) if ( k in f ) { proj[k] = f[k]; has = true; }
|
|
85
|
+
if ( has ) facts[id] = proj;
|
|
86
|
+
}
|
|
87
|
+
return { applies, cost, facts };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Σ_sep NON-REGRESSION (the E7 rule, refined by the 2026-06-30 Laurie confront): a created/merged method must not
|
|
91
|
+
// WIDEN the bounded-context HORIZON. The sound object is the bag-intersection MINIMAL-INTERFACE width (`bagInterface`,
|
|
92
|
+
// Robertson-Seymour 1986) — NOT the scalar min-fill treewidth (an uncoupled upper bound → false-admits a real
|
|
93
|
+
// regression) NOR the size-1 articulation `widened` alone (blind to a size-1→size-2 cut). Both are kept as DIAGNOSTICS.
|
|
94
|
+
// FIX (finding): the prior code fed `forkPlan({common: tree})`, which `conceptCliques` does NOT unwrap → 0 cliques →
|
|
95
|
+
// the gate was VACUOUS (`widened` always []). The trees are the bare childConcepts form; feed them DIRECTLY.
|
|
96
|
+
function interfaceRegression( chainTree, abstractTree ) {
|
|
97
|
+
let before, after;
|
|
98
|
+
try { before = forkPlan(chainTree); } catch ( e ) { before = null; }
|
|
99
|
+
try { after = forkPlan(abstractTree); } catch ( e ) { after = null; }
|
|
100
|
+
const sepB = new Set((before && before.separators) || []);
|
|
101
|
+
const sepA = (after && after.separators) || [];
|
|
102
|
+
const widened = sepA.filter(( s ) => !sepB.has(s)); // (diagnostic) size-1 articulation keys the abstraction ADDED
|
|
103
|
+
const bagB = bagInterface(conceptCliques(chainTree)), bagA = bagInterface(conceptCliques(abstractTree));
|
|
104
|
+
const mB = bagB.minimalInterface.size, mA = bagA.minimalInterface.size;
|
|
105
|
+
// the SOUND gate fires only on a genuine CROSS-TILE widening — BOTH sides divisible (a real thin interface exists
|
|
106
|
+
// on each) and the after's thinnest cut is wider. A fusion that COLLAPSES a chain to a single concept makes the
|
|
107
|
+
// after INDIVISIBLE (a black box); its internal clique is the UTILITY gate's concern, NOT a bounded-context
|
|
108
|
+
// regression (the original `interfaceRegression` comment, vindicated). So indivisible-after is never an interface regress.
|
|
109
|
+
const interfaceRegressed = !bagA.indivisible && !bagB.indivisible && mA > mB;
|
|
110
|
+
return {
|
|
111
|
+
separatorsBefore: [...sepB].sort(), separatorsAfter: sepA.slice().sort(), widened,
|
|
112
|
+
treewidthBefore: bagB.treewidth, treewidthAfter: bagA.treewidth, // diagnostic (NOT gated — see confront)
|
|
113
|
+
minimalInterfaceBefore: bagB.minimalInterface, minimalInterfaceAfter: bagA.minimalInterface,
|
|
114
|
+
indivisibleBefore: bagB.indivisible, indivisibleAfter: bagA.indivisible,
|
|
115
|
+
interfaceRegressed, // the SOUND gate: the cross-tile horizon widened
|
|
116
|
+
sigmaSepBefore: bagB.sigmaSep, sigmaSepAfter: bagA.sigmaSep
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Same set of objects, same value for every compared key on each.
|
|
121
|
+
function factsEqual( a, b ) {
|
|
122
|
+
const ka = Object.keys(a), kb = Object.keys(b);
|
|
123
|
+
if ( ka.length !== kb.length ) return false;
|
|
124
|
+
for ( const id of ka ) {
|
|
125
|
+
if ( !(id in b) ) return false;
|
|
126
|
+
if ( JSON.stringify(a[id]) !== JSON.stringify(b[id]) ) return false;
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Evaluate whether `abstractTree` should be admitted over `chainTree`.
|
|
133
|
+
* @param opts.seed/providers the representative episode to measure on
|
|
134
|
+
* @param opts.chainTree the incumbent grammar (the constituents)
|
|
135
|
+
* @param opts.abstractTree the candidate grammar (with the abstract method)
|
|
136
|
+
* @param opts.equivKeys the external output facts that define equivalence
|
|
137
|
+
* @param opts.validate opts forwarded to validateConceptTree (palette, etc.)
|
|
138
|
+
* @returns {{ valid, errors, equivalent, chainApplies, abstractApplies, gain, admit }}
|
|
139
|
+
*/
|
|
140
|
+
async function evaluate( opts ) {
|
|
141
|
+
opts = opts || {};
|
|
142
|
+
const { errors } = validateConceptTree(opts.abstractTree, opts.validate || {});
|
|
143
|
+
const valid = errors.length === 0;
|
|
144
|
+
const chain = await bootMeasure(opts.chainTree, opts.seed, opts.providers, Object.assign({ label: 'chain' }, opts));
|
|
145
|
+
const abstract = await bootMeasure(opts.abstractTree, opts.seed, opts.providers, Object.assign({ label: 'abstract' }, opts));
|
|
146
|
+
const equivalent = factsEqual(chain.facts, abstract.facts);
|
|
147
|
+
const gain = chain.applies - abstract.applies;
|
|
148
|
+
const costGain = chain.cost - abstract.cost; // U4: the LLM-cost currency (model calls saved)
|
|
149
|
+
const iface = interfaceRegression(opts.chainTree, opts.abstractTree);
|
|
150
|
+
// E7 (refined): the SOUND gate is the bag-intersection minimal-interface non-regression (`interfaceRegressed`);
|
|
151
|
+
// the size-1 articulation `widened` is kept as an additional necessary check (a new size-1 cut key is a regression
|
|
152
|
+
// the interface-width can miss when the cut count, not its width, grows). The scalar treewidth is NOT gated.
|
|
153
|
+
const interfaceOk = !iface.interfaceRegressed && iface.widened.length === 0;
|
|
154
|
+
// admit on the CORRECT currency (model-cost), equivalence, validity, and no interface regression. `gain`
|
|
155
|
+
// (applies) stays reported for continuity; a degenerate corpus with no providers falls back to applies.
|
|
156
|
+
const utility = (chain.cost > 0 || abstract.cost > 0) ? costGain : gain;
|
|
157
|
+
return {
|
|
158
|
+
valid, errors,
|
|
159
|
+
equivalent,
|
|
160
|
+
chainApplies: chain.applies, abstractApplies: abstract.applies, gain,
|
|
161
|
+
chainCost: chain.cost, abstractCost: abstract.cost, costGain,
|
|
162
|
+
interface: iface, interfaceOk,
|
|
163
|
+
admit: valid && equivalent && utility > 0 && interfaceOk,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Adapt the MDL/utility gate to the shape `authorConcept`'s `spec.gate` expects, so the
|
|
169
|
+
* CEGIS loop self-admits abstractions: only an 'add' proposal that `evaluate` admits (valid
|
|
170
|
+
* + fixpoint-equivalent + net-cheaper vs the reference `chainTree`) is installed.
|
|
171
|
+
* @param opts.chainTree/seed/providers/equivKeys the reference episode (as for evaluate)
|
|
172
|
+
* @param opts.asAbstractTree (proposal)->tree how to read the candidate grammar from a
|
|
173
|
+
* proposal (default: the single proposed schema as a tree)
|
|
174
|
+
* @returns async (graph, proposal) => { admit, reason, eval }
|
|
175
|
+
*/
|
|
176
|
+
function makeAbstractionGate( opts ) {
|
|
177
|
+
opts = opts || {};
|
|
178
|
+
const asAbstractTree = opts.asAbstractTree
|
|
179
|
+
|| ((proposal) => ({ childConcepts: { [proposal.schema._id]: proposal.schema } }));
|
|
180
|
+
return async function gate( graph, proposal ) {
|
|
181
|
+
if ( (proposal.op || 'add') !== 'add' ) return { admit: true }; // gate only ADDs
|
|
182
|
+
const r = await evaluate({
|
|
183
|
+
seed: opts.seed, providers: opts.providers, chainTree: opts.chainTree,
|
|
184
|
+
abstractTree: asAbstractTree(proposal), equivKeys: opts.equivKeys, validate: opts.validate,
|
|
185
|
+
});
|
|
186
|
+
return { admit: r.admit, reason: r.admit ? null : `not admitted (equivalent=${r.equivalent}, gain=${r.gain})`, eval: r };
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = { evaluate, bootMeasure, factsEqual, makeAbstractionGate, interfaceRegression, providerNames };
|