skynet-graph 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +22 -0
- package/LICENSE +661 -0
- package/README.md +245 -0
- package/bin/sg +20 -0
- package/concepts/README.md +23 -0
- package/concepts/_substrate/Claim/Confidence/Trusted.json +6 -0
- package/concepts/_substrate/Claim/Confidence.json +10 -0
- package/concepts/_substrate/Claim/Freshness/Stale.json +7 -0
- package/concepts/_substrate/Claim/Freshness.json +5 -0
- package/concepts/_substrate/Claim/Verification/Refuted.json +6 -0
- package/concepts/_substrate/Claim/Verification.json +6 -0
- package/concepts/_substrate/Claim.json +4 -0
- package/concepts/_substrate/Frontier/InBeam.json +6 -0
- package/concepts/_substrate/Frontier/Stuck.json +6 -0
- package/concepts/_substrate/Frontier.json +5 -0
- package/concepts/_substrate/Intake/ToTask.json +22 -0
- package/concepts/_substrate/Intake/Typed.json +10 -0
- package/concepts/_substrate/Intake.json +27 -0
- package/concepts/_substrate/Task/Answer.json +12 -0
- package/concepts/_substrate/Task/Complexity/Atomic.json +5 -0
- package/concepts/_substrate/Task/Complexity/Compound/Expansion.json +7 -0
- package/concepts/_substrate/Task/Complexity/Compound.json +6 -0
- package/concepts/_substrate/Task/Complexity.json +6 -0
- package/concepts/_substrate/Task/EvalComplexity.json +13 -0
- package/concepts/_substrate/Task/ReportUp.json +6 -0
- package/concepts/_substrate/Task/Rollup.json +10 -0
- package/concepts/_substrate/Task.json +5 -0
- package/concepts/clinical/Diagnosis/Medication.json +6 -0
- package/concepts/clinical/Diagnosis.json +15 -0
- package/concepts/clinical/Observation/LabValue/OutOfRange.json +6 -0
- package/concepts/clinical/Observation/LabValue.json +5 -0
- package/concepts/clinical/Observation.json +6 -0
- package/concepts/common/Document/pathBasket.json +6 -0
- package/concepts/common/Document.json +3 -0
- package/concepts/common/Edge/Distance.json +13 -0
- package/concepts/common/Edge/Stay/LongStay.json +8 -0
- package/concepts/common/Edge/Stay.json +8 -0
- package/concepts/common/Edge/Travel/LongTravel.json +16 -0
- package/concepts/common/Edge/Travel/ShortTravel.json +11 -0
- package/concepts/common/Edge/Travel/targetNode.json +7 -0
- package/concepts/common/Edge/Travel.json +7 -0
- package/concepts/common/Edge.json +3 -0
- package/concepts/common/Vertice.json +3 -0
- package/concepts/supply/Fulfillment.json +10 -0
- package/concepts/supply/Inventory/Reorder.json +7 -0
- package/concepts/supply/Inventory.json +5 -0
- package/concepts/supply/Procurement/SupplierConfirm.json +6 -0
- package/concepts/supply/Procurement.json +8 -0
- package/concepts/supply/Transport.json +5 -0
- package/doc/API.md +556 -0
- package/doc/CAPABILITIES.md +373 -0
- package/doc/MODELISATION.md +511 -0
- package/doc/architecture.md +267 -0
- package/doc/concept-as-graph.md +379 -0
- package/doc/concept-learning.md +168 -0
- package/doc/creative-loop-map.md +202 -0
- package/doc/original-2016-doc.md +148 -0
- package/doc/usage.md +501 -0
- package/lib/authoring/abstract.js +401 -0
- package/lib/authoring/abstraction.js +190 -0
- package/lib/authoring/adapt.js +393 -0
- package/lib/authoring/ancestry.js +216 -0
- package/lib/authoring/author.js +147 -0
- package/lib/authoring/bounded-merge.js +57 -0
- package/lib/authoring/canon.js +158 -0
- package/lib/authoring/clock.js +123 -0
- package/lib/authoring/combinator.js +92 -0
- package/lib/authoring/compete.js +76 -0
- package/lib/authoring/compose-hotspot.js +314 -0
- package/lib/authoring/compress.js +279 -0
- package/lib/authoring/concept-net.js +254 -0
- package/lib/authoring/concepts.js +66 -0
- package/lib/authoring/context-project.js +277 -0
- package/lib/authoring/contract.js +476 -0
- package/lib/authoring/corpus-pack.js +125 -0
- package/lib/authoring/cost-probe.js +86 -0
- package/lib/authoring/crystallize.js +555 -0
- package/lib/authoring/dag-decompose.js +256 -0
- package/lib/authoring/dataset-adapter.js +253 -0
- package/lib/authoring/debug-provider.js +92 -0
- package/lib/authoring/decompose.js +343 -0
- package/lib/authoring/emittability.js +236 -0
- package/lib/authoring/equilibrium.js +160 -0
- package/lib/authoring/extract.js +153 -0
- package/lib/authoring/forest.js +55 -0
- package/lib/authoring/forge-fallback.js +116 -0
- package/lib/authoring/givens.js +121 -0
- package/lib/authoring/glossary.js +177 -0
- package/lib/authoring/grammar-graph.js +191 -0
- package/lib/authoring/granularity.js +80 -0
- package/lib/authoring/graph-net.js +108 -0
- package/lib/authoring/ground.js +148 -0
- package/lib/authoring/higher-order.js +85 -0
- package/lib/authoring/hotspot.js +132 -0
- package/lib/authoring/hysteresis.js +85 -0
- package/lib/authoring/index.js +106 -0
- package/lib/authoring/lattice-morphism.js +61 -0
- package/lib/authoring/lattice-pack.js +148 -0
- package/lib/authoring/leaf-io.js +77 -0
- package/lib/authoring/library.js +156 -0
- package/lib/authoring/lifecycle.js +92 -0
- package/lib/authoring/loop.js +193 -0
- package/lib/authoring/master-loop.js +132 -0
- package/lib/authoring/mdl.js +209 -0
- package/lib/authoring/memo-stability.js +122 -0
- package/lib/authoring/method-explorer.js +138 -0
- package/lib/authoring/method-pack.js +181 -0
- package/lib/authoring/method.js +202 -0
- package/lib/authoring/mine.js +510 -0
- package/lib/authoring/mount.js +119 -0
- package/lib/authoring/negotiate.js +100 -0
- package/lib/authoring/parametric.js +139 -0
- package/lib/authoring/reaggregate.js +75 -0
- package/lib/authoring/rebalance.js +157 -0
- package/lib/authoring/recall.js +135 -0
- package/lib/authoring/registry.js +350 -0
- package/lib/authoring/relearn.js +147 -0
- package/lib/authoring/retention.js +89 -0
- package/lib/authoring/segment-proxy.js +114 -0
- package/lib/authoring/serve-leaf.js +64 -0
- package/lib/authoring/slot-aware-serve.js +77 -0
- package/lib/authoring/sound-invoke.js +59 -0
- package/lib/authoring/split-serve.js +74 -0
- package/lib/authoring/ste.js +239 -0
- package/lib/authoring/stock.js +101 -0
- package/lib/authoring/store.js +94 -0
- package/lib/authoring/supervise.js +138 -0
- package/lib/authoring/support.js +131 -0
- package/lib/authoring/task-mirror.js +51 -0
- package/lib/authoring/typed-loop.js +160 -0
- package/lib/authoring/validate.js +569 -0
- package/lib/authoring/widen.js +88 -0
- package/lib/combos/appliance.js +189 -0
- package/lib/combos/critique.js +377 -0
- package/lib/combos/defaults.js +86 -0
- package/lib/combos/durable-runner.js +115 -0
- package/lib/combos/forge.js +209 -0
- package/lib/combos/index.js +86 -0
- package/lib/combos/learning-library.js +253 -0
- package/lib/combos/mixture-serve.js +193 -0
- package/lib/combos/plan-loop.js +192 -0
- package/lib/combos/proxy-cache.js +272 -0
- package/lib/combos/self-mod.js +94 -0
- package/lib/durable/audit.js +102 -0
- package/lib/durable/checkpoint-store.js +550 -0
- package/lib/durable/fold.js +62 -0
- package/lib/durable/interpreter.js +242 -0
- package/lib/durable/xlate.js +185 -0
- package/lib/graph/Graph.js +2486 -0
- package/lib/graph/expr.js +307 -0
- package/lib/graph/index.js +26 -0
- package/lib/graph/log.js +134 -0
- package/lib/graph/objects/Concept.js +382 -0
- package/lib/graph/objects/Entity.js +503 -0
- package/lib/graph/objects/Node.js +57 -0
- package/lib/graph/objects/PathMap.js +557 -0
- package/lib/graph/objects/Segment.js +100 -0
- package/lib/graph/tasks/stabilize.js +59 -0
- package/lib/graph/tasks/taskflow.js +247 -0
- package/lib/index.js +122 -0
- package/lib/load.js +132 -0
- package/lib/providers/backends.js +117 -0
- package/lib/providers/borderline.js +129 -0
- package/lib/providers/cache.js +226 -0
- package/lib/providers/canonicalize.js +198 -0
- package/lib/providers/constat.js +107 -0
- package/lib/providers/geo.js +53 -0
- package/lib/providers/index.js +116 -0
- package/lib/providers/intake.js +263 -0
- package/lib/providers/llm-local.js +88 -0
- package/lib/providers/llm.js +236 -0
- package/lib/providers/local-host.js +197 -0
- package/lib/providers/merge-consistency.js +162 -0
- package/lib/providers/nogood.js +149 -0
- package/lib/providers/reason-loop.js +223 -0
- package/lib/providers/semiring.js +285 -0
- package/lib/providers/solver-fork.js +163 -0
- package/lib/providers/stats.js +228 -0
- package/lib/providers/verify.js +120 -0
- package/lib/runtime/index.js +135 -0
- package/lib/runtime/invoke-pool.js +92 -0
- package/lib/runtime/protocol.js +210 -0
- package/lib/runtime/transport-socket.js +77 -0
- package/lib/runtime/worker-entry.js +31 -0
- package/lib/sg/cli.js +601 -0
- package/lib/sg/log-sinks.js +214 -0
- package/lib/sg/mcp.js +395 -0
- package/lib/sg/proxy-run.js +66 -0
- package/lib/sg/serve-demo.js +185 -0
- package/lib/sg/serve.js +205 -0
- package/lib/sg/trace.js +118 -0
- package/lib/studio/protocol.js +45 -0
- package/lib/studio/public/app.js +310 -0
- package/lib/studio/public/components/ConceptEditor.js +43 -0
- package/lib/studio/public/components/ConceptTree.js +43 -0
- package/lib/studio/public/components/CorpusPanel.js +68 -0
- package/lib/studio/public/components/ForkTree.js +36 -0
- package/lib/studio/public/components/GrammarGraph.js +99 -0
- package/lib/studio/public/components/GraphCanvas.js +156 -0
- package/lib/studio/public/components/Inspector.js +48 -0
- package/lib/studio/public/components/LearningPanel.js +85 -0
- package/lib/studio/public/components/PromptConsole.js +46 -0
- package/lib/studio/public/components/ProviderTrace.js +39 -0
- package/lib/studio/public/components/SessionSplit.js +47 -0
- package/lib/studio/public/components/TilingOverlay.js +40 -0
- package/lib/studio/public/components/Timeline.js +57 -0
- package/lib/studio/public/index.html +26 -0
- package/lib/studio/public/styles.css +185 -0
- package/lib/studio/public/ws.js +58 -0
- package/lib/studio/server.js +130 -0
- package/lib/studio/session.js +397 -0
- package/lib/studio/studio.js +149 -0
- package/package.json +42 -0
|
@@ -0,0 +1,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 } };
|