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,138 @@
|
|
|
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
|
+
* method-explorer — LIST + JUDGE a concept-method population (host-side, ZERO-CORE, fs-free). The owner's
|
|
10
|
+
* "outil/explorer" ask (2026-07-05): give every crystallized method a TITLE / mini-DESCRIPTION / CATEGORY, and
|
|
11
|
+
* summarize the population so its QUALITY, its OPENNESS (diversity), and its COVERAGE (which declared task-
|
|
12
|
+
* classes have a method — and which are GAPS) are legible. Reads any library SOURCE method-pack normalizes
|
|
13
|
+
* (a master loop / recall index / { entries } / array / a `.sgc methods` bundle's `.methods`).
|
|
14
|
+
*
|
|
15
|
+
* A concept-method is a TYPED decomposition: a class SIGNATURE (the K1 `structure` discriminant keys) → a
|
|
16
|
+
* derivation over `content` holes → reusable templates (`templatesBySig`). So:
|
|
17
|
+
* - CATEGORY = the canonical structure signature (the class the method serves).
|
|
18
|
+
* - TITLE = the structure values, human-legible.
|
|
19
|
+
* - COVERAGE = per structure-key, which VALUES have a method; against a declared vocabulary (a registry enum
|
|
20
|
+
* or an explicit `expected`), the MISSING values = the population's gaps (the actionable metric).
|
|
21
|
+
* - OPENNESS = distinct classes, singleton fraction (one-off vs reused), templates/method (reuse depth),
|
|
22
|
+
* and the Shannon ENTROPY of the class distribution (spread vs concentration).
|
|
23
|
+
*
|
|
24
|
+
* const { describeLibrary, formatLibrary } = require('skynet-graph/lib/authoring/method-explorer');
|
|
25
|
+
* const report = describeLibrary(masterLoop, { registry }); // registry → coverage gaps vs the declared canon
|
|
26
|
+
* console.log(formatLibrary(report)); // a text listing + population summary
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const { toEntries } = require('./method-pack.js');
|
|
30
|
+
|
|
31
|
+
function canon( x ) {
|
|
32
|
+
if ( x === undefined ) return 'null';
|
|
33
|
+
if ( x === null || typeof x !== 'object' ) return JSON.stringify(x);
|
|
34
|
+
if ( Array.isArray(x) ) return '[' + x.map(canon).join(',') + ']';
|
|
35
|
+
return '{' + Object.keys(x).sort().map(( k ) => JSON.stringify(k) + ':' + canon(x[k])).join(',') + '}';
|
|
36
|
+
}
|
|
37
|
+
function templateCountOf( method ) {
|
|
38
|
+
if ( !method || typeof method !== 'object' ) return 0;
|
|
39
|
+
if ( method.templatesBySig && typeof method.templatesBySig === 'object' ) return Object.keys(method.templatesBySig).length;
|
|
40
|
+
if ( Array.isArray(method.templates) ) return method.templates.length;
|
|
41
|
+
return method.template || method.derivation ? 1 : 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A single method's descriptor: id, title, category, mini-description + the typed keys and template count.
|
|
46
|
+
* @param entry a normalized `{ structure, content, method }`
|
|
47
|
+
*/
|
|
48
|
+
function describeMethod( entry ) {
|
|
49
|
+
const s = entry.structure || {}, c = entry.content || {};
|
|
50
|
+
const sKeys = Object.keys(s).sort(), cKeys = Object.keys(c).sort();
|
|
51
|
+
const category = sKeys.length ? sKeys.map(( k ) => k + '=' + s[k]).join(',') : '(untyped)';
|
|
52
|
+
const title = sKeys.length ? sKeys.map(( k ) => String(s[k])).join(' · ') : '(untyped)';
|
|
53
|
+
const templates = templateCountOf(entry.method);
|
|
54
|
+
const id = (entry.method && entry.method.id) || 'm:' + canon(s);
|
|
55
|
+
return {
|
|
56
|
+
id, title, category,
|
|
57
|
+
description: `keys on {${sKeys.join(', ') || '∅'}} → derives {${cKeys.join(', ') || '∅'}} · ${templates} template${templates === 1 ? '' : 's'}`,
|
|
58
|
+
structureKeys: sKeys, contentKeys: cKeys, templateCount: templates
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function entropyBits( counts ) {
|
|
63
|
+
const total = counts.reduce(( a, b ) => a + b, 0);
|
|
64
|
+
if ( total <= 0 ) return 0;
|
|
65
|
+
let h = 0;
|
|
66
|
+
for ( const n of counts ) if ( n > 0 ) { const p = n / total; h -= p * Math.log2(p); }
|
|
67
|
+
return h;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Describe + judge a whole method population.
|
|
72
|
+
* @param source any library source (see `toEntries`) — a master loop / recall index / { entries } / array /
|
|
73
|
+
* a `.sgc methods` bundle's `.methods`.
|
|
74
|
+
* @param opts { registry } to compute coverage GAPS against the declared enum vocab, or { expected:{ <key>:
|
|
75
|
+
* [values] } } for an explicit declared value-space per structure key.
|
|
76
|
+
* @returns {{ methods, population:{ count, categories, coverage, openness } }}
|
|
77
|
+
*/
|
|
78
|
+
function describeLibrary( source, opts ) {
|
|
79
|
+
opts = opts || {};
|
|
80
|
+
const entries = toEntries(source);
|
|
81
|
+
const methods = entries.map(describeMethod);
|
|
82
|
+
|
|
83
|
+
// class distribution (the CATEGORY = the served signature)
|
|
84
|
+
const catCount = {};
|
|
85
|
+
for ( const m of methods ) catCount[m.category] = (catCount[m.category] || 0) + 1;
|
|
86
|
+
const categories = Object.keys(catCount).map(( category ) => ({ category, count: catCount[category] }))
|
|
87
|
+
.sort(( a, b ) => b.count - a.count || (a.category < b.category ? -1 : 1));
|
|
88
|
+
|
|
89
|
+
// COVERAGE — per structure-key present values, and the declared-vocab GAPS (missing values with no method)
|
|
90
|
+
const keyVals = {};
|
|
91
|
+
for ( const e of entries ) for ( const k of Object.keys(e.structure || {}) ) (keyVals[k] = keyVals[k] || new Set()).add(String(e.structure[k]));
|
|
92
|
+
const expectedFor = ( key ) => {
|
|
93
|
+
if ( opts.expected && opts.expected[key] ) return opts.expected[key];
|
|
94
|
+
const entry = opts.registry && opts.registry.keys && opts.registry.keys[key];
|
|
95
|
+
return entry && entry.enum ? entry.enum : null;
|
|
96
|
+
};
|
|
97
|
+
const coverage = Object.keys(keyVals).sort().map(( key ) => {
|
|
98
|
+
const present = [...keyVals[key]].sort();
|
|
99
|
+
const c = { key, present, covered: present.length };
|
|
100
|
+
const exp = expectedFor(key);
|
|
101
|
+
if ( exp ) { const has = keyVals[key]; c.expected = exp.length; c.missing = exp.filter(( v ) => !has.has(String(v))).sort(); c.fraction = exp.length ? present.filter(( v ) => exp.map(String).includes(v)).length / exp.length : 0; }
|
|
102
|
+
return c;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// OPENNESS — diversity of the population
|
|
106
|
+
const singleton = categories.filter(( c ) => c.count === 1 ).length;
|
|
107
|
+
const totalTemplates = methods.reduce(( s, m ) => s + m.templateCount, 0);
|
|
108
|
+
const openness = {
|
|
109
|
+
distinctClasses: categories.length,
|
|
110
|
+
singletonFraction: categories.length ? singleton / categories.length : 0,
|
|
111
|
+
avgTemplatesPerMethod: methods.length ? totalTemplates / methods.length : 0,
|
|
112
|
+
entropyBits: entropyBits(categories.map(( c ) => c.count )),
|
|
113
|
+
maxEntropyBits: categories.length > 1 ? Math.log2(categories.length) : 0
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return { methods, population: { count: methods.length, categories, coverage, openness } };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Render a `describeLibrary` report as a compact text listing + a population summary (the CLI/explorer view). */
|
|
120
|
+
function formatLibrary( report ) {
|
|
121
|
+
const L = [];
|
|
122
|
+
const p = report.population;
|
|
123
|
+
L.push(`CONCEPT-METHODS — ${p.count} method(s), ${p.openness.distinctClasses} distinct class(es)`);
|
|
124
|
+
L.push('');
|
|
125
|
+
for ( const m of report.methods ) L.push(` • ${m.title.padEnd(24)} [${m.category}] ${m.description}`);
|
|
126
|
+
L.push('');
|
|
127
|
+
L.push(`POPULATION`);
|
|
128
|
+
L.push(` classes (top): ${p.categories.slice(0, 8).map(( c ) => c.category + '×' + c.count).join(' · ')}`);
|
|
129
|
+
L.push(` openness: ${p.openness.distinctClasses} classes · singletons ${(100 * p.openness.singletonFraction).toFixed(0)}% · templates/method ${p.openness.avgTemplatesPerMethod.toFixed(2)} · entropy ${p.openness.entropyBits.toFixed(2)}/${p.openness.maxEntropyBits.toFixed(2)} bits`);
|
|
130
|
+
L.push(` coverage:`);
|
|
131
|
+
for ( const c of p.coverage ) {
|
|
132
|
+
if ( c.expected != null ) L.push(` ${c.key}: ${c.covered}/${c.expected} (${(100 * c.fraction).toFixed(0)}%)${c.missing.length ? ' · GAPS: ' + c.missing.join(', ') : ' · complete'}`);
|
|
133
|
+
else L.push(` ${c.key}: ${c.covered} value(s) present [${c.present.join(', ')}]`);
|
|
134
|
+
}
|
|
135
|
+
return L.join('\n');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = { describeMethod, describeLibrary, formatLibrary, templateCountOf };
|
|
@@ -0,0 +1,181 @@
|
|
|
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
|
+
* method-pack — the `.sgc` CRYSTALLIZED-METHOD PACKAGE (host-side, ZERO-CORE, fs-free). M3 of the
|
|
10
|
+
* productization campaign / master-graph study persistence rung. The SIBLING of `corpus-pack.js`: that one
|
|
11
|
+
* packages the AUTHORED grammar (a concept tree → a portable bundle with a derived manifest); THIS one
|
|
12
|
+
* packages the LEARNED method library (the forged + crystallized methods the master loop accumulates) so a
|
|
13
|
+
* warm library MOVES between deployments — not just survives a local restart (that is M2 / `store.js`).
|
|
14
|
+
*
|
|
15
|
+
* It reuses the SAME `.sgc` envelope (`format:'sgc'` + `sgcVersion` + `manifest`), discriminated by a
|
|
16
|
+
* `kind` field: `'corpus'` (the existing grammar bundle) vs `'methods'` (this one). One exchange format,
|
|
17
|
+
* two payload kinds; `corpus-pack.js` is untouched and a reader dispatches on `kind` (default `'corpus'`).
|
|
18
|
+
*
|
|
19
|
+
* const bundle = packMethods(masterLoop, { name: 'travel', version: 'v3' }); // .sgc JSON
|
|
20
|
+
* // ... ship the bundle to another deployment, write it to disk via store.saveSgc ...
|
|
21
|
+
* const r = loadMethods(bundle, freshLoop, { version: 'v3' }); // re-hydrate the library
|
|
22
|
+
* // r.added entries recalled; r.exactReplayed entries warm the 0-call cache (iff versions match)
|
|
23
|
+
*
|
|
24
|
+
* THE PORTABLE UNIT is the recall-index entry `{ structure, content, method }` — signature-addressed, so a
|
|
25
|
+
* receiver can RECALL→VERIFY it (U5). The exact content-address cache is a DERIVED acceleration: its key is
|
|
26
|
+
* recomputable from each entry's signature via the host loop's own `keyOf`, so packing the index is enough.
|
|
27
|
+
*
|
|
28
|
+
* THE LOAD-BEARING SOUNDNESS LINE (B8 — method-version pinning, made portable):
|
|
29
|
+
* A method is `(typed signature) → derivation`; that FUNCTION is defined by the grammar + the providers,
|
|
30
|
+
* whose identity IS the version token. Replaying a packaged method verbatim is sound in a receiving host
|
|
31
|
+
* IFF the host's version matches the package's — otherwise the same typed input may now map to a DIFFERENT
|
|
32
|
+
* derivation, so NO verbatim replay (neither the exact 0-call cache NOR a recall→verify `'full'` hit, which
|
|
33
|
+
* also replays the stored method at 0 cost) is trustworthy. So the version gate covers BOTH replay paths:
|
|
34
|
+
* - versions AGREE → hydrate the recall index AND the exact cache (full 0-call replay, sound).
|
|
35
|
+
* - versions DIFFER → hydrate NEITHER: the host re-forges (sound). The entries stay readable via
|
|
36
|
+
* `unpackMethods` for a host that wants to re-forge FROM the skeletons (a future version-aware-recall
|
|
37
|
+
* refinement could downgrade a cross-version `'full'` to a partial re-forge; not built — refuse beats
|
|
38
|
+
* a stale replay: "fuzziness in recall, exactness in truth" — a cross-version method is not TRUTH here).
|
|
39
|
+
* Versions are opt-in (like `cache.js` `version()`): the gate enforces iff BOTH sides declare a version
|
|
40
|
+
* (absent ⇒ permissive — the host explicitly opted out of pinning). Independently, the typed VERIFY
|
|
41
|
+
* (`recall.js#verify`) ALSO runs on the receiver, so even a same-version package never replays a method
|
|
42
|
+
* whose typed STRUCTURE mismatches a local query (the canonicalize.js line holds across hosts).
|
|
43
|
+
*
|
|
44
|
+
* A typed SCHEMA is DERIVED from the entries (mirrors `corpus-pack.js#deriveManifest`): the method-CLASS
|
|
45
|
+
* discriminants (the K1 `structure` keys) and the derived `content` keys — self-describing, no hand-upkeep,
|
|
46
|
+
* so a receiving host knows what typed inputs key each method class without reading the bodies.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
// stable, key-sorted, recursive stringify (digest-safe; the same canon the recall index + master loop use).
|
|
50
|
+
function canon( x ) {
|
|
51
|
+
if ( x === undefined ) return 'null';
|
|
52
|
+
if ( x === null || typeof x !== 'object' ) return JSON.stringify(x);
|
|
53
|
+
if ( Array.isArray(x) ) return '[' + x.map(canon).join(',') + ']';
|
|
54
|
+
return '{' + Object.keys(x).sort().map(( k ) => JSON.stringify(k) + ':' + canon(x[k])).join(',') + '}';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function clone( x ) { return x == null ? x : JSON.parse(JSON.stringify(x)); }
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Normalize any reasonable library SOURCE into a flat array of `{ structure, content, method }` entries:
|
|
61
|
+
* - a master loop (has `.index` recall index) → its index entries
|
|
62
|
+
* - a recall index (has `.entries` + `.recall`) → its entries
|
|
63
|
+
* - `{ entries:[...] }` → those entries
|
|
64
|
+
* - a bare array → itself
|
|
65
|
+
* Each raw entry may be `{ sig:{structure,content}, method }` (the index shape) or already
|
|
66
|
+
* `{ structure, content, method }`. Missing structure/content default to `{}`.
|
|
67
|
+
*/
|
|
68
|
+
function toEntries( source ) {
|
|
69
|
+
let raw;
|
|
70
|
+
if ( Array.isArray(source) ) raw = source;
|
|
71
|
+
else if ( source && source.index && Array.isArray(source.index.entries) ) raw = source.index.entries;
|
|
72
|
+
else if ( source && Array.isArray(source.entries) ) raw = source.entries;
|
|
73
|
+
else raw = [];
|
|
74
|
+
return raw.map(( e ) => {
|
|
75
|
+
const structure = (e.sig ? e.sig.structure : e.structure) || {};
|
|
76
|
+
const content = (e.sig ? e.sig.content : e.content) || {};
|
|
77
|
+
return { structure: clone(structure), content: clone(content), method: clone(e.method) };
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Derive the typed SCHEMA off the method entries — the self-describing contract a receiver needs.
|
|
83
|
+
* @returns {{ structureKeys, contentKeys, classes:[{structure,count}] }}
|
|
84
|
+
* structureKeys the union of K1 method-CLASS discriminant keys (what defines a method class).
|
|
85
|
+
* contentKeys the union of derived `content` keys (the per-instance holes).
|
|
86
|
+
* classes the distinct typed structures present, with how many entries each covers.
|
|
87
|
+
*/
|
|
88
|
+
function deriveMethodSchema( entries ) {
|
|
89
|
+
const sKeys = new Set(), cKeys = new Set(), classes = new Map();
|
|
90
|
+
for ( const e of entries ) {
|
|
91
|
+
for ( const k of Object.keys(e.structure || {}) ) sKeys.add(k);
|
|
92
|
+
for ( const k of Object.keys(e.content || {}) ) cKeys.add(k);
|
|
93
|
+
const key = canon(e.structure || {});
|
|
94
|
+
const c = classes.get(key) || { structure: clone(e.structure || {}), count: 0 };
|
|
95
|
+
c.count++; classes.set(key, c);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
structureKeys: [...sKeys].sort(),
|
|
99
|
+
contentKeys : [...cKeys].sort(),
|
|
100
|
+
classes : [...classes.values()].sort(( a, b ) => canon(a.structure) < canon(b.structure) ? -1 : 1)
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Pack a learned method library into a portable `.sgc` methods bundle (plain JSON).
|
|
106
|
+
* @param source a master loop / recall index / { entries } / array (see `toEntries`)
|
|
107
|
+
* @param opts { name, version, description }
|
|
108
|
+
*/
|
|
109
|
+
function packMethods( source, opts ) {
|
|
110
|
+
opts = opts || {};
|
|
111
|
+
const entries = toEntries(source);
|
|
112
|
+
return {
|
|
113
|
+
format : 'sgc',
|
|
114
|
+
sgcVersion: 1,
|
|
115
|
+
kind : 'methods',
|
|
116
|
+
manifest : {
|
|
117
|
+
name : opts.name || 'methods',
|
|
118
|
+
version : opts.version || '0.0.0',
|
|
119
|
+
description: opts.description || '',
|
|
120
|
+
methodCount: entries.length,
|
|
121
|
+
schema : deriveMethodSchema(entries)
|
|
122
|
+
},
|
|
123
|
+
methods : entries
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Unpack a `.sgc` methods bundle. Pure read — returns the entries + manifest + derived schema and a
|
|
129
|
+
* version-gate verdict (does NOT mutate any host). Throws on a non-methods bundle.
|
|
130
|
+
* @param bundle a packMethods() output
|
|
131
|
+
* @param opts { hostVersion } — if given, compared against the package version for `exactReplaySafe`.
|
|
132
|
+
* @returns {{ methods, manifest, schema, versionPackage, exactReplaySafe }}
|
|
133
|
+
*/
|
|
134
|
+
function unpackMethods( bundle, opts ) {
|
|
135
|
+
opts = opts || {};
|
|
136
|
+
if ( !bundle || bundle.format !== 'sgc' ) throw new Error('not an .sgc bundle');
|
|
137
|
+
if ( bundle.kind !== 'methods' ) throw new Error('not a .sgc methods bundle (kind=' + bundle.kind + ')');
|
|
138
|
+
const methods = bundle.methods || [];
|
|
139
|
+
const manifest = bundle.manifest || {};
|
|
140
|
+
return {
|
|
141
|
+
methods, manifest, schema: manifest.schema || deriveMethodSchema(methods),
|
|
142
|
+
versionPackage : manifest.version,
|
|
143
|
+
exactReplaySafe: versionsAgree(opts.hostVersion, manifest.version)
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// B8 gating: active iff BOTH sides declare a version (opt-in, like cache.js#version). Absent ⇒ permissive.
|
|
148
|
+
function versionsAgree( vHost, vPkg ) {
|
|
149
|
+
if ( vHost == null || vPkg == null ) return true; // no pinning requested → permissive
|
|
150
|
+
return vHost === vPkg;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Re-hydrate a `.sgc` methods bundle INTO a host library, gated by the version (B8). On a version MATCH it
|
|
155
|
+
* feeds BOTH replay paths — the recall index AND the exact 0-call cache; on a MISMATCH it hydrates NEITHER
|
|
156
|
+
* (the host re-forges; the stale bodies never write a derivation here). See the soundness line in the header.
|
|
157
|
+
*
|
|
158
|
+
* @param bundle a packMethods() output
|
|
159
|
+
* @param host { index, cache, keyOf } — e.g. a master loop (it exposes exactly these). `cache`/`keyOf`
|
|
160
|
+
* optional: without them only the recall index is hydrated.
|
|
161
|
+
* @param opts { version } — the host's method-lib version (B8). Omit to opt out of version pinning.
|
|
162
|
+
* @returns {{ added, exactReplayed, exactReplaySafe, skipped, versionHost, versionPackage }}
|
|
163
|
+
*/
|
|
164
|
+
function loadMethods( bundle, host, opts ) {
|
|
165
|
+
opts = opts || {};
|
|
166
|
+
const { methods, versionPackage } = unpackMethods(bundle, { hostVersion: opts.version });
|
|
167
|
+
const safe = versionsAgree(opts.version, versionPackage);
|
|
168
|
+
let added = 0, exactReplayed = 0;
|
|
169
|
+
if ( !safe ) // version mismatch → refuse to inject stale methods into the live replay paths (re-forge instead).
|
|
170
|
+
return { added: 0, exactReplayed: 0, exactReplaySafe: false, skipped: methods.length, versionHost: opts.version, versionPackage };
|
|
171
|
+
for ( const e of methods ) {
|
|
172
|
+
const sig = { structure: e.structure, content: e.content };
|
|
173
|
+
if ( host && host.index && host.index.add ) { host.index.add(sig, clone(e.method)); added++; } // recall path
|
|
174
|
+
if ( host && host.cache && host.cache.set && typeof host.keyOf === 'function' ) {
|
|
175
|
+
host.cache.set(host.keyOf(sig), clone(e.method)); exactReplayed++; // exact 0-call path
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return { added, exactReplayed, exactReplaySafe: true, skipped: 0, versionHost: opts.version, versionPackage };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = { packMethods, unpackMethods, deriveMethodSchema, loadMethods, toEntries };
|
|
@@ -0,0 +1,202 @@
|
|
|
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
|
+
* method — HIGHER-ORDER methods-as-graphs: a method receives a SUB-GRAPH as a typed named-slot parameter
|
|
10
|
+
* and applies it (host-side, builds on `abstract.js`; engine-native splice via `pushMutation`). BRICK 1 of
|
|
11
|
+
* the conception build (doc/WIP/studies/2026-06-28-concept-as-graph-conception-assembled.md §3 / C2).
|
|
12
|
+
*
|
|
13
|
+
* The parameter is FIRST-ORDER in mechanics: a body is a PARAMETERIZED template (holes from
|
|
14
|
+
* `abstract.js#relativize`) bound BY NAME at a call site (`instantiate`) — never inferred. So "method takes
|
|
15
|
+
* a sub-graph" is decidable substitution, not higher-order unification.
|
|
16
|
+
*
|
|
17
|
+
* Landmines respected: each application gets a FRESH id-base (finding #30 — else N applications collapse
|
|
18
|
+
* onto one body via the existing-object merge path), the splice goes through the SEQUENCED `pushMutation`
|
|
19
|
+
* (determinism), and an unbound frontier ref THROWS (a leak) rather than splicing an unsound partial.
|
|
20
|
+
*/
|
|
21
|
+
const { instantiate, refHolesOf } = require('./abstract.js');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Bind a parameterized body sub-graph to a NEW call site → a ground template. Throws on an unbound frontier
|
|
25
|
+
* ref (a leak) — never returns a partially-bound template.
|
|
26
|
+
* @param paramBody parameterized template (holes via `relativize`)
|
|
27
|
+
* @param ctx { base, refs:{name:id} } the call site (fresh base + the slot's frontier ids)
|
|
28
|
+
* @returns the ground (bound) template
|
|
29
|
+
*/
|
|
30
|
+
function bindSubgraphArg( paramBody, ctx ) {
|
|
31
|
+
const bound = instantiate(paramBody, ctx);
|
|
32
|
+
if ( bound == null )
|
|
33
|
+
throw new Error('applySubgraphArg: unbound frontier ref (leak) for refs=' + JSON.stringify((ctx && ctx.refs) || {}));
|
|
34
|
+
return bound;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Bind a body sub-graph into a call-site SLOT and splice it SEQUENCED under the slot (parented via
|
|
39
|
+
* `_origin = targetId`).
|
|
40
|
+
* @param graph
|
|
41
|
+
* @param paramBody the parameterized body
|
|
42
|
+
* @param ctx { base, refs } the call site (fresh base + frontier ids)
|
|
43
|
+
* @param targetId the slot object the splice mounts under (the parent)
|
|
44
|
+
* @param cb called once the splice settles
|
|
45
|
+
* @returns the bound template
|
|
46
|
+
*/
|
|
47
|
+
function applySubgraphArg( graph, paramBody, ctx, targetId, cb ) {
|
|
48
|
+
const bound = bindSubgraphArg(paramBody, ctx);
|
|
49
|
+
graph.pushMutation(bound, targetId, false, undefined, undefined, cb);
|
|
50
|
+
return bound;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* BUILD the MAP combinator's template (PURE — no graph mutation): apply a body sub-graph to EACH element of
|
|
55
|
+
* a collection, each instance with its OWN fresh id-base (so N elements never collide onto one body, #30).
|
|
56
|
+
* Returned as ONE combined template — a provider returns this via `cb` (the ENGINE applies it, parented under
|
|
57
|
+
* the cast slot), or `mapSubgraph` pushes it directly.
|
|
58
|
+
* @param opts.elements [id] the collection element node ids (the CASES)
|
|
59
|
+
* @param opts.body the parameterized body (the PARAM); frontier `elem` is bound to each element
|
|
60
|
+
* @param opts.basePrefix id-base prefix per element (default 'map' → map0, map1, …)
|
|
61
|
+
* @param opts.refsOf optional (elem,i) => extra frontier refs to bind besides `elem`
|
|
62
|
+
* @returns the combined ground template (array)
|
|
63
|
+
*/
|
|
64
|
+
function mapTemplate( opts ) {
|
|
65
|
+
opts = opts || {};
|
|
66
|
+
const elements = opts.elements || [];
|
|
67
|
+
const prefix = opts.basePrefix || 'map';
|
|
68
|
+
const tpl = [];
|
|
69
|
+
elements.forEach(function ( elem, i ) {
|
|
70
|
+
const refs = Object.assign({ elem: elem }, opts.refsOf ? opts.refsOf(elem, i) : {});
|
|
71
|
+
tpl.push.apply(tpl, bindSubgraphArg(opts.body, { base: prefix + i, refs: refs }));
|
|
72
|
+
});
|
|
73
|
+
return tpl;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The MAP combinator, HOST-driven: build the fan-out template and splice it SEQUENCED under the slot.
|
|
78
|
+
* (The ENGINE-driven form is a provider returning `mapTemplate(...)` — see method-subgraph.test.js.)
|
|
79
|
+
* @param graph
|
|
80
|
+
* @param opts as `mapTemplate` + `opts.slotId` (the map segment the bodies mount under)
|
|
81
|
+
* @param cb called once the map settles
|
|
82
|
+
* @returns the combined ground template
|
|
83
|
+
*/
|
|
84
|
+
function mapSubgraph( graph, opts, cb ) {
|
|
85
|
+
const tpl = mapTemplate(opts);
|
|
86
|
+
if ( !tpl.length ) { if ( cb ) setTimeout(cb); return tpl; }
|
|
87
|
+
graph.pushMutation(tpl, opts.slotId, false, undefined, undefined, cb);
|
|
88
|
+
return tpl;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------- the method LINT (Brick 2)
|
|
92
|
+
|
|
93
|
+
// kinds we treat as K1-typed slot containers; anything else (incl. these prose markers) is a memo footgun.
|
|
94
|
+
const TYPED_KINDS = new Set(['enum', 'id', 'number', 'bool', 'list', 'subgraph']);
|
|
95
|
+
const PROSE_TYPES = new Set(['prose', 'text', 'string', 'str', 'freetext', 'free-text']);
|
|
96
|
+
|
|
97
|
+
// the fact keys a pre/post entry touches: the $refs it mentions, or — if it is a bare key (no $) — itself.
|
|
98
|
+
function frameKeys( entries ) {
|
|
99
|
+
const keys = new Set();
|
|
100
|
+
for ( const e of (entries || []) ) {
|
|
101
|
+
if ( typeof e !== 'string' ) continue;
|
|
102
|
+
const refs = e.match(/\$\$?([A-Za-z_][\w.:]*)/g);
|
|
103
|
+
if ( refs ) refs.forEach(( r ) => keys.add(r.replace(/^\$\$?/, '')));
|
|
104
|
+
else keys.add(e.trim());
|
|
105
|
+
}
|
|
106
|
+
return keys;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const isTypedName = ( t, opts ) =>
|
|
110
|
+
typeof t === 'string' && t.length > 0 && !PROSE_TYPES.has(t.toLowerCase()) &&
|
|
111
|
+
(!opts || !opts.types || opts.types.indexOf(t) >= 0);
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* lintMethod — enforce the decidability invariants + the footprint/frame check on a METHOD DEFINITION, so the
|
|
115
|
+
* "decidable line" is CHECKED, not emergent (design doc §3 / C4). Mirrors `validate.js`: `{ errors, warnings }`
|
|
116
|
+
* with records `{ method, kind, message, slot? }`. Errors = a method that breaks an invariant; an uncontracted
|
|
117
|
+
* method is a WARNING (the cost gradient — a runtime micro-LLM fallback, §0.1), not an error.
|
|
118
|
+
*
|
|
119
|
+
* @param def { name, slots:{ <name>:{ role:'param'|'case', kind, frontier?, in?, out?, elem? } }, body?, contract? }
|
|
120
|
+
* @param opts { types? } optional host type-alphabet — if given, slot type names must be in it.
|
|
121
|
+
*/
|
|
122
|
+
function lintMethod( def, opts ) {
|
|
123
|
+
opts = opts || {};
|
|
124
|
+
const errors = [], warnings = [];
|
|
125
|
+
const method = (def && def.name) || '?';
|
|
126
|
+
const err = ( kind, message, slot ) => errors.push({ method, kind, message, slot });
|
|
127
|
+
const slots = (def && def.slots) || {};
|
|
128
|
+
const used = def && def.body != null ? refHolesOf(def.body) : null;
|
|
129
|
+
|
|
130
|
+
for ( const name of Object.keys(slots) ) {
|
|
131
|
+
const s = slots[name] || {};
|
|
132
|
+
if ( !name ) { err('unnamed-slot', 'a slot has an empty name'); continue; } // (a)
|
|
133
|
+
if ( s.infer ) err('inference-slot', `slot "${name}" is infer:true — a body is SUPPLIED, never solved-for (undecidable)`, name); // (c)
|
|
134
|
+
if ( !TYPED_KINDS.has(s.kind) ) // (b) K1
|
|
135
|
+
err('prose-slot', `slot "${name}" kind "${s.kind}" is not K1-typed — a prose/untyped key re-keys every run (memo death)`, name);
|
|
136
|
+
if ( s.kind === 'subgraph' ) {
|
|
137
|
+
if ( !isTypedName(s.in, opts) || !isTypedName(s.out, opts) ) // (b) typed interface
|
|
138
|
+
err('prose-interface', `sub-graph slot "${name}" has a non-typed in/out interface (in=${s.in}, out=${s.out})`, name);
|
|
139
|
+
if ( s.role === 'param' && !('frontier' in s) ) // (c) supplied
|
|
140
|
+
err('unbound-param', `param sub-graph slot "${name}" has no frontier — it must be supplied by binding, not inferred`, name);
|
|
141
|
+
if ( 'frontier' in s && !Array.isArray(s.frontier) ) // (d) fixed tentacles
|
|
142
|
+
err('variable-tentacles', `slot "${name}" frontier must be a FIXED array, got ${JSON.stringify(s.frontier)}`, name);
|
|
143
|
+
if ( Array.isArray(s.frontier) && used ) // (c2) decl ↔ impl
|
|
144
|
+
lintFrontierMatch(s.frontier, used, name, err);
|
|
145
|
+
}
|
|
146
|
+
if ( s.kind === 'list' && !isTypedName(s.elem, opts) ) // (b) typed elem
|
|
147
|
+
err('untyped-collection', `collection slot "${name}" has no typed elem type (got ${JSON.stringify(s.elem)})`, name);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const c = def && def.contract; // (e) frame
|
|
151
|
+
if ( !c ) {
|
|
152
|
+
warnings.push({ method, kind: 'uncontracted', message: `method "${method}" declares no contract — composition falls back to a runtime micro-LLM (cost gradient, §0.1)` });
|
|
153
|
+
} else {
|
|
154
|
+
const read = new Set(c.read || []), write = new Set(c.write || []);
|
|
155
|
+
for ( const k of frameKeys(c.post) ) if ( !write.has(k) )
|
|
156
|
+
err('frame-violation', `postcondition key "${k}" is outside the declared write-footprint {${[...write].join(', ')}}`);
|
|
157
|
+
for ( const k of frameKeys(c.pre) ) if ( !read.has(k) )
|
|
158
|
+
err('frame-violation', `precondition key "${k}" is outside the declared read-footprint {${[...read].join(', ')}}`);
|
|
159
|
+
}
|
|
160
|
+
return { errors, warnings };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function lintFrontierMatch( frontier, used, name, err ) {
|
|
164
|
+
const declared = new Set(frontier);
|
|
165
|
+
for ( const d of declared ) if ( !used.has(d) )
|
|
166
|
+
err('frontier-mismatch', `slot "${name}" declares frontier ref "${d}" the body never uses`, name);
|
|
167
|
+
for ( const u of used ) if ( !declared.has(u) )
|
|
168
|
+
err('frontier-mismatch', `the body uses ref "${u}" not declared in slot "${name}" frontier`, name);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ------------------------------------------------------------------ case-parameterized SELECTION (Brick 3)
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* selectCluster — generate a concept-tree fragment of MUTUALLY-EXCLUSIVE, typed-gated SELECTORS that pick
|
|
175
|
+
* WHICH method applies to a problem from the CASE's structural (typed) facts, step by step (design doc §4A;
|
|
176
|
+
* "la bonne boucle est définie par les conditions initiales"). Selection lives in the CONCEPT layer (gated
|
|
177
|
+
* on the case facts already on the segment via the param/case role cut), NOT in a blind getPaths enumeration.
|
|
178
|
+
*
|
|
179
|
+
* Mutual exclusion is the confluence guarantee: at most one selector casts per segment. A `fallback` selector
|
|
180
|
+
* fires iff NO typed rule matched — that is the cost-gradient hand-off to a runtime micro-LLM (§0.1), not a
|
|
181
|
+
* hard failure. Each rule's provider must set the rule's `_name` cast marker (finding #33).
|
|
182
|
+
*
|
|
183
|
+
* @param rules [{ name, when:"<assert expr over the case's typed facts>", provider:"Ns::fn" }]
|
|
184
|
+
* @param opts { trigger:'toSelect', on:[factKeys that must be PRESENT to consider selection],
|
|
185
|
+
* fallback?:name, fallbackProvider?:"Ns::fn" }
|
|
186
|
+
* @returns a `childConcepts` map (drop into a concept tree under `common`).
|
|
187
|
+
*/
|
|
188
|
+
function selectCluster( rules, opts ) {
|
|
189
|
+
opts = opts || {};
|
|
190
|
+
const require = ['Segment', opts.trigger || 'toSelect'].concat(opts.on || []);
|
|
191
|
+
const childConcepts = {}, conds = [];
|
|
192
|
+
for ( const r of (rules || []) ) {
|
|
193
|
+
childConcepts[r.name] = { _id: r.name, _name: r.name, require: require.slice(), assert: [r.when], provider: [r.provider] };
|
|
194
|
+
conds.push('(' + r.when + ')');
|
|
195
|
+
}
|
|
196
|
+
if ( opts.fallback ) // fires iff NO typed rule matched → micro-LLM
|
|
197
|
+
childConcepts[opts.fallback] = { _id: opts.fallback, _name: opts.fallback, require: require.slice(),
|
|
198
|
+
assert: [conds.length ? '!(' + conds.join(' || ') + ')' : 'true'], provider: [opts.fallbackProvider] };
|
|
199
|
+
return childConcepts;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
module.exports = { applySubgraphArg, mapSubgraph, mapTemplate, bindSubgraphArg, lintMethod, selectCluster };
|