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,510 @@
|
|
|
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
|
+
* mine — sub-forest mining for crystallization (host-side, ZERO-CORE; study
|
|
21
|
+
* doc/WIP/studies/2026-06-26-…, the #13 loop core).
|
|
22
|
+
*
|
|
23
|
+
* Inventing a nonterminal for a recurring sub-derivation = SUBDUE/SEQUITUR on the engine's
|
|
24
|
+
* derivation forest. `mineChains` reads a corpus of apply-records (the `onConceptApply`
|
|
25
|
+
* trace: `{concept, target}` firings) plus the concept schemas, and finds frequent
|
|
26
|
+
* producer->consumer edges A->B (B `require`s a fact A produces, and both fired on the same
|
|
27
|
+
* target). The top edges are candidate abstractions; `composeProviders` chains the
|
|
28
|
+
* constituents' providers into one (threading each output into the next), so the proposed
|
|
29
|
+
* abstract method can be measured by the MDL/utility gate (`abstraction.evaluate`).
|
|
30
|
+
*
|
|
31
|
+
* const chains = mineChains(applyRecords, conceptTree); // [{from,to,via,count}, …]
|
|
32
|
+
* const M = composeProviders(provA, provB); // one cast does both
|
|
33
|
+
* // -> evaluate({chainTree, abstractTree:{…M…}, …}) decides admission.
|
|
34
|
+
*
|
|
35
|
+
* `mineChains` keys only on STATIC produced facts (self-flag + applyMutations template keys
|
|
36
|
+
* — the same extraction validate.js uses); provider-written facts are not statically known,
|
|
37
|
+
* so the mined edge is the self-flag/template producer->consumer chain (the auditable spine).
|
|
38
|
+
*/
|
|
39
|
+
const { eachConcept, refsOf, refKeyOf, templateKeys } = require('./validate.js');
|
|
40
|
+
const { relativize, antiUnify, canon, hasHoles, emitMethodAsSubgraph, BASE } = require('./abstract.js');
|
|
41
|
+
const { memoSurfaceKeys } = require('./memo-stability.js');
|
|
42
|
+
const { digest } = require('../providers/canonicalize.js');
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Find frequent producer->consumer concept chains in a trace corpus.
|
|
46
|
+
* @param records [{ concept, target }] apply-records (onConceptApply firings)
|
|
47
|
+
* @param tree the concept tree (for static produces/requires)
|
|
48
|
+
* @returns [{ from, to, via, count }] sorted by co-firing count (desc)
|
|
49
|
+
*/
|
|
50
|
+
function mineChains( records, tree ) {
|
|
51
|
+
const concepts = [];
|
|
52
|
+
eachConcept(tree, (c) => { if ( c._name ) concepts.push(c); });
|
|
53
|
+
|
|
54
|
+
const produces = {}, requires = {};
|
|
55
|
+
for ( const c of concepts ) {
|
|
56
|
+
const schema = c._schema || c;
|
|
57
|
+
produces[c._name] = new Set([c._name, ...templateKeys(schema.applyMutations)]);
|
|
58
|
+
requires[c._name] = refsOf(schema.require, false).map((r) => refKeyOf(r).key);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const firedOn = {}; // concept -> Set(target)
|
|
62
|
+
for ( const r of (records || []) ) {
|
|
63
|
+
if ( !r || !r.concept ) continue;
|
|
64
|
+
(firedOn[r.concept] = firedOn[r.concept] || new Set()).add(r.target);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const out = [];
|
|
68
|
+
for ( const A of concepts ) for ( const B of concepts ) {
|
|
69
|
+
if ( A === B ) continue;
|
|
70
|
+
let via = null;
|
|
71
|
+
for ( const f of (requires[B._name] || []) )
|
|
72
|
+
if ( produces[A._name] && produces[A._name].has(f) ) { via = f; break; }
|
|
73
|
+
if ( !via ) continue;
|
|
74
|
+
const tA = firedOn[A._name] || new Set(), tB = firedOn[B._name] || new Set();
|
|
75
|
+
let count = 0;
|
|
76
|
+
for ( const t of tA ) if ( tB.has(t) ) count++;
|
|
77
|
+
if ( count > 0 ) out.push({ from: A._name, to: B._name, via, count });
|
|
78
|
+
}
|
|
79
|
+
out.sort((x, y) => y.count - x.count);
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Chain N cb-style providers into one. Each provider's output facts are threaded into a
|
|
85
|
+
* shadow scope for the next, and all output facts are merged into a single `_parent`
|
|
86
|
+
* template — so one cast reproduces the whole chain (the inlined abstract method).
|
|
87
|
+
* @param {...Function} fns providers `(graph, concept, scope, argz, cb)` -> cb(err, template)
|
|
88
|
+
* @returns Function a single provider with the same signature
|
|
89
|
+
*/
|
|
90
|
+
function composeProviders( ...fns ) {
|
|
91
|
+
return function ( graph, concept, scope, argz, cb ) {
|
|
92
|
+
const merged = {};
|
|
93
|
+
const shadowFacts = Object.assign({}, (scope && scope._) || {});
|
|
94
|
+
let i = 0;
|
|
95
|
+
(function step() {
|
|
96
|
+
if ( i >= fns.length ) {
|
|
97
|
+
// the composed concept must mark ITS OWN self-flag cast (a provider concept
|
|
98
|
+
// writes its self-flag in its template, else the engine never sees it cast and
|
|
99
|
+
// re-fires it to the apply-cap). The constituents' flags are merged data facts.
|
|
100
|
+
if ( concept && concept._name ) merged[concept._name] = true;
|
|
101
|
+
return cb(null, Object.assign({ $_id: '_parent' }, merged));
|
|
102
|
+
}
|
|
103
|
+
const fn = fns[i++];
|
|
104
|
+
const shadowScope = { _: shadowFacts, getRef: scope && scope.getRef ? scope.getRef.bind(scope) : undefined };
|
|
105
|
+
fn(graph, concept, shadowScope, argz, function ( err, tpl ) {
|
|
106
|
+
if ( err ) return cb(err);
|
|
107
|
+
const objs = Array.isArray(tpl) ? tpl : (tpl ? [tpl] : []);
|
|
108
|
+
for ( const o of objs ) {
|
|
109
|
+
if ( !o || typeof o !== 'object' ) continue;
|
|
110
|
+
for ( const raw of Object.keys(o) ) {
|
|
111
|
+
if ( raw === '$_id' || raw === '$$_id' || raw === '_id' ) continue;
|
|
112
|
+
const key = raw.replace(/^\$+/, '');
|
|
113
|
+
merged[key] = o[raw];
|
|
114
|
+
shadowFacts[key] = o[raw];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
step();
|
|
118
|
+
});
|
|
119
|
+
})();
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Normalize engine trace records (cfg.onConceptApply `_rec`) into mineChains input. */
|
|
124
|
+
function recordsFromTrace( rawRecords ) {
|
|
125
|
+
return (rawRecords || [])
|
|
126
|
+
.filter((r) => r && r.conceptName)
|
|
127
|
+
.map((r) => ({ concept: r.conceptName, target: r.targetId }));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* A trace collector you plug into `cfg.onConceptApply` (or `graph.on('conceptApply', …)`),
|
|
132
|
+
* then mine the accumulated firings. Keeps the corpus off the graph (host-side), so the
|
|
133
|
+
* miner runs offline/between-episodes — never mid-stabilize.
|
|
134
|
+
* const miner = traceMiner();
|
|
135
|
+
* new Graph(seed, { …, onConceptApply: miner.onConceptApply }, conceptMap);
|
|
136
|
+
* const chains = miner.chains(conceptTree); // [{from,to,via,count}, …]
|
|
137
|
+
*/
|
|
138
|
+
function traceMiner() {
|
|
139
|
+
const records = [];
|
|
140
|
+
return {
|
|
141
|
+
records,
|
|
142
|
+
onConceptApply: function ( rec ) { if ( rec && rec.conceptName ) records.push({ concept: rec.conceptName, target: rec.targetId }); },
|
|
143
|
+
chains: function ( tree ) { return mineChains(records, tree); },
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ─────────────────────────────── STRUCTURAL method mining (Gap B+D — the antiUnify-in-the-loop) ───────────────
|
|
148
|
+
//
|
|
149
|
+
// `mineChains` finds frequent producer->consumer EDGES (the spine of the provider-fusion crystallizer). For a
|
|
150
|
+
// STRUCTURAL method — one cast that CREATES a sub-graph (intermediate node + child segments) — the recurrence and
|
|
151
|
+
// the soundness check live at the level of the ground mutation TEMPLATE, not the static edge: a structural concept
|
|
152
|
+
// fires on N call sites, each producing a ground patch; the method is sound to crystallize iff those patches all
|
|
153
|
+
// generalize to ONE stable skeleton (Plotkin LGG, `abstract.antiUnify`) whose content leaves are a function of the
|
|
154
|
+
// firing's typed K1 signature (the canonicalization barrier — a content leaf NOT determined by the signature is the
|
|
155
|
+
// prose case, REFUSE). `mineMethods` is that check, a pure function over the enriched trace.
|
|
156
|
+
|
|
157
|
+
// engine bookkeeping that rides in a captured `rec.patch` (the serialized `_revs[r].tpl`) but is NOT method content:
|
|
158
|
+
// `_rev` is a per-apply monotonic counter (would falsely read as a content-var that varies on every firing — the
|
|
159
|
+
// probe that built this caught it), `_origin` is the creating-apply id (base-derived, redundant). Strip both before
|
|
160
|
+
// relativize/antiUnify so the skeleton is the method, not the bookkeeping. (`getMutationFromPath` strips `_rev` too.)
|
|
161
|
+
const SYS_KEYS = new Set(['_rev', '_origin']);
|
|
162
|
+
|
|
163
|
+
// `extra` (optional Set) = DECLARED prose/untracked keys (the model's free text). They ride in the cast
|
|
164
|
+
// patch but, like the canonicalization barrier in `llm.js` keeps prose out of the FactsDigest, they must be
|
|
165
|
+
// kept OFF the mined surface — prose varies per instance, so leaving it in would falsely read as a content-var
|
|
166
|
+
// and break signature-determination (the K1 "prose stays in the model" line). Stripping them lets the typed
|
|
167
|
+
// STRUCTURE crystallize while the prose stays per-instance (never baked into the method).
|
|
168
|
+
function stripPatch( x, extra ) {
|
|
169
|
+
if ( Array.isArray(x) ) return x.map(( e ) => stripPatch(e, extra));
|
|
170
|
+
if ( x && typeof x === 'object' ) {
|
|
171
|
+
const out = {};
|
|
172
|
+
for ( const k of Object.keys(x) ) if ( !SYS_KEYS.has(k) && !(extra && extra.has(k)) ) out[k] = stripPatch(x[k], extra);
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
return x;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// every id this patch CREATES (its `$$_id`/`$_id`/`_id` values; `_parent` resolves to the base) — so the frontier
|
|
179
|
+
// inference never mistakes a created child for a call-site endpoint.
|
|
180
|
+
function createdIds( patch, base ) {
|
|
181
|
+
const ids = new Set([base]);
|
|
182
|
+
for ( const o of (Array.isArray(patch) ? patch : [patch]) ) {
|
|
183
|
+
if ( !o || typeof o !== 'object' ) continue;
|
|
184
|
+
for ( const k of ['$$_id', '$_id', '_id'] ) {
|
|
185
|
+
const v = o[k];
|
|
186
|
+
if ( typeof v === 'string' ) ids.add(v === '_parent' ? base : v.replace(/^\$+/, ''));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return ids;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// walk a template's string VALUES with their immediate object KEY, in a deterministic (array-index, then
|
|
193
|
+
// sorted-key) order. `key` is the nearest object field the value sits under (an array carries its parent key).
|
|
194
|
+
function eachKeyedValue( tpl, fn, key ) {
|
|
195
|
+
if ( Array.isArray(tpl) ) { for ( const x of tpl ) eachKeyedValue(x, fn, key); return; }
|
|
196
|
+
if ( tpl && typeof tpl === 'object' ) { for ( const k of Object.keys(tpl).sort() ) eachKeyedValue(tpl[k], fn, k); return; }
|
|
197
|
+
if ( typeof tpl === 'string' ) fn(key, tpl);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Read the call-site ctx ({ base, refs, fields }) off a GROUND structural patch: the base is the cast target; a
|
|
202
|
+
* frontier ref is an id-valued string that (a) is not the base, (b) is not created by this patch, (c) names an
|
|
203
|
+
* EXISTING external object (∈ knownIds). Each distinct external id gets a deterministic name `f<i>` in scan order, so
|
|
204
|
+
* two structurally-identical firings produce ALIGNED parameterized forms (the same endpoint → the same hole name →
|
|
205
|
+
* antiUnify sees a structural hole, not a content diff). `fields[name]` records WHICH object field that endpoint sat
|
|
206
|
+
* under (e.g. `originNode`) so the crystallized provider can re-read it off a new call site (`ctxFromScope`). A value
|
|
207
|
+
* that is none of these is left LITERAL (it is content payload).
|
|
208
|
+
*/
|
|
209
|
+
function inferCtx( patch, base, knownIds ) {
|
|
210
|
+
const created = createdIds(patch, base);
|
|
211
|
+
const refs = {}, fields = {}, seen = new Map();
|
|
212
|
+
let n = 0;
|
|
213
|
+
eachKeyedValue(patch, ( key, v ) => {
|
|
214
|
+
if ( v === base || created.has(v) ) return;
|
|
215
|
+
if ( base && v.indexOf(base + '_') === 0 ) return; // base-derived created id
|
|
216
|
+
if ( knownIds && !knownIds.has(v) ) return; // not a real external object → content, leave literal
|
|
217
|
+
if ( !seen.has(v) ) { const name = 'f' + (n++); seen.set(v, name); refs[name] = v; fields[name] = key; }
|
|
218
|
+
});
|
|
219
|
+
return { base, refs, fields };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ─────────────────────────────── DECLARED frontier (Phase 1: INFERRED → DECLARED) ─────────────────────────────
|
|
223
|
+
//
|
|
224
|
+
// `inferCtx` GUESSES the frontier by scanning the literal-id surface (a value ∈ knownIds, not base/created). That
|
|
225
|
+
// breaks the moment an endpoint is wired by a `$`-ref TOKEN (`$start`, `_parent:originNode`) — it isn't in knownIds
|
|
226
|
+
// so it reads as content, the frontier is empty, and the replay never rebinds (study 2026-06-30). `declaredCtx` reads
|
|
227
|
+
// each endpoint from its DECLARED field instead: the value at that field (whatever it is — a literal id OR a `$`-ref
|
|
228
|
+
// token) that is EXTERNAL (not the base, not created by this patch, not base-derived) IS the endpoint. So the value
|
|
229
|
+
// becomes a `⟦@ref⟧` hole regardless of its syntactic form, which is the whole Phase-1 fix. SAME `{base,refs,fields}`
|
|
230
|
+
// return shape as `inferCtx`, so every downstream consumer (relativize/antiUnify/signatureDetermined/ctxFromScope) is
|
|
231
|
+
// unchanged — this is a pure substitution at the ctx layer.
|
|
232
|
+
|
|
233
|
+
// Normalize a host-declared frontier into the canonical param list. Accepts an array of param specs, a plain
|
|
234
|
+
// `{name: field}` map, or `{name: {field, sort, role, requiredFacts}}`. `sort`/`role` default to a node-ref endpoint
|
|
235
|
+
// (the path-section common case) — the Laurie schema fields land here so endpoint and behavioral params are ONE shape
|
|
236
|
+
// differing only by `sort`/`role` (Phase 1 is the actual common substrate, not a behavioral-param bolt-on).
|
|
237
|
+
function normalizeFrontierParams( decl ) {
|
|
238
|
+
if ( !decl ) return null;
|
|
239
|
+
const arr = Array.isArray(decl) ? decl : Object.keys(decl).map(( name ) => {
|
|
240
|
+
const v = decl[name];
|
|
241
|
+
return typeof v === 'string' ? { name, field: v } : Object.assign({ name }, v);
|
|
242
|
+
});
|
|
243
|
+
// preserve the full param shape (a behavioral param carries its typed in/out/frontier interface) — only DEFAULT
|
|
244
|
+
// the universal fields; never drop the interface, else a typed behavioral param arrives at the lint untyped.
|
|
245
|
+
return arr.map(( p ) => Object.assign({}, p, {
|
|
246
|
+
name: p.name, field: p.field, sort: p.sort || 'node-ref',
|
|
247
|
+
role: p.role || 'endpoint', requiredFacts: p.requiredFacts || [],
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Read the call-site ctx off a GROUND structural patch from a DECLARED frontier (vs `inferCtx`'s knownIds scan).
|
|
253
|
+
* @param patch the ground structural patch (array of template objects)
|
|
254
|
+
* @param base the cast target id (= `_parent`)
|
|
255
|
+
* @param params normalized frontier params (`normalizeFrontierParams`); only `role:'endpoint'` params have a patch
|
|
256
|
+
* location (behavioral params are indexed for selection, not mined into the body ctx — Phase 1).
|
|
257
|
+
* @returns { base, refs:{name:value}, fields:{name:field} } — identical shape to `inferCtx`.
|
|
258
|
+
*/
|
|
259
|
+
function declaredCtx( patch, base, params ) {
|
|
260
|
+
const created = createdIds(patch, base);
|
|
261
|
+
const refs = {}, fields = {}, ambiguous = [];
|
|
262
|
+
for ( const p of (params || []) ) {
|
|
263
|
+
if ( (p.role || 'endpoint') !== 'endpoint' || !p.field ) continue;
|
|
264
|
+
// the patch field for a "a:b" replay path is its LEAF segment (the field the value actually sits under).
|
|
265
|
+
const patchField = p.field.indexOf(':') >= 0 ? p.field.split(':').pop() : p.field;
|
|
266
|
+
const found = new Set(); // distinct EXTERNAL values at this declared field (scan order)
|
|
267
|
+
eachKeyedValue(patch, ( key, v ) => {
|
|
268
|
+
if ( key !== patchField ) return;
|
|
269
|
+
if ( v === base || created.has(v) ) return; // internal / the base itself → not the frontier
|
|
270
|
+
if ( base && v.indexOf(base + '_') === 0 ) return; // base-derived created id
|
|
271
|
+
found.add(v);
|
|
272
|
+
});
|
|
273
|
+
// SOUNDNESS: a declared field that resolves to >1 distinct external value is UNDER-DECLARED — relativize would
|
|
274
|
+
// hole only the first and leave the rest LITERAL, so a learning-episode id leaks into a fresh replay (#30, an
|
|
275
|
+
// orphaned segment via Segment.js:70-72). Flag it → the crystallizer REFUSES rather than mis-replaying. (Declare
|
|
276
|
+
// one param per endpoint to resolve — never a cap on expressivity, a disambiguation of the declaration.)
|
|
277
|
+
if ( found.size > 1 ) ambiguous.push({ name: p.name, field: p.field, values: [...found] });
|
|
278
|
+
refs[p.name] = found.size ? [...found][0] : undefined; // undefined ⇒ no hole ⇒ replay bypasses (never mis-binds)
|
|
279
|
+
fields[p.name] = p.field;
|
|
280
|
+
}
|
|
281
|
+
const ctx = { base, refs, fields };
|
|
282
|
+
if ( ambiguous.length ) ctx.ambiguous = ambiguous;
|
|
283
|
+
// SOUNDNESS (collapse): two DISTINCT declared endpoints resolving to the SAME value can't be told apart by
|
|
284
|
+
// `relativize` (it matches by value, first-ref-wins) → they merge into ONE hole and the second mis-binds at a
|
|
285
|
+
// replay site where they differ. Flag → the crystallizer refuses (the endpoints are structurally indistinguishable).
|
|
286
|
+
const byVal = {};
|
|
287
|
+
for ( const name of Object.keys(refs) ) { const v = refs[name]; if ( v != null ) (byVal[v] = byVal[v] || []).push(name); }
|
|
288
|
+
const collapsed = Object.keys(byVal).filter(( v ) => byVal[v].length > 1).map(( v ) => ({ value: v, names: byVal[v] }));
|
|
289
|
+
if ( collapsed.length ) ctx.collapsed = collapsed;
|
|
290
|
+
return ctx;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// SOUNDNESS net — a segment endpoint must rebind at replay, so it has to be either a BOUND frontier hole (`⟦@ref:…⟧`)
|
|
294
|
+
// or a base-derived id that names an object the method actually CREATES. Two ways it isn't, each ORPHANS/mis-wires at a
|
|
295
|
+
// fresh replay (#30) and each was adversarial-review-reproduced:
|
|
296
|
+
// • LITERAL — a plain external id the declaration missed (`hasHoles===false`): a baked learning id.
|
|
297
|
+
// • PHANTOM — a base-derived hole `⟦@base⟧<suffix>` whose suffix names NO created object: `relativizeVal` mis-holes
|
|
298
|
+
// an external endpoint whose id collides with `<castTarget>_…` into the base id-space (so `hasHoles` is
|
|
299
|
+
// true and the LITERAL check is blind); at replay it resolves to a non-existent `<newBase><suffix>`.
|
|
300
|
+
// `createdHoles` = the base itself (the cast target) ∪ every base-derived id the param declares as an object id. Reuses
|
|
301
|
+
// `abstract.hasHoles`/`BASE` (the ⟦…⟧ sentinel stays defined in ONE place).
|
|
302
|
+
const ENDPOINT_FIELDS = ['originNode', 'targetNode'];
|
|
303
|
+
const ID_KEYS = ['$$_id', '$_id', '_id'];
|
|
304
|
+
function leakedEndpoints( param ) {
|
|
305
|
+
const objs = Array.isArray(param) ? param : [param];
|
|
306
|
+
const createdHoles = new Set([BASE]); // the base (cast target) is a valid base-derived endpoint
|
|
307
|
+
for ( const o of objs ) {
|
|
308
|
+
if ( !o || typeof o !== 'object' ) continue;
|
|
309
|
+
for ( const k of ID_KEYS ) { const v = o[k]; if ( typeof v === 'string' && v.indexOf(BASE) === 0 ) createdHoles.add(v); }
|
|
310
|
+
}
|
|
311
|
+
const out = [];
|
|
312
|
+
for ( const o of objs ) {
|
|
313
|
+
if ( !o || typeof o !== 'object' ) continue;
|
|
314
|
+
for ( const f of ENDPOINT_FIELDS ) {
|
|
315
|
+
const v = o[f];
|
|
316
|
+
if ( typeof v !== 'string' ) continue;
|
|
317
|
+
if ( !hasHoles(v) ) out.push({ field: f, value: v, kind: 'literal' }); // a plain external literal
|
|
318
|
+
else if ( v.indexOf(BASE) === 0 && !createdHoles.has(v) ) out.push({ field: f, value: v, kind: 'phantom' }); // mis-holed external
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return out;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// CONTENT-BLIND structural key: the parameterized form with every CONTENT scalar (anything that is not a hole
|
|
325
|
+
// sentinel) replaced by a placeholder, so all firings of ONE method (same structure + frontier, content varying by
|
|
326
|
+
// premise) group together — while a genuinely different structure (an extra intermediate, a different field set)
|
|
327
|
+
// gets a different key. Reuses `abstract.hasHoles` to recognise a sentinel (never re-derives the ⟦…⟧ format).
|
|
328
|
+
function blankContent( x ) {
|
|
329
|
+
if ( Array.isArray(x) ) return x.map(blankContent);
|
|
330
|
+
if ( x && typeof x === 'object' ) { const o = {}; for ( const k of Object.keys(x) ) o[k] = blankContent(x[k]); return o; }
|
|
331
|
+
if ( typeof x === 'string' && hasHoles(x) ) return x; // a base/ref hole is structural → keep
|
|
332
|
+
return '§'; // content scalar → blank
|
|
333
|
+
}
|
|
334
|
+
function shapeKey( param ) { return canon(blankContent(param)); }
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Mine RECURRENT STRUCTURAL methods from an enriched trace.
|
|
338
|
+
* @param records [{ concept, target, patch, premise }] — enriched firings (see `methodTrace`)
|
|
339
|
+
* @param tree the concept tree (for the produced-fact set per concept)
|
|
340
|
+
* @param opts.minCount distinct call sites required to consider a method (default 2)
|
|
341
|
+
* @param opts.knownIds Set of existing object ids (frontier-vs-content disambiguation; pass graph object ids)
|
|
342
|
+
* @returns [{ concept, count, stable, signatureDetermined, admissible, skeleton, contentVars, frontier,
|
|
343
|
+
* instances:[{target,ground,ctx,premise,premiseDigest,param}], templatesByDigest, produced }]
|
|
344
|
+
* one entry per (concept × structural-skeleton) bucket, sorted by count desc.
|
|
345
|
+
*/
|
|
346
|
+
function mineMethods( records, tree, opts ) {
|
|
347
|
+
opts = opts || {};
|
|
348
|
+
const minCount = opts.minCount || 2;
|
|
349
|
+
const knownIds = opts.knownIds || null;
|
|
350
|
+
const declaredParams = normalizeFrontierParams(opts.declaredFrontier); // Phase 1: DECLARED frontier (else inferred)
|
|
351
|
+
// DECLARED prose/untracked keys to strip before mining (the LLM free-text that must not fragment the method).
|
|
352
|
+
const proseSet = ( opts.proseKeys && opts.proseKeys.length ) ? new Set(opts.proseKeys) : null;
|
|
353
|
+
|
|
354
|
+
// static produced-fact set per concept (the contract write obligation, Gap C input).
|
|
355
|
+
const produced = {};
|
|
356
|
+
eachConcept(tree, ( c ) => { if ( c._name ) { const s = c._schema || c; produced[c._name] = [c._name, ...templateKeys(s.applyMutations)]; } });
|
|
357
|
+
|
|
358
|
+
// 1. keep only STRUCTURAL firings (a patch that creates a sub-graph: > 1 object), relativize each.
|
|
359
|
+
const byBucket = {}; // "concept skeletonCanon" -> instances
|
|
360
|
+
for ( const r of (records || []) ) {
|
|
361
|
+
if ( !r || !r.concept || !Array.isArray(r.patch) || r.patch.length <= 1 ) continue;
|
|
362
|
+
const ground = stripPatch(r.patch, proseSet);
|
|
363
|
+
const ctx = declaredParams ? declaredCtx(ground, r.target, declaredParams) : inferCtx(ground, r.target, knownIds);
|
|
364
|
+
const param = relativize(ground, ctx);
|
|
365
|
+
const key = r.concept + ' ' + shapeKey(param); // content-BLIND: one method, content varies by premise
|
|
366
|
+
(byBucket[key] = byBucket[key] || []).push({
|
|
367
|
+
concept: r.concept, target: r.target, ground, ctx, premise: r.premise || {},
|
|
368
|
+
premiseDigest: digest(r.premise || {}), param,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// 2. per bucket: distinct-site count, pairwise-antiUnify stability, K1 signatureDetermined.
|
|
373
|
+
const out = [];
|
|
374
|
+
for ( const key of Object.keys(byBucket) ) {
|
|
375
|
+
const inst = byBucket[key];
|
|
376
|
+
const concept = inst[0].concept;
|
|
377
|
+
const distinct = new Set(inst.map(( i ) => i.target));
|
|
378
|
+
const count = distinct.size;
|
|
379
|
+
|
|
380
|
+
// stable iff every pair of instances generalizes to one skeleton (no tree-shape difference).
|
|
381
|
+
let stable = true; const cvPaths = new Set();
|
|
382
|
+
for ( let i = 1; i < inst.length && stable; i++ ) {
|
|
383
|
+
const r = antiUnify(inst[0].ground, inst[0].ctx, inst[i].ground, inst[i].ctx);
|
|
384
|
+
if ( !r.stable ) stable = false;
|
|
385
|
+
else for ( const d of r.contentVars ) cvPaths.add(d.path);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// K1 ceiling (the canonicalization barrier): content must be a FUNCTION of the typed signature — i.e. all
|
|
389
|
+
// firings sharing a premise digest must produce the IDENTICAL parameterized method. A premise-class with two
|
|
390
|
+
// different parameterized forms = content depends on something outside the signature (prose/nondeterminism) →
|
|
391
|
+
// not signature-determined → REFUSE (it would graveyard a wrong typed fact through the cascade).
|
|
392
|
+
let signatureDetermined = true;
|
|
393
|
+
const byDigest = {};
|
|
394
|
+
for ( const i of inst ) {
|
|
395
|
+
const pc = canon(i.param);
|
|
396
|
+
if ( byDigest[i.premiseDigest] === undefined ) byDigest[i.premiseDigest] = pc;
|
|
397
|
+
else if ( byDigest[i.premiseDigest] !== pc ) signatureDetermined = false;
|
|
398
|
+
}
|
|
399
|
+
// a representative parameterized template per premise class (the crystallized provider replays these).
|
|
400
|
+
const templatesByDigest = {};
|
|
401
|
+
for ( const i of inst ) if ( !(i.premiseDigest in templatesByDigest) ) templatesByDigest[i.premiseDigest] = i.param;
|
|
402
|
+
|
|
403
|
+
// T2b: validate the engine-native emit reproduces the body (behind opts.graph); the replay param stays captured.
|
|
404
|
+
const emitChk = opts.graph ? emitEquivalence(opts.graph, inst[0].ground, inst[0].ctx, inst[0].target) : null;
|
|
405
|
+
|
|
406
|
+
// SOUNDNESS net: any un-holed segment endpoint across the bucket's parameterized forms = a baked external id
|
|
407
|
+
// (an endpoint the frontier missed). Surfaced for the crystallizer to refuse (a leak would orphan/mis-wire).
|
|
408
|
+
const leakSeen = {}, leak = [];
|
|
409
|
+
for ( const i of inst ) for ( const e of leakedEndpoints(i.param) ) { const k = e.field + '=' + e.value; if ( !leakSeen[k] ) { leakSeen[k] = 1; leak.push(e); } }
|
|
410
|
+
|
|
411
|
+
out.push({
|
|
412
|
+
concept, count, stable, signatureDetermined,
|
|
413
|
+
admissible: stable && signatureDetermined && count >= minCount,
|
|
414
|
+
skeleton: inst[0].param, contentVars: [...cvPaths].sort(),
|
|
415
|
+
frontier: Object.keys(inst[0].ctx.refs).sort(), frontierFields: inst[0].ctx.fields,
|
|
416
|
+
instances: inst, templatesByDigest, produced: produced[concept] || [concept],
|
|
417
|
+
emitEquivalent: emitChk ? emitChk.equivalent : undefined,
|
|
418
|
+
leak: leak.length ? leak : undefined,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
out.sort(( a, b ) => b.count - a.count);
|
|
422
|
+
return out;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ─────────────────────────────── T2b — ENGINE-NATIVE emit, behind opts.graph (validated, not the replay param) ────
|
|
426
|
+
//
|
|
427
|
+
// `emitMethodAsSubgraph` (abstract.js) closes the F6/U1a loop: serialize a derived sub-graph through the engine-native
|
|
428
|
+
// `getMutationFromPath` and relativize it into a re-mountable method. It was dead-but-tested; this wires it behind
|
|
429
|
+
// `opts.graph`. VERIFY-BEFORE-BUILD FINDING (scratchpad probe): for the modeled input the engine-native emit is NOT
|
|
430
|
+
// byte-identical to `relativize(capturedPatch, ctx)` — emit serializes the LIVE BODY ONLY (no parent cast object) and
|
|
431
|
+
// `getMutationFromPath` keeps `_origin` + emits `_id` where the captured patch carries `$$_id`. They ARE equivalent up
|
|
432
|
+
// to that bookkeeping (same skeleton, same holes, same content). So the crystallizer KEEPS the captured-patch param for
|
|
433
|
+
// replay (it also carries the parent's cast facts — Refined/alts — emit drops); emit is exercised + VALIDATED here. The
|
|
434
|
+
// plan's "fall back to relativize if the equivalence test fails" therefore resolves to: always relativize the captured
|
|
435
|
+
// patch (the kill-gate-proven path), with `emitEquivalent` recording that the F6 emit faithfully reproduces the body.
|
|
436
|
+
|
|
437
|
+
const idOfTplObj = ( o, base ) => {
|
|
438
|
+
if ( !o || typeof o !== 'object' ) return null;
|
|
439
|
+
if ( o.$_id === '_parent' ) return base;
|
|
440
|
+
const v = o.$$_id != null ? o.$$_id : (o.$_id != null ? o.$_id : o._id);
|
|
441
|
+
return typeof v === 'string' ? v.replace(/^\$+/, '') : v;
|
|
442
|
+
};
|
|
443
|
+
// normalize away the divergences the probe identified (unify the id key, drop `_origin`), then sort by id, so the two
|
|
444
|
+
// relativized bodies compare on STRUCTURE + CONTENT, not serialization form.
|
|
445
|
+
function normalizeBody( tpl ) {
|
|
446
|
+
return (Array.isArray(tpl) ? tpl : [tpl]).map(( o ) => {
|
|
447
|
+
if ( !o || typeof o !== 'object' ) return o;
|
|
448
|
+
const out = {};
|
|
449
|
+
for ( const k of Object.keys(o) ) {
|
|
450
|
+
if ( k === '_origin' ) continue;
|
|
451
|
+
out[(k === '_id' || k === '$$_id' || k === '$_id') ? 'id' : k] = o[k];
|
|
452
|
+
}
|
|
453
|
+
return out;
|
|
454
|
+
}).sort(( a, b ) => String(a.id).localeCompare(String(b.id)));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Validate the engine-native emit (`emitMethodAsSubgraph`) against the captured-patch body for ONE firing, behind a
|
|
459
|
+
* live `graph`. Returns { equivalent, emit, captured } — `equivalent` true iff the F6 serialize reproduces the body up
|
|
460
|
+
* to engine bookkeeping. Used to annotate `m.emitEquivalent`; the crystallizer's replay param stays the captured patch.
|
|
461
|
+
*/
|
|
462
|
+
function emitEquivalence( graph, ground, ctx, base ) {
|
|
463
|
+
if ( !graph ) return { equivalent: undefined, emit: null, captured: null };
|
|
464
|
+
const bodyIds = [...createdIds(ground, base)].filter(( id ) => id !== base );
|
|
465
|
+
const emit = emitMethodAsSubgraph(graph, bodyIds, ctx);
|
|
466
|
+
const capturedBody = relativize((Array.isArray(ground) ? ground : [ground]).filter(( o ) => idOfTplObj(o, base) !== base), ctx);
|
|
467
|
+
const equivalent = emit != null && canon(normalizeBody(emit)) === canon(normalizeBody(capturedBody));
|
|
468
|
+
return { equivalent, emit, captured: capturedBody };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// the target's facts projected onto a concept's MEMO SURFACE (require/ensure/assert keys) — the typed K1 premise the
|
|
472
|
+
// firing was keyed on. Captured at apply time from the live graph (require keys aren't overwritten by the cast's own
|
|
473
|
+
// patch, so the post-apply projection is the firing premise).
|
|
474
|
+
function premiseOf( graph, conceptName, targetId ) {
|
|
475
|
+
const keys = memoSurfaceKeys(graph, conceptName);
|
|
476
|
+
const etty = graph.getEtty && graph.getEtty(targetId);
|
|
477
|
+
const facts = etty && etty._;
|
|
478
|
+
if ( !facts ) return {};
|
|
479
|
+
const proj = {};
|
|
480
|
+
for ( const k of keys ) if ( k in facts ) proj[k] = facts[k];
|
|
481
|
+
return proj;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* An ENRICHED trace collector for structural mining — keeps the ground patch + the firing premise (which `traceMiner`
|
|
486
|
+
* drops). Register it on the live graph's `conceptApply` event (so it can read the premise off the graph):
|
|
487
|
+
* const mt = methodTrace();
|
|
488
|
+
* const g = new Graph(seed, cfg, conceptMap); mt.listen(g);
|
|
489
|
+
* await nextStable(g);
|
|
490
|
+
* const methods = mt.methods(tree, { knownIds: new Set(Object.keys(g._objById)) });
|
|
491
|
+
*/
|
|
492
|
+
function methodTrace() {
|
|
493
|
+
const records = [];
|
|
494
|
+
function onApply( graph, rec ) {
|
|
495
|
+
if ( !rec ) { rec = graph; graph = null; } // tolerate the cfg.onConceptApply (rec-only) form
|
|
496
|
+
if ( !rec || !rec.conceptName ) return;
|
|
497
|
+
records.push({
|
|
498
|
+
concept: rec.conceptName, target: rec.targetId, patch: rec.patch, applyId: rec.applyId,
|
|
499
|
+
premise: graph ? premiseOf(graph, rec.conceptName, rec.targetId) : (rec.premise || {}),
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
records, onApply, onConceptApply: onApply,
|
|
504
|
+
listen: function ( graph ) { graph.on('conceptApply', onApply); return this; },
|
|
505
|
+
methods: function ( tree, o ) { return mineMethods(records, tree, o); },
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
module.exports = { mineChains, composeProviders, recordsFromTrace, traceMiner,
|
|
510
|
+
mineMethods, methodTrace, stripPatch, inferCtx, declaredCtx, normalizeFrontierParams, emitEquivalence, premiseOf };
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
* mount — the 3-REGIME MOUNT POLICY (host-side meta-controller, ZERO-CORE). U2 / master-graph study §2.x.
|
|
10
|
+
*
|
|
11
|
+
* A learned method can be MOUNTED three ways; the controller picks one by (depth, efficiency, reliability),
|
|
12
|
+
* with HYSTERESIS (banded promote/demote so it doesn't flap) and a well-founded MOUNT-RANK whose bottom is
|
|
13
|
+
* the K1 "stay-in-LLM" floor (termination of the adapt loop). The regimes:
|
|
14
|
+
*
|
|
15
|
+
* - INSTANCE (fork-per-case) — the SAFE DEFAULT. Each case in its own world (single-world JTMS forces
|
|
16
|
+
* this for unproven / write-frontier methods). Always sound.
|
|
17
|
+
* - INLINE (addConcept) — mount into the live graph. Sound ONLY for a READ-ONLY frontier (confluence
|
|
18
|
+
* is undecidable; an inline write-frontier method can race consumers).
|
|
19
|
+
* Restricted to shallow, proven, read-only-frontier methods.
|
|
20
|
+
* - FROZEN (warm-cache replay) — a proven, "dry" method replayed from the warm cache + a deopt-guard
|
|
21
|
+
* (NOT a stabilization-skipping executor — §2 Correction 2). The cheapest.
|
|
22
|
+
* - ESCALATE (stay in the LLM) — the floor: a method that has DEOPTed K times is demoted off the library
|
|
23
|
+
* and always re-forged/escalated. This is the well-founded bottom (K1).
|
|
24
|
+
*
|
|
25
|
+
* MOUNT-RANK μ = (deoptBudget, …): each deopt strictly decrements deoptBudget; at 0 the method is pinned to
|
|
26
|
+
* ESCALATE. So the collapse→re-forge→re-mount loop cannot cycle forever — it descends to the LLM floor in
|
|
27
|
+
* ≤K deopts (HotSpot/PyPy deopt-pinning, Hölzle-Chambers-Ungar 1992; Bolz et al. 2009). A success replenishes
|
|
28
|
+
* nothing automatically (demotion is sticky until an explicit re-admit), so μ is monotone non-increasing.
|
|
29
|
+
*
|
|
30
|
+
* const ctl = createMountController();
|
|
31
|
+
* ctl.decide('M_AB', { reliability: 0.9, hitRate: 0.95, depth: 1, readOnlyFrontier: true }); // -> 'frozen'
|
|
32
|
+
* ctl.recordDeopt('M_AB'); // a guaranteeing value drifted → partial-collapse + re-forge; budget--
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const DEFAULTS = {
|
|
36
|
+
maxDeopt: 3, // K: deopts before a method is pinned to ESCALATE (the floor)
|
|
37
|
+
freezeHi: 0.85, freezeLo: 0.70, // hysteresis band for FROZEN (promote at Hi, demote below Lo)
|
|
38
|
+
inlineHi: 0.65, inlineLo: 0.50, // hysteresis band for INLINE
|
|
39
|
+
inlineMaxDepth: 1, // INLINE only for shallow methods (deep → fork, to bound context at the join)
|
|
40
|
+
widenCap: 5 // §6.4: the megamorphic WIDTH cap — distinct widens before a method degrades to ESCALATE
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Pure classifier: given signals + the CURRENT regime (for hysteresis), pick the regime. No state.
|
|
44
|
+
function classify( signals, current, t ) {
|
|
45
|
+
t = Object.assign({}, DEFAULTS, t || {});
|
|
46
|
+
const s = signals || {};
|
|
47
|
+
const score = Math.min(s.reliability == null ? 0 : s.reliability, s.hitRate == null ? (s.reliability || 0) : s.hitRate);
|
|
48
|
+
const readOnly = !!s.readOnlyFrontier;
|
|
49
|
+
const depth = s.depth == null ? 99 : s.depth;
|
|
50
|
+
|
|
51
|
+
if ( (s.deoptCount || 0) >= t.maxDeopt ) return 'escalate'; // the K1 floor (absorbing)
|
|
52
|
+
|
|
53
|
+
// FROZEN — proven + dry + read-only frontier. Hysteresis: promote at freezeHi; once frozen, hold until <freezeLo.
|
|
54
|
+
const freezeThresh = (current === 'frozen') ? t.freezeLo : t.freezeHi;
|
|
55
|
+
if ( readOnly && score >= freezeThresh ) return 'frozen';
|
|
56
|
+
|
|
57
|
+
// INLINE — shallow + read-only frontier + moderately proven (confluence: read-only only).
|
|
58
|
+
const inlineThresh = (current === 'inline') ? t.inlineLo : t.inlineHi;
|
|
59
|
+
if ( readOnly && depth <= t.inlineMaxDepth && score >= inlineThresh ) return 'inline';
|
|
60
|
+
|
|
61
|
+
// INSTANCE — the safe default for everything else (unproven, deep, or write-frontier).
|
|
62
|
+
return 'instance';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function createMountController( opts ) {
|
|
66
|
+
opts = opts || {};
|
|
67
|
+
const t = Object.assign({}, DEFAULTS, opts.thresholds || {});
|
|
68
|
+
const state = new Map(); // methodId -> { regime, deoptCount }
|
|
69
|
+
|
|
70
|
+
function get( id ) { if ( !state.has(id) ) state.set(id, { regime: 'instance', deoptCount: 0, widenCount: 0 }); return state.get(id); }
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
thresholds: t,
|
|
74
|
+
/** decide (and remember) the regime for a method given live signals; applies hysteresis + the deopt floor. */
|
|
75
|
+
decide( id, signals ) {
|
|
76
|
+
const st = get(id);
|
|
77
|
+
const sig = Object.assign({ deoptCount: st.deoptCount }, signals || {});
|
|
78
|
+
const regime = classify(sig, st.regime, t);
|
|
79
|
+
st.regime = regime;
|
|
80
|
+
return { regime, deoptCount: st.deoptCount, reason: reasonFor(regime, sig, t) };
|
|
81
|
+
},
|
|
82
|
+
/** a guaranteeing value drifted → the method DEOPTs (partial-collapse + re-forge). Decrements μ. */
|
|
83
|
+
recordDeopt( id ) {
|
|
84
|
+
const st = get(id);
|
|
85
|
+
st.deoptCount++;
|
|
86
|
+
if ( st.deoptCount >= t.maxDeopt ) st.regime = 'escalate'; // pinned to the floor
|
|
87
|
+
return st.deoptCount;
|
|
88
|
+
},
|
|
89
|
+
/**
|
|
90
|
+
* §6.4 — a method's pre was WIDENED (the S-boundary climbed). This is a SUCCESS, NOT a deopt:
|
|
91
|
+
* • it DEMOTES FROZEN→INSTANCE (G2 — the newly-admitted cases must re-hit `assertPost`; the caller MUST call
|
|
92
|
+
* this BEFORE issuing the gate-relax `patchConcept` so the relaxed gate never admits a case under a frozen regime);
|
|
93
|
+
* • it consumes its OWN megamorphic WIDTH budget (PIC degradation, Hölzle-Chambers-Ungar 1991/92), NEVER μ —
|
|
94
|
+
* a healthy generalizer must not be pinned to the deopt floor with zero failures. At `widenCap` distinct
|
|
95
|
+
* widens the method is too polymorphic to be a useful specialization → ESCALATE (degrade to generic).
|
|
96
|
+
*/
|
|
97
|
+
recordWiden( id ) {
|
|
98
|
+
const st = get(id);
|
|
99
|
+
const demotedFrom = st.regime;
|
|
100
|
+
if ( st.regime === 'frozen' ) st.regime = 'instance'; // G2: re-guard with assertPost (never elide on widened cases)
|
|
101
|
+
st.widenCount++;
|
|
102
|
+
if ( st.widenCount >= t.widenCap ) st.regime = 'escalate'; // megamorphic width cap (NOT μ)
|
|
103
|
+
return { regime: st.regime, widenCount: st.widenCount, demotedFrom };
|
|
104
|
+
},
|
|
105
|
+
regimeOf( id ) { return get(id).regime; },
|
|
106
|
+
deoptBudget( id ) { return Math.max(0, t.maxDeopt - get(id).deoptCount); }, // the well-founded rank μ (deopts only)
|
|
107
|
+
widenBudget( id ) { return Math.max(0, t.widenCap - get(id).widenCount); }, // §6.4 — the megamorphic width budget
|
|
108
|
+
state
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function reasonFor( regime, s, t ) {
|
|
113
|
+
if ( regime === 'escalate' ) return `deopt floor (${s.deoptCount}/${t.maxDeopt}) — stay in the LLM`;
|
|
114
|
+
if ( regime === 'frozen' ) return 'proven + dry + read-only frontier → warm-cache replay';
|
|
115
|
+
if ( regime === 'inline' ) return 'shallow + read-only frontier + proven → addConcept';
|
|
116
|
+
return 'unproven / deep / write-frontier → fork-per-case (safe default)';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { createMountController, classify, DEFAULTS };
|