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,160 @@
|
|
|
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
|
+
* equilibrium — implicit differentiation through a forward-chaining FIXPOINT (the DEQ method),
|
|
21
|
+
* for learning across the COMPOSITE concept topology. Host-side, ZERO-CORE. Study
|
|
22
|
+
* doc/WIP/studies/2026-06-26-concepts-dynamiques-grammaire-induction.md; theory brief = Laurie.
|
|
23
|
+
*
|
|
24
|
+
* The engine's stabilization is a Picard iteration z_{t+1}=F(z_t,θ) driven to a fixpoint
|
|
25
|
+
* z*=F(z*,θ). To train params θ that live inside concept-nets, we want dL(z*)/dθ WITHOUT
|
|
26
|
+
* differentiating through the iteration. Differentiate the fixpoint condition instead
|
|
27
|
+
* (Almeida/Pineda 1987; DEQ, Bai-Kolter-Koltun 2019):
|
|
28
|
+
*
|
|
29
|
+
* J_z := ∂F/∂z |_{z*} (n×n) J_θ := ∂F/∂θ |_{z*} (n×p) — ONE sweep, at z*
|
|
30
|
+
* dz* / dθ = (I − J_z)^{-1} J_θ
|
|
31
|
+
* dL/dθ = (∇_z L) (I − J_z)^{-1} J_θ
|
|
32
|
+
*
|
|
33
|
+
* Reverse mode (never form the inverse): solve the adjoint (I − J_z)^T u = ∇_z L once, then
|
|
34
|
+
* dL/dθ = J_θ^T u. Well-posed iff ρ(J_z) < 1 (the contraction that also makes the forward
|
|
35
|
+
* fixpoint exist & be unique). Neumann: (I − J_z)^{-1} = Σ_k J_z^k, so u_K = Σ_{k≤K}(J_z^T)^k g
|
|
36
|
+
* — the truncation depth K is the exact analogue of the engine's apply-cap, and its bias ~ρ^K
|
|
37
|
+
* binds precisely as ρ→1. This module is the differentiable MIRROR of the composite topology;
|
|
38
|
+
* the real engine stays discrete (a trained net is baked back via ste.js, STE at the boundary).
|
|
39
|
+
*
|
|
40
|
+
* const fp = solveFixpoint((z) => F(z, θ, x), z0, { maxIter, tol });
|
|
41
|
+
* const { grad } = implicitGrad(jacZ(fp.z), jacTheta(fp.z), gradL, { mode: 'direct' });
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
// ---- small dense linear algebra (POC scale, n ≲ tens — a direct solve is free) ----
|
|
45
|
+
|
|
46
|
+
// Solve A u = b by Gaussian elimination with partial pivoting. A is n×n (row-major), b is n.
|
|
47
|
+
function solveLinear( A, b ) {
|
|
48
|
+
const n = b.length, M = A.map((row, i) => row.slice().concat(b[i])); // augmented
|
|
49
|
+
for ( let col = 0; col < n; col++ ) {
|
|
50
|
+
let piv = col;
|
|
51
|
+
for ( let r = col + 1; r < n; r++ ) if ( Math.abs(M[r][col]) > Math.abs(M[piv][col]) ) piv = r;
|
|
52
|
+
if ( Math.abs(M[piv][col]) < 1e-300 ) throw new Error('equilibrium: singular system (I − J_z not invertible; ρ ≥ 1?)');
|
|
53
|
+
if ( piv !== col ) { const tmp = M[piv]; M[piv] = M[col]; M[col] = tmp; }
|
|
54
|
+
for ( let r = 0; r < n; r++ ) {
|
|
55
|
+
if ( r === col ) continue;
|
|
56
|
+
const f = M[r][col] / M[col][col];
|
|
57
|
+
for ( let c = col; c <= n; c++ ) M[r][c] -= f * M[col][c];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return M.map((row, i) => row[n] / row[i]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// (J^T v)[i] = Σ_j J[j][i] v[j] — transpose-times-vector (J is r×c, v is r → c-vector)
|
|
64
|
+
function matTvec( J, v ) {
|
|
65
|
+
const r = J.length, c = r ? J[0].length : 0, out = new Array(c).fill(0);
|
|
66
|
+
for ( let i = 0; i < r; i++ ) for ( let j = 0; j < c; j++ ) out[j] += J[i][j] * v[i];
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Picard iteration to a fixpoint z* = F(z*). Faithful to the engine's stabilization sweep.
|
|
72
|
+
* @returns { z, iters, residual, converged } residual = max|z_{t+1} − z_t|.
|
|
73
|
+
*/
|
|
74
|
+
function solveFixpoint( F, z0, opts ) {
|
|
75
|
+
opts = opts || {};
|
|
76
|
+
const maxIter = opts.maxIter || 1000, tol = opts.tol == null ? 1e-12 : opts.tol;
|
|
77
|
+
let z = z0.slice(), iters = 0, residual = Infinity;
|
|
78
|
+
for ( ; iters < maxIter; ) {
|
|
79
|
+
const zn = F(z);
|
|
80
|
+
residual = 0;
|
|
81
|
+
for ( let i = 0; i < zn.length; i++ ) residual = Math.max(residual, Math.abs(zn[i] - z[i]));
|
|
82
|
+
z = zn; iters++;
|
|
83
|
+
if ( residual < tol ) break;
|
|
84
|
+
}
|
|
85
|
+
return { z: z, iters: iters, residual: residual, converged: residual < tol };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Implicit (DEQ) gradient dL/dθ = J_θ^T u where (I − J_z)^T u = gradL.
|
|
90
|
+
* @param Jz ∂F/∂z at z* (n×n)
|
|
91
|
+
* @param Jtheta ∂F/∂θ at z* (n×p)
|
|
92
|
+
* @param gradL ∇_z L at z* (n)
|
|
93
|
+
* @param opts.mode 'direct' (dense solve, the gold value) | { neumann: K } (truncated series)
|
|
94
|
+
* @returns { grad (p), u (n) }
|
|
95
|
+
*/
|
|
96
|
+
function implicitGrad( Jz, Jtheta, gradL, opts ) {
|
|
97
|
+
opts = opts || {};
|
|
98
|
+
const n = gradL.length;
|
|
99
|
+
let u;
|
|
100
|
+
if ( opts.mode && opts.mode.neumann != null ) {
|
|
101
|
+
// u = Σ_{k=0..K} (Jz^T)^k gradL — each step is one VJP v ↦ Jz^T v (Jacobian-free)
|
|
102
|
+
const K = opts.mode.neumann;
|
|
103
|
+
u = gradL.slice();
|
|
104
|
+
let term = gradL.slice();
|
|
105
|
+
for ( let k = 0; k < K; k++ ) {
|
|
106
|
+
term = matTvec(Jz, term); // Jz^T · term
|
|
107
|
+
for ( let i = 0; i < n; i++ ) u[i] += term[i];
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
// direct: A = (I − Jz)^T, A[i][j] = δ_ij − Jz[j][i]
|
|
111
|
+
const A = [];
|
|
112
|
+
for ( let i = 0; i < n; i++ ) { const row = new Array(n); for ( let j = 0; j < n; j++ ) row[j] = (i === j ? 1 : 0) - Jz[j][i]; A.push(row); }
|
|
113
|
+
u = solveLinear(A, gradL);
|
|
114
|
+
}
|
|
115
|
+
return { grad: matTvec(Jtheta, u), u: u };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Central finite-difference Jacobian of fn: R^d → R^n, returned as n×d. The general way to get
|
|
120
|
+
* J_z / J_θ of one composite sweep F without hand-deriving per-unit VJPs — at POC scale (n,d ≲
|
|
121
|
+
* tens) it's cheap and exact-to-O(ε²). For an STE (hard-gate) population, feed it the SOFT sweep
|
|
122
|
+
* (sigmoid gates) at z* — that IS the straight-through surrogate Jacobian for the backward.
|
|
123
|
+
*/
|
|
124
|
+
function numJac( fn, point, eps ) {
|
|
125
|
+
eps = eps || 1e-6;
|
|
126
|
+
const d = point.length, f0 = fn(point), n = f0.length;
|
|
127
|
+
const J = Array.from({ length: n }, () => new Array(d));
|
|
128
|
+
for ( let k = 0; k < d; k++ ) {
|
|
129
|
+
const a = point.slice(), b = point.slice(); a[k] += eps; b[k] -= eps;
|
|
130
|
+
const fa = fn(a), fb = fn(b);
|
|
131
|
+
for ( let i = 0; i < n; i++ ) J[i][k] = (fa[i] - fb[i]) / (2 * eps);
|
|
132
|
+
}
|
|
133
|
+
return J;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Spectral radius of J_z — the regime instrument (ρ→1 ⇒ slow forward, deep Neumann, large
|
|
138
|
+
* gradient). Estimated as the geometric-mean growth rate ρ = lim ‖J_z^k v‖^{1/k} (a normalized
|
|
139
|
+
* power iteration averaging the per-step log-growth). Unlike a last-step power-iteration estimate
|
|
140
|
+
* this is correct when the dominant eigenvalues are TIED in magnitude or complex — e.g. the cycle
|
|
141
|
+
* J_z=[[0,b],[c,0]] has eigenvalues ±√(b·c) (equal magnitude), where the per-step factors form a
|
|
142
|
+
* 2-cycle whose geometric mean is exactly √(b·c).
|
|
143
|
+
*/
|
|
144
|
+
function spectralRadius( Jz, opts ) {
|
|
145
|
+
opts = opts || {};
|
|
146
|
+
const iters = opts.iters || 100, burn = opts.burn == null ? Math.floor((opts.iters || 100) / 2) : opts.burn, n = Jz.length;
|
|
147
|
+
const matVec = (M, v) => M.map((row) => row.reduce((s, m, j) => s + m * v[j], 0));
|
|
148
|
+
let v = new Array(n).fill(0).map((_, i) => 1 / Math.sqrt(n) + 1e-3 * (i % 2 ? -1 : 1)); // off-axis seed
|
|
149
|
+
let sumLog = 0, count = 0;
|
|
150
|
+
for ( let k = 0; k < iters; k++ ) {
|
|
151
|
+
const w = matVec(Jz, v);
|
|
152
|
+
const norm = Math.sqrt(w.reduce((s, x) => s + x * x, 0));
|
|
153
|
+
if ( norm < 1e-300 ) return 0;
|
|
154
|
+
if ( k >= burn ) { sumLog += Math.log(norm); count++; }
|
|
155
|
+
v = w.map((x) => x / norm);
|
|
156
|
+
}
|
|
157
|
+
return count ? Math.exp(sumLog / count) : 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = { solveFixpoint, implicitGrad, spectralRadius, solveLinear, matTvec, numJac };
|
|
@@ -0,0 +1,153 @@
|
|
|
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
|
+
* extract — BOUNDED SUBGRAPH EXTRACTION for fork / multi-process ship (host-side, ZERO-CORE; the 2026-07-01 fork-perf
|
|
10
|
+
* measure + Laurie confront, verdict SOUND-WITH-CORRECTION).
|
|
11
|
+
*
|
|
12
|
+
* WHY. `fork()` with no seed deep-copies the WHOLE working graph (`JSON.parse(serialize().graph)`) then re-mounts it.
|
|
13
|
+
* Measured (`doc/WIP/experiments/2026-07-01-fork-perf/`): the deep-copy is O(working-graph) and a bounded-seed fork is
|
|
14
|
+
* 27–245× cheaper (growing with N). The lever for the MULTI-PROCESS/worker case (`lib/runtime` ships a seed to a
|
|
15
|
+
* separate process — no shared memory) is shipping a bounded SLICE, not the whole graph. COW can't help cross-process.
|
|
16
|
+
*
|
|
17
|
+
* WHAT. This is PROGRAM SLICING (Weiser 1984) on the reference-dependence graph: `focus` is the slicing criterion; a
|
|
18
|
+
* ship-able slice is a CLOSED backward slice with its free variables FROZEN as inputs. A segment-closed **k-hop ball**
|
|
19
|
+
* (`opts.hops`, default 1): the interior grows by (hops-1) BFS rings from the focus + all their incident segments; the
|
|
20
|
+
* segments' non-interior endpoints are the FROZEN frontier (their current facts copied as constants). Sound iff the
|
|
21
|
+
* frozen frontier is FRAME-COMPLETE — `contract.js` G1 applied at the fork boundary: every
|
|
22
|
+
* cross-cut reference reads a frozen value, none is WRITTEN by the slice. Merge-back = SINGLE-WRITER (separation-logic
|
|
23
|
+
* `*`-disjointness; Reynolds 2002) + an ASSUMPTION-RECHECK (the frozen frontier still holds at merge time — the
|
|
24
|
+
* drift-defeasance moved to the fork frontier; de Kleer ATMS 1986: the frozen frontier is the assumption environment).
|
|
25
|
+
*
|
|
26
|
+
* The seed is a `serialize()`-shaped record (`{ lastRev, conceptMaps:[{...facts}], bagRefs }`) → `graph.fork(seed)`
|
|
27
|
+
* locally OR `spawnGraph({ seed })` to a worker. `lib/graph` is UNTOUCHED.
|
|
28
|
+
*
|
|
29
|
+
* DEFERRED / refused (Laurie): a cyclic cross-cut ref (contract G3 footprint-cycle — refuse), a bagRef spanning the cut
|
|
30
|
+
* (needs manager replication), a not-yet-materialized cross-cut producer (the frame-problem impossible case — refuse),
|
|
31
|
+
* multi-writer frontier (the ATMS multi-context, FILED §6.1); the horizon gate on the precise cross-cut-REF facts (v0
|
|
32
|
+
* gates all frontier facts).
|
|
33
|
+
*/
|
|
34
|
+
const factsOf = ( graph, id ) => { const o = graph._objById[id]; return o && o._etty && o._etty._; };
|
|
35
|
+
const idOf = ( o ) => o && (o.$$_id != null ? o.$$_id : (o.$_id != null ? o.$_id : o._id));
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Extract a bounded, ship-able subgraph around `focus`.
|
|
39
|
+
* @param graph the parent graph.
|
|
40
|
+
* @param focus a node id / segment id / array of ids (the slicing criterion). A focus segment contributes its
|
|
41
|
+
* endpoints as focus nodes.
|
|
42
|
+
* @param opts.tree optional concept tree → run `separatorGate` on the frozen-frontier facts (horizon check).
|
|
43
|
+
* @param opts.refuseAboveHorizon throw if a frozen frontier fact is above Σ_sep (default false → reported only).
|
|
44
|
+
* @returns { seed, interior, segments, frontier, frozen, focusNodes, horizonOk?, aboveHorizon? }
|
|
45
|
+
*/
|
|
46
|
+
function extractSubgraph( graph, focus, opts ) {
|
|
47
|
+
opts = opts || {};
|
|
48
|
+
const focusIds = Array.isArray(focus) ? focus.slice() : [focus];
|
|
49
|
+
const focusNodes = new Set(), focusSegs = new Set();
|
|
50
|
+
for ( const id of focusIds ) {
|
|
51
|
+
const f = factsOf(graph, id);
|
|
52
|
+
if ( !f ) throw new Error('extractSubgraph: unknown focus id "' + id + '"');
|
|
53
|
+
if ( f.Segment ) { focusSegs.add(id); if ( f.originNode ) focusNodes.add(f.originNode); if ( f.targetNode ) focusNodes.add(f.targetNode); }
|
|
54
|
+
else focusNodes.add(id);
|
|
55
|
+
}
|
|
56
|
+
// segment-closed k-hop ball. `hops` (default 1) = the INTERIOR radius: grow the interior by (hops-1) rings of nodes
|
|
57
|
+
// (BFS over incident segments), then include every segment incident to an interior node; a segment's endpoint NOT in
|
|
58
|
+
// the interior is a FROZEN frontier node (the boundary, pinned). hops=1 → interior={focus}, frontier=1-hop neighbours
|
|
59
|
+
// (byte-identical to v0); hops=2 → interior=focus+1-hop, frontier=2-hop; etc.
|
|
60
|
+
const hops = opts.hops == null ? 1 : Math.max(1, opts.hops | 0);
|
|
61
|
+
const incidentOf = ( n ) => { const node = graph._objById[n]; return [].concat((node && node._outgoing) || [], (node && node._incoming) || []); };
|
|
62
|
+
const interiorNodes = new Set(focusNodes);
|
|
63
|
+
let ring = new Set(focusNodes);
|
|
64
|
+
for ( let d = 0; d < hops - 1; d++ ) { // grow the interior by (hops-1) BFS rings
|
|
65
|
+
const next = new Set();
|
|
66
|
+
for ( const n of ring ) for ( const sid of incidentOf(n) ) {
|
|
67
|
+
const sf = factsOf(graph, sid) || {};
|
|
68
|
+
for ( const end of [sf.originNode, sf.targetNode] ) if ( end && !interiorNodes.has(end) ) { interiorNodes.add(end); next.add(end); }
|
|
69
|
+
}
|
|
70
|
+
ring = next;
|
|
71
|
+
if ( !ring.size ) break; // the component is fully inside the ball
|
|
72
|
+
}
|
|
73
|
+
// include every segment incident to an interior node; non-interior endpoints become the frozen frontier.
|
|
74
|
+
const segs = new Set(focusSegs), frontierNodes = new Set();
|
|
75
|
+
for ( const n of interiorNodes ) for ( const sid of incidentOf(n) ) {
|
|
76
|
+
segs.add(sid);
|
|
77
|
+
const sf = factsOf(graph, sid) || {};
|
|
78
|
+
for ( const end of [sf.originNode, sf.targetNode] ) if ( end && !interiorNodes.has(end) ) frontierNodes.add(end);
|
|
79
|
+
}
|
|
80
|
+
// freeze = copy the raw facts of every slice object (interior nodes + incident segments + frozen frontier nodes).
|
|
81
|
+
const sliceIds = [...interiorNodes, ...segs, ...frontierNodes];
|
|
82
|
+
const conceptMaps = sliceIds.map(( id ) => ({ ...factsOf(graph, id) }));
|
|
83
|
+
const frozen = {}; for ( const n of frontierNodes ) frozen[n] = { ...factsOf(graph, n) };
|
|
84
|
+
|
|
85
|
+
const extraction = {
|
|
86
|
+
seed: { lastRev: graph.getCurrentRevision ? graph.getCurrentRevision() : 0, conceptMaps, bagRefs: {} },
|
|
87
|
+
interior: [...interiorNodes], segments: [...segs], frontier: [...frontierNodes], frozen, focusNodes: [...focusNodes], focusSegs: [...focusSegs],
|
|
88
|
+
};
|
|
89
|
+
// horizon gate (optional): every frozen frontier FACT must be on the separator horizon (Σ_sep) or the bound regresses.
|
|
90
|
+
if ( opts.tree ) {
|
|
91
|
+
const { separatorGate } = require('./decompose.js');
|
|
92
|
+
const facts = {};
|
|
93
|
+
for ( const n of frontierNodes ) for ( const k of Object.keys(frozen[n]) ) if ( k !== '_id' && k !== '_rev' ) facts[k] = true;
|
|
94
|
+
const gate = separatorGate(opts.tree, Object.keys(facts));
|
|
95
|
+
extraction.horizonOk = gate.ok; extraction.aboveHorizon = gate.above;
|
|
96
|
+
if ( !gate.ok && opts.refuseAboveHorizon ) throw new Error('extractSubgraph: frozen frontier facts above Σ_sep: ' + gate.above.join(','));
|
|
97
|
+
}
|
|
98
|
+
return extraction;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The interior-only footprint of a merge template: the ids it writes. SOUND merge writes ONLY interior/focus ids —
|
|
103
|
+
* NEVER a frozen frontier id (that is a write across the cut → the ATMS multi-context contradiction). `$$_id` / `$_id`
|
|
104
|
+
* targets an existing object; a bare `_id` creates a new one (also fine — not a frontier write).
|
|
105
|
+
*/
|
|
106
|
+
function templateWrites( tpl ) {
|
|
107
|
+
return (Array.isArray(tpl) ? tpl : [tpl]).map(( o ) => o && (o.$$_id != null ? o.$$_id : (o.$_id != null ? o.$_id : null))).filter(( x ) => x != null);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Merge a bounded-slice worker/child result back into the parent — SOUNDLY.
|
|
112
|
+
* 1. ASSUMPTION-RECHECK: every frozen frontier fact must still equal the parent's current value (else the slice
|
|
113
|
+
* stabilized on a dead premise → REJECT and re-run; the drift-defeasance at the fork frontier).
|
|
114
|
+
* 2. SINGLE-WRITER: the project template must write ONLY interior ids, never a frozen frontier id (separation-logic
|
|
115
|
+
* disjointness) → else REJECT (the multi-writer/ATMS hazard).
|
|
116
|
+
* 3. apply via the sequenced taskflow (`pushMutation`) — never out-of-band.
|
|
117
|
+
* @param parent the parent graph.
|
|
118
|
+
* @param child the settled fork/worker child (or, cross-process, a plain object with `_objById`-like access via `factsGetter`).
|
|
119
|
+
* @param extraction the object returned by `extractSubgraph`.
|
|
120
|
+
* @param opts.project (child) -> mutationTemplate; default = write back the focus objects' produced facts onto their ids.
|
|
121
|
+
* @param opts.factsGetter (child, id) -> facts; default reads `child._objById[id]._etty._` (override for a cross-process JSON child).
|
|
122
|
+
* @returns { merged:true, template } | { merged:false, reason }
|
|
123
|
+
*/
|
|
124
|
+
function mergeSlice( parent, child, extraction, opts ) {
|
|
125
|
+
opts = opts || {};
|
|
126
|
+
const getFacts = opts.factsGetter || (( c, id ) => factsOf(c, id));
|
|
127
|
+
// (1) assumption-recheck — frozen frontier unchanged in the parent.
|
|
128
|
+
for ( const n of extraction.frontier ) {
|
|
129
|
+
const now = factsOf(parent, n) || {}, was = extraction.frozen[n] || {};
|
|
130
|
+
for ( const k of Object.keys(was) ) {
|
|
131
|
+
if ( k === '_id' || k === '_rev' ) continue;
|
|
132
|
+
if ( JSON.stringify(now[k]) !== JSON.stringify(was[k]) ) return { merged: false, reason: 'frontier drift: ' + n + '.' + k + ' changed since extraction' };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// build the write-back template (default = focus objects' current child facts, minus volatile markers).
|
|
136
|
+
const project = opts.project || function ( c ) {
|
|
137
|
+
return extraction.focusNodes.concat(extraction.focusSegs).map(function ( id ) {
|
|
138
|
+
const f = getFacts(c, id) || {}; const o = { $$_id: id };
|
|
139
|
+
for ( const k of Object.keys(f) ) if ( k !== '_id' && k !== '_rev' && k !== '_origin' ) o[k] = f[k];
|
|
140
|
+
return o;
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
const tpl = project(child);
|
|
144
|
+
// (2) single-writer — the template must not write a frozen frontier id.
|
|
145
|
+
const frontierSet = new Set(extraction.frontier);
|
|
146
|
+
const badWrite = templateWrites(tpl).find(( id ) => frontierSet.has(id) );
|
|
147
|
+
if ( badWrite ) return { merged: false, reason: 'single-writer violation: template writes frontier object "' + badWrite + '"' };
|
|
148
|
+
// (3) apply through the sequenced taskflow.
|
|
149
|
+
parent.pushMutation(tpl);
|
|
150
|
+
return { merged: true, template: tpl };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = { extractSubgraph, mergeSlice, templateWrites };
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
* forest — THE MULTI-PATH GENERALIZATION (roadmap 2026-07-09 §5(c); ZERO-CORE). The concept-method as a LIBRARY of
|
|
10
|
+
* alternative sub-paths (the derivation forest): "au moins un chemin complète, un seul reste actif". Re-opened now
|
|
11
|
+
* that the single-path P1-P6 + the transitivity (KG-PROXY-2) hold — the single-path is the degenerate forest.
|
|
12
|
+
*
|
|
13
|
+
* `makeForestServe({ forests, candidates, pool, forge? })` → a projection `serve(leaf)`: for a leaf, try its candidate
|
|
14
|
+
* sub-paths IN PREFERENCE ORDER; the FIRST that dispatches + mounts + GATES sound (P1/P3/P4) is SELECTED — the rest are
|
|
15
|
+
* never activated. That selection IS the confluence guarantee that dodges G3 (`footprintCycles`): we SELECT one path,
|
|
16
|
+
* we never COMPOSE mutually-retractable coupled methods, so there is no JTMS oscillation to the apply-cap. If every
|
|
17
|
+
* candidate fails, fall to the §5(b) forge (last-resort learning) or refuse.
|
|
18
|
+
*/
|
|
19
|
+
const { makeMethodServe } = require('./serve-leaf.js');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param spec.candidates { <candidateKey>: <makeMethodServe method spec> } the alternative sub-paths (each a method).
|
|
23
|
+
* @param spec.forests { <leafKey>: [candidateKey, ...] } the forest per leaf, in PREFERENCE order.
|
|
24
|
+
* @param spec.keyOf (leaf) => leafKey (default: leaf.produces || leaf.id).
|
|
25
|
+
* @param spec.pool a shared P3 invoke-pool (default: own).
|
|
26
|
+
* @param spec.forge async (args) => value the §5(b) fallback when the forest is EXHAUSTED (optional).
|
|
27
|
+
* @returns serve async (leaf, ctx) => { value, selected, tried } (+ serve.pool, serve.close()).
|
|
28
|
+
*/
|
|
29
|
+
function makeForestServe( spec ) {
|
|
30
|
+
spec = spec || {};
|
|
31
|
+
const pool = spec.pool || require('../index.js').createInvokePool();
|
|
32
|
+
const forests = spec.forests || {};
|
|
33
|
+
const keyOf = spec.keyOf || (( leaf ) => leaf.produces || leaf.id);
|
|
34
|
+
const serveOne = makeMethodServe({ methods: spec.candidates || {}, keyOf: ( l ) => l.__cand, pool });
|
|
35
|
+
|
|
36
|
+
async function serve( leaf, ctx ) {
|
|
37
|
+
const key = keyOf(leaf);
|
|
38
|
+
const candidates = forests[key] || [];
|
|
39
|
+
const tried = [];
|
|
40
|
+
for ( const c of candidates ) {
|
|
41
|
+
try {
|
|
42
|
+
const value = await serveOne(Object.assign({}, leaf, { __cand: c, id: leaf.id + ':' + c }), ctx); // dispatch + mount + GATE
|
|
43
|
+
return { value, selected: c, tried: tried.concat(c) }; // FIRST sound → SELECTED (one stays active)
|
|
44
|
+
} catch ( e ) { tried.push(c); } // this path didn't complete → try the next (no coupling → no G3)
|
|
45
|
+
}
|
|
46
|
+
if ( typeof spec.forge === 'function' ) // forest EXHAUSTED → §5(b) last-resort learning
|
|
47
|
+
return { value: await spec.forge({ leaf, stack: (ctx && ctx.stack) || [], contract: (ctx && ctx.contract) || {}, reason: 'forest-exhausted', tried }), selected: 'forged', tried };
|
|
48
|
+
return { value: undefined, selected: null, tried, refusal: 'forest-exhausted' };
|
|
49
|
+
}
|
|
50
|
+
serve.pool = pool;
|
|
51
|
+
serve.close = () => spec.pool ? Promise.resolve() : pool.close();
|
|
52
|
+
return serve;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { makeForestServe };
|
|
@@ -0,0 +1,116 @@
|
|
|
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
|
+
* forge-fallback — THE LAST-RESORT LEARNING FALLBACK, wired (roadmap 2026-07-09 §5(b); ZERO-CORE). P2's segment-proxy
|
|
10
|
+
* exposes a `forge` HOOK + `reconstructStack` (the bounded problem stack, walked UP the parentSeg chain). §5(b) wires
|
|
11
|
+
* the pipeline behind that hook:
|
|
12
|
+
*
|
|
13
|
+
* reconstructStack → STACK-TO-PROMPT → forge (the model) → RE-ENTER the gate (assertPost) → INDEX-BACK (amortise)
|
|
14
|
+
*
|
|
15
|
+
* The LEVER (owner): the fallback's success is the SERIALIZATION of the stack into a prompt — if `stackToPrompt`
|
|
16
|
+
* reconstitutes "what we're doing", the model only has to answer simple-but-relevant. When it fails, you work the
|
|
17
|
+
* info-provisioning + the prompt, NOT the model. The forged method is PROPOSE-only: it must satisfy the contract
|
|
18
|
+
* (assertPost DISPOSES) before it casts — it is typed sub-graph, so determinism holds. A forged method is INDEXED so
|
|
19
|
+
* the next matching case is a dispatch HIT (0-call) — the amortisation. The `forge` model is injected (a real LLM in
|
|
20
|
+
* production; a deterministic stub in tests).
|
|
21
|
+
*
|
|
22
|
+
* BOUNDED BLAME-DRIVEN RETRY (2026-07-09, residue USE-2(d), RE-SCOPED by the 2-lens confront). `maxRounds > 1` turns
|
|
23
|
+
* the one-shot forge into a bounded loop: on a gate refusal, the graph's PUSHBACK (the violated post atoms + the
|
|
24
|
+
* rejected-history) is folded into the next prompt (`serializeBlame`, the §5b LEVER one level up) and the model
|
|
25
|
+
* revises, up to `maxRounds`, then a typed refusal. This is the OPEN-regime DUAL of `negotiate.js`: negotiate is the
|
|
26
|
+
* CLOSED-domain constructive dialogue (it ENUMERATES admissible options and refuses on empty-options); here the space
|
|
27
|
+
* is OPEN (a method/castTemplate, not an enum), so the pushback is the BLAME and refusal is on max-rounds — importing
|
|
28
|
+
* negotiate's `optionsOf` is inert here (no enumerable domain) / gold-leaks on a closed enum. The three invariants
|
|
29
|
+
* hold, re-earned for the open regime: 0-FALSE (the admit path is the UNCHANGED `assertPost` — a non-gated forge is
|
|
30
|
+
* never returned, any round), HONEST-but-OPERATIONAL REFUSAL (refuse when no gated answer was produced within the
|
|
31
|
+
* bound — an operational, not epistemic, refusal; the graph never fabricates one), TERMINATION (`maxRounds`).
|
|
32
|
+
* LIVE-PROVEN (Ministral-Q8, `WIP/experiments/2026-07-09-blame-retry-killgate/`): on an OPEN sum+mod3 target, one-shot
|
|
33
|
+
* 17/24 → retry 24/24 at 0-FALSE; the dominant lever is the bounded retry-with-memory (+6/7), the violated-atom
|
|
34
|
+
* CONTENT adds a thin-but-robust +1 (a stubborn case resampling can't reach). `maxRounds` DEFAULTS to 1 → the one-shot
|
|
35
|
+
* behavior is byte-identical (fully backward-compatible).
|
|
36
|
+
*/
|
|
37
|
+
const { assertPost } = require('./contract.js');
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* defaultStackToPrompt — the tunable serialization. Reconstitutes each level's TYPED interface (statement + produces
|
|
41
|
+
* ⊢ / needs ⊨) + the required output (contract.write) — "what we're doing", bounded, no prose.
|
|
42
|
+
*/
|
|
43
|
+
function defaultStackToPrompt( stack, contract, args ) {
|
|
44
|
+
const levels = (stack || []).map(( s, i ) => 'L' + i + ' ' + (s.statement || s.id)
|
|
45
|
+
+ (s.produces ? ' ⊢' + s.produces : '') + (s.needs && s.needs.length ? ' ⊨' + s.needs.join(',') : '')).join(' | ');
|
|
46
|
+
const goal = 'PRODUCE ' + (((contract || {}).write) || []).join(',');
|
|
47
|
+
return 'STACK[' + levels + '] · ' + ((args && args.reason) || 'forge') + ' → ' + goal;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* defaultSerializeBlame — the graph's PUSHBACK folded into the revision prompt (the §5b LEVER, one level up). The
|
|
52
|
+
* ONLY thing beyond a bare resample is the CONTENT: the violated post atoms + the rejected-history. Bounded, no prose.
|
|
53
|
+
* @param violations the `assertPost` violations of the last-rejected candidate ({kind,detail}).
|
|
54
|
+
* @param rejected the summaries rejected so far (so the model does NOT repeat them).
|
|
55
|
+
* @param round the current (0-indexed) round about to be attempted.
|
|
56
|
+
*/
|
|
57
|
+
function defaultSerializeBlame( violations, rejected, round ) {
|
|
58
|
+
const atoms = (violations || []).filter(( v ) => v.kind === 'post-violated').map(( v ) => v.detail);
|
|
59
|
+
const undecl = (violations || []).filter(( v ) => v.kind === 'undeclared-write').map(( v ) => v.detail);
|
|
60
|
+
const why = [];
|
|
61
|
+
if ( atoms.length ) why.push('violated constraints: [' + atoms.join(', ') + ']');
|
|
62
|
+
if ( undecl.length ) why.push('undeclared writes: [' + undecl.join(', ') + ']');
|
|
63
|
+
return ' | REVISE (attempt #' + (round + 1) + '): the previous answer was REJECTED — ' + (why.join('; ') || 'the post did not hold')
|
|
64
|
+
+ '. Already-rejected (do NOT repeat): [' + (rejected || []).map(( r ) => JSON.stringify(r)).join(' ') + ']. Produce a NEW answer satisfying ALL.';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* makeForgeFallback(spec) — build a `forge` fn wired for a segment-proxy's hook.
|
|
69
|
+
* @param spec.forge (prompt, args) => { name, castTemplate, summary?, footprint?, method? } | null REQUIRED — the model.
|
|
70
|
+
* castTemplate = the cast the proxy applies (a typed sub-graph); summary/footprint = what it
|
|
71
|
+
* produces (re-gated); method = a method spec to index-back.
|
|
72
|
+
* @param spec.stackToPrompt (stack, contract, args) => prompt the LEVER (default: defaultStackToPrompt).
|
|
73
|
+
* @param spec.serializeBlame (violations, rejected, round) => promptSuffix the retry pushback (default: defaultSerializeBlame).
|
|
74
|
+
* @param spec.maxRounds the bounded-retry cap (default 1 = one-shot, byte-identical to the pre-retry behavior).
|
|
75
|
+
* @param spec.oracle optional G2 ground-truth probe forwarded to `assertPost` (for an effecting post).
|
|
76
|
+
* @param spec.index Map<libraryKey, method> the library the forged method is indexed into (amortise; default: own).
|
|
77
|
+
* @returns fallback async (args) => castTemplate (args = the P2 hook: { scope, graph, contract, stack, reason, blame }).
|
|
78
|
+
* fallback.index exposes the growing library.
|
|
79
|
+
*/
|
|
80
|
+
function makeForgeFallback( spec ) {
|
|
81
|
+
spec = spec || {};
|
|
82
|
+
const stackToPrompt = spec.stackToPrompt || defaultStackToPrompt;
|
|
83
|
+
const serializeBlame = spec.serializeBlame || defaultSerializeBlame;
|
|
84
|
+
const maxRounds = spec.maxRounds || 1;
|
|
85
|
+
const forge = spec.forge;
|
|
86
|
+
const index = spec.index || new Map();
|
|
87
|
+
if ( typeof forge !== 'function' ) throw new Error('makeForgeFallback needs spec.forge(prompt, args)');
|
|
88
|
+
|
|
89
|
+
const fallback = async function ( args ) {
|
|
90
|
+
args = args || {};
|
|
91
|
+
const base = stackToPrompt(args.stack || [], args.contract || {}, args); // the LEVER: stack → prompt
|
|
92
|
+
const rejected = [];
|
|
93
|
+
let lastViolations = null;
|
|
94
|
+
for ( let round = 0; round < maxRounds; round++ ) {
|
|
95
|
+
const prompt = round === 0 ? base : base + serializeBlame(lastViolations, rejected, round); // fold the pushback in (round>0)
|
|
96
|
+
const cand = await forge(prompt, args); // the model — PROPOSE
|
|
97
|
+
if ( !cand || !cand.castTemplate ) throw new Error('forge-fallback: the model produced no candidate');
|
|
98
|
+
// RE-ENTER the gate (DISPOSE): the forged output must satisfy the contract — a bad forge cannot corrupt the caller.
|
|
99
|
+
if ( cand.summary ) {
|
|
100
|
+
const post = assertPost(args.contract || {}, cand.summary, cand.footprint || Object.keys(cand.summary), { oracle: spec.oracle });
|
|
101
|
+
if ( !post.ok ) {
|
|
102
|
+
rejected.push(cand.summary); lastViolations = post.violations;
|
|
103
|
+
if ( round + 1 < maxRounds ) continue; // RETRY with the graph's pushback in the prompt
|
|
104
|
+
throw new Error('forge-fallback: the forged method is REFUSED by the gate (' + post.violations.map(( v ) => v.kind).join(',') + ')');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if ( cand.method ) index.set(cand.name || (args.stack && args.stack[0] && args.stack[0].id) || 'forged', cand.method); // INDEX-BACK (amortise)
|
|
108
|
+
return cand.castTemplate;
|
|
109
|
+
}
|
|
110
|
+
throw new Error('forge-fallback: no gated candidate within ' + maxRounds + ' rounds'); // (unreachable when maxRounds>=1; a guard)
|
|
111
|
+
};
|
|
112
|
+
fallback.index = index;
|
|
113
|
+
return fallback;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = { makeForgeFallback, defaultStackToPrompt, defaultSerializeBlame };
|
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
* givens — the TYPED BASE-FACT FRONT-DOOR (ZERO-CORE, host-side). Closes the tiered-plan gap (i): a leaf served
|
|
10
|
+
* through the context projection saw its UPSTREAM values but never the task's own literals (the 6 and the 4 of
|
|
11
|
+
* "a 6×4 rectangle"), so the executor re-guessed them. This brick extracts the task's base facts DETERMINISTICALLY
|
|
12
|
+
* (no model), names each one with a stable typed key, and the projection seeds them into the top pool as
|
|
13
|
+
* `val_<given>` — a leaf that cites a given key in its `needs` gets the VALUE injected into its bounded context,
|
|
14
|
+
* exactly like a producer's output. Nothing here is domain reasoning: it is a DECLARED front-door — two extractors
|
|
15
|
+
* (numeric literals in prose; numeric cells of a table), both pure surface, both fail-closed (what is not a clean
|
|
16
|
+
* numeric literal is NOT a given).
|
|
17
|
+
*
|
|
18
|
+
* const { numberGivens, givensBlock, seedOf } = require('skynet-graph/lib/authoring/givens');
|
|
19
|
+
* const givens = numberGivens(task); // [{ key:'g1_wide', value:6, snippet:'is 6 wide' }]
|
|
20
|
+
* const leaves = await decompose(task + '\n' + givensBlock(givens)); // parts cite given keys in `needs`
|
|
21
|
+
* await createPlanLoop({ decompose, serveLeaf }).run(task, { givens: seedOf(givens) });
|
|
22
|
+
*
|
|
23
|
+
* The KEY is positional + a lexical slug (`g<i>_<word>`): stable, typed, never a sentence — the human-readable
|
|
24
|
+
* meaning rides the SNIPPET (presentation), never the key (dispatch), per the K1 discipline.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
// a numeric literal in prose: optional $, digits with thousand-commas, optional decimals, optional %.
|
|
28
|
+
// NOTE (owner 07-10): the front-door stays DIGITS-ONLY on purpose — translating prose quantities ("ten",
|
|
29
|
+
// "twice") into numbers host-side is the wrong layer; the DECOMPOSER is instructed to restate any non-given
|
|
30
|
+
// base fact explicitly in the consuming part's instruction (DECOMPOSE_SYSTEM rule 6), which covers spelled
|
|
31
|
+
// numbers, fractions in words, and world knowledge alike.
|
|
32
|
+
const NUM_RE = /\$?\s?\d[\d,]*(?:\.\d+)?%?/g;
|
|
33
|
+
const WORD_RE = /[a-z][a-z'-]*/i;
|
|
34
|
+
|
|
35
|
+
// slug = the nearest content word AFTER the number (else BEFORE) — purely lexical, lowercased, bounded.
|
|
36
|
+
const STOP = { the: 1, a: 1, an: 1, of: 1, to: 1, and: 1, or: 1, is: 1, are: 1, was: 1, were: 1, it: 1, its: 1, in: 1, on: 1, at: 1, for: 1, with: 1, then: 1, than: 1, by: 1, per: 1, each: 1, more: 1, less: 1, had: 1, has: 1, have: 1, them: 1, they: 1, those: 1, these: 1, this: 1, that: 1, his: 1, her: 1, their: 1, he: 1, she: 1, we: 1, as: 1, so: 1, but: 1, if: 1, how: 1, many: 1, much: 1, did: 1, does: 1, do: 1 };
|
|
37
|
+
function slugNear( text, start, end ) {
|
|
38
|
+
const pick = ( s ) => {
|
|
39
|
+
let m;
|
|
40
|
+
const re = new RegExp(WORD_RE.source, 'gi');
|
|
41
|
+
while ( (m = re.exec(s)) ) if ( !STOP[m[0].toLowerCase()] ) return m[0].toLowerCase().replace(/[^a-z]/g, '').slice(0, 12);
|
|
42
|
+
return '';
|
|
43
|
+
};
|
|
44
|
+
return pick(text.slice(end, end + 24)) || pick(text.slice(Math.max(0, start - 24), start).split(/\s+/).reverse().join(' '));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* numberGivens(text) → [{ key, value, snippet }] — every clean numeric literal of the prose, in reading order.
|
|
49
|
+
* Deterministic, fail-closed, DIGITS-ONLY (see the NOTE above — prose quantities are the decomposer's job,
|
|
50
|
+
* by prompt rule, not a host-side translation table). `value` is the bare Number ($/commas/% stripped).
|
|
51
|
+
*/
|
|
52
|
+
function numberGivens( text ) {
|
|
53
|
+
const s = String(text == null ? '' : text);
|
|
54
|
+
const out = [];
|
|
55
|
+
let m;
|
|
56
|
+
NUM_RE.lastIndex = 0;
|
|
57
|
+
while ( (m = NUM_RE.exec(s)) ) {
|
|
58
|
+
const raw = m[0];
|
|
59
|
+
const v = Number(raw.replace(/[$,%\s]/g, ''));
|
|
60
|
+
if ( !isFinite(v) ) continue;
|
|
61
|
+
const slug = slugNear(s, m.index, m.index + raw.length);
|
|
62
|
+
const key = 'g' + (out.length + 1) + (slug ? '_' + slug : '');
|
|
63
|
+
out.push({ key: key, value: v, snippet: s.slice(Math.max(0, m.index - 18), m.index + raw.length + 18).trim().replace(/\s+/g, ' ') });
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* cellGivens(table) → [{ key, value, snippet, cell:{r,c} }] — every numeric cell of a row-major table (the FinQA
|
|
70
|
+
* front-door; row 0 = headers by convention). Key = `c<r>_<c>_<slug-of-row-label>`; snippet = "rowLabel · colHeader".
|
|
71
|
+
*/
|
|
72
|
+
function cellGivens( table ) {
|
|
73
|
+
const rows = Array.isArray(table) ? table : [];
|
|
74
|
+
const headers = Array.isArray(rows[0]) ? rows[0] : [];
|
|
75
|
+
const out = [];
|
|
76
|
+
for ( let r = 1; r < rows.length; r++ ) {
|
|
77
|
+
const row = rows[r] || [];
|
|
78
|
+
const label = String(row[0] == null ? '' : row[0]);
|
|
79
|
+
for ( let c = 1; c < row.length; c++ ) {
|
|
80
|
+
let raw = String(row[c] == null ? '' : row[c]).trim();
|
|
81
|
+
if ( !raw ) continue;
|
|
82
|
+
const neg = /^\(.*\)$/.test(raw); // (1,234) = accounting negative
|
|
83
|
+
if ( !neg ) raw = raw.replace(/\(\s*[^)]*\)\s*$/, '').trim(); // "-17.1 ( 17.1 )" = FinQA dup-in-parens → keep the head
|
|
84
|
+
const v = Number(raw.replace(/[$,%()\s]/g, '')) * (neg ? -1 : 1);
|
|
85
|
+
if ( !isFinite(v) || !/\d/.test(raw) ) continue;
|
|
86
|
+
const slug = (label.toLowerCase().match(WORD_RE) || [''])[0].replace(/[^a-z]/g, '').slice(0, 12);
|
|
87
|
+
out.push({ key: 'c' + r + '_' + c + (slug ? '_' + slug : ''), value: v, cell: { r: r, c: c },
|
|
88
|
+
snippet: (label + ' · ' + String(headers[c] == null ? '' : headers[c])).trim() });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** givensBlock(givens) → the prompt block appended to the decompose USER message (keys citable in `needs`). */
|
|
95
|
+
function givensBlock( givens ) {
|
|
96
|
+
if ( !givens || !givens.length ) return '';
|
|
97
|
+
return 'GIVENS (base facts already available — a part that uses one MUST cite its key in "needs"; do NOT restate the number): '
|
|
98
|
+
+ givens.map(( g ) => g.key + '=' + g.value + ' ("' + g.snippet + '")' ).join(' · ');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** seedOf(givens) → { key: value } — the map the projection seeds as `val_<key>` (plan-loop's `ctx.givens`). */
|
|
102
|
+
function seedOf( givens ) {
|
|
103
|
+
const map = {};
|
|
104
|
+
for ( const g of (givens || []) ) map[g.key] = g.value;
|
|
105
|
+
return map;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* labelsOf(givens) → { key: snippet } — provenance labels for leaf prompts, STRUCTURED PROVENANCE ONLY (the
|
|
110
|
+
* "cells" rule, ablation-verified at N=40 and N=200): a table cell's MEANING (row · col header) is invisible in
|
|
111
|
+
* its bare value, so the label fixes leaf mis-localization (wrong cell/base-year/sign); a PROSE given is already
|
|
112
|
+
* restated self-contained by the decomposer (rule 7), so labelling it is pure prompt perturbation (net-negative),
|
|
113
|
+
* and upstream-producer labels help nowhere. Wire as `ctx.labels` (plan-loop → context-project).
|
|
114
|
+
*/
|
|
115
|
+
function labelsOf( givens ) {
|
|
116
|
+
const map = {};
|
|
117
|
+
for ( const g of (givens || []) ) if ( g && g.cell && g.snippet ) map[g.key] = String(g.snippet).slice(0, 70);
|
|
118
|
+
return map;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = { numberGivens, cellGivens, givensBlock, seedOf, labelsOf };
|