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.
Files changed (213) hide show
  1. package/CONTRIBUTING.md +22 -0
  2. package/LICENSE +661 -0
  3. package/README.md +245 -0
  4. package/bin/sg +20 -0
  5. package/concepts/README.md +23 -0
  6. package/concepts/_substrate/Claim/Confidence/Trusted.json +6 -0
  7. package/concepts/_substrate/Claim/Confidence.json +10 -0
  8. package/concepts/_substrate/Claim/Freshness/Stale.json +7 -0
  9. package/concepts/_substrate/Claim/Freshness.json +5 -0
  10. package/concepts/_substrate/Claim/Verification/Refuted.json +6 -0
  11. package/concepts/_substrate/Claim/Verification.json +6 -0
  12. package/concepts/_substrate/Claim.json +4 -0
  13. package/concepts/_substrate/Frontier/InBeam.json +6 -0
  14. package/concepts/_substrate/Frontier/Stuck.json +6 -0
  15. package/concepts/_substrate/Frontier.json +5 -0
  16. package/concepts/_substrate/Intake/ToTask.json +22 -0
  17. package/concepts/_substrate/Intake/Typed.json +10 -0
  18. package/concepts/_substrate/Intake.json +27 -0
  19. package/concepts/_substrate/Task/Answer.json +12 -0
  20. package/concepts/_substrate/Task/Complexity/Atomic.json +5 -0
  21. package/concepts/_substrate/Task/Complexity/Compound/Expansion.json +7 -0
  22. package/concepts/_substrate/Task/Complexity/Compound.json +6 -0
  23. package/concepts/_substrate/Task/Complexity.json +6 -0
  24. package/concepts/_substrate/Task/EvalComplexity.json +13 -0
  25. package/concepts/_substrate/Task/ReportUp.json +6 -0
  26. package/concepts/_substrate/Task/Rollup.json +10 -0
  27. package/concepts/_substrate/Task.json +5 -0
  28. package/concepts/clinical/Diagnosis/Medication.json +6 -0
  29. package/concepts/clinical/Diagnosis.json +15 -0
  30. package/concepts/clinical/Observation/LabValue/OutOfRange.json +6 -0
  31. package/concepts/clinical/Observation/LabValue.json +5 -0
  32. package/concepts/clinical/Observation.json +6 -0
  33. package/concepts/common/Document/pathBasket.json +6 -0
  34. package/concepts/common/Document.json +3 -0
  35. package/concepts/common/Edge/Distance.json +13 -0
  36. package/concepts/common/Edge/Stay/LongStay.json +8 -0
  37. package/concepts/common/Edge/Stay.json +8 -0
  38. package/concepts/common/Edge/Travel/LongTravel.json +16 -0
  39. package/concepts/common/Edge/Travel/ShortTravel.json +11 -0
  40. package/concepts/common/Edge/Travel/targetNode.json +7 -0
  41. package/concepts/common/Edge/Travel.json +7 -0
  42. package/concepts/common/Edge.json +3 -0
  43. package/concepts/common/Vertice.json +3 -0
  44. package/concepts/supply/Fulfillment.json +10 -0
  45. package/concepts/supply/Inventory/Reorder.json +7 -0
  46. package/concepts/supply/Inventory.json +5 -0
  47. package/concepts/supply/Procurement/SupplierConfirm.json +6 -0
  48. package/concepts/supply/Procurement.json +8 -0
  49. package/concepts/supply/Transport.json +5 -0
  50. package/doc/API.md +556 -0
  51. package/doc/CAPABILITIES.md +373 -0
  52. package/doc/MODELISATION.md +511 -0
  53. package/doc/architecture.md +267 -0
  54. package/doc/concept-as-graph.md +379 -0
  55. package/doc/concept-learning.md +168 -0
  56. package/doc/creative-loop-map.md +202 -0
  57. package/doc/original-2016-doc.md +148 -0
  58. package/doc/usage.md +501 -0
  59. package/lib/authoring/abstract.js +401 -0
  60. package/lib/authoring/abstraction.js +190 -0
  61. package/lib/authoring/adapt.js +393 -0
  62. package/lib/authoring/ancestry.js +216 -0
  63. package/lib/authoring/author.js +147 -0
  64. package/lib/authoring/bounded-merge.js +57 -0
  65. package/lib/authoring/canon.js +158 -0
  66. package/lib/authoring/clock.js +123 -0
  67. package/lib/authoring/combinator.js +92 -0
  68. package/lib/authoring/compete.js +76 -0
  69. package/lib/authoring/compose-hotspot.js +314 -0
  70. package/lib/authoring/compress.js +279 -0
  71. package/lib/authoring/concept-net.js +254 -0
  72. package/lib/authoring/concepts.js +66 -0
  73. package/lib/authoring/context-project.js +277 -0
  74. package/lib/authoring/contract.js +476 -0
  75. package/lib/authoring/corpus-pack.js +125 -0
  76. package/lib/authoring/cost-probe.js +86 -0
  77. package/lib/authoring/crystallize.js +555 -0
  78. package/lib/authoring/dag-decompose.js +256 -0
  79. package/lib/authoring/dataset-adapter.js +253 -0
  80. package/lib/authoring/debug-provider.js +92 -0
  81. package/lib/authoring/decompose.js +343 -0
  82. package/lib/authoring/emittability.js +236 -0
  83. package/lib/authoring/equilibrium.js +160 -0
  84. package/lib/authoring/extract.js +153 -0
  85. package/lib/authoring/forest.js +55 -0
  86. package/lib/authoring/forge-fallback.js +116 -0
  87. package/lib/authoring/givens.js +121 -0
  88. package/lib/authoring/glossary.js +177 -0
  89. package/lib/authoring/grammar-graph.js +191 -0
  90. package/lib/authoring/granularity.js +80 -0
  91. package/lib/authoring/graph-net.js +108 -0
  92. package/lib/authoring/ground.js +148 -0
  93. package/lib/authoring/higher-order.js +85 -0
  94. package/lib/authoring/hotspot.js +132 -0
  95. package/lib/authoring/hysteresis.js +85 -0
  96. package/lib/authoring/index.js +106 -0
  97. package/lib/authoring/lattice-morphism.js +61 -0
  98. package/lib/authoring/lattice-pack.js +148 -0
  99. package/lib/authoring/leaf-io.js +77 -0
  100. package/lib/authoring/library.js +156 -0
  101. package/lib/authoring/lifecycle.js +92 -0
  102. package/lib/authoring/loop.js +193 -0
  103. package/lib/authoring/master-loop.js +132 -0
  104. package/lib/authoring/mdl.js +209 -0
  105. package/lib/authoring/memo-stability.js +122 -0
  106. package/lib/authoring/method-explorer.js +138 -0
  107. package/lib/authoring/method-pack.js +181 -0
  108. package/lib/authoring/method.js +202 -0
  109. package/lib/authoring/mine.js +510 -0
  110. package/lib/authoring/mount.js +119 -0
  111. package/lib/authoring/negotiate.js +100 -0
  112. package/lib/authoring/parametric.js +139 -0
  113. package/lib/authoring/reaggregate.js +75 -0
  114. package/lib/authoring/rebalance.js +157 -0
  115. package/lib/authoring/recall.js +135 -0
  116. package/lib/authoring/registry.js +350 -0
  117. package/lib/authoring/relearn.js +147 -0
  118. package/lib/authoring/retention.js +89 -0
  119. package/lib/authoring/segment-proxy.js +114 -0
  120. package/lib/authoring/serve-leaf.js +64 -0
  121. package/lib/authoring/slot-aware-serve.js +77 -0
  122. package/lib/authoring/sound-invoke.js +59 -0
  123. package/lib/authoring/split-serve.js +74 -0
  124. package/lib/authoring/ste.js +239 -0
  125. package/lib/authoring/stock.js +101 -0
  126. package/lib/authoring/store.js +94 -0
  127. package/lib/authoring/supervise.js +138 -0
  128. package/lib/authoring/support.js +131 -0
  129. package/lib/authoring/task-mirror.js +51 -0
  130. package/lib/authoring/typed-loop.js +160 -0
  131. package/lib/authoring/validate.js +569 -0
  132. package/lib/authoring/widen.js +88 -0
  133. package/lib/combos/appliance.js +189 -0
  134. package/lib/combos/critique.js +377 -0
  135. package/lib/combos/defaults.js +86 -0
  136. package/lib/combos/durable-runner.js +115 -0
  137. package/lib/combos/forge.js +209 -0
  138. package/lib/combos/index.js +86 -0
  139. package/lib/combos/learning-library.js +253 -0
  140. package/lib/combos/mixture-serve.js +193 -0
  141. package/lib/combos/plan-loop.js +192 -0
  142. package/lib/combos/proxy-cache.js +272 -0
  143. package/lib/combos/self-mod.js +94 -0
  144. package/lib/durable/audit.js +102 -0
  145. package/lib/durable/checkpoint-store.js +550 -0
  146. package/lib/durable/fold.js +62 -0
  147. package/lib/durable/interpreter.js +242 -0
  148. package/lib/durable/xlate.js +185 -0
  149. package/lib/graph/Graph.js +2486 -0
  150. package/lib/graph/expr.js +307 -0
  151. package/lib/graph/index.js +26 -0
  152. package/lib/graph/log.js +134 -0
  153. package/lib/graph/objects/Concept.js +382 -0
  154. package/lib/graph/objects/Entity.js +503 -0
  155. package/lib/graph/objects/Node.js +57 -0
  156. package/lib/graph/objects/PathMap.js +557 -0
  157. package/lib/graph/objects/Segment.js +100 -0
  158. package/lib/graph/tasks/stabilize.js +59 -0
  159. package/lib/graph/tasks/taskflow.js +247 -0
  160. package/lib/index.js +122 -0
  161. package/lib/load.js +132 -0
  162. package/lib/providers/backends.js +117 -0
  163. package/lib/providers/borderline.js +129 -0
  164. package/lib/providers/cache.js +226 -0
  165. package/lib/providers/canonicalize.js +198 -0
  166. package/lib/providers/constat.js +107 -0
  167. package/lib/providers/geo.js +53 -0
  168. package/lib/providers/index.js +116 -0
  169. package/lib/providers/intake.js +263 -0
  170. package/lib/providers/llm-local.js +88 -0
  171. package/lib/providers/llm.js +236 -0
  172. package/lib/providers/local-host.js +197 -0
  173. package/lib/providers/merge-consistency.js +162 -0
  174. package/lib/providers/nogood.js +149 -0
  175. package/lib/providers/reason-loop.js +223 -0
  176. package/lib/providers/semiring.js +285 -0
  177. package/lib/providers/solver-fork.js +163 -0
  178. package/lib/providers/stats.js +228 -0
  179. package/lib/providers/verify.js +120 -0
  180. package/lib/runtime/index.js +135 -0
  181. package/lib/runtime/invoke-pool.js +92 -0
  182. package/lib/runtime/protocol.js +210 -0
  183. package/lib/runtime/transport-socket.js +77 -0
  184. package/lib/runtime/worker-entry.js +31 -0
  185. package/lib/sg/cli.js +601 -0
  186. package/lib/sg/log-sinks.js +214 -0
  187. package/lib/sg/mcp.js +395 -0
  188. package/lib/sg/proxy-run.js +66 -0
  189. package/lib/sg/serve-demo.js +185 -0
  190. package/lib/sg/serve.js +205 -0
  191. package/lib/sg/trace.js +118 -0
  192. package/lib/studio/protocol.js +45 -0
  193. package/lib/studio/public/app.js +310 -0
  194. package/lib/studio/public/components/ConceptEditor.js +43 -0
  195. package/lib/studio/public/components/ConceptTree.js +43 -0
  196. package/lib/studio/public/components/CorpusPanel.js +68 -0
  197. package/lib/studio/public/components/ForkTree.js +36 -0
  198. package/lib/studio/public/components/GrammarGraph.js +99 -0
  199. package/lib/studio/public/components/GraphCanvas.js +156 -0
  200. package/lib/studio/public/components/Inspector.js +48 -0
  201. package/lib/studio/public/components/LearningPanel.js +85 -0
  202. package/lib/studio/public/components/PromptConsole.js +46 -0
  203. package/lib/studio/public/components/ProviderTrace.js +39 -0
  204. package/lib/studio/public/components/SessionSplit.js +47 -0
  205. package/lib/studio/public/components/TilingOverlay.js +40 -0
  206. package/lib/studio/public/components/Timeline.js +57 -0
  207. package/lib/studio/public/index.html +26 -0
  208. package/lib/studio/public/styles.css +185 -0
  209. package/lib/studio/public/ws.js +58 -0
  210. package/lib/studio/server.js +130 -0
  211. package/lib/studio/session.js +397 -0
  212. package/lib/studio/studio.js +149 -0
  213. 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 };