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,242 @@
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
+ * interpreter — LAYER B of the durable executor: the per-record TOKEN-FLOW interpreter. It walks case records
10
+ * through a compiled workflow-net (`xlate.js`) VIA the `CheckpointStore` (`checkpoint-store.js`), so position is
11
+ * durable + crash-resumable and each step is content-memoized. This is the skynet-NATIVE half the convergence
12
+ * study (§5 Layer B) keeps out of the commodity plumbing: typed routing (from `selectCluster`'s gates) and
13
+ * per-case determinism live here.
14
+ *
15
+ * const store = createCheckpointStore({ file });
16
+ * const net = compileMethod(spec);
17
+ * store.ensureRun(runId, net); store.inject(runId, records);
18
+ * const m = runFlow(store, runId, net, { runTask }); // drains to a fixpoint; resume by calling it again
19
+ *
20
+ * Per-case determinism: each token carries its OWN payload (its world); cross-record interleaving is
21
+ * non-deterministic THROUGHPUT, not belief (the §12.3 belief↔durable line). A transition fires by kind:
22
+ * - select : evaluate the typed gates (the SAME `when` exprs as `selectCluster`, via the safe `expr.js`
23
+ * evaluator) against the record's facts → move to the matching branch; no match → the fallback.
24
+ * - task : a provider MICRO-TASK. Content-addressed memo (C5): key = FactsDigest(canonical step input); a
25
+ * HIT replays at 0 calls (this is what makes crash-resume cheap AND amortizes a recurrent stream).
26
+ * A null `keyOf` BYPASSES the memo (fail-open, like cache.js); a null task = a no-op passthrough.
27
+ * - map : Brick-1 fan-out — one child token per element of the `over` collection, each with its own
28
+ * `elemKey` (fresh per-child payload, #30) + `_i` (element index) + `_n` (the cardinality).
29
+ * - join : the fan-IN (the JTMS-at-merge point). Layer A's `joinArrive` parks the held child and, when all
30
+ * `_n` siblings of the fan-out group have arrived, spawns ONE collector at the fold place carrying
31
+ * their payloads. A BOUNDED PROJECTION (N retained-for-audit → 1), not a destructive ref-swap.
32
+ * - fold : reduce the collected siblings to one value. A declared MONOID (`fold.js`, pure + order-independent
33
+ * — non-deterministic THROUGHPUT, deterministic BELIEF) OR a micro-TASK (small-LLM reconciliation,
34
+ * content-memoized like any task). map.reduce = "map ∘ reduce" end to end.
35
+ *
36
+ * Termination = fuel (`maxSteps`): a step-indexed cutoff (a cyclic/divergent net stops; §4B), not a claim that
37
+ * the net is sound (defeasance makes static workflow-soundness undecidable — we run to a fuel-bounded fixpoint).
38
+ */
39
+ const { compileExpression } = require('../graph/expr.js');
40
+ const { indexByFrom } = require('./xlate.js');
41
+ const { stableStringify } = require('../providers/cache.js');
42
+ const { foldSiblings } = require('./fold.js');
43
+
44
+ // resolve a `$ref` token against a flat/dotted record payload (the case's typed facts). selectCluster gates are
45
+ // flat typed facts; dotted access is supported for convenience. (Cross-object `:` walks are an engine concern.)
46
+ function resolveFact( payload, ref ) {
47
+ if ( payload == null ) return undefined;
48
+ if ( ref.indexOf('.') < 0 ) return payload[ref];
49
+ return ref.split('.').reduce(( o, k ) => (o == null ? undefined : o[k]), payload);
50
+ }
51
+
52
+ // default content-memo key: the task + the FULL canonical payload, via a RECURSIVE key-sorted stringify (NOT
53
+ // `canonicalize.digest`, whose sorted-keys arg is a JSON.stringify ALLOWLIST applied at every nesting level → it
54
+ // silently STRIPS nested payload keys → every element collapses to one key → a false hit; finding #35, the F4
55
+ // risk in the default). This default is SOUND but conservative (incidental fields re-key); a host projects to the
56
+ // TRACKED facts via `keyOf` for amortization (the K1 discipline — see `examples/poc/durable-flow.js`).
57
+ function defaultKeyOf( tr, token ) { return 'task:' + tr.task + '|' + stableStringify(token.payload || {}); }
58
+
59
+ /**
60
+ * Drain a run to a fixpoint over the CheckpointStore. Idempotent + resumable: call it again after a crash (the
61
+ * caller should `store.rollbackInflight(runId)` first to recover any in-flight token).
62
+ *
63
+ * @param store a CheckpointStore
64
+ * @param runId
65
+ * @param net a compiled net (`compileMethod`)
66
+ * @param opts.runTask (task, token) => ({ payload?, created? }) | Promise<...> — REQUIRED: run a micro-task
67
+ * (provider/LLM). May be ASYNC (a real LLM call). Returns a payload PATCH (merged forward)
68
+ * + optional createdRefs. Pure-compute or effecting.
69
+ * @param opts.keyOf (transition, token) => string|null — memo key (default: task+canonical payload).
70
+ * @param opts.lease ms a claimed token is held (default 30000)
71
+ * @param opts.batch tokens leased per claim (default 32)
72
+ * @param opts.maxSteps fuel — transition firings before a divergent cutoff (default 100000)
73
+ * @param opts.foldKeyOf (transition, sortedSiblings) => string|null — memo key for a micro-TASK fold.
74
+ * @returns { steps, taskCalls, memoHits, bypass, fanOut, routed, joins, joinParked, folds, done, failed } — counters.
75
+ */
76
+ async function runFlow( store, runId, net, opts ) {
77
+ opts = opts || {};
78
+ const runTask = opts.runTask;
79
+ if ( typeof runTask !== 'function' ) throw new Error('runFlow: opts.runTask (task, token) => {payload?,created?} is required');
80
+ const keyOf = opts.keyOf || defaultKeyOf;
81
+ // the fold-task memo key: keyed on the COLLECTED sibling inputs (a superset of what the fold reads → sound).
82
+ // Recursive stringify (NOT digest — same #35 nested-strip trap as defaultKeyOf).
83
+ const foldKeyOf = opts.foldKeyOf || (( tr, sibs ) => 'fold:' + tr.reduce.task + '|' + stableStringify(sibs));
84
+ // the per-step C-contract guard (assert-before-commit). Lazy-required so an executor-only run without any
85
+ // transition `contract` never loads the authoring layer; a host may inject its own `assertStep`.
86
+ const assertStep = opts.assertStep || (( contract, facts, touched, o ) => require('../authoring/contract.js').assertPost(contract, facts, touched, o));
87
+ // fail a token; if it is a REDUCING-map child (stamped `_join`), FAIL-FAST its whole group so the join can't
88
+ // hang waiting for a sibling that will never arrive (C-fail). A plain token just dead-letters.
89
+ // a REDUCING-map child (stamped `_join`) that fails: the GROUP POLICY (`_onFail`, stamped at fan-out) decides —
90
+ // 'survivors' DROPS just this shard (joinFail → the join still folds the rest + a `_partial` marker), 'failfast'
91
+ // (default) fails the WHOLE group so the join can't hang waiting on a sibling that will never arrive. A plain
92
+ // token just dead-letters. (`byFrom` is captured by closure — resolved at call-time, after it's initialized.)
93
+ const failToken = ( tk, reason ) => {
94
+ const p = tk.payload || {};
95
+ if ( !p._join ) return store.fail(tk, reason);
96
+ if ( p._onFail === 'survivors' ) {
97
+ const joinTr = byFrom[p._join];
98
+ return store.joinFail(tk, p._join, { expected: p._n, foldPlace: joinTr && joinTr.foldPlace, failPlace: net.fail, reason });
99
+ }
100
+ return store.failGroup(tk, p._join, net.fail, reason);
101
+ };
102
+ // the C-fail LADDER at a task-failure site (a task throw OR a contract blame), BEFORE the terminal fail:
103
+ // retry (bounded `tr.retry`, for transient/non-deterministic tasks — re-ready the SAME place; the failure was
104
+ // never memoized so it re-runs fresh) → escalate (once, `tr.escalate` → a bigger micro-task place that
105
+ // rejoins the flow on success) → fall through to failToken (group policy). Counters: retried/escalated.
106
+ // `_retry`/`_escalated` are LOGICAL payload counters (NOT the lease `attempts` field — keeps crash accounting
107
+ // intact, the plan's constraint); they ride the durable payload so a retry survives a crash/restart.
108
+ const recover = ( t, tr, reason ) => {
109
+ const p = t.payload || {};
110
+ if ( tr.retry && (p._retry || 0) < tr.retry ) {
111
+ store.move(t, tr.from, { payload: { _retry: (p._retry || 0) + 1, _failReason: String(reason) } });
112
+ c.retried++; return true;
113
+ }
114
+ if ( tr.escalate && !p._escalated ) {
115
+ store.move(t, tr.escalate, { payload: { _escalated: true, _failReason: String(reason) } });
116
+ c.escalated++; return true;
117
+ }
118
+ return false;
119
+ };
120
+ const lease = opts.lease || 30000, batch = opts.batch || 32, fuel = opts.maxSteps || 100000;
121
+
122
+ const byFrom = indexByFrom(net);
123
+ const gateFns = {}; // precompile each select's gates once
124
+ for ( const t of net.transitions ) if ( t.kind === 'select' )
125
+ gateFns[t.from] = (t.gates || []).map(( g ) => ({ to: g.to, fn: compileExpression(g.when, { empty: false }) }));
126
+
127
+ const c = { steps: 0, taskCalls: 0, memoHits: 0, bypass: 0, fanOut: 0, routed: 0, joins: 0, joinParked: 0, folds: 0, blamed: 0, retried: 0, escalated: 0, done: 0, failed: 0 };
128
+
129
+ while ( c.steps < fuel ) {
130
+ const toks = store.claim(runId, { limit: batch, lease });
131
+ if ( !toks.length ) break; // nothing claimable → fixpoint (or all in terminal places)
132
+ for ( const t of toks ) {
133
+ if ( c.steps >= fuel ) break;
134
+ c.steps++;
135
+ const tr = byFrom[t.placeId];
136
+ if ( !tr ) { store.fail(t, 'no transition from place ' + t.placeId); c.failed++; continue; }
137
+
138
+ if ( tr.kind === 'select' ) {
139
+ let to = tr.fallback;
140
+ for ( const g of gateFns[tr.from] ) if ( g.fn(( ref ) => resolveFact(t.payload, ref)) ) { to = g.to; break; }
141
+ store.move(t, to); c.routed++;
142
+ continue;
143
+ }
144
+
145
+ if ( tr.kind === 'map' ) {
146
+ const coll = resolveFact(t.payload, tr.over);
147
+ if ( !Array.isArray(coll) || !coll.length ) { store.move(t, tr.empty); continue; }
148
+ const places = coll.map(() => tr.bodyStart);
149
+ const p = t.payload || {};
150
+ // NESTED-fold: push this fan-out onto the group / cardinality / element-index STACKS (the join reads the
151
+ // TOP, the fold POPS one level). 1-level = a single-entry stack (the fold pops to empty → clean output).
152
+ // _join (REDUCING map only) + _onFail = the group recovery policy read by the C-fail ladder (failToken).
153
+ const groups = (p._groups || []).concat(String(t.id)), cards = (p._cards || []).concat(coll.length);
154
+ const base = tr.joinPlace ? { _n: coll.length, _join: tr.joinPlace, _onFail: tr.onFail || 'failfast' } : { _n: coll.length };
155
+ const payloads = coll.map(( elem, i ) => Object.assign({}, p, base,
156
+ { [tr.elemKey]: elem, _i: i, _groups: groups, _cards: cards, _is: (p._is || []).concat(i) }));
157
+ store.move(t, places, { payloads }); c.fanOut += coll.length;
158
+ continue;
159
+ }
160
+
161
+ if ( tr.kind === 'join' ) { // the fan-IN: park-until-complete (Layer A is atomic)
162
+ const jp = t.payload || {};
163
+ // NESTED-fold: the group + cardinality come from the TOP of the stacks (1-level falls back to _n/parentId).
164
+ const expected = (jp._cards && jp._cards.length) ? jp._cards[jp._cards.length - 1] : jp._n;
165
+ const group = (jp._groups && jp._groups.length) ? jp._groups[jp._groups.length - 1] : (t.parentId == null ? null : String(t.parentId));
166
+ const res = store.joinArrive(t, tr.from, { expected, foldPlace: tr.foldPlace, failPlace: net.fail, mode: tr.onFail, group });
167
+ if ( res && res.ready ) c.joins++; else if ( res && res.failed ) c.failed++; else c.joinParked++;
168
+ continue;
169
+ }
170
+
171
+ if ( tr.kind === 'fold' ) { // reduce the collected siblings → one value
172
+ const sibs = (((t.payload || {})._siblings) || []).slice().sort(( a, b ) => ((a && a._i) || 0) - ((b && b._i) || 0));
173
+ let outPayload;
174
+ if ( tr.reduce && tr.reduce.task ) { // a micro-task reconciliation join — memoized like a task
175
+ const key = foldKeyOf(tr, sibs);
176
+ let out;
177
+ if ( key != null && (out = store.memoGet(key)) !== undefined ) { c.memoHits++; }
178
+ else { out = (await runTask(tr.reduce.task, t)) || {}; c.taskCalls++; if ( key != null ) store.memoSet(key, out); else c.bypass++; }
179
+ outPayload = out.payload;
180
+ } else { // a declared monoid — pure, order-independent, deterministic
181
+ outPayload = foldSiblings(sibs, tr.reduce);
182
+ }
183
+ // SURVIVORS: propagate the completeness markers so the folded VALUE and its incompleteness travel
184
+ // together (no silent partial — a downstream gate can read `_partial`/`_dropped`). failfast collectors
185
+ // carry no `_partial`, so this is inert for them.
186
+ const cp = t.payload || {};
187
+ if ( cp._partial !== undefined ) outPayload = Object.assign({}, outPayload, { _partial: cp._partial, _dropped: cp._dropped, _expected: cp._expected });
188
+ // NESTED-fold: if the siblings carried an OUTER level on their stack, POP one level (reconstructed from a
189
+ // sibling — the collector itself doesn't carry the stacks) and restore the OUTER element index so a
190
+ // non-commutative outer fold stays deterministic. The popped token flows to the outer join, where
191
+ // top(_groups) now selects the outer group. 1-level (single-entry stack) adds nothing → output stays clean.
192
+ const sibGroups = (sibs[0] && sibs[0]._groups) || [];
193
+ if ( sibGroups.length > 1 ) {
194
+ const _is = (sibs[0]._is || []).slice(0, -1);
195
+ outPayload = Object.assign({}, outPayload, { _groups: sibGroups.slice(0, -1), _cards: (sibs[0]._cards || []).slice(0, -1),
196
+ _is, _i: _is.length ? _is[_is.length - 1] : 0 });
197
+ }
198
+ store.move(t, tr.to, { payload: outPayload }); c.folds++;
199
+ continue;
200
+ }
201
+
202
+ // kind === 'task'
203
+ if ( tr.task == null ) { store.move(t, tr.to); continue; } // no-op passthrough (enter/noop)
204
+ const key = keyOf(tr, t);
205
+ let out, hit = false;
206
+ if ( key != null && (out = store.memoGet(key)) !== undefined ) {
207
+ c.memoHits++; hit = true; // C5 replay — 0 model calls (cheap resume + amortization)
208
+ } else {
209
+ try { out = (await runTask(tr.task, t)) || {}; } // runTask may be a real (async) LLM micro-task
210
+ catch ( e ) { // an erroring task: recover via the C-fail ladder, else fail the token (never crashes the drain)
211
+ c.taskCalls++;
212
+ const reason = 'task-error:' + ((e && e.message) || e);
213
+ if ( recover(t, tr, reason) ) continue;
214
+ failToken(t, reason); c.failed++; continue;
215
+ }
216
+ c.taskCalls++;
217
+ }
218
+ // C-CONTRACT GUARD (assert-at-runtime, BEFORE commit — the §2 defeasible contract realized in the EXECUTE
219
+ // layer; addresses the adversary's #3: catch a wrong learned post BEFORE an irreversible downstream
220
+ // commit). A violation QUARANTINES the token (route to `fail` with a blame reason — a seed of C-fail), and
221
+ // a fresh violating output is NOT memoized (never cache a bad result). G1 frame-completeness + G2 oracle ride along.
222
+ if ( tr.contract ) {
223
+ const merged = Object.assign({}, t.payload, out.payload);
224
+ const v = assertStep(tr.contract, merged, Object.keys(out.payload || {}), { oracle: opts.oracle });
225
+ if ( !v.ok ) { // a contract blame: same ladder (escalate is the meaningful recovery; a deterministic re-run re-blames, so retry is for non-det)
226
+ const reason = 'contract:' + v.blame.kind + (v.violations[0] && v.violations[0].detail ? ':' + v.violations[0].detail : '');
227
+ if ( recover(t, tr, reason) ) continue;
228
+ failToken(t, reason); c.blamed++; continue;
229
+ }
230
+ }
231
+ if ( !hit ) { if ( key != null ) store.memoSet(key, out); else c.bypass++; }
232
+ store.move(t, tr.to, { payload: out.payload, created: out.created });
233
+ }
234
+ }
235
+
236
+ const st = store.stats(runId);
237
+ c.done = st.done; c.failed = st.failed;
238
+ if ( c.steps >= fuel ) c.divergent = true; // fuel exhausted = a cutoff, NOT a semantic fixpoint
239
+ return c;
240
+ }
241
+
242
+ module.exports = { runFlow, resolveFact, defaultKeyOf };
@@ -0,0 +1,185 @@
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
+ * xlate — C-xlate v0 (the conception's "translation" step, component C-xlate). Compile ONE skynet method into a
10
+ * WORKFLOW-NET definition the durable executor (`checkpoint-store.js` + `interpreter.js`) runs over a stream of
11
+ * case records. This is the bridge from the BUILT/validated method (the belief-view, decidable, traceable) to
12
+ * the EXECUTE side (durable, mass, crash-resumable) — the §0.1 build/execute separation.
13
+ *
14
+ * The method SPEC is the compact declarative form of the Brick-1/3 example (`tests/integration/method-select`):
15
+ * {
16
+ * name,
17
+ * select: { on?:[factKeys], rules:[{ when:"<expr over typed case facts>", method }], fallback:<methodName> },
18
+ * methods: {
19
+ * <name>: {
20
+ * steps?: [{ task:"Ns::fn" }], // a linear path of micro-tasks (the for/fold path)
21
+ * map?: { over:"<collKey>", body:[{ task }], elemKey?:"elem", empty?:"<place>",
22
+ * reduce?: { monoid:"sum"|… , key?, into? } | { task:"Ns::fold" } } // Brick-1 fan-out + fan-IN
23
+ * }
24
+ * }
25
+ * }
26
+ *
27
+ * compileMethod → a net { start, sinks, fail, transitions:[ ... ] } where a transition is one of:
28
+ * { kind:'select', from, gates:[{ when, to }], fallback } typed routing (selectCluster's gates → places)
29
+ * { kind:'task', from, task, to } a provider micro-task step (memoized by Layer B)
30
+ * { kind:'map', from, over, elemKey, bodyStart, empty } unordered FAN-OUT, one child token per element
31
+ * { kind:'join', from, foldPlace } the cardinality JOIN (fan-in): park until all
32
+ * siblings arrive, then spawn ONE collector (Layer A)
33
+ * { kind:'fold', from, reduce, to } reduce the collected siblings → one value (Layer B)
34
+ *
35
+ * v0 scope (honest, per §4B / §9.2): select + linear task paths + map FAN-OUT, and — NEW — the fold-back /
36
+ * cardinality JOIN (`map.reduce`): the body's last step routes to a join place, the completing arrival spawns a
37
+ * collector, the fold reduces it (a declared monoid = pure/order-independent, or a micro-task). A `map` WITHOUT a
38
+ * `reduce` is the prior behaviour (each element flows to a sink independently — no rejoin).
39
+ */
40
+
41
+ const START = 'start', DONE = 'done', FAIL = 'failed';
42
+
43
+ function entryOf( method ) { return '@' + method; } // a method's in-place (distinct from step/body places)
44
+
45
+ /**
46
+ * Compile a method spec → a workflow-net def. Pure (no IO). Throws on a malformed spec.
47
+ */
48
+ function compileMethod( spec ) {
49
+ if ( !spec || !spec.methods ) throw new Error('compileMethod: spec.methods is required');
50
+ const transitions = [];
51
+ const sel = spec.select;
52
+
53
+ if ( sel ) {
54
+ const gates = (sel.rules || []).map(( r ) => {
55
+ if ( !r.when || !r.method ) throw new Error('compileMethod: a select rule needs { when, method }');
56
+ if ( !spec.methods[r.method] ) throw new Error(`compileMethod: select rule routes to unknown method "${r.method}"`);
57
+ return { when: r.when, to: entryOf(r.method) };
58
+ });
59
+ if ( sel.fallback && !spec.methods[sel.fallback] ) throw new Error(`compileMethod: fallback method "${sel.fallback}" is undefined`);
60
+ transitions.push({ id: 'select', from: START, kind: 'select', gates, fallback: sel.fallback ? entryOf(sel.fallback) : FAIL });
61
+ }
62
+
63
+ for ( const m of Object.keys(spec.methods) ) compileBody(m, spec.methods[m], transitions);
64
+
65
+ // with no select, the single method is the entry — wire start → its in-place via a no-op task.
66
+ if ( !sel ) {
67
+ const names = Object.keys(spec.methods);
68
+ if ( names.length !== 1 ) throw new Error('compileMethod: a spec without a `select` must have exactly one method');
69
+ transitions.unshift({ id: 'enter', from: START, kind: 'task', task: null, to: entryOf(names[0]) });
70
+ }
71
+
72
+ const net = { start: START, sinks: [DONE], fail: FAIL, transitions };
73
+ const issues = validateNet(net);
74
+ if ( issues.length ) throw new Error('compileMethod: malformed net — ' + issues.join('; '));
75
+ return net;
76
+ }
77
+
78
+ // emit a task transition, plus — when the step declares `escalate` — an ESCALATION place: a bigger
79
+ // micro-task at `<from>@escalate` whose `to` rejoins the SAME downstream place, so a recovered shard
80
+ // flows on as if the original had succeeded (the C-fail escalate rung). `retry` (a bounded re-exec
81
+ // count, for transient/non-det tasks) rides the transition itself. ZERO Layer-A.
82
+ function emitTask( transitions, id, from, step, to ) {
83
+ step = step || {};
84
+ const t = { id, from, kind: 'task', task: step.task != null ? step.task : null, to, contract: step.contract };
85
+ if ( step.retry ) t.retry = step.retry;
86
+ if ( step.escalate ) {
87
+ const escPlace = from + '@escalate';
88
+ t.escalate = escPlace;
89
+ transitions.push(t);
90
+ transitions.push({ id: id + '.esc', from: escPlace, kind: 'task',
91
+ task: typeof step.escalate === 'string' ? step.escalate : step.escalate.task, to, contract: step.contract });
92
+ return;
93
+ }
94
+ transitions.push(t);
95
+ }
96
+
97
+ // emit a map fan-out + body + (optional) fan-IN fold, routing the final fold to `toPlace`. RECURSIVE: a body step
98
+ // may be a linear task OR a NESTED map (`{ map:{…} }`) — the inner fold's `to` rejoins the OUTER flow (its next body
99
+ // place / the outer join). v0 ships + tests depth 2 (the runtime group/_i stacks are depth-agnostic).
100
+ function emitMap( ns, map, fromPlace, toPlace, transitions ) {
101
+ if ( !map.over ) throw new Error(`compileMethod: map "${ns}" needs an \`over\` collection key`);
102
+ const reduce = map.reduce;
103
+ const joinPlace = ns + '@join', foldPlace = ns + '@fold';
104
+ const bodyEnd = reduce ? joinPlace : toPlace; // with a reduce, the body REJOINS at the join place
105
+ const bsteps = map.body || [];
106
+ const bodyStart = bsteps.length ? (ns + '@body') : bodyEnd;
107
+ transitions.push({ id: ns + '.map', from: fromPlace, kind: 'map', over: map.over, elemKey: map.elemKey || 'elem',
108
+ bodyStart, empty: map.empty || (reduce ? foldPlace : toPlace), joinPlace: reduce ? joinPlace : undefined,
109
+ onFail: reduce ? reduce.onFail : undefined });
110
+ let bp = bodyStart;
111
+ bsteps.forEach(( step, i ) => {
112
+ const stepTo = i === bsteps.length - 1 ? bodyEnd : (ns + '@body#' + (i + 1));
113
+ if ( step && step.map ) emitMap(ns + '@body.m' + i, step.map, bp, stepTo, transitions); // a NESTED map-reduce
114
+ else emitTask(transitions, ns + '.body.s' + i, bp, step, stepTo);
115
+ bp = stepTo;
116
+ });
117
+ if ( reduce ) { // the fan-IN: park-until-complete → spawn collector → fold
118
+ transitions.push({ id: ns + '.join', from: joinPlace, kind: 'join', foldPlace, onFail: reduce.onFail });
119
+ // a post-fold completeness handler — a linear `then` chain the fold routes to instead of `toPlace`. REQUIRED
120
+ // for a 'survivors' fold (it must not route a possibly-partial aggregate straight to a sink; validateNet
121
+ // enforces it — the no-silent-partial invariant). A `then` step can carry a `contract` reading `_partial`.
122
+ const thenSteps = reduce.then || [];
123
+ const foldTo = thenSteps.length ? (ns + '@then') : toPlace;
124
+ transitions.push({ id: ns + '.fold', from: foldPlace, kind: 'fold', reduce, to: foldTo, onFail: reduce.onFail });
125
+ let tp = ns + '@then';
126
+ thenSteps.forEach(( step, i ) => {
127
+ const stepTo = i === thenSteps.length - 1 ? toPlace : (ns + '@then#' + (i + 1));
128
+ emitTask(transitions, ns + '.then.s' + i, tp, step, stepTo);
129
+ tp = stepTo;
130
+ });
131
+ }
132
+ }
133
+
134
+ function compileBody( m, def, transitions ) {
135
+ def = def || {};
136
+ const steps = def.steps || [];
137
+ let place = entryOf(m);
138
+
139
+ steps.forEach(( step, i ) => {
140
+ const last = i === steps.length - 1 && !def.map;
141
+ const to = last ? DONE : (m + '#' + (i + 1));
142
+ emitTask(transitions, m + '.s' + i, place, step, to);
143
+ place = to;
144
+ });
145
+
146
+ if ( def.map ) emitMap(m, def.map, place, DONE, transitions);
147
+ else if ( !steps.length ) transitions.push({ id: m + '.noop', from: place, kind: 'task', task: null, to: DONE }); // a degenerate empty method
148
+ }
149
+
150
+ /**
151
+ * Structural lint (validate the STRUCTURE, not the grammar — methodology §0.6): every place a transition routes
152
+ * TO must be a sink/fail or have its own outgoing transition (no dangling place that strands a token); no two
153
+ * transitions share a `from` place (the marking is 1-out per place — `select` is the only multi-way, via gates).
154
+ * @returns string[] of issues (empty = sound).
155
+ */
156
+ function validateNet( net ) {
157
+ const issues = [], froms = new Set(), terminals = new Set([].concat(net.sinks || [], net.fail || []));
158
+ const targets = [];
159
+ for ( const t of net.transitions ) {
160
+ if ( froms.has(t.from) ) issues.push(`two transitions leave place "${t.from}" (marking is 1-out per place)`);
161
+ froms.add(t.from);
162
+ if ( t.kind === 'select' ) { (t.gates || []).forEach(( g ) => targets.push(g.to)); if ( t.fallback ) targets.push(t.fallback); }
163
+ else if ( t.kind === 'map' ) { targets.push(t.bodyStart); targets.push(t.empty); }
164
+ else if ( t.kind === 'join' ) targets.push(t.foldPlace);
165
+ else { targets.push(t.to); if ( t.escalate ) targets.push(t.escalate); } // task / fold (+ optional escalation place)
166
+ }
167
+ for ( const tgt of targets ) if ( !terminals.has(tgt) && !froms.has(tgt) )
168
+ issues.push(`place "${tgt}" is routed-to but has no outgoing transition and is not a sink`);
169
+ if ( !froms.has(net.start) ) issues.push(`the start place "${net.start}" has no outgoing transition`);
170
+ // HARD lint (the no-silent-partial invariant): a 'survivors' fold may emit an INCOMPLETE aggregate (some shards
171
+ // dropped), so it MUST route to a completeness gate (a `reduce.then` handler reading `_partial`/`_dropped`), never
172
+ // straight to a sink where a partial would pass as complete. Stricter than the general uncontracted=warning stance.
173
+ for ( const t of net.transitions ) if ( t.kind === 'fold' && t.onFail === 'survivors' && terminals.has(t.to) )
174
+ issues.push(`a 'survivors' fold ("${t.id}") routes directly to a sink — it must be followed by a completeness gate (declare \`reduce.then\` reading _partial/_dropped); the no-silent-partial invariant`);
175
+ return issues;
176
+ }
177
+
178
+ // index transitions by their `from` place (one per place) — the interpreter's dispatch table.
179
+ function indexByFrom( net ) {
180
+ const byFrom = {};
181
+ for ( const t of net.transitions ) byFrom[t.from] = t;
182
+ return byFrom;
183
+ }
184
+
185
+ module.exports = { compileMethod, validateNet, indexByFrom, entryOf, places: { START, DONE, FAIL } };