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,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
+ * Declarative AI-authoring as a CEGIS loop (counterexample-guided inductive
21
+ * synthesis — Solar-Lezama 2006), the host-side capstone of roadmap #10
22
+ * (doc/MODELISATION.md §6.5). Zero core change: it composes the engine's
23
+ * `addConcept`/`patchConcept` with the author-time validator.
24
+ *
25
+ * The concept schema IS a typed grammar; the AI authors TERMS of it, the host owns
26
+ * the PRIMITIVES (the provider palette + the ref alphabet). Two oracles bound the
27
+ * search:
28
+ * - author-time oracle = `validateConceptTree` — a malformed candidate (prose
29
+ * dependency / bad expr / unknown ref / off-palette provider) is rejected
30
+ * BEFORE it touches the graph; the rejection is a counterexample;
31
+ * - behavioral oracle = the live graph — install, stabilize, test the goal
32
+ * predicate; an unmet goal is a counterexample.
33
+ * Each counterexample is appended to `history` and handed to the next proposal, so
34
+ * the candidate space strictly shrinks (CEGIS convergence / monotonicity).
35
+ *
36
+ * const { authorConcept } = require('./author');
37
+ * const res = await authorConcept(graph, {
38
+ * goal: (g) => ({ met: <bool>, counterexample: <string|null> }),
39
+ * propose: async ({ goal, history, round }) => ({ op:'add'|'patch', ... }),
40
+ * validate:{ palette, knownFacts, collectionKeys, strict },
41
+ * maxRounds: 5,
42
+ * });
43
+ * // res = { ok, concept?, rounds:[{proposal, outcome, counterexample?}], reason? }
44
+ *
45
+ * A `proposal` is one of:
46
+ * { op:'add', parent:<nameOrId|null>, schema:<conceptSchema> }
47
+ * { op:'patch', nameOrId:<concept>, updates:<partialSchema> }
48
+ *
49
+ * The proposer is INJECTED (an LLM::complete call in production; a deterministic
50
+ * stub in tests) — same backend-agnostic discipline as the providers.
51
+ */
52
+ const dmerge = require('deepmerge');
53
+ const { validateConceptTree } = require('./validate');
54
+
55
+ const replaceArrays = { arrayMerge: function ( _dest, src ) { return src; } };
56
+
57
+ // Wrap a single concept schema as a tree the validator can walk.
58
+ function asTree( schema ) {
59
+ return { childConcepts: { [schema._id]: schema } };
60
+ }
61
+
62
+ // Await one stabilize after a structural op; surface a synchronous engine throw
63
+ // (e.g. duplicate _id, unknown parent) as a rejected promise.
64
+ //
65
+ // Engine gotcha (verified): `stabilize(cb)` only fires `cb` once something settles —
66
+ // i.e. only if the op destabilized at least one object. A NO-OP op (a patch that
67
+ // changes no cast-state, or an add that matches nothing) destabilizes nothing, so the
68
+ // "stabilize" event never comes and the callback would hang. But a no-op means the
69
+ // graph is ALREADY quiescent, so honour the callback on the next tick. A real op
70
+ // leaves `_unstable`/`_triggeredCastCount` non-empty and resolves via the event.
71
+ function applyOp( graph, proposal ) {
72
+ return new Promise(function ( resolve, reject ) {
73
+ var done = false;
74
+ var finish = function () { if ( !done ) { done = true; resolve(); } };
75
+ try {
76
+ if ( proposal.op === 'patch' )
77
+ graph.patchConcept(proposal.nameOrId, proposal.updates, finish);
78
+ else
79
+ graph.addConcept(proposal.parent == null ? null : proposal.parent, proposal.schema, finish);
80
+ } catch ( e ) { return reject(e); }
81
+ if ( !graph._unstable.length && !graph._triggeredCastCount ) setTimeout(finish);
82
+ });
83
+ }
84
+
85
+ async function authorConcept( graph, spec ) {
86
+ const goal = spec.goal;
87
+ const propose = spec.propose;
88
+ const validate = spec.validate || {};
89
+ const maxRounds = spec.maxRounds || 5;
90
+ const history = [];
91
+
92
+ for ( let round = 0; round < maxRounds; round++ ) {
93
+ const proposal = await propose({ goal: spec.goalDescription, history, round });
94
+ if ( !proposal ) break; // proposer gave up
95
+ const op = proposal.op || 'add';
96
+
97
+ // 1. AUTHOR-TIME ORACLE — validate the candidate (for a patch, validate the
98
+ // schema it would PRODUCE, so a patch that introduces a bad expr/ref is caught too).
99
+ let tree;
100
+ if ( op === 'patch' ) {
101
+ const c = graph.getConceptByName(proposal.nameOrId);
102
+ const merged = c ? dmerge(c._schema, proposal.updates || {}, replaceArrays)
103
+ : Object.assign({ _id: proposal.nameOrId, _name: proposal.nameOrId }, proposal.updates);
104
+ tree = asTree(merged);
105
+ } else {
106
+ tree = asTree(proposal.schema);
107
+ }
108
+ const errs = validateConceptTree(tree, validate).errors;
109
+ if ( errs.length ) {
110
+ history.push({ proposal, outcome: 'rejected', counterexample: `validation-failed: ${errs[0].kind} — ${errs[0].message}` });
111
+ continue;
112
+ }
113
+
114
+ // 1.5 ADMISSION GATE (optional) — a semantic gate beyond well-formedness, e.g. the
115
+ // MDL/utility gate for an abstraction (lib/authoring/abstraction.js): admit only a
116
+ // candidate that is fixpoint-equivalent + net-cheaper. A non-admission is a
117
+ // counterexample (the candidate is NOT installed). The gate may be async and gets
118
+ // (graph, proposal, {history, round}); returning {admit:false, reason} rejects.
119
+ if ( spec.gate ) {
120
+ const g = await spec.gate(graph, proposal, { history, round });
121
+ if ( g && g.admit === false ) {
122
+ history.push({ proposal, outcome: 'rejected', counterexample: `gate-rejected: ${g.reason || 'not admitted'}` });
123
+ continue;
124
+ }
125
+ }
126
+
127
+ // 2. INSTALL — engine-level guards (dup _id, unknown parent) are counterexamples too.
128
+ try {
129
+ await applyOp(graph, proposal);
130
+ } catch ( e ) {
131
+ history.push({ proposal, outcome: 'rejected', counterexample: `install-failed: ${e.message}` });
132
+ continue;
133
+ }
134
+
135
+ // 3. BEHAVIORAL ORACLE — does the live graph now satisfy the goal?
136
+ const verdict = goal(graph) || {};
137
+ if ( verdict.met ) {
138
+ history.push({ proposal, outcome: 'met' });
139
+ return { ok: true, concept: op === 'patch' ? proposal.nameOrId : proposal.schema._id, rounds: history };
140
+ }
141
+ history.push({ proposal, outcome: 'unmet', counterexample: verdict.counterexample || 'goal not met' });
142
+ }
143
+
144
+ return { ok: false, rounds: history, reason: 'maxRounds exhausted without meeting goal' };
145
+ }
146
+
147
+ module.exports = { authorConcept };
@@ -0,0 +1,57 @@
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
+ * bounded-merge — the BOUNDED `project` at a fork/merge JOIN (host-side, ZERO-CORE). The enforcement gate
10
+ * Lens 3 flagged: `Graph#merge(child, targetId)` with NO project DEFAULTS to crossing the WHOLE serialized
11
+ * child (`{ forkResult: child.serialize() }`) — which re-creates the O(N) context blowup the architecture
12
+ * exists to avoid (a child's entire sub-graph leaks across the AND-join). The bounded-context contract
13
+ * (master-graph study §2.3, rule A1/A6) requires every join to cross only a separator-sized FRONTIER.
14
+ *
15
+ * `boundedProject` builds a `project` fn that crosses ONLY the declared frontier facts (the separator
16
+ * alphabet Σ_sep), snapped off one result object in the child — so the merge traffic is O(|frontier|),
17
+ * independent of the child's size. Pair it with `validate` (the author-time `validateMergeProjection`
18
+ * frontier-leak check) to assert no key escapes the alphabet.
19
+ *
20
+ * const project = boundedProject({ targetId: 'root', from: 'result', keys: ['answer', 'cost'] });
21
+ * parent.merge(child, 'root', project); // crosses {answer,cost} only — NOT the whole child
22
+ */
23
+ const { validateMergeProjection } = require('./validate.js');
24
+
25
+ /**
26
+ * @param spec.targetId the parent object the projection updates ($$_id)
27
+ * @param spec.from the child object id whose facts are the result (default: spec.targetId)
28
+ * @param spec.keys the frontier fact keys allowed to cross (the separator alphabet)
29
+ * @returns (child) => mutationTemplate a `project` fn for Graph#merge that crosses only `keys`
30
+ */
31
+ function boundedProject( spec ) {
32
+ spec = spec || {};
33
+ const targetId = spec.targetId, from = spec.from || spec.targetId, keys = spec.keys || [];
34
+ return function ( child ) {
35
+ const e = child.getEtty ? child.getEtty(from) : null;
36
+ const f = (e && e._) || {};
37
+ const out = { $$_id: targetId };
38
+ for ( const k of keys ) if ( f[k] !== undefined ) out[k] = f[k]; // ONLY the frontier crosses
39
+ return out;
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Author-time check that a produced merge template crosses only the declared frontier alphabet (no leak).
45
+ * Thin wrapper over `validate.js#validateMergeProjection` so a host can gate a merge before applying it.
46
+ * A frontier leak (a key crossing that isn't in the alphabet) is a `frontier-leak` warning (an error under
47
+ * `strict`); `ok` is false if anything leaked.
48
+ * @returns { errors:[…], warnings:[…], leaks:[…], ok:bool }
49
+ */
50
+ function validate( template, frontierKeys, opts ) {
51
+ const r = validateMergeProjection(template, Object.assign({ frontierAlphabet: frontierKeys || [] }, opts || {})) || {};
52
+ const errors = r.errors || [], warnings = r.warnings || [];
53
+ const leaks = warnings.filter(( w ) => w && w.kind === 'frontier-leak').concat(errors.filter(( e ) => e && e.kind === 'frontier-leak'));
54
+ return { errors, warnings, leaks, ok: errors.length === 0 && leaks.length === 0 };
55
+ }
56
+
57
+ module.exports = { boundedProject, validate };
@@ -0,0 +1,158 @@
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
+ * canon — the CANONICALIZATION BARRIER as a standalone brick (values AND structure).
10
+ *
11
+ * Two field-proven moves, promoted from experiment code where each served twice (2026-07-03):
12
+ *
13
+ * 1. VALUE snap (`snapToVocab`) — the intake barrier: a model-extracted SURFACE value is snapped onto the
14
+ * vocabulary of the system's OWN data (exact ci-match keeps the surface form; containment either way
15
+ * snaps to the canonical form; anything else survives RAW and is counted OOV — the honest path: a
16
+ * genuine out-of-vocabulary value must fail downstream verify, never be silently coerced).
17
+ *
18
+ * 2. STRUCTURAL canon (`makeStructuralCanon`) — the same barrier applied to a typed step-list: a model
19
+ * emits ONE plan at SEVERAL granularities (e.g. `[filter(f,v), aggregate]` vs `[aggregate(f,v)]`),
20
+ * related exactly by recurring digrams — the equivalence the system itself mines (`compress.js
21
+ * mineDigrams`, GO-gated 2026-07-03). The canon folds those digrams and whitelists per-step facts
22
+ * fail-closed, so emission STABILITY (and every downstream memo/LGG key) is claimed MODULO a learned
23
+ * equivalence, never modulo an ad-hoc regex.
24
+ *
25
+ * Both are K1 instruments: they keep dependency edges on DISCRETE, canonical keys (doc/API.md facts/prose
26
+ * contract). Fail-closed throughout — a fact that cannot be certified is DROPPED, never guessed:
27
+ * - facts live only on declared `factKinds` steps;
28
+ * - if the gating fact (default `value`) is absent or out of the data vocabulary, ALL facts drop for
29
+ * that step (an operation word leaked into `value` must not survive as a param);
30
+ * - a fold merges facts per-key: one side present → carried; both present and ci-equal → kept; a
31
+ * CONFLICT → the key is dropped (parallel-over-collapse, the §9 rule at fact grain).
32
+ */
33
+
34
+ const low = ( v ) => String(v).toLowerCase();
35
+
36
+ /**
37
+ * Snap a surface value onto a canonical vocabulary.
38
+ * @param value the extracted surface form
39
+ * @param vocab array of canonical strings (the system's own data values — never test golds)
40
+ * @param stats optional { kept, snapped, oov } counters, incremented in place
41
+ * @returns { value, verdict } — verdict ∈ exact|snapped|oov
42
+ */
43
+ function snapToVocab( value, vocab, stats ) {
44
+ stats = stats || {};
45
+ const v = low(value == null ? '' : value);
46
+ if ( vocab.some(( c ) => low(c) === v) ) {
47
+ stats.kept = (stats.kept || 0) + 1;
48
+ return { value: value, verdict: 'exact' };
49
+ }
50
+ // containment must be UNIQUE to snap: a hypernym surface ('plug' ⊂ europlug|usplug|ukplug) matches
51
+ // several vocab words — snapping to the first is an arbitrary wrong pick that then OVERRIDES a correct
52
+ // explicit fact downstream (measured: the G3 plugs domain). Ambiguous containment → OOV, fail-closed.
53
+ const hits = v ? vocab.filter(( c ) => v.includes(low(c)) || low(c).includes(v) ) : [];
54
+ if ( hits.length === 1 ) {
55
+ stats.snapped = (stats.snapped || 0) + 1;
56
+ return { value: hits[0], verdict: 'snapped' };
57
+ }
58
+ stats.oov = (stats.oov || 0) + 1;
59
+ if ( hits.length > 1 ) { stats.ambiguous = (stats.ambiguous || 0) + 1; return { value: value, verdict: 'oov', ambiguous: hits }; }
60
+ return { value: value, verdict: 'oov' };
61
+ }
62
+
63
+ /** Fold specs from a `compress.js#mineDigrams` result: each mined adjacent pair (a,b) above minSupport
64
+ * becomes a fold `[a, b] → b` (facts carried per the merge rule). The folds are thereby LEARNED from the
65
+ * system's own traces — the only legitimacy a structural rewrite gets. */
66
+ function foldsFromDigrams( digrams, opts ) {
67
+ opts = opts || {};
68
+ const min = opts.minSupport != null ? opts.minSupport : 2;
69
+ return (digrams || []).filter(( d ) => d && d.a && d.b && (d.support == null || d.support >= min) )
70
+ .map(( d ) => ({ a: d.a, b: d.b, into: opts.into || d.b }));
71
+ }
72
+
73
+ /**
74
+ * Build a structural canonicalizer over typed step-lists.
75
+ * @param opts {
76
+ * kindKey = 'stepKind' — the step's kind fact
77
+ * factKeys = [] — per-step content facts under whitelist discipline (e.g. ['field','value'])
78
+ * factKinds = null — kinds allowed to carry facts (null = all kinds)
79
+ * gateKey = 'value' — the fact whose vocabulary membership gates ALL facts of the step
80
+ * vocab = null — array/Set of data values (ci); null = no vocabulary gate
81
+ * folds = [] — [{a, b, into}] adjacent-digram folds (see foldsFromDigrams)
82
+ * }
83
+ * @returns ( steps ) => canonical steps (null in → null out; input never mutated)
84
+ */
85
+ function makeStructuralCanon( opts ) {
86
+ opts = opts || {};
87
+ const kindKey = opts.kindKey || 'stepKind';
88
+ const factKeys = opts.factKeys || [];
89
+ const factKinds = opts.factKinds ? new Set(opts.factKinds) : null;
90
+ const gateKey = opts.gateKey || 'value';
91
+ const vocab = opts.vocab ? new Set([...opts.vocab].map(low)) : null;
92
+ const folds = opts.folds || [];
93
+
94
+ const clean = ( s ) => {
95
+ const kind = s[kindKey];
96
+ let keep = !factKinds || factKinds.has(kind);
97
+ if ( keep && vocab && factKeys.includes(gateKey) )
98
+ keep = s[gateKey] != null && vocab.has(low(s[gateKey]));
99
+ const out = Object.assign({}, s);
100
+ for ( const k of factKeys ) if ( !keep || out[k] == null ) delete out[k];
101
+ return out;
102
+ };
103
+
104
+ // per-key fact merge: one side → carried · ci-equal → kept · conflict → DROPPED (fail-closed)
105
+ const mergeFacts = ( a, b ) => {
106
+ const m = {};
107
+ for ( const k of factKeys ) {
108
+ if ( a[k] != null && b[k] != null ) { if ( low(a[k]) === low(b[k]) ) m[k] = a[k]; }
109
+ else if ( a[k] != null ) m[k] = a[k];
110
+ else if ( b[k] != null ) m[k] = b[k];
111
+ }
112
+ return m;
113
+ };
114
+
115
+ return function canon( steps ) {
116
+ if ( !Array.isArray(steps) ) return null;
117
+ let cur = steps.map(clean);
118
+ for ( let pass = 0; pass < steps.length; pass++ ) { // to fixpoint, bounded
119
+ const out = [];
120
+ let folded = false;
121
+ for ( let i = 0; i < cur.length; i++ ) {
122
+ const nxt = cur[i + 1];
123
+ const f = nxt && folds.find(( f ) => cur[i][kindKey] === f.a && nxt[kindKey] === f.b );
124
+ if ( f ) {
125
+ const facts = mergeFacts(cur[i], nxt);
126
+ const base = Object.assign({}, nxt);
127
+ for ( const k of factKeys ) delete base[k];
128
+ Object.assign(base, facts);
129
+ base[kindKey] = f.into || f.b;
130
+ out.push(base);
131
+ i++; folded = true;
132
+ }
133
+ else out.push(cur[i]);
134
+ }
135
+ cur = out;
136
+ if ( !folded ) break;
137
+ }
138
+ return cur;
139
+ };
140
+ }
141
+
142
+ /** The kinds-only key of a step-list — the SHAPE face of stability (G1's post-canon comparison). */
143
+ const shapeKey = ( steps, opts ) => steps == null ? null
144
+ : JSON.stringify(steps.map(( s ) => s[(opts && opts.kindKey) || 'stepKind'] ));
145
+
146
+ /** The shape+facts key (ci values) — the full canonical digest both experiments compared on. */
147
+ function canonKey( steps, opts ) {
148
+ if ( steps == null ) return null;
149
+ const kindKey = (opts && opts.kindKey) || 'stepKind';
150
+ const factKeys = (opts && opts.factKeys) || [];
151
+ return JSON.stringify(steps.map(( s ) => {
152
+ const o = { k: s[kindKey] };
153
+ for ( const f of factKeys ) o[f] = s[f] != null ? low(s[f]) : '';
154
+ return o;
155
+ }));
156
+ }
157
+
158
+ module.exports = { snapToVocab, foldsFromDigrams, makeStructuralCanon, shapeKey, canonKey };
@@ -0,0 +1,123 @@
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
+ * Host-side clock for freshness / TTL / epochs as facts (roadmap N1).
21
+ *
22
+ * Time enters the graph as an ordinary fact on a `clock` free-node — the engine has no
23
+ * internal wall-clock (and shouldn't: that keeps replay hermetic). A time-bound concept
24
+ * gates freshness in an `ensure`, e.g. `ensure:["$$clock:tick - $sensedAt < $ttl"]`.
25
+ *
26
+ * IMPORTANT: reference the global clock with a DOUBLE-`$` — `$$clock:tick`. A single
27
+ * `$clock` is a key on the *current* scope (almost always undefined); the ref regex
28
+ * consumes one `$`, so the global free-node ref needs two (HANDOFF §3). A provider reads
29
+ * it via `clockNow(graph)`.
30
+ *
31
+ * Advancing the clock (a one-line mutation) re-tests EXACTLY the concepts whose `ensure`
32
+ * follows `$$clock:tick` — the existing ensure/defeasance re-fire path. A fact that has
33
+ * gone stale (and its dependents) retracts in cascade; this is the cache-poisoning fix
34
+ * (a graven LLM/API fact otherwise lives forever). Near-zero new evaluation code.
35
+ *
36
+ * BOUNDARY (honest): invalidation is automatic + reliable. Automatic RE-FETCH is not —
37
+ * a provider is cast-once, so a stale provider-fact re-derives only on uncast→recast.
38
+ * Re-trigger it from the host (`refetch` below = uncast/recast, which re-runs the
39
+ * provider with the now-current clock) or bump its input. A fully-autonomous reaper that
40
+ * destabilizes+recasts stale nodes itself is an optional core primitive (see HANDOFF §5).
41
+ */
42
+
43
+ const CLOCK_ID = 'clock';
44
+
45
+ // A clock seed entry — spread into `seed.freeNodes`.
46
+ function clockSeed( tick ) { return { _id: CLOCK_ID, tick: tick || 0 }; }
47
+
48
+ // The current tick (for a provider to stamp `<x>At: clockNow(graph)`).
49
+ function clockNow( graph ) {
50
+ var c = graph._objById[CLOCK_ID];
51
+ return c ? c._etty._.tick : undefined;
52
+ }
53
+
54
+ // Advance the clock by `delta` (default 1). Returns the new tick. Re-tests every
55
+ // `$$clock:tick`-following concept → stale facts (and dependents) retract.
56
+ function advanceClock( graph, delta ) {
57
+ var tick = (clockNow(graph) || 0) + (delta == null ? 1 : delta);
58
+ graph.pushMutation({ $$_id: CLOCK_ID, tick: tick }, CLOCK_ID);
59
+ return tick;
60
+ }
61
+
62
+ // Host-triggered refetch of a cast-once provider concept: re-run it against the current
63
+ // clock. If still cast (forced early refresh) uncast first; if already stale-retracted,
64
+ // a direct re-cast re-runs the provider. The reliable refresh path (zero-core).
65
+ function refetch( graph, objId, conceptId, cb ) {
66
+ var etty = graph._objById[objId] && graph._objById[objId]._etty,
67
+ isCast = etty && etty._mappedConcepts && etty._mappedConcepts[conceptId];
68
+ if ( isCast ) graph.unCastConcept(objId, conceptId, function () { graph.castConcept(objId, conceptId, cb); });
69
+ else graph.castConcept(objId, conceptId, cb);
70
+ }
71
+
72
+ /**
73
+ * Freshness reaper (N1 follow-on) — a zero-core HOST helper over `refetch`: the host registers
74
+ * freshness contracts and calls `reap()` (e.g. right after `advanceClock`, or on a wall-clock
75
+ * timer) to re-run the cast-once provider for every node whose stamp has aged past its TTL.
76
+ * Invalidation is already automatic (a `$$clock`-gated `ensure` retracts a stale fact); this
77
+ * automates the RE-FETCH that a cast-once provider otherwise needs the host to trigger per node.
78
+ *
79
+ * (The fully-autonomous version — the ENGINE reaping itself by piggybacking the settle loop —
80
+ * is an optional CORE primitive, see HANDOFF §5; this is the host-loop form on existing parts.)
81
+ *
82
+ * const reaper = makeReaper(graph);
83
+ * reaper.watch('sensorNode', 'Sensor', { stampKey: 'sensedAt', ttl: 100 });
84
+ * advanceClock(graph, 200);
85
+ * reaper.reap((n) => { ... n stale provider-facts re-fetched ... });
86
+ *
87
+ * A provider under a reaped contract MUST re-stamp its `stampKey` with `clockNow(graph)` when it
88
+ * runs, or it stays stale and re-fetches every sweep.
89
+ */
90
+ function makeReaper( graph ) {
91
+ var contracts = [];
92
+ return {
93
+ // Register a freshness contract. ttl is in clock-tick units; stampKey is the fact the
94
+ // provider stamps with `clockNow(graph)`.
95
+ watch: function ( objId, conceptId, opts ) {
96
+ opts = opts || {};
97
+ contracts.push({ objId: objId, conceptId: conceptId, stampKey: opts.stampKey || 'sensedAt', ttl: opts.ttl });
98
+ return this;
99
+ },
100
+ // The contracts whose stamp is missing or older than ttl vs the current clock.
101
+ stale: function () {
102
+ var now = clockNow(graph) || 0, out = [];
103
+ contracts.forEach(function ( c ) {
104
+ var o = graph._objById[c.objId], etty = o && o._etty;
105
+ if ( !etty ) return; // node gone — nothing to reap
106
+ var stamp = etty._[c.stampKey];
107
+ if ( stamp == null || (now - stamp) >= c.ttl ) out.push(c);
108
+ });
109
+ return out;
110
+ },
111
+ // Re-fetch every stale contract; cb(count) fires once all re-fetches have settled.
112
+ reap: function ( cb ) {
113
+ var due = this.stale(), n = due.length, done = 0;
114
+ if ( !n ) { cb && cb(0); return 0; }
115
+ due.forEach(function ( c ) {
116
+ refetch(graph, c.objId, c.conceptId, function () { if ( ++done === n && cb ) cb(n); });
117
+ });
118
+ return n;
119
+ }
120
+ };
121
+ }
122
+
123
+ module.exports = { CLOCK_ID: CLOCK_ID, clockSeed: clockSeed, clockNow: clockNow, advanceClock: advanceClock, refetch: refetch, makeReaper: makeReaper };
@@ -0,0 +1,92 @@
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
+ * combinator — the dispatch→MOUNT bridge (P2.5; host-side, ZERO-CORE; study
10
+ * doc/WIP/studies/2026-06-30-creative-loop-two-level-grammar.md, brick B).
11
+ *
12
+ * Brick A (`library.js`) SELECTS a learned method for an abstract mechanism (an O(1) `libraryKey` lookup refined by
13
+ * application-conditions = structure-mapping). Brick B MOUNTS it: a higher-order method = a CONCEPT whose `require`
14
+ * is its behavioral hole; when the hole's fragment co-occurs justified at a site, the concept casts and its provider
15
+ * binds the DISPATCHED fragment's template to the call site and splices it — at 0 model calls. The hole is filled by
16
+ * the library method whose FrontierSignature the structuring grammar's target dispatched to, so a concept can REUSE
17
+ * another concept's learned method purely because their abstract interfaces match (recombination = the creative step).
18
+ *
19
+ * Engine landmines respected (mirrors crystallize.js#buildStructuralProvider): the returned template MUST self-flag
20
+ * the cast (provider-cast-marker gotcha, else re-fire to the apply-cap), an unbound frontier ref BYPASSES (never a
21
+ * wrong replay), and the splice rides the SEQUENCED cb (determinism). The interface alphabet must align: the
22
+ * combinator's frontier param NAMES must equal the fragment's hole names (the snapped separator).
23
+ */
24
+ const { dispatch } = require('./library.js');
25
+ const { ctxFromScope, instantiate } = require('./abstract.js');
26
+ const { digest } = require('../providers/canonicalize.js');
27
+
28
+ // the call-site facts projected onto a fragment's replay signature (mirrors crystallize.js#projectFacts).
29
+ const projectFacts = ( facts, keys ) => { const o = {}; for ( const k of (keys || []) ) if ( facts && k in facts ) o[k] = facts[k]; return o; };
30
+
31
+ // the re-fire GUARD fact a combinator gates on — a DISTINCT durable mutation fact, NOT the concept's own `_name`
32
+ // marker. Gating on the self-flag does not stop re-application (finding #27 "self-flag not durable" — verified: the
33
+ // concept-name marker set by a provider does not gate the same concept's `ensure`; the crystallizer gates on the
34
+ // fragment's `Refined`, not `!$CrystalRefine`). So we mint `<cryId>Done`.
35
+ const guardKey = ( cryId ) => cryId + 'Done';
36
+
37
+ // add the combinator's cast marker AND its durable re-fire guard to the mounted parent object (mirrors
38
+ // crystallize.js#injectCastMarker, plus the separate guard so the concept de-applies after one mount).
39
+ function injectMarker( ground, base, cryId ) {
40
+ return (Array.isArray(ground) ? ground : [ground]).map(( o ) => {
41
+ if ( !o || typeof o !== 'object' ) return o;
42
+ const idv = String(o.$$_id != null ? o.$$_id : (o.$_id != null ? o.$_id : o._id)).replace(/^\$+/, '');
43
+ return (o.$_id === '_parent' || idv === base) ? Object.assign({}, o, { [cryId]: true, [guardKey(cryId)]: true }) : o;
44
+ });
45
+ }
46
+
47
+ /**
48
+ * A provider that DISPATCHES a library fragment for the call site, then MOUNTS it bound to the site (0 model calls).
49
+ * @param spec.lib the method library (`library.js`).
50
+ * @param spec.cryId the combinator's cast-marker / concept name.
51
+ * @param spec.target { frontier, signatureKeys } — the abstract mechanism this combinator wants (the target
52
+ * FrontierSignature the structuring grammar describes). May be a fn (scope)->target.
53
+ * @param spec.frontierFields { name: factKey|"ref:path" } — where each frontier endpoint lives on the cast object (its
54
+ * names MUST equal the dispatched fragment's hole names).
55
+ * @param spec.pick optional (candidates, scope)->candidate (default = top by weight; the supervisor's choice).
56
+ */
57
+ function buildDispatchProvider( spec ) {
58
+ return function ( graph, concept, scope, argz, cb ) {
59
+ const noop = { $_id: '_parent', [spec.cryId]: true, [guardKey(spec.cryId)]: true }; // bypass: marker + guard (no re-fire)
60
+ const target = typeof spec.target === 'function' ? spec.target(scope) : spec.target;
61
+ const r = dispatch(spec.lib, target, scope._ || {});
62
+ const top = spec.pick ? spec.pick(r.candidates, scope) : r.candidates[0];
63
+ if ( !top ) return cb(null, noop); // the structuring grammar must forge (no library hit)
64
+ const cand = top.candidate;
65
+ const ctx = ctxFromScope(scope, { frontier: spec.frontierFields });
66
+ if ( !ctx ) return cb(null, noop); // unknown base → bypass
67
+ const tpl = (cand.templatesBySig || {})[digest(projectFacts(scope._ || {}, cand.signatureKeys || []))];
68
+ if ( !tpl ) return cb(null, noop); // the dispatched method has no template for this signature class
69
+ const ground = instantiate(tpl, ctx);
70
+ if ( !ground ) return cb(null, noop); // an unbound frontier ref → bypass
71
+ return cb(null, injectMarker(ground, ctx.base, spec.cryId)); // mount the dispatched fragment, self-flagged
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Build a higher-order combinator CONCEPT that mounts a dispatched library fragment via require-resolution. The
77
+ * concept casts when its `require` markers co-occur (the fragment is justified-present); its provider dispatches +
78
+ * mounts. Mirrors `method.js#selectCluster` / `relearn.js#relearnTree` (concept + provider, host-wired).
79
+ * @returns { schema, provider, providerName } — register the provider and drop the schema into a concept tree.
80
+ */
81
+ function dispatchConcept( opts ) {
82
+ const cryId = opts.name;
83
+ const provider = buildDispatchProvider({ lib: opts.lib, cryId, target: opts.target, frontierFields: opts.frontierFields, pick: opts.pick });
84
+ const providerName = 'Combinator::' + cryId;
85
+ // gate on the DURABLE guard fact `<cryId>Done` (set by the mount/bypass), NOT the self-flag `!$<cryId>` (which does
86
+ // not stop re-application — finding #27). require + this ensure = the higher-order require-resolution trigger.
87
+ const schema = { _id: cryId, _name: cryId, require: (opts.require || []).slice(), ensure: ['!$' + guardKey(cryId)], provider: [providerName] };
88
+ if ( opts.assert ) schema.assert = Array.isArray(opts.assert) ? opts.assert.slice() : [opts.assert];
89
+ return { schema, provider, providerName };
90
+ }
91
+
92
+ module.exports = { buildDispatchProvider, dispatchConcept, injectMarker, guardKey };
@@ -0,0 +1,76 @@
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
+ * compete — §6.1 MULTI-PATH `Construct` → the dominance-gated competitive crystallizer (host-side, ZERO-CORE; spec
10
+ * §6.1, refined by the 2026-06-30 Laurie residency confront).
11
+ *
12
+ * A `Construct` is the abstract, multi-hypothesis stage of the Construct→Method gradient: an LLM response is a SET of
13
+ * candidate decompositions that expand. This provider runs that competition — propose N candidates, SELECT the survivor
14
+ * by the `pareto` semiring (`semiring.js#paretoSelect`, the realized multi-criteria outcome, not a static heuristic) —
15
+ * and EMITS the winner's decomposition as a bounded STRUCTURAL sub-graph so the existing trace→mine→crystallize pipeline
16
+ * (the kill-gate's) turns a recurrent winner into a re-mountable `Method`. The next same-class problem then DISPATCHES
17
+ * the winner at 0 model calls, eliding the WHOLE N-way rollout.
18
+ *
19
+ * RESIDENCY = isolated forks / per-instance competition (ZERO-CORE). True co-residency of contradictory branches = an
20
+ * ATMS multi-context (de Kleer 1986) over a single-context JTMS engine — FILED (its only justifying case, a non-local
21
+ * frontier, is what `contract.js` G1 already forbids). The candidate ELABORATION is the host's `propose` (a real
22
+ * `graph.fork` rollout live, like `examples/poc/problem-compete.js`; a deterministic scorer in tests).
23
+ *
24
+ * THE LOAD-BEARING SOUNDNESS GATE (Laurie pt2 — else the survivor is a SILENT MIS-DISPATCHER): crystallizing the
25
+ * survivor with `pre:[]` is candidate elimination (Mitchell 1982) with the NEGATIVES (the losing siblings) discarded.
26
+ * The fix reifies the SELECTION CRITERION, not the siblings:
27
+ * • emit the winner's STRUCTURAL decomposition ONLY on a CLEAN DOMINANCE (`front.length===1`, one strict Pareto
28
+ * dominator); a Pareto TIE (`front.length>1`, a trade-off / equally-good sibling) → a FLAT head marker only, which
29
+ * the structural miner SKIPS (the existing flat-patch-skip IS the tie-gate — an arbitrary pick is unsound);
30
+ * • a winner that FLIPS for the same typed premise → two winner templates for one signature → the crystalliser's
31
+ * `signatureDetermined` (K1) REFUSES — winner-determinacy lifted from content-determinacy, no false crystallization.
32
+ *
33
+ * Engine landmines respected: a flat length-1 fact patch is skipped by the miner (so the WINNING cast emits a
34
+ * multi-object sub-graph); a wired provider does NOT auto-flag its cast, so the head sets BOTH the cast marker
35
+ * `{ Compete:true }` and a DISTINCT durable re-fire guard `{ Competed:true }`.
36
+ *
37
+ * @param opts.propose (scope) -> [{ id, ...criteria, decomp }] | Promise<...> the candidate decompositions;
38
+ * each carries its pareto-criteria values + a `decomp(base, originNode, targetNode) ->
39
+ * [obj,...]` builder for its structural sub-graph. Injectable: a fork rollout live, a stub in tests.
40
+ * @param opts.criteria {name: bandList|{dir}} the pareto comparison criteria (REQUIRED to discriminate).
41
+ * @param opts.lex criterion-priority order for the deterministic tie-break (default: criteria key order).
42
+ * @param opts.discriminantKey the typed premise fact that should determine the winner (default 'taskClass') — echoed
43
+ * onto the head only if NOT a require key (avoids the echoed-require crystallise hazard).
44
+ * @returns { Compete: { compete } } a provider-map fragment (concept provider ref `Compete::compete`).
45
+ */
46
+ const { paretoSelect } = require('../providers/semiring.js');
47
+
48
+ function makeCompeteProvider( opts ) {
49
+ opts = opts || {};
50
+ const propose = opts.propose;
51
+ const criteria = opts.criteria || {};
52
+ const discKey = opts.discriminantKey || 'taskClass';
53
+
54
+ return { Compete: {
55
+ compete: function ( graph, concept, scope, argz, cb ) {
56
+ Promise.resolve(propose(scope)).then(function ( cands ) {
57
+ cands = cands || [];
58
+ const sel = paretoSelect(cands, criteria, { idKey: 'id', lex: opts.lex });
59
+ const base = scope._._id, o = scope._.originNode, t = scope._.targetNode;
60
+ // the head: the cast marker + the DISTINCT durable re-fire guard + the dominance discriminant (frontSize).
61
+ const head = { $_id: '_parent', Compete: true, Competed: true, frontSize: sel.front.length };
62
+ graph.traceProvider && graph.traceProvider(concept, scope, { compete: { frontIds: sel.frontIds, selectedId: sel.selectedId } });
63
+ const winner = cands.filter(function ( c ) { return c.id === sel.selectedId; })[0];
64
+
65
+ // TIE-GATE: a Pareto trade-off (no single dominator) → emit ONLY the flat head marker → the structural
66
+ // miner skips it (an arbitrary pick among equally-good siblings is unsound to crystallise).
67
+ if ( sel.front.length !== 1 || !winner || typeof winner.decomp !== 'function' ) return cb(null, head);
68
+
69
+ // CLEAN DOMINANCE → emit the winner's structural decomposition (minable → crystallisable iff winner = f(premise)).
70
+ cb(null, [head].concat(winner.decomp(base, o, t)));
71
+ }).catch(function ( e ) { cb(null, { $_id: '_parent', Compete: true, Competed: true, llmError: e.message }); });
72
+ },
73
+ } };
74
+ }
75
+
76
+ module.exports = { makeCompeteProvider };