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,86 @@
|
|
|
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
|
+
* cost-probe — the CONSTITUENT-COST utility gate for `compress.js` (the half `compose-hotspot.js` structurally CANNOT
|
|
10
|
+
* measure). G-0 answers *does a composite RECUR?*; this answers the REAL build-gate: *does a composite-memo elide FORGE
|
|
11
|
+
* calls that a per-primitive LEAF-memo cannot?* — measured in ELIDED FORGE CALLS net of the leaf floor, NEVER in MDL/bits
|
|
12
|
+
* (a bit-compressing composite over cheap or leaf-memoizable constituents elides ZERO model calls; MDL is the
|
|
13
|
+
* DreamCoder/Stitch *search-cost* objective, not the call-elision utility).
|
|
14
|
+
*
|
|
15
|
+
* WHY this is the decision axis, not structure (confront 2026-07-01 — Laurie + a library-learning SOTA, both converged):
|
|
16
|
+
* `savedCalls = distinctTasks − 1` (compose-hotspot) is an UPPER BOUND that ASSUMES a per-task forge; it is blind to
|
|
17
|
+
* whether the leaf memo already elides the constituents at ~0 marginal calls. A composite PAYS iff, per constituent, it is
|
|
18
|
+
* (a) FORGE-COSTLY (a real model call), and
|
|
19
|
+
* (b) NOT individually leaf-memoizable — its STANDALONE typed key varies per instance so the leaf memo MISSES — while
|
|
20
|
+
* (c) the composite-as-a-unit is K1-STABLE: its typed ENVELOPE key IS a function of the composite input.
|
|
21
|
+
* That is the **canonicalization-in-context band**: an expensive, standalone-UNSTABLE interior under a stable typed
|
|
22
|
+
* ENVELOPE. It is structurally THIN because in a data-flow DAG **linearity ↔ cheapness are anti-correlated**: an in=out=1
|
|
23
|
+
* (linear, poly-gate-visible) stage tends to be a cheap deterministic transform (leaf-memo already floors it), whereas an
|
|
24
|
+
* expensive reasoning stage emits a multiply-consumed rich artifact → fan-out → FORK → outside the poly gate (SUBDUE-beam
|
|
25
|
+
* scope). So the composites worth compressing are the ones G-a misses, and the ones G-a sees are not worth compressing —
|
|
26
|
+
* the double bind that keeps compress.js FILED until a real payer is EXHIBITED on this axis.
|
|
27
|
+
*
|
|
28
|
+
* The engine already embodies this band at the SINGLE-method level: `Intake::type` is canonicalization-in-context — free
|
|
29
|
+
* prose (standalone-unstable) → a stable typed digest (envelope), the borderline gate + synonym ring making the envelope
|
|
30
|
+
* stable across paraphrases. compress.js would be the MULTI-step generalization; this probe is its kill-gate.
|
|
31
|
+
*
|
|
32
|
+
* MODEL (charitable to compress.js — it may use the leaf memo INSIDE the composite):
|
|
33
|
+
* leaf floor : a constituent forges iff its leafKey is NEW (else the leaf memo replays it).
|
|
34
|
+
* + composite : on a composite HIT (envelopeKey seen) the whole composite is replayed → 0 forge; on a MISS, forge only
|
|
35
|
+
* the still-unmemoized leaves. So the composite memo elides a forge iff (leafKey NEW ∧ envelopeKey OLD).
|
|
36
|
+
* net = Σ forgeCost·[leaf NEW ∧ envelope OLD] − bloat. GO iff net > bloat-adjusted 0.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param occurrences stream-ordered [{ envelopeKey, constituents:[{ concept, forgeCost?=1, leafKey }] }]
|
|
41
|
+
* — one entry per whole-task instance of the candidate composite. `envelopeKey` = the composite's typed
|
|
42
|
+
* input digest (the K1 envelope); `leafKey` = a constituent's STANDALONE memo key; `forgeCost` = model
|
|
43
|
+
* calls a cold constituent costs (0 = a cheap deterministic autoflag).
|
|
44
|
+
* @param opts.bloat library overhead charged against the saving (default 0).
|
|
45
|
+
* @returns { net, pays, leafFloorForges, compositeForges, bloat, n, perConstituent:[{ concept, forgeCost, n, leafMisses,
|
|
46
|
+
* leafMissRate, band }] } — band ∈ 'payer-interior' (forge-costly ∧ leaf-unstable) | 'leaf-floored'
|
|
47
|
+
* (forge-costly ∧ leaf-stable) | 'cheap' (forgeCost 0).
|
|
48
|
+
*/
|
|
49
|
+
function costProbe( occurrences, opts ) {
|
|
50
|
+
opts = opts || {};
|
|
51
|
+
var bloat = opts.bloat || 0;
|
|
52
|
+
var seenLeaf = Object.create(null), seenEnv = Object.create(null);
|
|
53
|
+
var leafFloorForges = 0, compositeForges = 0, saved = 0;
|
|
54
|
+
var per = Object.create(null);
|
|
55
|
+
|
|
56
|
+
for ( var o = 0; o < (occurrences || []).length; o++ ) {
|
|
57
|
+
var occ = occurrences[o];
|
|
58
|
+
var envNew = !seenEnv[occ.envelopeKey];
|
|
59
|
+
if ( envNew ) seenEnv[occ.envelopeKey] = true;
|
|
60
|
+
var cons = occ.constituents || [];
|
|
61
|
+
for ( var i = 0; i < cons.length; i++ ) {
|
|
62
|
+
var k = cons[i], cost = k.forgeCost == null ? 1 : k.forgeCost;
|
|
63
|
+
var leafNew = !seenLeaf[k.leafKey];
|
|
64
|
+
if ( leafNew ) seenLeaf[k.leafKey] = true;
|
|
65
|
+
if ( leafNew ) leafFloorForges += cost; // the leaf memo must forge a new leaf
|
|
66
|
+
if ( leafNew && envNew ) compositeForges += cost; // the composite memo forges only on a new envelope
|
|
67
|
+
if ( leafNew && !envNew ) saved += cost; // the payer event: leaf missed BUT the composite hit
|
|
68
|
+
var p = per[k.concept] || (per[k.concept] = { concept: k.concept, forgeCost: cost, n: 0, leafMisses: 0 });
|
|
69
|
+
p.n++; if ( leafNew ) p.leafMisses++;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
var perConstituent = Object.keys(per).map(function ( c ) {
|
|
73
|
+
var p = per[c], rate = p.n ? p.leafMisses / p.n : 0;
|
|
74
|
+
p.leafMissRate = rate;
|
|
75
|
+
p.band = p.forgeCost === 0 ? 'cheap' : (rate > 0.5 ? 'payer-interior' : 'leaf-floored');
|
|
76
|
+
return p;
|
|
77
|
+
});
|
|
78
|
+
var net = saved - bloat;
|
|
79
|
+
return { net: net, pays: net > 0, saved: saved, leafFloorForges: leafFloorForges, compositeForges: compositeForges,
|
|
80
|
+
bloat: bloat, n: (occurrences || []).length, perConstituent: perConstituent };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// the go/no-go boolean for BUILDING compress.js on a candidate: does a composite-memo strictly out-elide the leaf floor?
|
|
84
|
+
function paysToCompress( occurrences, opts ) { return costProbe(occurrences, opts).pays; }
|
|
85
|
+
|
|
86
|
+
module.exports = { costProbe: costProbe, paysToCompress: paysToCompress };
|
|
@@ -0,0 +1,555 @@
|
|
|
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
|
+
* crystallize — the live crystallization loop (host-side, ZERO-CORE; study
|
|
21
|
+
* doc/WIP/studies/2026-06-26-…, pass 3). Composes the pass-1/2 bricks end-to-end:
|
|
22
|
+
*
|
|
23
|
+
* run an episode with a trace miner → mine the dominant producer→consumer chain
|
|
24
|
+
* → compose the constituents' providers into ONE cast → gate by MDL/utility
|
|
25
|
+
* (fixpoint-equivalent + net-cheaper) → (adopt) install under a memo-stability guard.
|
|
26
|
+
*
|
|
27
|
+
* This is "observe → crystallize a typed production" — promoting a recurring sub-derivation
|
|
28
|
+
* (a nonterminal in the derivation forest) into a first-class, auditable, defeasible concept.
|
|
29
|
+
* `crystallize` makes the DECISION (offline, on fresh boots — never mid-stabilize); `adopt`
|
|
30
|
+
* installs an admitted candidate into a target grammar, fail-closed on memo regression.
|
|
31
|
+
*
|
|
32
|
+
* const r = await crystallize({ episodeTree, seed, providers, equivKeys });
|
|
33
|
+
* if (r.admitted) await adopt(nextGraph, r.candidate); // adopt into the next episode's grammar
|
|
34
|
+
*/
|
|
35
|
+
const Graph = require('../graph/index.js');
|
|
36
|
+
const { nextStable } = require('./supervise.js');
|
|
37
|
+
const { traceMiner, composeProviders, methodTrace, mineMethods } = require('./mine.js');
|
|
38
|
+
const { rankCandidates } = require('./mdl.js');
|
|
39
|
+
const { evaluate, bootMeasure, factsEqual } = require('./abstraction.js');
|
|
40
|
+
const { assertMemoStable } = require('./memo-stability.js');
|
|
41
|
+
const { createLifecycle } = require('./lifecycle.js');
|
|
42
|
+
const { normalize } = require('./contract.js');
|
|
43
|
+
const { instantiate, ctxFromScope, canon, emitMethodAsSubgraph } = require('./abstract.js');
|
|
44
|
+
const { normalizeFrontierParams } = require('./mine.js');
|
|
45
|
+
const { lintMethod } = require('./method.js');
|
|
46
|
+
const { memoSurfaceKeys } = require('./memo-stability.js');
|
|
47
|
+
const { digest } = require('../providers/canonicalize.js');
|
|
48
|
+
|
|
49
|
+
const clone = (x) => JSON.parse(JSON.stringify(x));
|
|
50
|
+
|
|
51
|
+
// find a concept schema by name/key anywhere in a tree
|
|
52
|
+
function conceptByName( tree, name ) {
|
|
53
|
+
let found = null;
|
|
54
|
+
(function walk( n ) {
|
|
55
|
+
if ( !n || typeof n !== 'object' || found ) return;
|
|
56
|
+
const kids = n.childConcepts;
|
|
57
|
+
if ( kids ) for ( const k of Object.keys(kids) ) {
|
|
58
|
+
if ( kids[k]._name === name || k === name ) { found = kids[k]; return; }
|
|
59
|
+
walk(kids[k]);
|
|
60
|
+
}
|
|
61
|
+
})(tree);
|
|
62
|
+
return found;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// resolve a concept's provider function from a providers map ("Ns::fn" | ["Ns::fn", …])
|
|
66
|
+
function providerFn( providers, provRef ) {
|
|
67
|
+
const p = Array.isArray(provRef) ? provRef[0] : provRef;
|
|
68
|
+
if ( !p ) return null;
|
|
69
|
+
const parts = String(p).split('::');
|
|
70
|
+
return providers[parts[0]] && providers[parts[0]][parts[1]];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Decide whether the dominant mined chain should crystallize into one production.
|
|
75
|
+
* @param opts.episodeTree/seed/providers/equivKeys the episode + the equivalence contract
|
|
76
|
+
* @param opts.minCount minimum co-firing count to consider a chain (default 2)
|
|
77
|
+
* @param opts.idFor (chain)->id name the crystallized concept (default Crystal_<from>_<to>)
|
|
78
|
+
* @returns { chain, candidate:{schema,providerName,provider}|null, verdict?, admitted, reason? }
|
|
79
|
+
*/
|
|
80
|
+
async function crystallize( opts ) {
|
|
81
|
+
const { episodeTree, seed, providers, equivKeys } = opts;
|
|
82
|
+
const minCount = opts.minCount || 2;
|
|
83
|
+
|
|
84
|
+
// 1. run the episode with a trace miner (offline; fresh boot)
|
|
85
|
+
Graph._providers = Object.assign({}, Graph._providers || {}, providers || {});
|
|
86
|
+
const miner = traceMiner();
|
|
87
|
+
const g = new Graph(clone(seed), {
|
|
88
|
+
label: 'crystallize', isMaster: true, autoMount: true, conceptSets: ['common'],
|
|
89
|
+
bagRefManagers: {}, logLevel: 'error', onConceptApply: miner.onConceptApply,
|
|
90
|
+
}, { common: clone(episodeTree) });
|
|
91
|
+
await nextStable(g);
|
|
92
|
+
|
|
93
|
+
const chains = miner.chains(episodeTree);
|
|
94
|
+
// MDL rank (cheap, O(corpus), NO boot): order candidates by bits saved across the whole
|
|
95
|
+
// trace, best first. A pure RANKER by default — `evaluate` stays the admit AUTHORITY (MDL
|
|
96
|
+
// is conservative at tiny N and can't verify equivalence/model-cost). Opt-in `mdlPrefilter`
|
|
97
|
+
// lets MDL cheaply skip a clearly-unprofitable (ΔL≥0) candidate before the expensive boot.
|
|
98
|
+
const ranked = rankCandidates(chains, { tree: episodeTree, records: miner.records, alphabet: opts.alphabet || { knownFacts: [], palette: [] } });
|
|
99
|
+
const top = ranked[0];
|
|
100
|
+
if ( !top || top.count < minCount )
|
|
101
|
+
return { chain: top ? plainChain(top) : null, candidate: null, admitted: false, reason: 'no frequent chain' };
|
|
102
|
+
const mdl = top.mdl;
|
|
103
|
+
if ( opts.mdlPrefilter && mdl && !mdl.admit )
|
|
104
|
+
return { chain: plainChain(top), candidate: null, admitted: false, reason: 'mdl-prefilter (ΔL≥0, not worth a boot)', mdl };
|
|
105
|
+
|
|
106
|
+
// 2. compose the from/to providers into one cast
|
|
107
|
+
const fromS = conceptByName(episodeTree, top.from), toS = conceptByName(episodeTree, top.to);
|
|
108
|
+
const fromFn = providerFn(providers, fromS && fromS.provider), toFn = providerFn(providers, toS && toS.provider);
|
|
109
|
+
if ( !fromFn || !toFn )
|
|
110
|
+
return { chain: plainChain(top), candidate: null, admitted: false, reason: 'non-provider constituent (v0 composes provider concepts)', mdl };
|
|
111
|
+
|
|
112
|
+
const id = opts.idFor ? opts.idFor(top) : ('Crystal_' + top.from + '_' + top.to);
|
|
113
|
+
const composed = composeProviders(fromFn, toFn);
|
|
114
|
+
const providerName = 'Crystal::' + id;
|
|
115
|
+
const augmented = Object.assign({}, providers, { Crystal: Object.assign({}, providers.Crystal, { [id]: composed }) });
|
|
116
|
+
const schema = { _id: id, _name: id, require: (fromS.require || []).slice(), provider: [providerName] };
|
|
117
|
+
|
|
118
|
+
// 3. MDL/utility gate (fixpoint-equivalent + net-cheaper vs the chain)
|
|
119
|
+
const verdict = await evaluate({ seed, providers: augmented, chainTree: episodeTree, abstractTree: { childConcepts: { [id]: schema } }, equivKeys });
|
|
120
|
+
return { chain: plainChain(top), candidate: { schema, providerName, provider: composed }, verdict, admitted: verdict.admit, mdl };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// the plain mined-chain shape (drop the attached `mdl` annotation so `res.chain` stays the
|
|
124
|
+
// stable { from, to, via, count } record).
|
|
125
|
+
function plainChain( c ) {
|
|
126
|
+
const out = { from: c.from, to: c.to, via: c.via, count: c.count };
|
|
127
|
+
if ( c.length != null ) out.length = c.length;
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Install an admitted candidate into a target graph, fail-closed on memo regression
|
|
133
|
+
* (assertMemoStable over the existing incumbents). Registers the composed provider first.
|
|
134
|
+
* @returns the memoDiff (stable) — throws on a memo-stability violation.
|
|
135
|
+
*/
|
|
136
|
+
async function adopt( graph, candidate ) {
|
|
137
|
+
const parts = candidate.providerName.split('::');
|
|
138
|
+
Graph._providers = Object.assign({}, Graph._providers, {
|
|
139
|
+
[parts[0]]: Object.assign({}, Graph._providers[parts[0]], { [parts[1]]: candidate.provider }),
|
|
140
|
+
});
|
|
141
|
+
const incumbents = Object.keys(graph._conceptLib || {});
|
|
142
|
+
return assertMemoStable(graph, incumbents, () => new Promise((res) => graph.addConcept(null, candidate.schema, () => res())));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Offline adoption: rewrite the episode grammar to USE the crystal — drop the chain's
|
|
146
|
+
// constituents and add the crystallized production. This materializes the refactor win (one
|
|
147
|
+
// cast instead of the chain) WITHOUT needing a core deleteConcept: the next episode is simply
|
|
148
|
+
// authored with the new grammar (the form the adversarial lens endorsed).
|
|
149
|
+
function rewriteAdopt( episodeTree, chain, candidateSchema ) {
|
|
150
|
+
const t = clone(episodeTree);
|
|
151
|
+
const kids = t.childConcepts || (t.childConcepts = {});
|
|
152
|
+
for ( const k of Object.keys(kids) )
|
|
153
|
+
if ( k === chain.from || k === chain.to || kids[k]._name === chain.from || kids[k]._name === chain.to ) delete kids[k];
|
|
154
|
+
kids[candidateSchema._id] = candidateSchema;
|
|
155
|
+
return t;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Multi-episode consolidation (CLS): crystallize a candidate, ADOPT it by rewriting the
|
|
160
|
+
* grammar (chain → crystal), then over `rounds` episodes verify it reproduces the baseline
|
|
161
|
+
* and feed the outcome to the plasticity ledger — a proven crystal anneals to FROZEN.
|
|
162
|
+
* @returns { candidate, chain, verdict, adoptedTree, lifecycle, plasticity, regime, reputation,
|
|
163
|
+
* applies:{chain,adopted}, consolidated } (or the crystallize decision if not admitted)
|
|
164
|
+
*/
|
|
165
|
+
async function consolidate( opts ) {
|
|
166
|
+
const rounds = opts.rounds || 3;
|
|
167
|
+
const lc = opts.lifecycle || createLifecycle(opts.lifecycleOpts);
|
|
168
|
+
const dec = await crystallize(opts);
|
|
169
|
+
if ( !dec.admitted ) return Object.assign({}, dec, { lifecycle: lc, consolidated: false });
|
|
170
|
+
|
|
171
|
+
const id = dec.candidate.schema._id;
|
|
172
|
+
lc.register(id);
|
|
173
|
+
const augmented = Object.assign({}, opts.providers, { Crystal: Object.assign({}, opts.providers && opts.providers.Crystal, { [id]: dec.candidate.provider }) });
|
|
174
|
+
const baseline = (await bootMeasure(opts.episodeTree, opts.seed, augmented, { equivKeys: opts.equivKeys })).facts;
|
|
175
|
+
const adoptedTree = rewriteAdopt(opts.episodeTree, dec.chain, dec.candidate.schema);
|
|
176
|
+
|
|
177
|
+
let adoptedApplies = 0;
|
|
178
|
+
for ( let r = 0; r < rounds; r++ ) {
|
|
179
|
+
const run = await bootMeasure(adoptedTree, opts.seed, augmented, { equivKeys: opts.equivKeys });
|
|
180
|
+
adoptedApplies = run.applies;
|
|
181
|
+
lc.record(id, factsEqual(baseline, run.facts)); // genuine outcome (equivalent to baseline?)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
candidate: dec.candidate, chain: dec.chain, verdict: dec.verdict, adoptedTree,
|
|
186
|
+
lifecycle: lc, plasticity: lc.plasticity(id), regime: lc.regime(id), reputation: lc.reputation(id),
|
|
187
|
+
applies: { chain: dec.verdict.chainApplies, adopted: adoptedApplies },
|
|
188
|
+
consolidated: lc.regime(id) === 'frozen',
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ─────────────────────────────── Gap C — born-defeasible contract synthesis ───────────────────────────────────
|
|
193
|
+
//
|
|
194
|
+
// The paper novelty: a crystallized method is BORN with a typed DEFEASIBLE contract `{ read, write, pre, post,
|
|
195
|
+
// effect }`, so drift retracts it (assertPost/reviseOnBlame) and composition is checkable box-CLOSED (checkCompose).
|
|
196
|
+
// The post is the OBSERVED INVARIANT over the method's output (`equivKeys`) expressed in contract.js's abstract-
|
|
197
|
+
// domain FRAGMENT (presence/enum/interval) — never a raw structured value (that would graveyard or be unverifiable).
|
|
198
|
+
// If no fragment-expressible post exists → return null = REFUSE the contract path (the K1 ceiling, again).
|
|
199
|
+
|
|
200
|
+
const SYS = new Set(['$_id', '$$_id', '_id', '_name', '_rev', '_origin']);
|
|
201
|
+
|
|
202
|
+
// the facts the cast sets on its PARENT (the object whose id is the base / `_parent`) — the method's downstream-
|
|
203
|
+
// visible output, stripped of structural/bookkeeping keys.
|
|
204
|
+
function parentFacts( ground, base ) {
|
|
205
|
+
for ( const o of (Array.isArray(ground) ? ground : [ground]) ) {
|
|
206
|
+
if ( !o || typeof o !== 'object' ) continue;
|
|
207
|
+
const idv = o.$_id === '_parent' ? base : String(o.$$_id || o.$_id || o._id || '').replace(/^\$+/, '');
|
|
208
|
+
if ( o.$_id === '_parent' || idv === base ) {
|
|
209
|
+
const f = {};
|
|
210
|
+
for ( const k of Object.keys(o) ) if ( !SYS.has(k) ) f[k.replace(/^\$+/, '')] = o[k];
|
|
211
|
+
return f;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return {};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// the union of the facts the cast sets on its parent, across instances — the WRITTEN set (an output, never part of
|
|
218
|
+
// the pre-cast signature). Derived from the patches (a provider concept's writes aren't statically known).
|
|
219
|
+
function parentFactKeys( instances ) {
|
|
220
|
+
const keys = new Set();
|
|
221
|
+
for ( const i of (instances || []) ) for ( const k of Object.keys(parentFacts(i.ground, i.target)) ) keys.add(k);
|
|
222
|
+
return keys;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const isScalar = ( v ) => v == null || typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean';
|
|
226
|
+
function litAtom( key, v ) { return typeof v === 'string' ? key + "=='" + v + "'" : key + '==' + v; }
|
|
227
|
+
|
|
228
|
+
// the fragment-expressible invariant over ONE output key across the observed instances, or null (refuse).
|
|
229
|
+
function invariantAtom( key, vals ) {
|
|
230
|
+
const uniq = [...new Set(vals.map(( v ) => JSON.stringify(v)))].map(( s ) => JSON.parse(s));
|
|
231
|
+
if ( !uniq.every(isScalar) ) return null; // a structured (object/array) output → not in-fragment
|
|
232
|
+
if ( uniq.length === 1 ) return litAtom(key, uniq[0]); // constant → presence/enum invariant
|
|
233
|
+
if ( uniq.every(( v ) => typeof v === 'number') ) { // numeric → interval band
|
|
234
|
+
return '$' + key + '>=' + Math.min(...uniq) + ' && $' + key + '<=' + Math.max(...uniq);
|
|
235
|
+
}
|
|
236
|
+
if ( uniq.every(( v ) => typeof v === 'string') ) return key + ' in [' + uniq.join(', ') + ']'; // enum band
|
|
237
|
+
return null; // mixed / bool-varying → not a clean invariant
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Synthesize a method's defeasible contract from the mined instances.
|
|
242
|
+
* @param opts.concept the crystallized concept name (its self-flag is part of `write`)
|
|
243
|
+
* @param opts.read the read-set (the method's require / signature keys)
|
|
244
|
+
* @param opts.instances the mined firings (each `{ ground, target }`) — output observed on the parent
|
|
245
|
+
* @param opts.equivKeys the output facts that define the method's effect (the post is induced over these)
|
|
246
|
+
* @param opts.effect 'pure' (default) | 'external' | 'irreversible'
|
|
247
|
+
* @returns { read, write, pre, post, effect } or null if no fragment-expressible post (refuse the contract path)
|
|
248
|
+
*/
|
|
249
|
+
function synthesizeContract( opts ) {
|
|
250
|
+
opts = opts || {};
|
|
251
|
+
const read = [...new Set(opts.read || [])];
|
|
252
|
+
const writeSet = new Set([opts.concept].filter(Boolean));
|
|
253
|
+
for ( const i of (opts.instances || []) ) for ( const k of Object.keys(parentFacts(i.ground, i.target)) ) writeSet.add(k);
|
|
254
|
+
|
|
255
|
+
const post = [];
|
|
256
|
+
for ( const k of (opts.equivKeys || []) ) {
|
|
257
|
+
const vals = (opts.instances || []).map(( i ) => parentFacts(i.ground, i.target)[k]).filter(( v ) => v !== undefined);
|
|
258
|
+
if ( !vals.length ) continue;
|
|
259
|
+
const atom = invariantAtom(k, vals);
|
|
260
|
+
if ( atom == null ) return null; // an output not expressible in the fragment → refuse
|
|
261
|
+
post.push(atom);
|
|
262
|
+
}
|
|
263
|
+
if ( !post.length ) return null; // nothing verifiable to assert → refuse the contract path
|
|
264
|
+
if ( normalize(post).refuse.length ) return null; // belt-and-braces: the post must parse clean in-fragment
|
|
265
|
+
return { read, write: [...writeSet], pre: [], post, effect: opts.effect || 'pure' };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ─────────────────────────────── Gap A — STRUCTURAL crystallization (re-mountable method) ─────────────────────
|
|
269
|
+
//
|
|
270
|
+
// The provider-fusion `crystallize` inlines a 2-step PROVIDER chain into one cast. `crystallizeStructural` does the
|
|
271
|
+
// harder thing the F6 slice (abstract.js) enables: turn a RECURRENT structural cast (one that creates a sub-graph)
|
|
272
|
+
// into a re-mountable parameterized METHOD — a concept whose provider, at a NEW call site, binds the learned
|
|
273
|
+
// sub-graph template (structural holes from the site + content from the learned per-signature table) and applies it
|
|
274
|
+
// at ZERO model calls. Novel-but-related problems sharing a typed signature reuse the structural decision (#30/F6,
|
|
275
|
+
// now crystallized into a first-class defeasible concept), and an unseen signature BYPASSES (no false replay).
|
|
276
|
+
//
|
|
277
|
+
// v0 ASSUMPTIONS (honest scope): call-site endpoints are LITERAL ids in the patch (the canonical aetheris/epikeo
|
|
278
|
+
// structural-mount style abstract.js generalizes) — a provider wiring endpoints via `$`-refs would yield an empty
|
|
279
|
+
// inferred frontier and not rebind. The premise is captured POST-apply, so a method that ECHOES one of its require
|
|
280
|
+
// keys is REFUSED (the soundness guard below). Content must be a function of the TYPED signature (the K1 ceiling);
|
|
281
|
+
// an undeclared corpus-correlated input the provider reads can still mis-key — the inherent canon limit `cache.js`
|
|
282
|
+
// shares (a modeling-discipline violation, not crystallizer-detectable).
|
|
283
|
+
|
|
284
|
+
const projectFacts = ( facts, keys ) => { const o = {}; for ( const k of (keys || []) ) if ( facts && k in facts ) o[k] = facts[k]; return o; };
|
|
285
|
+
|
|
286
|
+
// add the crystal's OWN cast marker to the parent object of an instantiated template (the provider-cast-marker
|
|
287
|
+
// discipline: a wired provider must self-flag the cast, else the engine re-fires it to the apply-cap → divergent).
|
|
288
|
+
function injectCastMarker( ground, base, cryId ) {
|
|
289
|
+
return (Array.isArray(ground) ? ground : [ground]).map(( o ) => {
|
|
290
|
+
if ( !o || typeof o !== 'object' ) return o;
|
|
291
|
+
const idv = String(o.$$_id != null ? o.$$_id : (o.$_id != null ? o.$_id : o._id)).replace(/^\$+/, '');
|
|
292
|
+
return (o.$_id === '_parent' || idv === base) ? Object.assign({}, o, { [cryId]: true }) : o;
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* The crystallized structural provider. Keys on the typed K1 signature (the pre-cast memo-surface keys minus the
|
|
298
|
+
* ones the cast writes, so the replay digest matches the mining digest); on a hit it RE-MOUNTS the learned sub-graph
|
|
299
|
+
* bound to the new call site (0 model calls), on a miss/unknown-site it casts a NO-OP marker (never re-fires).
|
|
300
|
+
*/
|
|
301
|
+
function buildStructuralProvider( spec ) {
|
|
302
|
+
// T4 — the ctxFromScope frontier map { name: field } is DERIVED from the reified FrontierSignature's role:'endpoint'
|
|
303
|
+
// params (behavioral params are indexed for selection, never bound to a call-site node here). A `field` of "a:b"
|
|
304
|
+
// form is a cross-object ref path — ctxFromScope already resolves it (the actual `$`-ref runtime rebind). Falls back
|
|
305
|
+
// to a directly-supplied `spec.frontierFields` (the inferred path / a test passing the map verbatim).
|
|
306
|
+
const frontierFields = spec.frontierFields || ((spec.frontier && spec.frontier.params) || [])
|
|
307
|
+
.filter(( p ) => (p.role || 'endpoint') === 'endpoint')
|
|
308
|
+
.reduce(( m, p ) => { if ( p.field ) m[p.name] = p.field; return m; }, {});
|
|
309
|
+
return function ( graph, concept, scope, argz, cb ) {
|
|
310
|
+
const noop = { $_id: '_parent', [spec.cryId]: true }; // a cast marker with no structural output (bypass)
|
|
311
|
+
const ctx = ctxFromScope(scope, { frontier: frontierFields });
|
|
312
|
+
if ( !ctx ) return cb(null, noop); // unknown base → bypass
|
|
313
|
+
const tpl = spec.templatesBySig[digest(projectFacts(scope._ || {}, spec.signatureKeys))];
|
|
314
|
+
if ( !tpl ) return cb(null, noop); // unseen signature class → bypass (no model in the crystal)
|
|
315
|
+
const ground = instantiate(tpl, ctx);
|
|
316
|
+
if ( !ground ) return cb(null, noop); // an unbound frontier ref → bypass
|
|
317
|
+
return cb(null, injectCastMarker(ground, ctx.base, spec.cryId)); // 0-call structural re-mount
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const asArr = ( v ) => v == null ? [] : Array.isArray(v) ? v : [v];
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Reify the FrontierSignature — "which elements are generic for which method" (study §"the precious meta-info"): the
|
|
325
|
+
* LGG result `antiUnify` computes (the structural holes = the frontier params; the content vars = the bound payload).
|
|
326
|
+
* - `params` the holes, each `{ name, sort, field, role, requiredFacts }` — the declared endpoints (+ any
|
|
327
|
+
* host-declared behavioral params); inferred episodes synthesize node-ref endpoints from the mine.
|
|
328
|
+
* - `summaryFacts` the SOUND post the Phase-2 proxy must carry across the frontier (← contract.post / equivKeys).
|
|
329
|
+
* - `appConditions` the parent NACs (require/assert) — the application conditions that orient selection.
|
|
330
|
+
* - `summary` the I6 home (declared in Phase 1, ENFORCED in Phase 2: a JTMS-justified function of the body).
|
|
331
|
+
* The two Laurie schema fields land NOW (free typed fields; retrofitting post-ship = a migration): `param.sort`
|
|
332
|
+
* (node-ref|method-ref|predicate-ref — endpoint and behavioral params are the SAME hole, different sort) and `summary`.
|
|
333
|
+
*/
|
|
334
|
+
function reifyFrontier( declaredParams, top, fromS, contract, equivKeys ) {
|
|
335
|
+
const params = declaredParams
|
|
336
|
+
? declaredParams.map(( p ) => Object.assign({}, p, { name: p.name, sort: p.sort || 'node-ref', field: p.field, role: p.role || 'endpoint', requiredFacts: p.requiredFacts || [] }))
|
|
337
|
+
: (top.frontier || []).map(( name ) => ({ name, sort: 'node-ref', field: (top.frontierFields || {})[name], role: 'endpoint', requiredFacts: [] }));
|
|
338
|
+
const summaryFacts = contract ? contract.post.slice() : asArr(equivKeys).slice();
|
|
339
|
+
return {
|
|
340
|
+
params, summaryFacts,
|
|
341
|
+
appConditions: { require: asArr(fromS && fromS.require).slice(), assert: asArr(fromS && fromS.assert).slice() },
|
|
342
|
+
summary: { facts: summaryFacts },
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* The O(1) library-INDEX key (H2): `digest(canonicalTuple(params.map(role:sort)) + signatureKeys)`. Dispatch is a
|
|
348
|
+
* dictionary lookup on this key — NEVER an HRG recognizer (membership is NP-complete). It is deliberately BODY-BLIND
|
|
349
|
+
* (it never inspects the method body — else the subgraph-iso/NP cliff returns, study Gap C), so it is a COARSE BUCKET
|
|
350
|
+
* key, NOT a unique method id: two methods with the same role:sort multiset + the same signature key-names collide on
|
|
351
|
+
* purpose (e.g. two interface-identical wirings). `sort` and signature route across buckets (I4); within a bucket,
|
|
352
|
+
* final selection refines on the full FrontierSignature (param names/fields) + application conditions — still body-blind.
|
|
353
|
+
* (Not yet consumed at runtime — stored on `schema.libraryKey` for the Phase-2 library index.)
|
|
354
|
+
*/
|
|
355
|
+
function libraryKey( frontierSignature, signatureKeys ) {
|
|
356
|
+
const tuple = (frontierSignature.params || []).map(( p ) => (p.role || 'endpoint') + ':' + (p.sort || 'node-ref')).sort();
|
|
357
|
+
return digest({ params: tuple, signature: (signatureKeys || []).slice().sort() });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// H1/H3 — a param `sort` maps to a `lintMethod` slot kind: a node-ref endpoint is an id slot; a behavioral param
|
|
361
|
+
// (method-ref / predicate-ref) is a typed SUB-GRAPH slot (it MUST carry a typed in/out interface + a fixed frontier,
|
|
362
|
+
// else it re-keys every run = memo death — the constraint IS the library-selection benefit).
|
|
363
|
+
const SORT_KIND = { 'node-ref': 'id', 'method-ref': 'subgraph', 'predicate-ref': 'subgraph' };
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Lint a reified FrontierSignature through `method.js#lintMethod` (single source of truth; validate STRUCTURE not
|
|
367
|
+
* grammar). Endpoint params are id slots (always typed); a behavioral param becomes a sub-graph slot that lintMethod
|
|
368
|
+
* rejects if its interface/frontier is untyped/absent (H3 — an untyped behavioral param can never be a clean
|
|
369
|
+
* selection key). Phase 1 ADMITS + LINTS + INDEXES behavioral params; it does NOT yet MOUNT them (that is Phase 2).
|
|
370
|
+
*/
|
|
371
|
+
function lintFrontier( frontierSignature, opts ) {
|
|
372
|
+
opts = opts || {};
|
|
373
|
+
const method = opts.name || 'crystal';
|
|
374
|
+
const extra = [], slots = {};
|
|
375
|
+
for ( const p of (frontierSignature.params || []) ) {
|
|
376
|
+
const role = p.role || 'endpoint';
|
|
377
|
+
if ( role === 'endpoint' ) { slots[p.name] = { role: 'param', kind: SORT_KIND[p.sort] || 'id' }; continue; }
|
|
378
|
+
// a behavioral param is ALWAYS a typed sub-graph slot (regardless of sort), and its sort must itself be
|
|
379
|
+
// behavioral — a node-ref behavioral param is a mis-declaration (the review's H1/H3 nit: keying typedness off
|
|
380
|
+
// `sort` alone admitted a `role:submethod, sort:node-ref` param as an id slot; bind role→kind, not sort→kind).
|
|
381
|
+
if ( p.sort !== 'method-ref' && p.sort !== 'predicate-ref' )
|
|
382
|
+
extra.push({ method, kind: 'role-sort-mismatch', slot: p.name,
|
|
383
|
+
message: `${role} param "${p.name}" must have a behavioral sort (method-ref|predicate-ref), got ${JSON.stringify(p.sort)}` });
|
|
384
|
+
slots[p.name] = { role: 'param', kind: 'subgraph', in: p.in, out: p.out, frontier: p.frontier };
|
|
385
|
+
}
|
|
386
|
+
const r = lintMethod({ name: method, slots, contract: opts.contract }, { types: opts.types });
|
|
387
|
+
return { errors: extra.concat(r.errors), warnings: r.warnings };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const hasBehavioral = ( frontierSignature ) => (frontierSignature.params || []).some(( p ) => (p.role || 'endpoint') !== 'endpoint');
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Decide + build a STRUCTURAL crystal from a live episode.
|
|
394
|
+
* @param opts.episodeTree/seed/providers the episode to observe (a structural concept firing on ≥minCount sites)
|
|
395
|
+
* @param opts.equivKeys output facts defining the method's effect (the induced defeasible post)
|
|
396
|
+
* @param opts.minCount distinct call sites required (default 2)
|
|
397
|
+
* @param opts.idFor (method)->id name the crystal (default Crystal_<concept>)
|
|
398
|
+
* @param opts.effect 'pure' (default) | 'external' | 'irreversible'
|
|
399
|
+
* @returns { method, candidate:{schema,providerName,provider,signatureKeys,frontierFields,templatesBySig}|null,
|
|
400
|
+
* contract, admitted, reason?, methods }
|
|
401
|
+
*/
|
|
402
|
+
async function crystallizeStructural( opts ) {
|
|
403
|
+
const { episodeTree, seed, providers, equivKeys } = opts;
|
|
404
|
+
const minCount = opts.minCount || 2;
|
|
405
|
+
|
|
406
|
+
Graph._providers = Object.assign({}, Graph._providers || {}, providers || {});
|
|
407
|
+
const mt = methodTrace();
|
|
408
|
+
const g = new Graph(clone(seed), {
|
|
409
|
+
label: 'crystallize-structural', isMaster: true, autoMount: true, conceptSets: ['common'],
|
|
410
|
+
bagRefManagers: {}, logLevel: 'error',
|
|
411
|
+
}, { common: clone(episodeTree) });
|
|
412
|
+
mt.listen(g);
|
|
413
|
+
await nextStable(g);
|
|
414
|
+
|
|
415
|
+
// Phase 1: a DECLARED frontier (each endpoint read off its declared field) supersedes the inferred knownIds scan —
|
|
416
|
+
// fixes the `$`-ref endpoint case, unblocks k-ary, and reifies the FrontierSignature below. Falls back to inference.
|
|
417
|
+
const declaredParams = normalizeFrontierParams(opts.declaredFrontier);
|
|
418
|
+
const methods = mt.methods(episodeTree, { minCount, knownIds: new Set(Object.keys(g._objById)),
|
|
419
|
+
declaredFrontier: declaredParams, graph: g, proseKeys: opts.proseKeys });
|
|
420
|
+
const admissibleAll = methods.filter(( m ) => m.admissible);
|
|
421
|
+
const top = admissibleAll[0];
|
|
422
|
+
if ( !top ) return { method: null, candidate: null, admitted: false, reason: 'no admissible structural method', methods };
|
|
423
|
+
const build = ( m ) => buildCandidateFromMined(m, { episodeTree, g, equivKeys, opts, declaredParams });
|
|
424
|
+
const first = build(top);
|
|
425
|
+
const res = Object.assign({ method: top, methods }, first);
|
|
426
|
+
// opts.all — promote EVERY admissible (concept × skeleton) bucket: the mining buckets by structural skeleton
|
|
427
|
+
// (mine.js shapeKey), so a heterogeneous trace yields ONE method per structure class; a library-building host
|
|
428
|
+
// indexes them all (pass an `idFor` that disambiguates, else the schema ids collide).
|
|
429
|
+
if ( opts.all ) res.candidates = admissibleAll.map(( m ) => Object.assign({ method: m }, m === top ? first : build(m)));
|
|
430
|
+
return res;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* The post-boot half of crystallizeStructural: mine + build candidates from ALREADY-CAPTURED trace records
|
|
435
|
+
* (`methodTrace().records` off a LIVE run) — no episode re-run, no model re-spend. The live harness use:
|
|
436
|
+
* capture per-task firings, then crystallize per class from the accumulated trace.
|
|
437
|
+
* @param opts.records the enriched firings [{concept,target,patch,premise}]
|
|
438
|
+
* @param opts.episodeTree the concept tree the firings came from
|
|
439
|
+
* @param opts.schemaGraph ANY graph booted with that tree (schema source for the memo surface — an empty-seed
|
|
440
|
+
* boot suffices; the task graphs themselves may be long discarded)
|
|
441
|
+
* @param opts.equivKeys/proseKeys/declaredFrontier/idFor/minCount/all as crystallizeStructural
|
|
442
|
+
* @returns same shape as crystallizeStructural ({ method, candidate, admitted, reason?, methods, candidates? })
|
|
443
|
+
*/
|
|
444
|
+
function crystallizeFromRecords( opts ) {
|
|
445
|
+
const declaredParams = normalizeFrontierParams(opts.declaredFrontier);
|
|
446
|
+
const methods = mineMethods(opts.records, opts.episodeTree, { minCount: opts.minCount || 2,
|
|
447
|
+
declaredFrontier: declaredParams, proseKeys: opts.proseKeys });
|
|
448
|
+
const admissibleAll = methods.filter(( m ) => m.admissible);
|
|
449
|
+
const top = admissibleAll[0];
|
|
450
|
+
if ( !top ) return { method: null, candidate: null, admitted: false, reason: 'no admissible structural method', methods };
|
|
451
|
+
const build = ( m ) => buildCandidateFromMined(m, { episodeTree: opts.episodeTree, g: opts.schemaGraph, equivKeys: opts.equivKeys, opts, declaredParams });
|
|
452
|
+
const first = build(top);
|
|
453
|
+
const res = Object.assign({ method: top, methods }, first);
|
|
454
|
+
if ( opts.all ) res.candidates = admissibleAll.map(( m ) => Object.assign({ method: m }, m === top ? first : build(m)));
|
|
455
|
+
return res;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// the per-bucket candidate build: soundness gates → contract → signature → templates → frontier → schema.
|
|
459
|
+
// Returns { admitted, candidate|null, reason?, contract?, frontier?, libraryKey?, lint? } (no method/methods —
|
|
460
|
+
// the caller attaches them).
|
|
461
|
+
function buildCandidateFromMined( top, env ) {
|
|
462
|
+
const { episodeTree, g, equivKeys, opts, declaredParams } = env;
|
|
463
|
+
|
|
464
|
+
// SOUNDNESS GATE (the declared path must keep the id-space invariant the inferred `knownIds` gate enforced: every
|
|
465
|
+
// created segment endpoint is base-derived or a BOUND, DISTINCT frontier hole — else a learning id leaks/mis-wires
|
|
466
|
+
// at replay, #30). Three refusals, each adversarial-review-reproduced:
|
|
467
|
+
// • ambiguous — a declared field resolves to >1 external value (under-declared field) → relativize holes only the first.
|
|
468
|
+
// • collapsed — two distinct declared endpoints share a value → relativize merges them into one hole → mis-bind.
|
|
469
|
+
// • leak — a parameterized segment endpoint is still a plain external literal (an endpoint the frontier missed).
|
|
470
|
+
const ambig = top.instances.map(( i ) => i.ctx && i.ctx.ambiguous).filter(Boolean)[0];
|
|
471
|
+
if ( ambig )
|
|
472
|
+
return { candidate: null, admitted: false,
|
|
473
|
+
reason: 'frontier-field-ambiguous (' + ambig.map(( a ) => a.field + '→[' + a.values.join(',') + ']' ).join('; ') + ' — declare one param per endpoint)' };
|
|
474
|
+
const collapsed = top.instances.map(( i ) => i.ctx && i.ctx.collapsed).filter(Boolean)[0];
|
|
475
|
+
if ( collapsed )
|
|
476
|
+
return { candidate: null, admitted: false,
|
|
477
|
+
reason: 'frontier-endpoints-collapsed (' + collapsed.map(( c ) => c.names.join('=') + '@' + c.value ).join('; ') + ' — distinct endpoints coincide in value, indistinguishable)' };
|
|
478
|
+
if ( top.leak )
|
|
479
|
+
return { candidate: null, admitted: false,
|
|
480
|
+
reason: 'frontier-endpoint-leak (' + top.leak.map(( e ) => e.field + '=' + e.value + (e.kind === 'phantom' ? ' [phantom: mis-holed external]' : '') ).join(', ') + ' — an external endpoint is not covered by the declared frontier)' };
|
|
481
|
+
|
|
482
|
+
const fromS = conceptByName(episodeTree, top.concept);
|
|
483
|
+
const requireKeys = fromS ? (Array.isArray(fromS.require) ? fromS.require.slice() : (fromS.require ? [fromS.require] : [])) : [];
|
|
484
|
+
const cryId = opts.idFor ? opts.idFor(top) : ('Crystal_' + top.concept);
|
|
485
|
+
|
|
486
|
+
// born-defeasible contract (the paper novelty) — null = refuse the contract path (no fragment-expressible post).
|
|
487
|
+
const contract = synthesizeContract({ concept: cryId, read: requireKeys, instances: top.instances, equivKeys: equivKeys || [], effect: opts.effect });
|
|
488
|
+
|
|
489
|
+
// SOUNDNESS GUARD (the premise is captured POST-apply — `mine.js#premiseOf` — there is no ZERO-CORE pre-apply
|
|
490
|
+
// hook): if the cast ECHOES/overwrites one of its OWN require keys (e.g. `kind:'norm-'+kind`), that key's mined
|
|
491
|
+
// (post-cast) value differs from its pre-cast value AND excluding it from the signature would silently mis-replay
|
|
492
|
+
// the method onto an unseen value of it. We cannot soundly recover the pre-cast premise for such a key → REFUSE.
|
|
493
|
+
// (Reviewer-found hole; regression `Gap A — a require key echoed by the cast …`.) The canonical structural method
|
|
494
|
+
// writes only NEW output facts (self-flag/guard/created-ids), never a require key, so this never trips it.
|
|
495
|
+
const writtenParent = parentFactKeys(top.instances);
|
|
496
|
+
const echoedRequire = requireKeys.filter(( k ) => writtenParent.has(k));
|
|
497
|
+
if ( echoedRequire.length && !opts.signatureKeys )
|
|
498
|
+
return { candidate: null, admitted: false, reason: 'require-key-overwritten (' + echoedRequire.join(',') + ' — premise capture unreliable, cannot soundly mine)' };
|
|
499
|
+
|
|
500
|
+
// the K1 signature = the keys read PRE-cast (the concept's memo surface) MINUS any the cast itself writes (a
|
|
501
|
+
// written key is an output, not a premise; excluding it also keeps the replay digest — over pre-cast facts —
|
|
502
|
+
// aligned with the mining digest). After the guard above, no require key is in `written`, so require keys (the
|
|
503
|
+
// genuine pre-cast premise) are always kept. `opts.signatureKeys` can override (testing / a host-declared signature).
|
|
504
|
+
const written = new Set([...writtenParent, top.concept, ...(contract ? contract.write : [])]);
|
|
505
|
+
const signatureKeys = opts.signatureKeys || memoSurfaceKeys(g, top.concept).filter(( k ) => !written.has(k));
|
|
506
|
+
|
|
507
|
+
// AUTHORITATIVE K1 re-check at the REPLAY granularity (mineMethods' signatureDetermined is a coarse full-premise
|
|
508
|
+
// pre-filter; THIS is the gate that matches what the crystal will actually key on). Build one parameterized
|
|
509
|
+
// template per signature class — and REFUSE (never first-wins) if a class maps to two different methods: that is
|
|
510
|
+
// content not determined by the replay signature → a silent mis-replay risk (the canonicalization barrier).
|
|
511
|
+
const templatesBySig = {};
|
|
512
|
+
for ( const i of top.instances ) {
|
|
513
|
+
const sig = digest(projectFacts(i.premise, signatureKeys)), pc = canon(i.param);
|
|
514
|
+
if ( !(sig in templatesBySig) ) templatesBySig[sig] = { param: i.param, canon: pc };
|
|
515
|
+
else if ( templatesBySig[sig].canon !== pc )
|
|
516
|
+
return { candidate: null, admitted: false, reason: 'signature-insufficient (content not determined by the replay signature)' };
|
|
517
|
+
}
|
|
518
|
+
for ( const k of Object.keys(templatesBySig) ) templatesBySig[k] = templatesBySig[k].param;
|
|
519
|
+
|
|
520
|
+
// T3 — reify the FrontierSignature (the declared endpoints + the sound summary + the parent NACs). The provider's
|
|
521
|
+
// endpoint rebind map is DERIVED from it (T4); the signature is stored as `schema.frontier`, a sibling of
|
|
522
|
+
// `schema.contract`, so it serializes with the tree and round-trips through rollback.
|
|
523
|
+
const frontier = reifyFrontier(declaredParams, top, fromS, contract, equivKeys);
|
|
524
|
+
|
|
525
|
+
// H3 — a declared behavioral param (sub-method / predicate) must carry a typed interface + frontier, else it
|
|
526
|
+
// re-keys every run and can never be a clean selection key. Reject at author time (lintMethod, single source of
|
|
527
|
+
// truth). Endpoint-only signatures never trip this (an id slot is always typed) — zero regression on the existing flow.
|
|
528
|
+
if ( hasBehavioral(frontier) ) {
|
|
529
|
+
// lint the PARAM TYPING only (H3) — do NOT re-frame-check the contract here: it is already validated at
|
|
530
|
+
// synthesis (`synthesizeContract`/`contract.normalize`), and `method.js#frameKeys` expects `$`-prefixed post
|
|
531
|
+
// atoms whereas `synthesizeContract` emits bare `key==value` atoms (a known format mismatch → a false frame
|
|
532
|
+
// violation if fed). Passing no contract yields only an 'uncontracted' WARNING (benign), never an error.
|
|
533
|
+
const lint = lintFrontier(frontier);
|
|
534
|
+
if ( lint.errors.length )
|
|
535
|
+
return { candidate: null, admitted: false, frontier, lint,
|
|
536
|
+
reason: 'untyped-behavioral-param (' + lint.errors.map(( e ) => e.kind ).join(',') + ')' };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const libKey = libraryKey(frontier, signatureKeys);
|
|
540
|
+
const provider = buildStructuralProvider({ cryId, signatureKeys, frontier, templatesBySig });
|
|
541
|
+
const schema = { _id: cryId, _name: cryId, require: requireKeys.slice(), provider: ['Crystal::' + cryId] };
|
|
542
|
+
if ( fromS && fromS.ensure ) schema.ensure = clone(fromS.ensure); // carry the re-fire guard (e.g. !$Refined)
|
|
543
|
+
if ( contract ) schema.contract = contract;
|
|
544
|
+
schema.frontier = frontier;
|
|
545
|
+
schema.libraryKey = libKey;
|
|
546
|
+
|
|
547
|
+
return {
|
|
548
|
+
contract, frontier, libraryKey: libKey, admitted: true,
|
|
549
|
+
candidate: { schema, providerName: 'Crystal::' + cryId, provider, signatureKeys, frontier, libraryKey: libKey,
|
|
550
|
+
frontierFields: top.frontierFields, templatesBySig },
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
module.exports = { crystallize, adopt, consolidate, rewriteAdopt, synthesizeContract, crystallizeStructural, crystallizeFromRecords,
|
|
555
|
+
buildStructuralProvider, reifyFrontier, libraryKey, lintFrontier };
|