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,393 @@
|
|
|
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
|
+
* adapt — the adapt-or-forge CONTROLLER: the creative loop's drive (host-side, ZERO-CORE; study
|
|
10
|
+
* doc/WIP/studies/2026-06-30-creative-loop-two-level-grammar.md, brick C).
|
|
11
|
+
*
|
|
12
|
+
* The structuring grammar names a target mechanism; this drives retrieve-or-forge over the concept-DLL library:
|
|
13
|
+
* RETRIEVE dispatch (library.js) → a candidate with a template for THIS signature (K1-sound by construction) → HIT, 0 model calls.
|
|
14
|
+
* FORGE no hit → the model builds the method, REUSING the dispatched neighbours where it can (adapt = structural
|
|
15
|
+
* reuse + content forge; forge = fresh — the host's forge labels which) → VERIFIER GATE (it must carry a
|
|
16
|
+
* sound contract — the refuse-no-contract gate is HERE, not in crystallizeStructural, which admits a
|
|
17
|
+
* contractless method) → index back so the next encounter HITS
|
|
18
|
+
* (amortise). The measured master-loop retrieve-or-forge; the verifier is the born-defeasible contract.
|
|
19
|
+
*
|
|
20
|
+
* The contract's POST is an OUTPUT invariant (true after the method runs) — so it is NOT a pre-dispatch gate (that is
|
|
21
|
+
* the app-conditions, refined in dispatch). The gate here is "the forged method comes with a sound post" + an optional
|
|
22
|
+
* host `verify` hook (run it + `assertPost`); a drift that breaks the post is caught by the runtime monitor (the moat).
|
|
23
|
+
*/
|
|
24
|
+
const { dispatch, indexMethod, dispatchInterface, appConditionsHold, frontierOf } = require('./library.js');
|
|
25
|
+
const { generalizeContent, fillContentHoles, blendAtSegment, combineAtFork, BASE } = require('./abstract.js');
|
|
26
|
+
const { digest } = require('../providers/canonicalize.js');
|
|
27
|
+
const { checkCompose } = require('./contract.js');
|
|
28
|
+
|
|
29
|
+
const idOf = ( o ) => o && (o.$$_id != null ? o.$$_id : (o.$_id != null ? o.$_id : o._id));
|
|
30
|
+
// the child-segment SLOTS of a parameterized method (base-derived segments — the graftable positions, never the
|
|
31
|
+
// outer `_parent`/frontier). The blend's snapped-separator candidates.
|
|
32
|
+
function segmentSlots( tpl ) {
|
|
33
|
+
return (Array.isArray(tpl) ? tpl : [tpl]).filter(( o ) => o && o.originNode != null && o.targetNode != null && typeof idOf(o) === 'string' && idOf(o).indexOf(BASE + '_') === 0).map(idOf);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const projectFacts = ( facts, keys ) => { const o = {}; for ( const k of (keys || []) ) if ( facts && k in facts ) o[k] = facts[k]; return o; };
|
|
37
|
+
|
|
38
|
+
/* ── the antiUnify CONTENT-FORGE adapt operator (study §"conceptual blending"; brick C, the richer adapt). ──
|
|
39
|
+
* The crude adapt re-forged the whole method (or hard-coded "swap field X"). This DISCOVERS the content holes by
|
|
40
|
+
* generalizing the neighbour's own templates (Plotkin LGG over its `templatesBySig`), forges ONLY those holes for
|
|
41
|
+
* the new signature, and reuses the skeleton + structural holes verbatim. Domain-agnostic: the host supplies a
|
|
42
|
+
* `contentFor(contentVars, scopeFacts) -> { <holePath>: value }` (the model filling exactly the discovered holes),
|
|
43
|
+
* never the template surgery. */
|
|
44
|
+
|
|
45
|
+
/** The content holes of a learned method (its `templatesBySig`): { stable, skeleton, contentVars:[{path}] }. */
|
|
46
|
+
function methodContentHoles( neighbour ) {
|
|
47
|
+
return generalizeContent(Object.values((neighbour && neighbour.templatesBySig) || {}));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Build an adapted candidate = the neighbour + a NEW signature template (its skeleton with the forged content
|
|
51
|
+
* filled). Pure + sync (the async model call is the caller's, before this). Returns null if a hole went unforged. */
|
|
52
|
+
function buildAdaptedCandidate( neighbour, scopeFacts, skeleton, valuesByPath, signatureKeys ) {
|
|
53
|
+
const filled = fillContentHoles(skeleton, valuesByPath);
|
|
54
|
+
if ( !filled ) return null; // a content hole was not forged → refuse (no undefined leaf)
|
|
55
|
+
const keys = signatureKeys || neighbour.signatureKeys || [];
|
|
56
|
+
const sig = digest(projectFacts(scopeFacts, keys));
|
|
57
|
+
const templatesBySig = Object.assign({}, neighbour.templatesBySig, { [sig]: filled });
|
|
58
|
+
return { candidate: Object.assign({}, neighbour, { templatesBySig }), template: filled, sig };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The controller's adapt operator: discover the neighbour's content holes → `contentFor` fills them (the model)
|
|
62
|
+
* → build the adapted candidate. Returns null if the holes can't be auto-discovered (need ≥2 differing templates)
|
|
63
|
+
* or a hole went unforged → the caller falls back to a fresh forge. `contentFor` is sync here (the controller is
|
|
64
|
+
* sync); an async (live) host pre-forges and calls `buildAdaptedCandidate` directly. */
|
|
65
|
+
function antiUnifyAdapt( opts ) {
|
|
66
|
+
const gen = methodContentHoles(opts.neighbour);
|
|
67
|
+
if ( !gen.stable || !gen.contentVars.length ) return null; // no auto-discoverable content holes
|
|
68
|
+
const vals = opts.contentFor(gen.contentVars, opts.scopeFacts);
|
|
69
|
+
const built = buildAdaptedCandidate(opts.neighbour, opts.scopeFacts, gen.skeleton, vals, opts.signatureKeys);
|
|
70
|
+
return built && Object.assign(built, { outcome: 'adapt', contentVars: gen.contentVars });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* ── CONCEPTUAL BLENDING at the candidate level (Boden COMBINATIONAL creativity; study §"conceptual blending"). ──
|
|
74
|
+
* Graft a DONOR method's body into a HOST method's segment SLOT → a NEW method candidate that is neither (e.g. a
|
|
75
|
+
* 1-level decompose host + a decompose donor → a 2-level decompose). The blend's OUTER interface (frontier +
|
|
76
|
+
* signature) is the HOST's, unchanged — so the blended method dispatches + mounts exactly like the host, but with a
|
|
77
|
+
* deeper, recombined body. v0 INHERITS the host contract (the donor's writes are a superset; the runtime monitor
|
|
78
|
+
* `assertPost` catches drift — a COMPOSED contract over both write-sets is the follow-up). Returns null if the slot
|
|
79
|
+
* is absent / does not blend in every signature class. */
|
|
80
|
+
function blendMethods( hostCand, donorCand, opts ) {
|
|
81
|
+
opts = opts || {};
|
|
82
|
+
const donorTpl = Object.values((donorCand && donorCand.templatesBySig) || {})[0];
|
|
83
|
+
const entries = Object.entries((hostCand && hostCand.templatesBySig) || {});
|
|
84
|
+
if ( !donorTpl || !entries.length ) return null;
|
|
85
|
+
const slot = opts.atSegment || segmentSlots(entries[0][1])[0]; // default = the host's first child-segment slot
|
|
86
|
+
if ( !slot ) return null; // no graftable segment slot
|
|
87
|
+
const templatesBySig = {};
|
|
88
|
+
for ( const [sig, tpl] of entries ) { const b = blendAtSegment(tpl, slot, donorTpl); if ( !b ) return null; templatesBySig[sig] = b; }
|
|
89
|
+
const hid = hostCand.schema && hostCand.schema._id, did = donorCand.schema && donorCand.schema._id;
|
|
90
|
+
// The graft is CONTRACT-CHECKED at the interface (H2 — `checkCompose` was dead-wired; the compose path used to
|
|
91
|
+
// blindly union an UNVERIFIED contract that `hasSoundContract` then accepted). checkCompose(host, donor) discharges
|
|
92
|
+
// the donor's PRE against the host's POST on the shared write→read keys. DEFEASIBLE — "a formal system is a GROUND,
|
|
93
|
+
// not a ceiling": a provably-`unsound` graft (the donor requires what the host contradicts) is REFUSED; an
|
|
94
|
+
// under-determined (`escalate`) one is ADMITTED and FLAGGED (`composeVerdict`) so the runtime `assertPost` monitor —
|
|
95
|
+
// the moat — checks it at mount rather than the synthesis over-refusing what it cannot statically decide.
|
|
96
|
+
const verdict = checkCompose(hostCand.schema, donorCand.schema).verdict;
|
|
97
|
+
if ( verdict === 'unsound' ) return null; // donor.pre provably contradicts host.post → refuse the graft
|
|
98
|
+
// the blend's contract is DERIVED (composed) from both parents, not merely inherited — the donor's body adds
|
|
99
|
+
// writes/posts the host omits (closes the v0 "inherited contract" hole). Conservative union; the interface is now
|
|
100
|
+
// discharged above (the composed post is still runtime-monitored by `assertPost` for the `escalate` residue).
|
|
101
|
+
const composed = composeContract(hostCand.schema && hostCand.schema.contract, donorCand.schema && donorCand.schema.contract);
|
|
102
|
+
const schema = Object.assign({}, hostCand.schema, composed ? { contract: composed } : {});
|
|
103
|
+
return Object.assign({}, hostCand, { schema, templatesBySig, blendedFrom: [hid, did], blendSlot: slot, contractDerived: !!composed, composeVerdict: verdict });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* ── BINARY COMBINATION — the SECOND composition operator (sibling to blendMethods). Where a blend NESTS a donor
|
|
107
|
+
* into a host slot (series → a deeper single query, with an interface obligation the donor.pre ⊨ host.post check
|
|
108
|
+
* discharges), a combine runs TWO complete methods in PARALLEL from the same origin and joins their results with a
|
|
109
|
+
* SET OPERATION (∩/∪/−) → a NEW method whose result = op(left,right). This is a SQL set-op query (`A INTERSECT B`):
|
|
110
|
+
* two INDEPENDENT sub-queries over the same source. Because the branches are independent (no data flows left→right),
|
|
111
|
+
* there is NO interface to discharge — so no checkCompose (unlike blend); the combined contract is just the derived
|
|
112
|
+
* UNION of both. Provenance in `combinedFrom`. The combined method's BODY is the LEFT's frontier (origin→target),
|
|
113
|
+
* but it COVERS the set-op class, not LEFT's operand class — so `opts.outerClass` RE-KEYS it (frontier /
|
|
114
|
+
* signatureKeys / libraryKey from the class the combine is invoked for) so it dispatches on the set-op signature and
|
|
115
|
+
* never false-hits a plain operand task (which stays in its own bucket). Without `outerClass` it inherits LEFT's key
|
|
116
|
+
* (back-compat: the direct-mount path in combine-methods-setop.test.js keys it by hand and never dispatches).
|
|
117
|
+
* Returns null if either method is empty. 0 model calls (pure structural recombination of two distilled grammars). */
|
|
118
|
+
function combineMethods( leftCand, rightCand, op, outerClass ) {
|
|
119
|
+
const rEntries = Object.values((rightCand && rightCand.templatesBySig) || {});
|
|
120
|
+
const lEntries = Object.entries((leftCand && leftCand.templatesBySig) || {});
|
|
121
|
+
if ( !rEntries.length || !lEntries.length ) return null;
|
|
122
|
+
const rightTpl = rEntries[0];
|
|
123
|
+
const templatesBySig = {};
|
|
124
|
+
for ( const [sig, ltpl] of lEntries ) { const c = combineAtFork(ltpl, rightTpl, op); if ( !c ) return null; templatesBySig[sig] = c; }
|
|
125
|
+
const lid = leftCand.schema && leftCand.schema._id, rid = rightCand.schema && rightCand.schema._id;
|
|
126
|
+
const composed = composeContract(leftCand.schema && leftCand.schema.contract, rightCand.schema && rightCand.schema.contract);
|
|
127
|
+
const schema = Object.assign({}, leftCand.schema, composed ? { contract: composed } : {});
|
|
128
|
+
const combined = Object.assign({}, leftCand, { schema, templatesBySig, combinedFrom: [lid, rid], setop: op, contractDerived: !!composed });
|
|
129
|
+
// RE-KEY to the OUTER set-op class (opt-in) — so indexMethod/dispatch bucket the combined method by the set-op
|
|
130
|
+
// signature, not LEFT's operand class. The stale inherited libraryKey is dropped so keyOf recomputes from the
|
|
131
|
+
// new frontier+signatureKeys (unless outerClass carries an explicit libraryKey).
|
|
132
|
+
if ( outerClass ) {
|
|
133
|
+
if ( outerClass.frontier ) { combined.frontier = outerClass.frontier; schema.frontier = outerClass.frontier; }
|
|
134
|
+
if ( outerClass.signatureKeys ) combined.signatureKeys = outerClass.signatureKeys.slice();
|
|
135
|
+
if ( outerClass.libraryKey ) { combined.libraryKey = outerClass.libraryKey; schema.libraryKey = outerClass.libraryKey; }
|
|
136
|
+
else { delete combined.libraryKey; delete schema.libraryKey; }
|
|
137
|
+
}
|
|
138
|
+
return combined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The CANONICAL atom key — `$x==1` ≡ `x==1` ≡ `x == 1` (the Laurie-G3 atom-form fix: synthesizeContract emits
|
|
142
|
+
* bare atoms, reviseOnBlame/widenOnVerified emit `$`-prefixed ones; provenance matching must not care). Kept a
|
|
143
|
+
* READABLE atom string: leading ref-prefix stripped + whitespace removed — the same surface both the contract
|
|
144
|
+
* emitters produce, so a postFrom key stays greppable/debuggable (never an opaque serialization). */
|
|
145
|
+
const canonAtom = ( s ) => String(s).replace(/^\$\$?/, '').replace(/\s+/g, '');
|
|
146
|
+
|
|
147
|
+
/** Compose two born-defeasible contracts (crystallize.js#synthesizeContract shape) into the blend's contract:
|
|
148
|
+
* read/write/pre/post = the de-duplicated UNION; effect = pure iff BOTH pure. Conservative (over-approximates the
|
|
149
|
+
* write footprint, conjoins both posts). Returns null only if BOTH are absent.
|
|
150
|
+
* H3: the composed contract carries `postFrom` — which PARENT's post contributed each (canonical) post atom
|
|
151
|
+
* ('host' | 'donor' | 'both') — the graft-provenance the blame rule needs to avoid corrupting the standalone
|
|
152
|
+
* library (a=host, b=donor, matching the blendMethods call). */
|
|
153
|
+
function composeContract( a, b ) {
|
|
154
|
+
if ( !a && !b ) return null;
|
|
155
|
+
const postFrom = {};
|
|
156
|
+
for ( const p of ((a && a.post) || []) ) postFrom[canonAtom(p)] = 'host';
|
|
157
|
+
for ( const p of ((b && b.post) || []) ) { const k = canonAtom(p); postFrom[k] = postFrom[k] === 'host' ? 'both' : 'donor'; }
|
|
158
|
+
if ( !a || !b ) return Object.assign({}, a || b, { postFrom });
|
|
159
|
+
const uniq = ( xs ) => [...new Set(xs)];
|
|
160
|
+
return {
|
|
161
|
+
read: uniq([...(a.read || []), ...(b.read || [])]),
|
|
162
|
+
write: uniq([...(a.write || []), ...(b.write || [])]),
|
|
163
|
+
pre: uniq([...(a.pre || []), ...(b.pre || [])]),
|
|
164
|
+
post: uniq([...(a.post || []), ...(b.post || [])]),
|
|
165
|
+
effect: (a.effect === 'pure' && b.effect === 'pure') ? 'pure' : (a.effect || b.effect || 'pure'),
|
|
166
|
+
postFrom,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** H3 — graft-provenance BLAME (the Laurie-G rule; the naive "carry blendedFrom into reviseOnBlame and blame
|
|
171
|
+
* the donor" OVER-PENALIZES a good donor for a bad composition and corrupts the standalone library). Given the
|
|
172
|
+
* post atoms `assertPost` reported violated on a mounted COMPOSITE, attribute each to an axis:
|
|
173
|
+
* 'donor' the atom is donor-only provenance on a `sound` graft → the donor's OWN post failed where used →
|
|
174
|
+
* the fix is revising the DONOR's pre (reviseOnBlame on the donor);
|
|
175
|
+
* 'host' symmetric;
|
|
176
|
+
* 'graft' provenance 'both' / unknown atom / `composeVerdict === 'escalate'` (the statically-undischarged
|
|
177
|
+
* interface — the prime suspect is the graft itself) → revise/retract the COMPOSITE only, parents
|
|
178
|
+
* stay intact.
|
|
179
|
+
* Overall axis = 'donor' (resp. 'host') iff EVERY failed atom reads donor (resp. host); anything mixed or
|
|
180
|
+
* uncertain → 'graft' (conservative: when in doubt, narrow the composite, never a parent).
|
|
181
|
+
* @returns { perAtom:[{atom,axis}], axis, host, donor, slot } — host/donor/slot from blendedFrom/blendSlot. */
|
|
182
|
+
function attributeCompositeBlame( opts ) {
|
|
183
|
+
const composite = (opts && opts.composite) || {};
|
|
184
|
+
const contract = (composite.schema && composite.schema.contract) || composite.contract || {};
|
|
185
|
+
const postFrom = contract.postFrom || {};
|
|
186
|
+
const escalate = composite.composeVerdict === 'escalate';
|
|
187
|
+
const perAtom = ((opts && opts.failedAtoms) || []).map(( atom ) => {
|
|
188
|
+
const prov = postFrom[canonAtom(atom)];
|
|
189
|
+
const axis = escalate ? 'graft' : (prov === 'donor' ? 'donor' : (prov === 'host' ? 'host' : 'graft'));
|
|
190
|
+
return { atom, axis };
|
|
191
|
+
});
|
|
192
|
+
const axes = new Set(perAtom.map(( x ) => x.axis));
|
|
193
|
+
const axis = (axes.size === 1 && !axes.has('graft')) ? [...axes][0] : 'graft';
|
|
194
|
+
const bf = composite.blendedFrom || [];
|
|
195
|
+
return { perAtom, axis, host: bf[0], donor: bf[1], slot: composite.blendSlot };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** the signature digest a candidate would key its replay on at this site (null if it has no template for it = a miss). */
|
|
199
|
+
function hitTemplate( cand, scopeFacts ) {
|
|
200
|
+
const sig = digest(projectFacts(scopeFacts, cand.signatureKeys || []));
|
|
201
|
+
return (cand.templatesBySig || {})[sig] ? sig : null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const hasSoundContract = ( cand ) => !!(cand && cand.schema && cand.schema.contract);
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Drive retrieve-or-forge for a target mechanism at a site.
|
|
208
|
+
* @param opts.lib/target/scopeFacts the library + the abstract target FrontierSignature + the call-site facts.
|
|
209
|
+
* @param opts.forge (scopeFacts, neighbours) → { candidate, outcome?:'adapt'|'forge', calls?:number } | null
|
|
210
|
+
* the model: builds a new method (may REUSE the dispatched `neighbours` = adapt). Returns null = give up.
|
|
211
|
+
* @param opts.verify optional (candidate, scopeFacts) → boolean a stronger gate than "has a contract" (e.g. run + assertPost).
|
|
212
|
+
* @param opts.onForge optional (candidate) → void index the forged method (default: indexMethod into opts.lib).
|
|
213
|
+
* @param opts.requireContract default true — refuse a forged method with no sound post (the verifier gate).
|
|
214
|
+
* @returns { outcome:'hit'|'adapt'|'forge'|'reject', candidate?, sig?, calls, neighbours, reason? }
|
|
215
|
+
*/
|
|
216
|
+
function adaptOrForge( opts ) {
|
|
217
|
+
const pre = _retrieveOrPrepare(opts);
|
|
218
|
+
if ( pre.done ) return pre.done;
|
|
219
|
+
const { neighbours, donors } = pre;
|
|
220
|
+
|
|
221
|
+
// ADAPT (antiUnify content-forge — the principled path, built INTO the controller): if a content-forger is given
|
|
222
|
+
// and the top neighbour has auto-discoverable content holes, forge ONLY those holes + reuse the skeleton + the
|
|
223
|
+
// structural holes verbatim (the neighbour's contract is inherited). Falls back to `opts.forge` (fresh) otherwise.
|
|
224
|
+
let f = null;
|
|
225
|
+
if ( opts.adaptContent && neighbours.length ) {
|
|
226
|
+
const a = antiUnifyAdapt({ neighbour: neighbours[0], scopeFacts: opts.scopeFacts, signatureKeys: neighbours[0].signatureKeys, contentFor: opts.adaptContent });
|
|
227
|
+
if ( a && a.candidate ) f = { candidate: a.candidate, outcome: 'adapt', calls: 1 }; // one content-forge model call
|
|
228
|
+
}
|
|
229
|
+
// BLEND (B.4 — compositional reuse, Boden combinational; opt-in `opts.blend`): before paying for a fresh
|
|
230
|
+
// FORGE, GRAFT a donor method into the host neighbour's segment slot — a STRUCTURAL recombination of two
|
|
231
|
+
// existing methods (0 model calls, contract-checked in blendMethods). If it does not graft (no slot /
|
|
232
|
+
// provably unsound) it falls through to forge; the blended candidate is then verifier-gated like any forge.
|
|
233
|
+
if ( !f && opts.blend && neighbours.length ) {
|
|
234
|
+
const donor = donors[0] || opts.donor;
|
|
235
|
+
const b = donor && blendMethods(neighbours[0], donor, typeof opts.blend === 'object' ? opts.blend : {});
|
|
236
|
+
if ( b ) f = { candidate: b, outcome: 'blend', calls: 0 };
|
|
237
|
+
}
|
|
238
|
+
// COMBINE (the 2nd composition operator — set-op; opt-in `opts.combine`, sibling to blend): before a fresh FORGE,
|
|
239
|
+
// if the host resolves this task as a set-op over two library methods, run them in PARALLEL and join with the op
|
|
240
|
+
// → a NEW method at 0 model calls (combineMethods). The host callback OWNS the decomposition (which two operands +
|
|
241
|
+
// which op), like target/dispatchFacts; the controller supplies the dispatched lib + facts and re-keys the result
|
|
242
|
+
// to the set-op class (spec.outerClass, default opts.target) so it never false-hits a plain operand task.
|
|
243
|
+
if ( !f && opts.combine ) {
|
|
244
|
+
const spec = opts.combine(opts.scopeFacts, opts.lib, neighbours, donors);
|
|
245
|
+
const c = spec && spec.left && spec.right && spec.op && combineMethods(spec.left, spec.right, spec.op, spec.outerClass || opts.target);
|
|
246
|
+
if ( c ) f = { candidate: c, outcome: 'combine', calls: 0 };
|
|
247
|
+
}
|
|
248
|
+
// FORGE / ADAPT — the model builds it, reusing the neighbours (+ §6.2 donor skeletons) where it can. Verifier-gated, indexed.
|
|
249
|
+
if ( !f ) f = opts.forge ? opts.forge(opts.scopeFacts, neighbours, donors) : null;
|
|
250
|
+
const rej = _rejectForged(opts, f, neighbours);
|
|
251
|
+
if ( rej ) return rej;
|
|
252
|
+
if ( opts.verify && !opts.verify(f.candidate, opts.scopeFacts) )
|
|
253
|
+
return { outcome: 'reject', reason: 'forged method failed the host verifier', calls: f.calls == null ? 1 : f.calls, neighbours };
|
|
254
|
+
return _acceptForged(opts, f, neighbours);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The ASYNC twin of `adaptOrForge` — the SAME retrieve/adapt/forge ladder and the SAME gates (shared
|
|
259
|
+
* helpers below, single source), for a LIVE host whose `forge` / `adaptContent` / `verify` are model
|
|
260
|
+
* calls (async). This is the brick a thin assembly (lib/combos/learning-library.js P3) wires as the
|
|
261
|
+
* master-loop's FORGE arm — without it, an async forge's Promise would fail `f.candidate` and every
|
|
262
|
+
* forge would silently `reject` (the sync controller cannot await).
|
|
263
|
+
* Same params as `adaptOrForge`; `forge`/`adaptContent`/`verify` may return promises.
|
|
264
|
+
* @returns Promise<{ outcome:'hit'|'adapt'|'forge'|'reject', candidate?, sig?, calls, neighbours, reason? }>
|
|
265
|
+
*/
|
|
266
|
+
async function adaptOrForgeAsync( opts ) {
|
|
267
|
+
const pre = _retrieveOrPrepare(opts);
|
|
268
|
+
if ( pre.done ) return pre.done;
|
|
269
|
+
const { neighbours, donors } = pre;
|
|
270
|
+
|
|
271
|
+
// ADAPT — the awaited variant of `antiUnifyAdapt` (its docstring's "an async host pre-forges" path,
|
|
272
|
+
// packaged): discover the holes, AWAIT the content-forge, rebuild via the same pure builder.
|
|
273
|
+
let f = null;
|
|
274
|
+
if ( opts.adaptContent && neighbours.length ) {
|
|
275
|
+
const gen = methodContentHoles(neighbours[0]);
|
|
276
|
+
if ( gen.stable && gen.contentVars.length ) {
|
|
277
|
+
const vals = await opts.adaptContent(gen.contentVars, opts.scopeFacts);
|
|
278
|
+
const built = vals && buildAdaptedCandidate(neighbours[0], opts.scopeFacts, gen.skeleton, vals, neighbours[0].signatureKeys);
|
|
279
|
+
if ( built && built.candidate ) f = { candidate: built.candidate, outcome: 'adapt', calls: 1 };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// BLEND (B.4) — the async twin's compositional-reuse rung (opt-in `opts.blend`): graft a donor into the host
|
|
283
|
+
// neighbour before a fresh FORGE (0 model calls, contract-checked); the async verifier below gates it.
|
|
284
|
+
if ( !f && opts.blend && neighbours.length ) {
|
|
285
|
+
const donor = donors[0] || opts.donor;
|
|
286
|
+
const b = donor && blendMethods(neighbours[0], donor, typeof opts.blend === 'object' ? opts.blend : {});
|
|
287
|
+
if ( b ) f = { candidate: b, outcome: 'blend', calls: 0 };
|
|
288
|
+
}
|
|
289
|
+
// COMBINE (opt-in `opts.combine`) — the async twin's set-op composition rung (may resolve operands async).
|
|
290
|
+
if ( !f && opts.combine ) {
|
|
291
|
+
const spec = await opts.combine(opts.scopeFacts, opts.lib, neighbours, donors);
|
|
292
|
+
const c = spec && spec.left && spec.right && spec.op && combineMethods(spec.left, spec.right, spec.op, spec.outerClass || opts.target);
|
|
293
|
+
if ( c ) f = { candidate: c, outcome: 'combine', calls: 0 };
|
|
294
|
+
}
|
|
295
|
+
if ( !f ) f = opts.forge ? await opts.forge(opts.scopeFacts, neighbours, donors) : null;
|
|
296
|
+
const rej = _rejectForged(opts, f, neighbours);
|
|
297
|
+
if ( rej ) return rej;
|
|
298
|
+
if ( opts.verify && !(await opts.verify(f.candidate, opts.scopeFacts)) )
|
|
299
|
+
return { outcome: 'reject', reason: 'forged method failed the host verifier', calls: f.calls == null ? 1 : f.calls, neighbours };
|
|
300
|
+
return _acceptForged(opts, f, neighbours);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** RETRIEVE phase, shared by both controllers: dispatch → a template hit is K1-sound by construction
|
|
304
|
+
* (0 model calls, `done`); else the neighbours + the §6.2 interface-recall DONOR skeletons (the
|
|
305
|
+
* NAC-failing in-bucket methods exact dispatch dropped — skeleton sources ONLY, never replayed:
|
|
306
|
+
* `_rejectForged` re-asserts the FORGED method's OWN appConditions; Laurie confront). */
|
|
307
|
+
function _retrieveOrPrepare( opts ) {
|
|
308
|
+
const r = dispatch(opts.lib, opts.target, opts.scopeFacts);
|
|
309
|
+
const neighbours = r.candidates.map(( e ) => e.candidate );
|
|
310
|
+
for ( const cand of neighbours ) {
|
|
311
|
+
const sig = hitTemplate(cand, opts.scopeFacts);
|
|
312
|
+
if ( sig ) return { done: { outcome: 'hit', candidate: cand, sig, calls: 0, neighbours } };
|
|
313
|
+
}
|
|
314
|
+
const donors = opts.interfaceRecall
|
|
315
|
+
? dispatchInterface(opts.lib, opts.target, opts.scopeFacts, { k: opts.recallK || 3 }).proposals.map(( p ) => p.candidate)
|
|
316
|
+
: [];
|
|
317
|
+
return { neighbours, donors };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** The forged-method REJECT gates, shared by both controllers (single source — the two paths must never
|
|
321
|
+
* disagree): no candidate → reject; no sound contract (unless `requireContract:false`) → reject; under
|
|
322
|
+
* interface-recall, the forged method's OWN appConditions must HOLD at the site — a donor replayed
|
|
323
|
+
* verbatim (its dropped NAC still failing here) is REJECTED, never mounted (the #29 false-hit;
|
|
324
|
+
* `appConditionsHold` = PRESENCE for require, never `satisfies` truthiness). Returns null = pass. */
|
|
325
|
+
function _rejectForged( opts, f, neighbours ) {
|
|
326
|
+
if ( !f || !f.candidate ) return { outcome: 'reject', reason: 'no forge / forge failed', calls: f && f.calls || 0, neighbours };
|
|
327
|
+
if ( opts.requireContract !== false && !hasSoundContract(f.candidate) )
|
|
328
|
+
return { outcome: 'reject', reason: 'forged method has no sound contract (verifier gate)', calls: f.calls == null ? 1 : f.calls, neighbours };
|
|
329
|
+
if ( opts.interfaceRecall && !appConditionsHold(frontierOf(f.candidate), opts.scopeFacts || {}) )
|
|
330
|
+
return { outcome: 'reject', reason: 'forged method appConditions fail at the site (donor not replayed)', calls: f.calls == null ? 1 : f.calls, neighbours };
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Accept a gated forged/adapted method: index it (default into `opts.lib`) + the outcome record. */
|
|
335
|
+
function _acceptForged( opts, f, neighbours ) {
|
|
336
|
+
(opts.onForge || (( c ) => indexMethod(opts.lib, c)))(f.candidate);
|
|
337
|
+
const outcome = f.outcome === 'adapt' ? 'adapt' : f.outcome === 'blend' ? 'blend' : f.outcome === 'combine' ? 'combine' : 'forge';
|
|
338
|
+
return { outcome, candidate: f.candidate, calls: f.calls == null ? 1 : f.calls, neighbours, blendedFrom: f.candidate && f.candidate.blendedFrom, combinedFrom: f.candidate && f.candidate.combinedFrom };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** The structural DEPTH of a method = the deepest nesting of its created mid nodes (`⟦@base⟧_m0` = 1,
|
|
342
|
+
* `⟦@base⟧_a0_m0` = 2, …). The μ-measure the blend search descends on. */
|
|
343
|
+
function methodDepth( cand ) {
|
|
344
|
+
let max = 0;
|
|
345
|
+
for ( const tpl of Object.values((cand && cand.templatesBySig) || {}) ) {
|
|
346
|
+
for ( const o of (Array.isArray(tpl) ? tpl : [tpl]) ) {
|
|
347
|
+
const id = o && (o.$$_id != null ? o.$$_id : (o.$_id != null ? o.$_id : o._id));
|
|
348
|
+
if ( typeof id === 'string' && id.indexOf(BASE + '_') === 0 && /_m0$/.test(id) ) {
|
|
349
|
+
const d = (id.slice(BASE.length).match(/_/g) || []).length; // underscores in the suffix = nesting level
|
|
350
|
+
if ( d > max ) max = d;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return max;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/* ── the BLEND STRATEGY (the creativity AUTOMATION; study §"well-foundedness"): when no single dispatched method
|
|
358
|
+
* SATISFIES the goal, BLEND neighbours to DEEPEN — bounded compositional search with a TERMINATION guarantee.
|
|
359
|
+
* Greedy + μ-descent: host = top neighbour, donor = `opts.donor` (default the top neighbour, i.e. self-deepen);
|
|
360
|
+
* each blend strictly INCREASES `methodDepth` (the μ-measure), so the loop terminates at `maxDepth` (no `divergent`
|
|
361
|
+
* runaway — the AO* admissible-heuristic discipline the study demanded). 0 model calls: the deeper method is
|
|
362
|
+
* SYNTHESIZED from library parts, not forged. A cache cannot (it replays); crystallize cannot without ≥2 traces.
|
|
363
|
+
* @param opts.lib/target/scopeFacts as adaptOrForge (the dispatch inputs).
|
|
364
|
+
* @param opts.satisfies (candidate, scopeFacts) → bool the goal test (host runs/mounts, or a template predicate).
|
|
365
|
+
* @param opts.donor optional candidate to graft (default = the top dispatched neighbour).
|
|
366
|
+
* @param opts.maxDepth the blend-depth cap (default 3) — the well-foundedness bound.
|
|
367
|
+
* @returns { outcome:'retrieve'|'blend'|'reject', candidate, depth, calls, reason? }
|
|
368
|
+
*/
|
|
369
|
+
function synthesizeByBlend( opts ) {
|
|
370
|
+
const r = dispatch(opts.lib, opts.target, opts.scopeFacts);
|
|
371
|
+
const neighbours = r.candidates.map(( e ) => e.candidate );
|
|
372
|
+
if ( !neighbours.length ) return { outcome: 'reject', reason: 'no neighbour to compose', candidate: null, calls: 0 };
|
|
373
|
+
let cand = neighbours[0];
|
|
374
|
+
if ( opts.satisfies(cand, opts.scopeFacts) ) return { outcome: 'retrieve', candidate: cand, depth: methodDepth(cand), calls: 0 };
|
|
375
|
+
const donor = opts.donor || neighbours[0];
|
|
376
|
+
const maxDepth = opts.maxDepth || 3;
|
|
377
|
+
const underscores = ( s ) => (s.match(/_/g) || []).length;
|
|
378
|
+
let last = methodDepth(cand);
|
|
379
|
+
for ( let i = 0; i < maxDepth; i++ ) {
|
|
380
|
+
// graft at the DEEPEST current slot so iterated blends DESCEND (each step deepens that branch by one level).
|
|
381
|
+
const deepest = segmentSlots(Object.values(cand.templatesBySig)[0]).sort(( a, b ) => underscores(b) - underscores(a))[0];
|
|
382
|
+
if ( !deepest ) break;
|
|
383
|
+
const blended = blendMethods(cand, donor, { atSegment: deepest });
|
|
384
|
+
if ( !blended ) break; // no graftable slot → cannot deepen further
|
|
385
|
+
const d = methodDepth(blended);
|
|
386
|
+
if ( d <= last ) break; // μ-descent guard: no strict progress → stop (no spin)
|
|
387
|
+
last = d; cand = blended;
|
|
388
|
+
if ( opts.satisfies(cand, opts.scopeFacts) ) return { outcome: 'blend', candidate: cand, depth: d, calls: 0 };
|
|
389
|
+
}
|
|
390
|
+
return { outcome: 'reject', reason: 'bounded blend (maxDepth/μ-descent) did not satisfy the goal', candidate: null, depth: last, calls: 0 };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
module.exports = { adaptOrForge, adaptOrForgeAsync, hitTemplate, hasSoundContract, antiUnifyAdapt, methodContentHoles, buildAdaptedCandidate, blendMethods, combineMethods, segmentSlots, composeContract, attributeCompositeBlame, synthesizeByBlend, methodDepth, canonAtom };
|
|
@@ -0,0 +1,216 @@
|
|
|
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
|
+
* ancestry — §6.3(b) the ANCESTRY ORACLE: content→param PROMOTION (host-side, ZERO-CORE; spec §3.2, refined by the
|
|
10
|
+
* 2026-06-30 Laurie confront). Relative LGG / bounded ij-determinacy (Plotkin 1971; GOLEM, Muggleton-Feng 1990).
|
|
11
|
+
*
|
|
12
|
+
* A crystallized method's VARYING content leaf is one of three bins (NEVER dropped — "ignorable" is unsound):
|
|
13
|
+
* • BAKE (baked+keyed) — constant across ALL instances AND digests (`signatureDetermined` is within-digest only;
|
|
14
|
+
* `generalizeContent` folds across digests → a within-digest constant could bake a premise-dependent value
|
|
15
|
+
* above the memo key). The mount stays gated on a digest hit.
|
|
16
|
+
* • PROMOTE (rebound+verified) — the identity FD `value(f)=N(s).g` (or a deterministic field-projection `g.field`)
|
|
17
|
+
* holds, g is BELOW the separator horizon (g ∈ Σ_sep), and it SURVIVES a held-out strict-=== check. The leaf
|
|
18
|
+
* becomes a frontier ref bound from the ancestor at the call site, with an EXACT relational post `$leaf==$g`.
|
|
19
|
+
* • FORGE (forged+verified) — everything else (the always-sound catch-all: the model re-derives, assertPost verifies).
|
|
20
|
+
*
|
|
21
|
+
* THE SOUNDNESS SPINE (Laurie confront — the SAME §6.4 hazard): the promote bin is BY CONSTRUCTION the varying-leaf
|
|
22
|
+
* case, so `crystallize.js#invariantAtom` would give it an over-approximating BAND → a spurious g whose wrong value
|
|
23
|
+
* lands in the band passes `assertPost` silently. PAC (Džeroski-Muggleton-Russell 1992) does NOT save it (workload
|
|
24
|
+
* firings are non-iid; the engine deploys under drift). The fix:
|
|
25
|
+
* (1) minK floor ≥ 3 (the union-bound over |Σ_sep| is advisory — `opts.minK` raises it; k bounds the in-sample
|
|
26
|
+
* false-discovery rate, NOT soundness);
|
|
27
|
+
* (2) a MANDATORY held-out (≥1 withheld instance) verified by strict `===` — the negative-based reduction; the only
|
|
28
|
+
* in-sample test that kills an in-distribution spurious g;
|
|
29
|
+
* (3) the promoted post is the EXACT relation `$leaf==$g`, NOT the band → a future divergence is caught at mount.
|
|
30
|
+
* Ambiguity (≥2 survivors) → FORGE (the version-space join ⊤ = a fresh variable = forge IS the Plotkin/Mitchell answer).
|
|
31
|
+
*/
|
|
32
|
+
const { REF } = require('./abstract.js');
|
|
33
|
+
|
|
34
|
+
function canon( x ) {
|
|
35
|
+
if ( x === undefined ) return 'u';
|
|
36
|
+
if ( x === null || typeof x !== 'object' ) return JSON.stringify(x);
|
|
37
|
+
if ( Array.isArray(x) ) return '[' + x.map(canon).join(',') + ']';
|
|
38
|
+
return '{' + Object.keys(x).sort().map(( k ) => JSON.stringify(k) + ':' + canon(x[k])).join(',') + '}';
|
|
39
|
+
}
|
|
40
|
+
const eq = ( a, b ) => canon(a) === canon(b);
|
|
41
|
+
|
|
42
|
+
// the ancestor value a hypothesis reads: identity `N(s).g` or field-projection `N(s).g.field` (the ONLY two φ — a
|
|
43
|
+
// lookup-table φ is a memo, affine/multi-arg fit a free parameter → spurious-rate explosion, deferred).
|
|
44
|
+
function ancestorVal( ancestry, cand ) {
|
|
45
|
+
const g = (ancestry || {})[cand.g];
|
|
46
|
+
if ( cand.field == null ) return g;
|
|
47
|
+
return (g && typeof g === 'object') ? g[cand.field] : undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Decide the bin for ONE content leaf from its per-instance observations `[{ value, ancestry:{factName:value}, digest? }]`.
|
|
52
|
+
* @param opts.sigmaSep the separator horizon (decompose.js#separatorGate / bagInterface) — g must be ∈ this.
|
|
53
|
+
* @param opts.minK the determinacy floor (≥3; the union-bound over |Σ_sep| is the host's to raise).
|
|
54
|
+
* @param opts.fieldProjection allow the `g.field` φ (default false).
|
|
55
|
+
* @param opts.leafKey the fact key the leaf writes (for the exact post `$<leafKey>==$<ref>`; default 'leaf').
|
|
56
|
+
* @returns { bin:'bake'|'promote'|'forge', promotion?, post?, value?, reason, fitCandidates, survivors }
|
|
57
|
+
*/
|
|
58
|
+
function decideLeaf( opts ) {
|
|
59
|
+
opts = opts || {};
|
|
60
|
+
const obs = (opts.observations || []).filter(Boolean);
|
|
61
|
+
const sigma = new Set(opts.sigmaSep || []);
|
|
62
|
+
const minFit = Math.max(3, opts.minK || 3);
|
|
63
|
+
const leafKey = opts.leafKey || 'leaf';
|
|
64
|
+
|
|
65
|
+
// need ≥ minFit instances to FIT + ≥1 HELD-OUT (the negative-based reduction). Else FORGE (the safe catch-all).
|
|
66
|
+
if ( obs.length < minFit + 1 ) return { bin: 'forge', reason: 'insufficient-instances (need minK+1; got ' + obs.length + ')', fitCandidates: [], survivors: [] };
|
|
67
|
+
const fit = obs.slice(0, obs.length - 1), held = obs[obs.length - 1];
|
|
68
|
+
const vals = fit.map(( o ) => o.value );
|
|
69
|
+
|
|
70
|
+
// (4) BAKE — constant across ALL observations (incl. held-out). Cross-digest constancy is the soundness condition.
|
|
71
|
+
if ( obs.every(( o ) => eq(o.value, obs[0].value)) ) {
|
|
72
|
+
const digests = new Set(obs.map(( o ) => o.digest).filter(( d ) => d != null));
|
|
73
|
+
return { bin: 'bake', reason: digests.size > 1 ? 'constant-cross-digest' : 'constant', value: obs[0].value, crossDigest: digests.size > 1, fitCandidates: [], survivors: [] };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// the hypothesis space H = { identity g } ∪ { field-projection g.field }, g ∈ Σ_sep (the horizon).
|
|
77
|
+
const anc0 = fit[0].ancestry || {};
|
|
78
|
+
const H = [];
|
|
79
|
+
for ( const g of Object.keys(anc0) ) {
|
|
80
|
+
if ( !sigma.has(g) ) continue;
|
|
81
|
+
H.push({ g });
|
|
82
|
+
if ( opts.fieldProjection && anc0[g] && typeof anc0[g] === 'object' )
|
|
83
|
+
for ( const field of Object.keys(anc0[g]) ) H.push({ g, field });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// fit candidates: the FD holds on EVERY fit instance.
|
|
87
|
+
const fitCandidates = H.filter(( c ) => fit.every(( o, k ) => o.ancestry && eq(vals[k], ancestorVal(o.ancestry, c))));
|
|
88
|
+
// survivors: ALSO predict the held-out leaf by strict === (eliminate an in-distribution spurious g).
|
|
89
|
+
const survivors = [];
|
|
90
|
+
const seen = new Set();
|
|
91
|
+
for ( const c of fitCandidates ) {
|
|
92
|
+
if ( !eq(held.value, ancestorVal(held.ancestry, c)) ) continue;
|
|
93
|
+
const k = c.g + '|' + (c.field || '');
|
|
94
|
+
if ( !seen.has(k) ) { seen.add(k); survivors.push(c); }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if ( survivors.length === 1 ) {
|
|
98
|
+
// a colon-free ref name (a colon is the cross-walk syntax → would mis-resolve in `expr.js`/`assertPost`).
|
|
99
|
+
const c = survivors[0], refName = 'anc_' + c.g + (c.field ? '_' + c.field : '');
|
|
100
|
+
return { bin: 'promote', promotion: { ref: REF(refName), refName, ancestorFact: c.g, field: c.field || null, leafKey },
|
|
101
|
+
post: '$' + leafKey + '==$' + refName, fitCandidates, survivors };
|
|
102
|
+
}
|
|
103
|
+
return { bin: 'forge', fitCandidates, survivors,
|
|
104
|
+
reason: survivors.length ? 'ambiguous-version-space-join (' + survivors.length + ' survivors)' : 'no-determinate-ancestor' };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// the leaf's fact key from a generalizeContent path (`[1].state` → `state`; `state` → `state`).
|
|
108
|
+
function leafKeyOf( path ) { const parts = String(path).split('.'); return parts[parts.length - 1].replace(/\[\d+\]/g, '') || 'leaf'; }
|
|
109
|
+
|
|
110
|
+
// rewrite a generalizeContent skeleton: a promoted `{'§var':p}` hole → its frontier REF; a baked hole → its literal;
|
|
111
|
+
// a forged hole → left as the content hole (the model fills it).
|
|
112
|
+
function rewriteSkeleton( skel, byPath ) {
|
|
113
|
+
if ( Array.isArray(skel) ) return skel.map(( x ) => rewriteSkeleton(x, byPath));
|
|
114
|
+
if ( skel && typeof skel === 'object' ) {
|
|
115
|
+
if ( '§var' in skel && Object.keys(skel).length === 1 ) { const p = skel['§var']; return (p in byPath) ? byPath[p] : skel; }
|
|
116
|
+
const o = {}; for ( const k in skel ) o[k] = rewriteSkeleton(skel[k], byPath); return o;
|
|
117
|
+
}
|
|
118
|
+
return skel;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Batch the promotion over a method's content vars + rewrite the skeleton.
|
|
123
|
+
* @param opts.skeleton the `generalizeContent` skeleton (with `{'§var':path}` holes).
|
|
124
|
+
* @param opts.leaves [{ path, observations:[{value,ancestry,digest?}] }] — one per content var.
|
|
125
|
+
* @returns { promoted:[{path,ancestorFact,field,refName}], baked:[path], forged:[path], posts:[atom], bins:{path:bin}, skeleton }
|
|
126
|
+
*/
|
|
127
|
+
function promoteContentVars( opts ) {
|
|
128
|
+
opts = opts || {};
|
|
129
|
+
const out = { promoted: [], baked: [], forged: [], posts: [], bins: {} };
|
|
130
|
+
const byPath = {};
|
|
131
|
+
for ( const leaf of (opts.leaves || []) ) {
|
|
132
|
+
const d = decideLeaf({ observations: leaf.observations, sigmaSep: opts.sigmaSep, minK: opts.minK, fieldProjection: opts.fieldProjection, leafKey: leafKeyOf(leaf.path) });
|
|
133
|
+
out.bins[leaf.path] = d.bin;
|
|
134
|
+
if ( d.bin === 'promote' ) { out.promoted.push(Object.assign({ path: leaf.path }, d.promotion)); out.posts.push(d.post); byPath[leaf.path] = d.promotion.ref; }
|
|
135
|
+
else if ( d.bin === 'bake' ) { out.baked.push(leaf.path); byPath[leaf.path] = d.value; }
|
|
136
|
+
else out.forged.push(leaf.path);
|
|
137
|
+
}
|
|
138
|
+
out.skeleton = rewriteSkeleton(opts.skeleton, byPath);
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// parse a generalizeContent path (`[1].state`) into navigation tokens; set the value at it (immutably).
|
|
143
|
+
function parsePath( path ) {
|
|
144
|
+
const toks = [], re = /\[(\d+)\]|\.?([A-Za-z_]\w*)/g; let m;
|
|
145
|
+
while ( (m = re.exec(path)) ) { if ( m[1] != null ) toks.push({ i: +m[1] }); else if ( m[2] != null ) toks.push({ k: m[2] }); }
|
|
146
|
+
return toks;
|
|
147
|
+
}
|
|
148
|
+
function setAtPath( tpl, path, value ) {
|
|
149
|
+
const toks = parsePath(path), clone = JSON.parse(JSON.stringify(tpl));
|
|
150
|
+
let cur = clone;
|
|
151
|
+
for ( let i = 0; i < toks.length - 1; i++ ) { const t = toks[i]; cur = t.i != null ? cur[t.i] : cur[t.k]; if ( cur == null ) return clone; }
|
|
152
|
+
const last = toks[toks.length - 1];
|
|
153
|
+
if ( last.i != null ) cur[last.i] = value; else cur[last.k] = value;
|
|
154
|
+
return clone;
|
|
155
|
+
}
|
|
156
|
+
const projectFacts = ( facts, keys ) => { const o = {}; for ( const k of (keys || []) ) if ( facts && k in facts ) o[k] = facts[k]; return o; };
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* §6.3(b) WIRING — enhance a crystallized candidate with ancestry promotions. A content leaf the ancestry oracle
|
|
160
|
+
* promotes (`decideLeaf` → promote) becomes a frontier REF bound from the ancestor at the call site; the EXACT post
|
|
161
|
+
* `$leaf==$g` is added; and (opt-in `dropKeys`) the now-redundant signature key is DROPPED — so when promotion removes
|
|
162
|
+
* the SOLE differentiator the templates collapse to one and the method GENERALIZES to UNSEEN signature classes (it
|
|
163
|
+
* rebinds the leaf from the ancestor) instead of bypassing. Sound by the held-out FD + the exact post (the runtime monitor).
|
|
164
|
+
* @param candidate a crystallizeStructural candidate `{ schema, providerName, signatureKeys, frontierFields, templatesBySig }`.
|
|
165
|
+
* @param opts.leaves [{ path, observations }] — the content leaves + their per-instance (value, ancestry).
|
|
166
|
+
* @param opts.sigmaSep/minK/fieldProjection forwarded to `decideLeaf` (the horizon + the determinacy discipline).
|
|
167
|
+
* @param opts.ancestorField (g) => the scope field the ancestor fact lives on (default `g` — the cast object's own fact;
|
|
168
|
+
* pass e.g. (g)=>'originNode:'+g for a predecessor cross-walk, which `ctxFromScope` resolves).
|
|
169
|
+
* @param opts.dropKeys drop a promoted ancestor from `signatureKeys` (+ collapse templates) when it leaves an
|
|
170
|
+
* empty reduced signature and all rewritten templates coincide (the generalization). Default false.
|
|
171
|
+
* @returns the ENHANCED candidate (templates rewritten, frontier params + posts added, provider rebuilt) + `promotions`.
|
|
172
|
+
*/
|
|
173
|
+
function enhanceCandidateWithAncestry( candidate, opts ) {
|
|
174
|
+
opts = opts || {};
|
|
175
|
+
const { buildStructuralProvider } = require('./crystallize.js'); // lazy (avoid any load-order coupling)
|
|
176
|
+
const ancestorField = opts.ancestorField || (( g ) => g);
|
|
177
|
+
const cand = JSON.parse(JSON.stringify({ schema: candidate.schema, signatureKeys: candidate.signatureKeys || [], frontierFields: candidate.frontierFields || {}, templatesBySig: candidate.templatesBySig || {} }));
|
|
178
|
+
|
|
179
|
+
const promotions = [];
|
|
180
|
+
for ( const leaf of (opts.leaves || []) ) {
|
|
181
|
+
const d = decideLeaf({ observations: leaf.observations, sigmaSep: opts.sigmaSep, minK: opts.minK, fieldProjection: opts.fieldProjection, leafKey: leafKeyOf(leaf.path) });
|
|
182
|
+
if ( d.bin === 'promote' ) promotions.push({ path: leaf.path, refName: d.promotion.refName, ancestorFact: d.promotion.ancestorFact, field: d.promotion.field, post: d.post });
|
|
183
|
+
}
|
|
184
|
+
if ( !promotions.length ) return Object.assign({}, candidate, { promotions: [] });
|
|
185
|
+
|
|
186
|
+
// rewrite every template: the promoted leaf → its frontier REF.
|
|
187
|
+
for ( const sig of Object.keys(cand.templatesBySig) )
|
|
188
|
+
for ( const pr of promotions ) cand.templatesBySig[sig] = setAtPath(cand.templatesBySig[sig], pr.path, REF(pr.refName));
|
|
189
|
+
|
|
190
|
+
// add the frontier params + frontierFields + the EXACT posts; (opt-in) drop the redundant signature key.
|
|
191
|
+
cand.schema.frontier = cand.schema.frontier || { params: [], appConditions: { require: [], assert: [] } };
|
|
192
|
+
cand.schema.contract = cand.schema.contract || { read: [], write: [], pre: [], post: [], effect: 'pure' };
|
|
193
|
+
for ( const pr of promotions ) {
|
|
194
|
+
const fieldPath = ancestorField(pr.ancestorFact) + (pr.field ? '.' + pr.field : '');
|
|
195
|
+
cand.schema.frontier.params.push({ name: pr.refName, sort: 'node-ref', role: 'endpoint', field: fieldPath, requiredFacts: [] });
|
|
196
|
+
cand.frontierFields[pr.refName] = fieldPath;
|
|
197
|
+
if ( cand.schema.contract.post.indexOf(pr.post) < 0 ) cand.schema.contract.post.push(pr.post);
|
|
198
|
+
if ( opts.dropKeys ) cand.signatureKeys = cand.signatureKeys.filter(( k ) => k !== pr.ancestorFact);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// COLLAPSE: if dropping left an empty reduced signature and all rewritten templates now coincide (the promoted
|
|
202
|
+
// leaf was the SOLE differentiator), re-key to ONE template under the reduced-signature digest → generalizes.
|
|
203
|
+
if ( opts.dropKeys && !cand.signatureKeys.length ) {
|
|
204
|
+
const tpls = Object.values(cand.templatesBySig);
|
|
205
|
+
const allSame = tpls.every(( t ) => canon(t) === canon(tpls[0]));
|
|
206
|
+
if ( allSame && tpls.length ) cand.templatesBySig = { [digestOf(projectFacts({}, cand.signatureKeys))]: tpls[0] };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const provider = buildStructuralProvider({ cryId: cand.schema._name, frontier: cand.schema.frontier, frontierFields: cand.frontierFields, templatesBySig: cand.templatesBySig, signatureKeys: cand.signatureKeys });
|
|
210
|
+
return { schema: cand.schema, providerName: candidate.providerName, provider, signatureKeys: cand.signatureKeys, frontierFields: cand.frontierFields, templatesBySig: cand.templatesBySig, promotions };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// the digest the structural provider keys on (must match buildStructuralProvider's `digest(projectFacts(...))`).
|
|
214
|
+
const digestOf = require('../providers/canonicalize.js').digest;
|
|
215
|
+
|
|
216
|
+
module.exports = { decideLeaf, promoteContentVars, rewriteSkeleton, leafKeyOf, enhanceCandidateWithAncestry, setAtPath };
|