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,350 @@
|
|
|
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
|
+
* registry — the interface-alphabet REGISTRY (Σ_sep), G-1 rung 3b: the STAGE-1 keystone as a FIRST-CLASS,
|
|
10
|
+
* CURATED, VERSIONED object (the owner's library CATALOG). It promotes the DERIVED separator alphabet
|
|
11
|
+
* (`decompose.js#bagInterface.sigmaSep`) + the typed value vocabulary (enum members / curated synonym RINGS /
|
|
12
|
+
* grain / sort, from `prompt.facts`) from scattered-per-concept to ONE object where the vocabulary LIVES,
|
|
13
|
+
* versioned and enforceable. It is the home of three things this session built separately:
|
|
14
|
+
* • the curated SYNONYM RINGS (rung 1) — now sourced FROM the registry (`specForKey`), not re-inlined per concept;
|
|
15
|
+
* • the cross-method COHERENCE (rung 3) — now checkable against the FROZEN canon, not just within one tree;
|
|
16
|
+
* • the borderline PROPOSALS (rung 2) — admitted via `mergeRingProposals` (confluence re-checked), converging the ring.
|
|
17
|
+
*
|
|
18
|
+
* TIERS (the soundness/flexibility split — §9 vocab topology): a key ∈ `sigmaSep` (a cross-tile SEPARATOR) is
|
|
19
|
+
* **Tier-1** (the closed, frozen, producer↔consumer interface); everything else is **Tier-2** (open interior). Freezing
|
|
20
|
+
* is on Tier-1 only — a new Tier-1 value must go through a validated proposal (version bump); Tier-2 stays open.
|
|
21
|
+
*
|
|
22
|
+
* const reg = freezeRegistry(deriveRegistry(tree), 'v1'); // derive + stamp the canon
|
|
23
|
+
* const spec = specForKey(reg, 'severity'); // { enum, synonyms } for canonValue — the ring lives here
|
|
24
|
+
* const reg2 = mergeRingProposals(reg, borderlineProposals);// admit validated aliases → the ring converges (v2)
|
|
25
|
+
* const { errors } = checkTreeAgainstRegistry(otherTree, reg);// enforce: no off-canon Tier-1 value / un-registered key
|
|
26
|
+
*/
|
|
27
|
+
const { eachConcept, refsOf, refKeyOf } = require('./validate.js');
|
|
28
|
+
const { conceptCliques, bagInterface } = require('./decompose.js');
|
|
29
|
+
const { compileEnumMap, normToken } = require('../providers/canonicalize.js');
|
|
30
|
+
|
|
31
|
+
// the literal STRING values an applyMutations template WRITES per key (a closed-vocab value written by a template —
|
|
32
|
+
// part of the reachable vocabulary). Same discipline as validate.js#templateStringValues (kept local for independence).
|
|
33
|
+
function templateValues( tpl, out ) {
|
|
34
|
+
out = out || {};
|
|
35
|
+
(function walk( n ) {
|
|
36
|
+
if ( Array.isArray(n) ) return n.forEach(walk);
|
|
37
|
+
if ( n && typeof n === 'object' ) for ( const k in n ) {
|
|
38
|
+
const v = n[k];
|
|
39
|
+
if ( typeof v === 'string' && k[0] !== '$' && k !== '_id' && v[0] !== '$' ) (out[k] = out[k] || new Set()).add(v);
|
|
40
|
+
else if ( v && typeof v === 'object' ) walk(v);
|
|
41
|
+
}
|
|
42
|
+
})(tpl);
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const uniq = ( xs ) => [...new Set(xs)];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Derive the registry from a concept tree (does not FREEZE it — that is `freezeRegistry`). Total: any confluence
|
|
50
|
+
* conflict in the merged rings is RECORDED (`reg.conflicts`), never thrown (a tree can be inconsistent; the caller gates).
|
|
51
|
+
* @returns { version:null, frozen:false, keys:{ <key>: entry }, conflicts:[{key,error}] }
|
|
52
|
+
* entry = { tier:1|2, enum?:[..], synonyms?:{member:[alias]}, grain?, sort?, producers:[name], consumers:[name], values:[..] }
|
|
53
|
+
*/
|
|
54
|
+
function deriveRegistry( tree ) {
|
|
55
|
+
const sep = new Set(bagInterface(conceptCliques(tree)).sigmaSep); // the cross-tile separator alphabet (a Tier-1 signal)
|
|
56
|
+
const keys = Object.create(null);
|
|
57
|
+
const ent = ( k ) => (keys[k] = keys[k] || { producers: new Set(), consumers: new Set(), values: new Set() });
|
|
58
|
+
|
|
59
|
+
eachConcept(tree, ( c ) => {
|
|
60
|
+
if ( !c._name ) return;
|
|
61
|
+
const schema = c._schema || c;
|
|
62
|
+
ent(c._name).producers.add(c._name); // the self-flag is produced by the concept
|
|
63
|
+
const facts = schema.prompt && schema.prompt.facts;
|
|
64
|
+
if ( facts ) for ( const key of Object.keys(facts) ) {
|
|
65
|
+
const spec = facts[key], e = ent(key);
|
|
66
|
+
e.producers.add(c._name);
|
|
67
|
+
if ( spec && spec.enum ) {
|
|
68
|
+
e.enum = uniq([...(e.enum || []), ...spec.enum]);
|
|
69
|
+
for ( const m of spec.enum ) e.values.add(m);
|
|
70
|
+
if ( spec.synonyms ) { e.synonyms = e.synonyms || {};
|
|
71
|
+
for ( const member of Object.keys(spec.synonyms) )
|
|
72
|
+
e.synonyms[member] = uniq([...(e.synonyms[member] || []), ...(spec.synonyms[member] || [])]); }
|
|
73
|
+
}
|
|
74
|
+
if ( spec && spec.grain != null ) e.grain = spec.grain;
|
|
75
|
+
if ( spec && spec.sort != null ) e.sort = spec.sort;
|
|
76
|
+
}
|
|
77
|
+
const tv = templateValues(schema.applyMutations); // template writes → producer + reachable values
|
|
78
|
+
for ( const key of Object.keys(tv) ) { const e = ent(key); e.producers.add(c._name); for ( const v of tv[key] ) e.values.add(v); }
|
|
79
|
+
for ( const r of refsOf(schema.require, false).concat(refsOf(schema.ensure, true), refsOf(schema.assert, true)) )
|
|
80
|
+
ent(refKeyOf(r).key).consumers.add(c._name); // a gate → consumer of the key
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const conflicts = [];
|
|
84
|
+
const out = { version: null, frozen: false, keys: {}, conflicts };
|
|
85
|
+
for ( const k of Object.keys(keys) ) {
|
|
86
|
+
const e = keys[k];
|
|
87
|
+
// Tier-1 = a cross-tile SEPARATOR (sigmaSep) OR a typed enum interface that is BOTH produced and consumed (an
|
|
88
|
+
// actual interface vocabulary — the sigmaSep structural cut can miss it on small trees). Else Tier-2 (interior).
|
|
89
|
+
const tier = (sep.has(k) || (e.enum && e.producers.size && e.consumers.size)) ? 1 : 2;
|
|
90
|
+
const entry = { tier, producers: [...e.producers].sort(), consumers: [...e.consumers].sort(), values: [...e.values].sort() };
|
|
91
|
+
if ( e.enum ) entry.enum = e.enum;
|
|
92
|
+
if ( e.synonyms ) entry.synonyms = e.synonyms;
|
|
93
|
+
if ( e.grain != null ) entry.grain = e.grain;
|
|
94
|
+
if ( e.sort != null ) entry.sort = e.sort;
|
|
95
|
+
if ( entry.enum ) { try { compileEnumMap(specForEntry(entry)); } catch ( err ) { conflicts.push({ key: k, error: err.message }); } }
|
|
96
|
+
out.keys[k] = entry;
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// a canonValue spec ({enum, synonyms, grain}) for a registry ENTRY — the source of truth for the barrier (the ring LIVES here).
|
|
102
|
+
function specForEntry( entry ) {
|
|
103
|
+
const spec = {};
|
|
104
|
+
if ( entry.enum ) spec.enum = entry.enum;
|
|
105
|
+
if ( entry.synonyms ) spec.synonyms = entry.synonyms;
|
|
106
|
+
if ( entry.grain != null ) spec.grain = entry.grain;
|
|
107
|
+
return spec;
|
|
108
|
+
}
|
|
109
|
+
/** The canonValue spec for a registry KEY, or null if the key carries no typed vocabulary. */
|
|
110
|
+
function specForKey( reg, key ) {
|
|
111
|
+
const entry = reg && reg.keys && reg.keys[key];
|
|
112
|
+
if ( !entry || (!entry.enum && entry.grain == null) ) return null;
|
|
113
|
+
return specForEntry(entry);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Resolve a `prompt.facts` schema against the registry: a spec `{ ref: '<key>' }` is REPLACED by the registry's canonical
|
|
118
|
+
* spec for that key (enum + ring + grain) — so a concept REFERENCES the registry vocabulary instead of re-inlining the
|
|
119
|
+
* ring (one source of truth; the ring lives in the registry). A local `from` (the raw reply key) is preserved. A spec
|
|
120
|
+
* without `ref` passes through unchanged. An unknown/non-typed ref is left AS-IS and reported in `unresolved` (the caller
|
|
121
|
+
* decides: an author-time ERROR, or a runtime untyped/CanonMiss).
|
|
122
|
+
* @returns { facts, unresolved:[{key,ref}] }
|
|
123
|
+
*/
|
|
124
|
+
function resolveFactsSchema( factsSchema, registry ) {
|
|
125
|
+
const facts = {}, unresolved = [];
|
|
126
|
+
for ( const key of Object.keys(factsSchema || {}) ) {
|
|
127
|
+
const spec = factsSchema[key] || {};
|
|
128
|
+
if ( spec.ref ) {
|
|
129
|
+
const rspec = specForKey(registry, spec.ref);
|
|
130
|
+
if ( rspec ) facts[key] = Object.assign(rspec, spec.from != null ? { from: spec.from } : {});
|
|
131
|
+
else { facts[key] = spec; unresolved.push({ key, ref: spec.ref }); }
|
|
132
|
+
} else facts[key] = spec;
|
|
133
|
+
}
|
|
134
|
+
return { facts, unresolved };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Stamp a version + FREEZE the registry (Tier-1 becomes the closed canon; Tier-2 stays open). Returns a new object. */
|
|
138
|
+
function freezeRegistry( reg, version ) {
|
|
139
|
+
return Object.assign({}, reg, { frozen: true, version: version == null ? (reg.version || 'v1') : version });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Admit borderline / LILO ring PROPOSALS into the registry (propose-only → validated). Each `{key, alias, member}` is
|
|
144
|
+
* added to key's ring iff (a) member is a registered enum member of key, and (b) the resulting ring stays CONFLUENT
|
|
145
|
+
* (the critical-pair check — `compileEnumMap` does not throw). Rejected proposals are returned; on any admit the version
|
|
146
|
+
* bumps. Never mutates the input registry.
|
|
147
|
+
* @returns { registry, admitted:[{key,alias,member}], rejected:[{key,alias,member,reason}] }
|
|
148
|
+
*/
|
|
149
|
+
function mergeRingProposals( reg, proposals ) {
|
|
150
|
+
const keys = JSON.parse(JSON.stringify(reg.keys || {}));
|
|
151
|
+
const provenance = Object.assign({}, reg.ringProvenance || {}); // parallel { <provKey> -> {member,via} } — audit + retraction handle
|
|
152
|
+
const admitted = [], rejected = [];
|
|
153
|
+
for ( const p of (proposals || []) ) {
|
|
154
|
+
const entry = keys[p.key];
|
|
155
|
+
if ( !entry || !entry.enum ) { rejected.push(Object.assign({ reason: 'no such enum key' }, p)); continue; }
|
|
156
|
+
if ( entry.enum.indexOf(p.member) < 0 ) { rejected.push(Object.assign({ reason: 'member not in the enum' }, p)); continue; }
|
|
157
|
+
const trial = { enum: entry.enum, synonyms: Object.assign({}, entry.synonyms) };
|
|
158
|
+
trial.synonyms[p.member] = uniq([...(trial.synonyms[p.member] || []), p.alias]);
|
|
159
|
+
try { compileEnumMap(trial); }
|
|
160
|
+
catch ( err ) { rejected.push(Object.assign({ reason: 'breaks confluence: ' + err.message }, p)); continue; }
|
|
161
|
+
entry.synonyms = trial.synonyms; admitted.push(p);
|
|
162
|
+
// PROVENANCE (Laurie confront): tag every admitted alias with its source. Model-independent value — it makes an
|
|
163
|
+
// admission AUDITABLE and RETRACTABLE (the outcome-blame loop / a curator can find + retractRingAlias a model-
|
|
164
|
+
// sourced entry). Keyed post-normToken so it lines up with canonValue's lookup + retractRingAlias.
|
|
165
|
+
provenance[provKey(p.key, p.alias)] = { member: p.member, via: p.via || 'proposal' };
|
|
166
|
+
}
|
|
167
|
+
const version = admitted.length ? bumpVersion(reg.version) : reg.version;
|
|
168
|
+
return { registry: Object.assign({}, reg, { keys, version, ringProvenance: provenance }), admitted, rejected };
|
|
169
|
+
}
|
|
170
|
+
const provKey = ( key, alias ) => key + '::' + normToken(alias);
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* RETRACT a synonym-ring ALIAS — the un-learn verb for the ring, and the actual DEFUSER of a bad autonomous admission
|
|
174
|
+
* (Laurie confront: "8/8 does not certify an oracle" — Rule-of-Three 95% upper bound ≈ 37% on 8 samples; so soundness
|
|
175
|
+
* rests on RECOVERABILITY, not oracle reliability. A strong model lowers the RATE of a wrong admit; retraction bounds the
|
|
176
|
+
* DAMAGE). It is the vocabulary-level sibling of relearn.js's blame→un-learn, and the "teeth" that make the registry's
|
|
177
|
+
* "audited + retractable" envelope actually TRUE (there was no retraction path — the ring only ever ADDED).
|
|
178
|
+
*
|
|
179
|
+
* Removes `alias` (by normToken) from every member's ring under `key`, which DE-LOCKS it: because mergeRingProposals is
|
|
180
|
+
* first-writer-wins-under-confluence, a wrong `catastrophic→low` blocks the correct `catastrophic→high` ("breaks
|
|
181
|
+
* confluence"); after retraction the corrected proposal is admissible (removal can never break confluence). The version
|
|
182
|
+
* bumps iff something changed — and the bump IS the invalidation signal (B8 version-gates the replay paths, so a digest/
|
|
183
|
+
* cache entry typed via the retracted alias is gated, not silently served stale). Never mutates the input registry.
|
|
184
|
+
* @returns { registry, retracted:bool, member:(the member the alias mapped to, or null) }
|
|
185
|
+
*/
|
|
186
|
+
function retractRingAlias( reg, key, alias ) {
|
|
187
|
+
const keys = JSON.parse(JSON.stringify(reg.keys || {}));
|
|
188
|
+
const entry = keys[key];
|
|
189
|
+
const na = normToken(alias);
|
|
190
|
+
let member = null, changed = false;
|
|
191
|
+
if ( entry && entry.synonyms ) {
|
|
192
|
+
for ( const m of Object.keys(entry.synonyms) ) {
|
|
193
|
+
const before = entry.synonyms[m] || [];
|
|
194
|
+
const after = before.filter(( a ) => normToken(a) !== na);
|
|
195
|
+
if ( after.length !== before.length ) { member = m; changed = true; }
|
|
196
|
+
if ( after.length ) entry.synonyms[m] = after; else delete entry.synonyms[m];
|
|
197
|
+
}
|
|
198
|
+
if ( Object.keys(entry.synonyms).length === 0 ) delete entry.synonyms;
|
|
199
|
+
}
|
|
200
|
+
const provenance = Object.assign({}, reg.ringProvenance || {});
|
|
201
|
+
if ( provenance[provKey(key, alias)] ) { delete provenance[provKey(key, alias)]; changed = true; }
|
|
202
|
+
const version = changed ? bumpVersion(reg.version) : reg.version;
|
|
203
|
+
return { registry: Object.assign({}, reg, { keys, version, ringProvenance: provenance }), retracted: changed, member };
|
|
204
|
+
}
|
|
205
|
+
function bumpVersion( v ) {
|
|
206
|
+
const m = /^v(\d+)$/.exec(String(v || 'v1'));
|
|
207
|
+
return m ? 'v' + (Number(m[1]) + 1) : String(v || 'v1') + '+1';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* The vocabulary-grain ADMISSION rule (G4) — the ratchet's teeth applied to a surface ALIAS. Pure decision:
|
|
212
|
+
* given the episode verdicts under the TWO counterfactuals (with the proposed alias→member mapping applied,
|
|
213
|
+
* and without any mapping), admit iff the alias is LOAD-BEARING for the success — the verdict passes WITH it
|
|
214
|
+
* and fails WITHOUT it (the vacuity guard: a benign word whose episode succeeds anyway must never enter the
|
|
215
|
+
* ring). LOCALIZATION (a single pending alias exercised per episode — the 8d credit discipline, see
|
|
216
|
+
* parametric.js#attributeSlotCredit) is the CALLER's tooth: this rule assumes the two verdicts are
|
|
217
|
+
* attributable to THIS alias. Member-UNIQUENESS over the whole enum is deliberately NOT required — on one
|
|
218
|
+
* episode members are verdict-equivalent by category class (requiring uniqueness would self-seal a correct
|
|
219
|
+
* alias forever, the lab's C-arm defect at vocab grain); intra-class discrimination = the semantic prior
|
|
220
|
+
* (the model's proposal picks the member) + the defeasible envelope (support via `creditRingAlias`, damage
|
|
221
|
+
* bounded by `retractRingAlias`). Report `equivalents` (how many members would also pass) as a DIAGNOSTIC,
|
|
222
|
+
* never a gate.
|
|
223
|
+
* @param opts { member, withAlias:bool, withoutAlias:bool }
|
|
224
|
+
* @returns { admit, reason } — reason ∈ admit|no-proposal|vacuous|failed-verify
|
|
225
|
+
*/
|
|
226
|
+
function decideRingAdmission( opts ) {
|
|
227
|
+
if ( !opts || !opts.member || opts.member === 'none' ) return { admit: false, reason: 'no-proposal' };
|
|
228
|
+
if ( opts.withoutAlias ) return { admit: false, reason: 'vacuous' };
|
|
229
|
+
if ( !opts.withAlias ) return { admit: false, reason: 'failed-verify' };
|
|
230
|
+
return { admit: true, reason: 'admit' };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* CREDIT a ring alias on a VERIFIED reuse — the `confiance` half of the defeasible ring's {source, confiance}
|
|
235
|
+
* envelope (`via` = the source, `support` = verified-use count; the reading a curator or a selection pressure
|
|
236
|
+
* consumes). Rule-of-Three stays the doctrine: support bounds what verified use can certify — soundness rests
|
|
237
|
+
* on RECOVERABILITY (retractRingAlias), never on the count. An AUTHORED alias (in a ring with no provenance
|
|
238
|
+
* entry) gets `{via:'authored'}` on first credit, so confidence reads uniformly across sources. Does NOT bump
|
|
239
|
+
* the version: support changes no resolution semantics, so version-gated replay/caches must not invalidate on
|
|
240
|
+
* credit. Never mutates the input registry.
|
|
241
|
+
* @returns { registry, member, support } — support = the new count, or null when the alias resolves nowhere under key.
|
|
242
|
+
*/
|
|
243
|
+
function creditRingAlias( reg, key, alias ) {
|
|
244
|
+
const entry = reg && reg.keys && reg.keys[key];
|
|
245
|
+
const na = normToken(alias);
|
|
246
|
+
let member = null;
|
|
247
|
+
if ( entry && entry.synonyms )
|
|
248
|
+
for ( const m of Object.keys(entry.synonyms) )
|
|
249
|
+
if ( (entry.synonyms[m] || []).some(( a ) => normToken(a) === na) ) { member = m; break; }
|
|
250
|
+
if ( member == null ) return { registry: reg, member: null, support: null }; // exact enum words are not ring entries
|
|
251
|
+
const provenance = Object.assign({}, reg.ringProvenance || {});
|
|
252
|
+
const pk = provKey(key, alias);
|
|
253
|
+
const prev = provenance[pk] || { member, via: 'authored' };
|
|
254
|
+
provenance[pk] = Object.assign({}, prev, { support: (prev.support || 0) + 1 });
|
|
255
|
+
return { registry: Object.assign({}, reg, { ringProvenance: provenance }), member, support: provenance[pk].support };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Enforce a concept tree against a registry — "crystallize/dispatch consult the registry". Two findings, kept sound:
|
|
260
|
+
* • off-canon VALUE (ERROR when frozen) — a concept types a key with an enum value the registry's enum for that key
|
|
261
|
+
* does NOT list. A registered enum IS the closed vocabulary for that key (any tier), so this is a clear canon
|
|
262
|
+
* divergence → hard error on a frozen canon (a warning otherwise). SOUND: no false errors — subset trees are clean.
|
|
263
|
+
* • un-registered enum KEY (WARN) — the tree types an enum key the registry does not know. This is ADVISORY, never a
|
|
264
|
+
* hard error: it may be a legit Tier-2 (interior) extension OR a Tier-1 omission — the curator decides. (Freezing
|
|
265
|
+
* cannot soundly hard-refuse an unknown key without knowing it MUST be Tier-1.)
|
|
266
|
+
* @returns { errors, warnings } each `{ concept, kind, message }`.
|
|
267
|
+
*/
|
|
268
|
+
function checkTreeAgainstRegistry( tree, reg ) {
|
|
269
|
+
const errors = [], warnings = [];
|
|
270
|
+
const frozen = !!(reg && reg.frozen);
|
|
271
|
+
const known = (reg && reg.keys) || {};
|
|
272
|
+
eachConcept(tree, ( c ) => {
|
|
273
|
+
if ( !c._name ) return;
|
|
274
|
+
const facts = (c._schema || c).prompt && (c._schema || c).prompt.facts;
|
|
275
|
+
if ( !facts ) return;
|
|
276
|
+
for ( const key of Object.keys(facts) ) {
|
|
277
|
+
const spec = facts[key];
|
|
278
|
+
if ( !spec || !spec.enum ) continue;
|
|
279
|
+
const entry = known[key];
|
|
280
|
+
if ( !entry || !entry.enum ) { // an enum key the canon does not know → advisory only
|
|
281
|
+
warnings.push({ concept: c._name, kind: 'unregistered-interface-key', message: `typed interface key "${key}" is not in the registry — register it (Tier-1) or confirm it is a Tier-2 interior extension` });
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
for ( const v of spec.enum ) if ( entry.enum.indexOf(v) < 0 ) // a value outside the registered closed vocabulary
|
|
285
|
+
(frozen ? errors : warnings).push({ concept: c._name, kind: 'off-canon-value', tier: entry.tier,
|
|
286
|
+
message: `key "${key}" value "${v}" is not in the registry enum {${entry.enum.join(', ')}} — the tree diverged from the canon; add it via a validated proposal` });
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
return { errors, warnings };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// "validate consults the registry" — compose the author-time structural validator with the registry enforcement (kept
|
|
293
|
+
// here, not in validate.js, to avoid a require cycle: registry already depends on validate). Merges both finding sets.
|
|
294
|
+
function validateWithRegistry( tree, reg, opts ) {
|
|
295
|
+
const base = require('./validate.js').validateConceptTree(tree, opts);
|
|
296
|
+
const ren = checkTreeAgainstRegistry(tree, reg);
|
|
297
|
+
return { errors: base.errors.concat(ren.errors), warnings: base.warnings.concat(ren.warnings) };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ─────────────────────────── the AUTONOMOUS convergence loop (the living catalog — sibling of relearn.js) ──────────────
|
|
301
|
+
//
|
|
302
|
+
// The engine drives borderline PROPOSAL → validated ring GROWTH as REACTIVE concepts at the stabilize fixpoint, with NO
|
|
303
|
+
// host glue (mirrors `relearn.js`'s blame→revise loop). A proposer (the rung-2 borderline gate, or any) deposits a
|
|
304
|
+
// proposal on a FRESH proxy node (`proposalTemplate`, #22-safe: one node → one fire); a `RegistryMerge` meta-concept
|
|
305
|
+
// (`require:['proposalMember'], ensure:['!$merged']`) fires; `Reg::merge` admits it via `mergeRingProposals` (member∈enum
|
|
306
|
+
// ∧ CONFLUENCE re-checked) into a MUTABLE registry container so every closure (the `resolveFacts` seam) sees the grown
|
|
307
|
+
// ring — the exogenous vocabulary CONVERGES autonomously. An invalid proposal is REJECTED (registry unchanged) but still
|
|
308
|
+
// marks `merged` (no re-fire / no `divergent`, the #33 GOTCHA). Curation stays a gate: `mergeRingProposals` is the gate.
|
|
309
|
+
|
|
310
|
+
/** The reactive convergence flow as a concept tree. @param opts.name meta-concept id (default 'RegistryMerge'). */
|
|
311
|
+
function registryLoopTree( opts ) {
|
|
312
|
+
opts = opts || {};
|
|
313
|
+
const name = (opts && opts.name) || 'RegistryMerge';
|
|
314
|
+
const tree = { childConcepts: {} };
|
|
315
|
+
tree.childConcepts[name] = { _id: name, _name: name, require: ['proposalMember'], ensure: ['!$merged'], provider: ['Reg::merge'] };
|
|
316
|
+
return tree;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Build a proposal-deposit template (a FRESH proxy node per proposal → RegistryMerge fires exactly once). `via` carries
|
|
320
|
+
* the source (e.g. 'llm-borderline') into the admit provenance. */
|
|
321
|
+
function proposalTemplate( proposal, id ) {
|
|
322
|
+
return { $$_id: id, proposalKey: proposal.key, proposalAlias: proposal.alias, proposalMember: proposal.member, proposalVia: proposal.via };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* The reactive merge provider (host opt-in, like `makeRelearnProviders`). `Reg::merge` admits the pending proposal into a
|
|
327
|
+
* MUTABLE registry container (so the grown ring is visible to every closure — the living catalog).
|
|
328
|
+
* @param opts.regBox { registry } — the mutable container (merge swaps in the grown registry). Or pass opts.registry.
|
|
329
|
+
* @param opts.onMerge optional (mergeResult, regBox) => void — a hook (audit / persist the versioned registry).
|
|
330
|
+
* @returns { Reg: { merge } }
|
|
331
|
+
*/
|
|
332
|
+
function makeRegistryLoopProviders( opts ) {
|
|
333
|
+
opts = opts || {};
|
|
334
|
+
const regBox = opts.regBox || { registry: opts.registry };
|
|
335
|
+
return { Reg: {
|
|
336
|
+
merge: function ( graph, concept, scope, argz, cb ) {
|
|
337
|
+
const e = (scope && scope._) || {};
|
|
338
|
+
const p = { key: e.proposalKey, alias: e.proposalAlias, member: e.proposalMember, via: e.proposalVia };
|
|
339
|
+
const r = mergeRingProposals(regBox.registry, [p]);
|
|
340
|
+
regBox.registry = r.registry; // swap in the grown registry (closures re-read it)
|
|
341
|
+
if ( opts.onMerge ) { try { opts.onMerge(r, regBox); } catch ( _e ) {} }
|
|
342
|
+
cb(null, { $_id: '_parent', RegistryMerge: true, merged: true, // self-flag + re-fire guard (#33)
|
|
343
|
+
registryVersion: regBox.registry.version, admitted: r.admitted.length > 0,
|
|
344
|
+
rejectedReason: r.rejected.length ? r.rejected[0].reason : null });
|
|
345
|
+
}
|
|
346
|
+
} };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
module.exports = { deriveRegistry, freezeRegistry, specForKey, resolveFactsSchema, mergeRingProposals, retractRingAlias, decideRingAdmission, creditRingAlias, checkTreeAgainstRegistry, validateWithRegistry,
|
|
350
|
+
registryLoopTree, proposalTemplate, makeRegistryLoopProviders };
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Nathanael Braun
|
|
3
|
+
* @author : Nathanael BRAUN <pp9ping@gmail.com>
|
|
4
|
+
*
|
|
5
|
+
* This program is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
* (at your option) any later version.
|
|
9
|
+
*
|
|
10
|
+
* This program is distributed in the hope that it will be useful,
|
|
11
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
* GNU Affero General Public License for more details.
|
|
14
|
+
*
|
|
15
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
'use strict';
|
|
19
|
+
/**
|
|
20
|
+
* The STANDING / autonomous C-contract un-learn loop (§3.1) — the engine drives
|
|
21
|
+
* blame → revise → patch as REACTIVE concepts at the stabilize fixpoint, with NO host
|
|
22
|
+
* glue. The defeasance moat no RAG / CBR / skill-library has: when a learned method's
|
|
23
|
+
* typed premise falls, the JTMS retracts the belief (handled by the engine) AND the
|
|
24
|
+
* library autonomously, surgically NARROWS the method's applicability (this module) —
|
|
25
|
+
* versioned/undoable (N6/B8).
|
|
26
|
+
*
|
|
27
|
+
* This is the reactive sibling of the host-orchestrated `examples/poc/contract-unlearn.js`
|
|
28
|
+
* (whose steps 3-4 — reviseOnBlame + the gate narrowing — were plain host JS). It mirrors
|
|
29
|
+
* the shape of `supervise.js#reactiveSupervisorTree` / `makeSupervisorProviders`:
|
|
30
|
+
* - a `cleaner` (`Lib::blame`) deposits, on retraction, BOTH the typed constat AND a
|
|
31
|
+
* discrete `blamed` fact on the method's library node — a clean `require` trigger;
|
|
32
|
+
* - a `Revise` meta-concept (`require:['blamed']`, `ensure:["!$revised"]`) fires on the
|
|
33
|
+
* blame's appearance (#22-safe: keyed on a fact APPEARING, not a value change);
|
|
34
|
+
* - the `Lib::revise` provider revises the library contract (reviseOnBlame / CEGIS) AND
|
|
35
|
+
* `patchConcept`es the engine gate to match (queued mid-stabilize, #11.a) — then sets
|
|
36
|
+
* its own cast marker + the `revised` re-fire guard (the #33 GOTCHA).
|
|
37
|
+
*
|
|
38
|
+
* Wiring (see `examples/poc/contract-relearn.js`):
|
|
39
|
+
* const { makeRelearnProviders, relearnTree } = require('./relearn');
|
|
40
|
+
* Graph._providers = Object.assign({ App:{approve} }, createConstat(), makeRelearnProviders({ registry }));
|
|
41
|
+
* // tree: the defeasant method gets `cleaner:['Lib::blame'], discriminator:'region', constat:{...}`
|
|
42
|
+
* // + the relearnTree() Revise concept; seed a `lib:<Method>` node.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
const { recordConstat } = require('../providers/constat.js');
|
|
46
|
+
const { reviseOnBlame } = require('./contract.js');
|
|
47
|
+
|
|
48
|
+
// The exclusion atom a point-exclusion revision adds, in the engine/expr.js form
|
|
49
|
+
// (`$key!='val'` for strings, `$key!=val` for numbers — VERIFIED to parse on the real
|
|
50
|
+
// expr.js, single quotes included). Kept identical to reviseOnBlame's pre atom so the
|
|
51
|
+
// library contract and the engine `ensure` gate stay in lockstep.
|
|
52
|
+
function exclusionAtom( ce ) {
|
|
53
|
+
if ( ce == null || ce.key == null || ce.value === undefined ) return null;
|
|
54
|
+
return '$' + ce.key + '!=' + (typeof ce.value === 'string' ? "'" + ce.value + "'" : ce.value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The default revision STRATEGY (open R&D / pluggable, §7): point-exclude the failing
|
|
58
|
+
// discriminator (CEGIS specialize-pre, NOT method removal — which oscillates). Returns the
|
|
59
|
+
// new contract + the matching engine-gate atom so the gate sync is derived from the strategy
|
|
60
|
+
// (a different strategy — tighten-bound, etc. — returns its own atom).
|
|
61
|
+
function defaultStrategy( contract, ce ) {
|
|
62
|
+
return { contract: reviseOnBlame(contract, ce), ensureAtom: exclusionAtom(ce) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The reactive un-learn flow as a concept tree (vs the host-orchestrated loop in
|
|
67
|
+
* contract-unlearn.js). One meta-concept:
|
|
68
|
+
* <Method> ──retract──▶ (cleaner Lib::blame deposits `blamed` on lib:<Method>)
|
|
69
|
+
* blamed ──require──▶ Revise (ensure !revised) ──▶ Lib::revise narrows the library + the gate
|
|
70
|
+
* @param opts.name the meta-concept id/_name (default 'Revise')
|
|
71
|
+
*/
|
|
72
|
+
function relearnTree( opts ) {
|
|
73
|
+
opts = opts || {};
|
|
74
|
+
var name = opts.name || 'Revise';
|
|
75
|
+
var def = { _id: name, _name: name, require: ['blamed'], ensure: ['!$revised'], provider: ['Lib::revise'] };
|
|
76
|
+
var tree = { childConcepts: {} };
|
|
77
|
+
tree.childConcepts[name] = def;
|
|
78
|
+
return tree;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The Lib provider pair (host opt-in, like createConstat / makeSupervisorProviders).
|
|
83
|
+
* @param opts.registry { <Method>: contract } — the library's typed contracts; revise
|
|
84
|
+
* mutates the entry to the new (versioned) contract. Pass copies for B8.
|
|
85
|
+
* @param opts.strategy (contract, ce) => { contract, ensureAtom } — the revision policy
|
|
86
|
+
* (default = point-exclusion via reviseOnBlame). Injectable (§7).
|
|
87
|
+
* @returns { Lib: { blame, revise } }
|
|
88
|
+
*/
|
|
89
|
+
function makeRelearnProviders( opts ) {
|
|
90
|
+
opts = opts || {};
|
|
91
|
+
var registry = opts.registry || {};
|
|
92
|
+
var strategy = opts.strategy || defaultStrategy;
|
|
93
|
+
|
|
94
|
+
return { Lib: {
|
|
95
|
+
// cleaner: on a method's retraction, deposit (a) the typed constat on `mem` AND
|
|
96
|
+
// (b) a discrete `blamed` fact on the method's library node lib:<Method> (a clean
|
|
97
|
+
// `require` trigger for Revise). Multi-target template (pushMutation accepts arrays).
|
|
98
|
+
blame: function ( graph, concept, scope, argz, cb ) {
|
|
99
|
+
var cfg = Object.assign({ memId: 'mem', storeKey: 'lessons' },
|
|
100
|
+
concept._schema && concept._schema.constat, argz && argz[0]);
|
|
101
|
+
var disc = concept._schema && concept._schema.discriminator; // e.g. 'region'
|
|
102
|
+
var e = (scope && scope._) || {};
|
|
103
|
+
var libId = (concept._schema && concept._schema.libNode) || ('lib:' + concept._name);
|
|
104
|
+
var blameTpl = {
|
|
105
|
+
$$_id : libId,
|
|
106
|
+
blamed : true,
|
|
107
|
+
method : concept._name,
|
|
108
|
+
claim : cfg.claimKey != null && e[cfg.claimKey] != null ? e[cfg.claimKey] : null,
|
|
109
|
+
because : cfg.because != null ? cfg.because : null,
|
|
110
|
+
failingCase: e._id,
|
|
111
|
+
discKey : disc != null ? disc : null,
|
|
112
|
+
discVal : disc != null ? e[disc] : null,
|
|
113
|
+
atRev : graph.getCurrentRevision()
|
|
114
|
+
};
|
|
115
|
+
cb(null, [recordConstat(graph, concept, scope, cfg), blameTpl]);
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
// the standing revise: read the blame → revise the library contract + narrow the
|
|
119
|
+
// engine gate, autonomously. ZERO host orchestration (vs contract-unlearn.js:85-93).
|
|
120
|
+
revise: function ( graph, concept, scope, argz, cb ) {
|
|
121
|
+
var e = (scope && scope._) || {};
|
|
122
|
+
var M = e.method;
|
|
123
|
+
var ce = { key: e.discKey, value: e.discVal };
|
|
124
|
+
|
|
125
|
+
// 1 — revise the library contract (B8: a NEW contract; we version the registry entry).
|
|
126
|
+
var out = strategy(registry[M], ce);
|
|
127
|
+
if ( registry[M] != null ) registry[M] = out.contract;
|
|
128
|
+
|
|
129
|
+
// 2 — narrow the engine gate to match. Concept.patch REPLACES arrays → pass the FULL
|
|
130
|
+
// ensure. The patch is issued mid-stabilize → QUEUED to the quiescent boundary (#11.a).
|
|
131
|
+
var target = graph.getConceptByName(M);
|
|
132
|
+
var newEnsure = (target && target._schema.ensure || []).slice();
|
|
133
|
+
if ( out.ensureAtom && newEnsure.indexOf(out.ensureAtom) < 0 ) {
|
|
134
|
+
newEnsure.push(out.ensureAtom);
|
|
135
|
+
graph.patchConcept(M, { ensure: newEnsure });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 3 — own cast marker (#33) + `revised` re-fire guard; B8: the live narrowed pre is
|
|
139
|
+
// stored as a graph fact on lib:<Method> (typed DSL atoms, not prose) so rollbackTo
|
|
140
|
+
// restores both the concept (N6) and the recorded pre coherently.
|
|
141
|
+
cb(null, { $_id: '_parent', Revise: true, revised: true,
|
|
142
|
+
narrowedPre: out.contract && out.contract.pre, narrowedEnsure: newEnsure });
|
|
143
|
+
}
|
|
144
|
+
} };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = { makeRelearnProviders, relearnTree, defaultStrategy, exclusionAtom };
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
* retention — a USAGE-TRACKING, self-EVICTING store wrapper (host-side, ZERO-CORE, fs-free). The RETENTION
|
|
10
|
+
* axis of a concept's life-cycle, sibling of `lifecycle.js` (the PLASTICITY/consolidation axis): lifecycle
|
|
11
|
+
* answers "is it reliable?" (proven → freeze); retention answers "is it USED?" (reused → keep, never used →
|
|
12
|
+
* evict). The owner's compass (2026-07-05): a stock must EARN its keep — track per-entry reuse and REMOVE the
|
|
13
|
+
* dead weight, so the local operational stock stays MINIMAL and converges. Wraps any Map-like store the
|
|
14
|
+
* master-loop cache uses (`has`/`get`/`set`/`delete`); a served `get` on a present key counts a reuse.
|
|
15
|
+
*
|
|
16
|
+
* Two eviction rules (opt-in — with neither, the wrapper only MEASURES, never drops):
|
|
17
|
+
* • DEAD WEIGHT — an entry set but never re-served (`uses == 0`) past `evictGrace` operations is removed
|
|
18
|
+
* ("supprime ce qui n'est jamais utilisé"); a re-used entry (`uses > 0`) is NEVER dropped by this rule.
|
|
19
|
+
* • BOUNDED — beyond `maxStock` entries, evict the LEAST-RECENTLY-served (LRU) until back under the cap.
|
|
20
|
+
*
|
|
21
|
+
* The clock is a LOGICAL operation counter (deterministic, no wall-clock) → eviction + `usage()` replay
|
|
22
|
+
* identically in a test. `usage()` is the compass readout: size, reused/dead split, per-entry reuse, the
|
|
23
|
+
* running reuse rate, and the eviction tally (dead-weight vs LRU).
|
|
24
|
+
*
|
|
25
|
+
* const store = createRetentionStore(new Map(), { maxStock: 500, evictGrace: 50 });
|
|
26
|
+
* const px = createProxyCache({ frontierAsk, store }); // the proxy's stock now self-manages
|
|
27
|
+
* store.usage(); // { size, reused, deadWeight, reuseRate, evicted:{deadWeight,lru}, byKey:[…] }
|
|
28
|
+
*
|
|
29
|
+
* @param inner a Map-like store ({ has, get, set, delete }); default a fresh Map.
|
|
30
|
+
* @param opts { maxStock, evictGrace, autoEvict } — autoEvict (default true iff a rule is set) runs evict on set.
|
|
31
|
+
*/
|
|
32
|
+
function createRetentionStore( inner, opts ) {
|
|
33
|
+
opts = opts || {};
|
|
34
|
+
inner = inner || new Map();
|
|
35
|
+
const meta = new Map(); // key → { uses, addedAt, lastUsed }
|
|
36
|
+
let clock = 0;
|
|
37
|
+
const evicted = { count: 0, deadWeight: 0, lru: 0 };
|
|
38
|
+
const hasRule = opts.maxStock != null || (opts.evictGrace != null && isFinite(opts.evictGrace));
|
|
39
|
+
const autoEvict = opts.autoEvict != null ? opts.autoEvict : hasRule;
|
|
40
|
+
|
|
41
|
+
function drop( k ) { meta.delete(k); return inner.delete(k); }
|
|
42
|
+
|
|
43
|
+
function evict() {
|
|
44
|
+
const out = [];
|
|
45
|
+
if ( opts.evictGrace != null && isFinite(opts.evictGrace) ) { // (a) dead weight — never re-served past the grace window
|
|
46
|
+
for ( const [k, m] of meta ) if ( m.uses === 0 && (clock - m.addedAt) > opts.evictGrace ) { drop(k); out.push(k); evicted.deadWeight++; }
|
|
47
|
+
}
|
|
48
|
+
if ( opts.maxStock != null ) { // (b) bounded — evict the LEAST VALUABLE
|
|
49
|
+
while ( meta.size > opts.maxStock ) { // least-frequently-used first, then least-recently (keep what's reused)
|
|
50
|
+
let victimK = null, vUses = Infinity, vLast = Infinity;
|
|
51
|
+
for ( const [k, m] of meta ) if ( m.uses < vUses || (m.uses === vUses && m.lastUsed < vLast) ) { vUses = m.uses; vLast = m.lastUsed; victimK = k; }
|
|
52
|
+
if ( victimK == null ) break;
|
|
53
|
+
drop(victimK); out.push(victimK); evicted.lru++;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
evicted.count += out.length;
|
|
57
|
+
return { evicted: out };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const store = {
|
|
61
|
+
has( k ) { return inner.has(k); }, // a probe — NOT a reuse
|
|
62
|
+
get( k ) { if ( inner.has(k) ) { const m = meta.get(k); if ( m ) { m.uses++; m.lastUsed = clock++; } } return inner.get(k); }, // a served get = a reuse
|
|
63
|
+
set( k, v ) {
|
|
64
|
+
if ( !meta.has(k) ) meta.set(k, { uses: 0, addedAt: clock, lastUsed: clock });
|
|
65
|
+
clock++;
|
|
66
|
+
inner.set(k, v);
|
|
67
|
+
if ( autoEvict ) evict();
|
|
68
|
+
return store;
|
|
69
|
+
},
|
|
70
|
+
delete( k ) { meta.delete(k); return inner.delete(k); },
|
|
71
|
+
get size() { return meta.size; },
|
|
72
|
+
|
|
73
|
+
/** run eviction on demand → { evicted:[keys] }. */
|
|
74
|
+
evict,
|
|
75
|
+
/** the compass readout — size, reuse split, running reuse rate, eviction tally, per-entry usage. */
|
|
76
|
+
usage() {
|
|
77
|
+
let reused = 0, dead = 0;
|
|
78
|
+
const byKey = [];
|
|
79
|
+
for ( const [k, m] of meta ) { if ( m.uses > 0 ) reused++; else dead++; byKey.push({ key: k, uses: m.uses, idle: clock - m.lastUsed, age: clock - m.addedAt }); }
|
|
80
|
+
byKey.sort(( a, b ) => b.uses - a.uses );
|
|
81
|
+
return { size: meta.size, reused, deadWeight: dead, reuseRate: meta.size ? reused / meta.size : 0, evicted: Object.assign({}, evicted), byKey };
|
|
82
|
+
},
|
|
83
|
+
/** the underlying meta (advanced hosts / debugging). */
|
|
84
|
+
meta
|
|
85
|
+
};
|
|
86
|
+
return store;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { createRetentionStore };
|