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,101 @@
|
|
|
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
|
+
* stock — the reusable CORE of gold-gated concept-method STOCK-BUILDING (host-side, ZERO-CORE, engine-free).
|
|
10
|
+
* The WikiSQL pilot (2026-07-05) proved a stock of .sgc methods can be accumulated from a REAL externally-
|
|
11
|
+
* labelled corpus: `record → forge model decomposition → crystallizeFrom → GOLD-GATE → .sgc`. That pilot
|
|
12
|
+
* inlined the gate + the yield lever; THIS brick promotes them to the library so any dataset adapter reuses
|
|
13
|
+
* them — a host writes only the small per-dataset `{ sigOf, goldStepsOf, problemOf }` adapter + a forge, and
|
|
14
|
+
* the SOUNDNESS-bearing pieces (the gate that keeps the stock clean, the packaging) live here, tested once.
|
|
15
|
+
*
|
|
16
|
+
* The two findings the pilot measured, both captured here:
|
|
17
|
+
* 1. THE VALUE IS THE GATE, not the generation. `goldGate` admits a class method into the stock ONLY IF
|
|
18
|
+
* the model's decomposition is CONSISTENT across the class's instances AND MATCHES the gold shape AND
|
|
19
|
+
* crystallize admitted it (K1-sound). So the stock stays 100% clean (0 false admitted) whatever the
|
|
20
|
+
* small model renders — the oracle (the dataset's gold) + the gate carry soundness, not the model.
|
|
21
|
+
* 2. YIELD IS BOUNDED BY MODEL CONSISTENCY (the pilot's small model omitted/inconsistently-added a step).
|
|
22
|
+
* `consistencyVote` is the cheap, SOUNDNESS-PRESERVING lever: sample N decompositions of one instance,
|
|
23
|
+
* take the majority shape. It raises the fraction of instances that reach a consistent shape WITHOUT
|
|
24
|
+
* bypassing the gate (the gate still verifies the voted shape against gold) — never a soundness risk.
|
|
25
|
+
*
|
|
26
|
+
* Pure functions over shapes (arrays of typed step kinds) — NO engine, NO fs, NO model. The trace-producing
|
|
27
|
+
* run (a Plan decomposition through the engine) stays the host's; this brick verifies + packages its output.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const packMethods = require('./method-pack.js').packMethods;
|
|
31
|
+
|
|
32
|
+
/** the canonical SHAPE of a typed decomposition — the ordered step kinds joined (the class-method identity). */
|
|
33
|
+
function shapeOf( steps ) { return (steps || []).join('>'); }
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* CONSISTENCY VOTE (the yield lever) — given N sampled decompositions of the SAME instance, return the
|
|
37
|
+
* MAJORITY shape + the agreement fraction. Deterministic on ties (first-seen mode wins). Empty samples vote
|
|
38
|
+
* to an empty shape with agreement 0. Soundness-preserving: the voted shape is still gold-gated downstream.
|
|
39
|
+
* @param samples array of step-arrays (each a decomposition of one instance).
|
|
40
|
+
* @returns {{ steps, shape, agreement, votes:{ <shape>: count }, n }}
|
|
41
|
+
*/
|
|
42
|
+
function consistencyVote( samples ) {
|
|
43
|
+
samples = (samples || []).filter( Array.isArray );
|
|
44
|
+
const votes = {}, first = {};
|
|
45
|
+
let bestShape = '', bestCount = 0;
|
|
46
|
+
for ( let i = 0; i < samples.length; i++ ) {
|
|
47
|
+
const sh = shapeOf(samples[i]);
|
|
48
|
+
if ( !sh ) continue; // an empty decomposition is not a vote
|
|
49
|
+
votes[sh] = (votes[sh] || 0) + 1;
|
|
50
|
+
if ( first[sh] === undefined ) first[sh] = i;
|
|
51
|
+
if ( votes[sh] > bestCount || (votes[sh] === bestCount && first[sh] < first[bestShape]) ) { bestShape = sh; bestCount = votes[sh]; }
|
|
52
|
+
}
|
|
53
|
+
const n = samples.length;
|
|
54
|
+
const steps = bestShape ? bestShape.split('>') : [];
|
|
55
|
+
return { steps, shape: bestShape, agreement: n ? bestCount / n : 0, votes, n };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* THE GOLD-GATE — the verification rule that keeps a stock clean. Admit a class method into the stock iff:
|
|
60
|
+
* (a) the model's per-instance shapes are CONSISTENT (all equal + non-empty across the class's instances),
|
|
61
|
+
* (b) that shape MATCHES the gold shape (the dataset oracle), and
|
|
62
|
+
* (c) crystallize ADMITTED the method (K1-sound: the trace carried typed content, not free prose).
|
|
63
|
+
* By construction it NEVER admits a shape≠gold method — the stock's 0-false property is structural, not
|
|
64
|
+
* model-dependent. Reports a discriminating `reason` on refusal (for the LOG / a curator).
|
|
65
|
+
* @param opts { modelShapes:[…], goldSteps:[…]|goldShape:string, crystallized:bool }
|
|
66
|
+
* @returns {{ admitted, consistent, goldMatch, crystallized, modelShape, goldShape, reason }}
|
|
67
|
+
*/
|
|
68
|
+
function goldGate( opts ) {
|
|
69
|
+
opts = opts || {};
|
|
70
|
+
const modelShapes = (opts.modelShapes || []).map( ( s ) => Array.isArray(s) ? shapeOf(s) : String(s || '') );
|
|
71
|
+
const goldShape = opts.goldShape != null ? opts.goldShape : shapeOf(opts.goldSteps);
|
|
72
|
+
const crystallized = !!opts.crystallized;
|
|
73
|
+
const consistent = modelShapes.length > 0 && modelShapes.every( ( s ) => s === modelShapes[0] && s.length > 0 );
|
|
74
|
+
const modelShape = consistent ? modelShapes[0] : (modelShapes[0] || '');
|
|
75
|
+
const goldMatch = consistent && modelShape === goldShape;
|
|
76
|
+
const admitted = consistent && goldMatch && crystallized;
|
|
77
|
+
const reason = admitted ? 'admit'
|
|
78
|
+
: !consistent ? 'model-inconsistent'
|
|
79
|
+
: !goldMatch ? 'shape-mismatches-gold'
|
|
80
|
+
: 'crystallize-refused';
|
|
81
|
+
return { admitted, consistent, goldMatch, crystallized, modelShape, goldShape, reason };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Pack the ADMITTED class methods into a portable `.sgc` stock (thin over method-pack). Each admitted class
|
|
86
|
+
* is a recall-index entry keyed on its class signature (structure = `{ taskKind: sig }`).
|
|
87
|
+
* @param admitted array of `{ sig, candidate }` (the gold-verified class methods).
|
|
88
|
+
* @param opts { name, version, description, structureKey } — structureKey defaults to 'taskKind'.
|
|
89
|
+
* @returns a `.sgc kind:'methods'` bundle.
|
|
90
|
+
*/
|
|
91
|
+
function packStock( admitted, opts ) {
|
|
92
|
+
opts = opts || {};
|
|
93
|
+
const sk = opts.structureKey || 'taskKind';
|
|
94
|
+
const entries = (admitted || []).filter( ( a ) => a && a.candidate ).map( ( a ) => {
|
|
95
|
+
const structure = {}; structure[sk] = a.sig;
|
|
96
|
+
return { structure, content: {}, method: a.candidate };
|
|
97
|
+
} );
|
|
98
|
+
return packMethods({ entries: entries }, { name: opts.name || 'stock', version: opts.version || '0.0.0', description: opts.description || '' });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = { shapeOf, consistencyVote, goldGate, packStock };
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
* store — DISK-BACKED PERSISTENCE for the method library (host-side, opt-in; uses fs, like `lib/load.js` —
|
|
10
|
+
* the engine core stays fs-free). M2 of the productization campaign: the derivation cache `store` and the
|
|
11
|
+
* master-loop `cache` are both pluggable Map-likes, so a file-backed store makes the warm library SURVIVE a
|
|
12
|
+
* restart — a cold process re-loads it and replays recurrent methods at 0 model calls (cross-session
|
|
13
|
+
* amortization, the master-graph study's persistence rung U5).
|
|
14
|
+
*
|
|
15
|
+
* const store = createFileStore('./.sg-cache.json');
|
|
16
|
+
* const cache = createProviderCache({ store }); // the derivation cache persists
|
|
17
|
+
* const loop = createMasterLoop({ ..., cache: store }); // or the master-loop cache persists
|
|
18
|
+
*
|
|
19
|
+
* It is a write-through Map: load on construct, persist the whole map on each mutation (simple + correct at
|
|
20
|
+
* reasoning-grade volume; swap in an append-log/LRU for high throughput). Values must be JSON-serializable
|
|
21
|
+
* (the cached templates/results already are — they round-trip through `serialize`/the rev-log).
|
|
22
|
+
*/
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
|
|
25
|
+
function createFileStore( file, opts ) {
|
|
26
|
+
opts = opts || {};
|
|
27
|
+
const max = opts.max || 0; // 0 = unbounded; else oldest-eviction like createProviderCache
|
|
28
|
+
let map = new Map();
|
|
29
|
+
// LOAD: re-hydrate the library from disk (a "restart"). A missing/corrupt file → empty (fail-open).
|
|
30
|
+
try {
|
|
31
|
+
if ( fs.existsSync(file) ) {
|
|
32
|
+
const arr = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
33
|
+
if ( Array.isArray(arr) ) map = new Map(arr);
|
|
34
|
+
}
|
|
35
|
+
} catch ( e ) { map = new Map(); }
|
|
36
|
+
|
|
37
|
+
let timer = null;
|
|
38
|
+
function persist() {
|
|
39
|
+
// debounce-free, synchronous write-through (deterministic for tests); coalesce via opts.async if wanted.
|
|
40
|
+
const data = JSON.stringify([...map.entries()]);
|
|
41
|
+
if ( opts.async ) { clearTimeout(timer); timer = setTimeout(() => { try { fs.writeFileSync(file, data); } catch ( e ) {} }, opts.async); }
|
|
42
|
+
else try { fs.writeFileSync(file, data); } catch ( e ) {}
|
|
43
|
+
}
|
|
44
|
+
function evict() { if ( max && map.size > max ) map.delete(map.keys().next().value); }
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
has( k ) { return map.has(k); },
|
|
48
|
+
get( k ) { return map.get(k); },
|
|
49
|
+
set( k, v ) { map.set(k, v); evict(); persist(); return this; },
|
|
50
|
+
delete( k ) { const r = map.delete(k); persist(); return r; },
|
|
51
|
+
clear() { map.clear(); persist(); },
|
|
52
|
+
keys() { return map.keys(); },
|
|
53
|
+
get size() { return map.size; },
|
|
54
|
+
flush() { const data = JSON.stringify([...map.entries()]); try { fs.writeFileSync(file, data); } catch ( e ) {} },
|
|
55
|
+
file
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Persist / re-hydrate a recall INDEX (so fuzzy recall + partial reuse also survive a restart). The index
|
|
61
|
+
* stores { sig, method } entries (the vectors are recomputed on load — they are a pure function of sig).
|
|
62
|
+
*/
|
|
63
|
+
function saveIndex( index, file ) {
|
|
64
|
+
const entries = (index.entries || []).map(( e ) => ({ sig: e.sig, method: e.method }));
|
|
65
|
+
try { fs.writeFileSync(file, JSON.stringify(entries)); } catch ( e ) {}
|
|
66
|
+
return entries.length;
|
|
67
|
+
}
|
|
68
|
+
function loadIndex( index, file ) {
|
|
69
|
+
try {
|
|
70
|
+
if ( !fs.existsSync(file) ) return 0;
|
|
71
|
+
const arr = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
72
|
+
for ( const e of (Array.isArray(arr) ? arr : []) ) index.add(e.sig, e.method);
|
|
73
|
+
return arr.length;
|
|
74
|
+
} catch ( e ) { return 0; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Generic `.sgc` bundle file IO (works for BOTH a corpus-pack and a method-pack bundle — anything with
|
|
79
|
+
* `format:'sgc'`). The pack modules stay fs-free (they return/consume plain bundle objects); the disk
|
|
80
|
+
* round-trip lives here, the fs layer. `saveSgc` writes pretty JSON (a `.sgc` is meant to be inspected/
|
|
81
|
+
* diffed); `loadSgc` returns the parsed bundle or null on a missing/corrupt file (fail-open).
|
|
82
|
+
*/
|
|
83
|
+
function saveSgc( bundle, file ) {
|
|
84
|
+
try { fs.writeFileSync(file, JSON.stringify(bundle, null, 2)); return true; } catch ( e ) { return false; }
|
|
85
|
+
}
|
|
86
|
+
function loadSgc( file ) {
|
|
87
|
+
try {
|
|
88
|
+
if ( !fs.existsSync(file) ) return null;
|
|
89
|
+
const b = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
90
|
+
return (b && b.format === 'sgc') ? b : null;
|
|
91
|
+
} catch ( e ) { return null; }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { createFileStore, saveIndex, loadIndex, saveSgc, loadSgc };
|
|
@@ -0,0 +1,138 @@
|
|
|
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
|
+
* Hypothesis-and-test self-modification regime (roadmap #11.c.3, doc/MODELISATION.md
|
|
21
|
+
* §6.4) — the capstone of #11. Zero core change: it composes the now-complete safe
|
|
22
|
+
* instruments — re-entrant `add`/`patchConcept` (#11.a), the apply-ceiling backstop
|
|
23
|
+
* (#11.c.1), and concept-lib-versioned `rollbackTo` (#11.c.2 / N6) — into a supervised
|
|
24
|
+
* loop that may *try* a self-modification and cleanly *undo* it if it doesn't help.
|
|
25
|
+
*
|
|
26
|
+
* Design (per the engine author):
|
|
27
|
+
* - `Stuck` is a human-vocabulary fact (a sub-problem that exhausted its strategies /
|
|
28
|
+
* blew its budget) — the loop's TRIGGER, surfaced by `detectStuck`;
|
|
29
|
+
* - the "is it better / worse?" call is NOT an engine metric. Budget-spent is only the
|
|
30
|
+
* trigger; the judgment is a *supervisor* decision rendered by a higher-level concept
|
|
31
|
+
* with a better model → injected as `evaluate` (the better-model hook);
|
|
32
|
+
* - the repair STRATEGY is open R&D / plural → injected as `hypothesize`. Different
|
|
33
|
+
* strategies plug in here without touching the loop.
|
|
34
|
+
*
|
|
35
|
+
* The loop is HOST-orchestrated because `rollbackTo` must run at a quiescent boundary
|
|
36
|
+
* (it re-mounts; it is not queued like add/patchConcept). A fully-reactive supervisor
|
|
37
|
+
* concept (with queued rollback) is the next R&D step.
|
|
38
|
+
*
|
|
39
|
+
* const { supervise } = require('./supervise');
|
|
40
|
+
* const res = await supervise(graph, {
|
|
41
|
+
* detectStuck: (g) => <stuckId|null>, // the trigger (Stuck fact / budget)
|
|
42
|
+
* hypothesize: async (g, stuckId, ctx) => { ...self-mod...; return <label> }, // a strategy
|
|
43
|
+
* evaluate: async (g, stuckId, hyp) => ({ better: <bool>, ... }), // the supervisor model
|
|
44
|
+
* maxAttempts: 5,
|
|
45
|
+
* });
|
|
46
|
+
* // res = { resolved, attempts: [{ hyp, outcome:'kept'|'reverted', verdict }] }
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
// Await the next settle, or resolve immediately if the graph is already quiescent
|
|
50
|
+
// (finding #13: `stabilize(cb)` only fires `cb` on an actual settle — a no-op has none).
|
|
51
|
+
function nextStable( graph ) {
|
|
52
|
+
return new Promise(function ( resolve ) {
|
|
53
|
+
var done = false;
|
|
54
|
+
var fin = function () { if ( !done ) { done = true; resolve(); } };
|
|
55
|
+
graph.stabilize(fin);
|
|
56
|
+
if ( !graph._unstable.length && !graph._triggeredCastCount ) setTimeout(fin);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function supervise( graph, spec ) {
|
|
61
|
+
var detectStuck = spec.detectStuck;
|
|
62
|
+
var hypothesize = spec.hypothesize;
|
|
63
|
+
var evaluate = spec.evaluate;
|
|
64
|
+
var maxAttempts = spec.maxAttempts || 5;
|
|
65
|
+
var attempts = [];
|
|
66
|
+
|
|
67
|
+
await nextStable(graph);
|
|
68
|
+
|
|
69
|
+
for ( var i = 0; i < maxAttempts; i++ ) {
|
|
70
|
+
var stuck = detectStuck(graph);
|
|
71
|
+
if ( !stuck ) return { resolved: true, attempts: attempts };
|
|
72
|
+
|
|
73
|
+
// checkpoint BEFORE the hypothesis: the snapshot captured at the last settle, keyed
|
|
74
|
+
// by this rev, carries both the facts AND the concept-lib (N6) — so a rollback here
|
|
75
|
+
// fully undoes whatever the hypothesis adds/patches.
|
|
76
|
+
var rev = graph.getCurrentRevision();
|
|
77
|
+
|
|
78
|
+
var hyp = await hypothesize(graph, stuck, { attempt: i, attempts: attempts });
|
|
79
|
+
await nextStable(graph); // apply the hypothesis (its self-mod re-stabilizes)
|
|
80
|
+
|
|
81
|
+
var verdict = await evaluate(graph, stuck, hyp);
|
|
82
|
+
if ( verdict && verdict.better ) {
|
|
83
|
+
attempts.push({ hyp: hyp, outcome: 'kept', verdict: verdict });
|
|
84
|
+
} else {
|
|
85
|
+
graph.rollbackTo(rev); // safe at this quiescent boundary; N6 restores rules too
|
|
86
|
+
await nextStable(graph);
|
|
87
|
+
attempts.push({ hyp: hyp, outcome: 'reverted', verdict: verdict });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { resolved: !detectStuck(graph), attempts: attempts };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The supervisor as a REACTIVE concept flow (vs the host-orchestrated `supervise` loop).
|
|
96
|
+
* Enabled by queued rollback (#11.c.4): a concept's provider can now trigger a rollback
|
|
97
|
+
* mid-stabilize and it defers safely. The flow:
|
|
98
|
+
* Stuck ──require──▶ Supervise (hypothesizes a self-mod, records a rollback checkpoint)
|
|
99
|
+
* Supervise+hypothesized ──require──▶ Evaluate (the supervisor/better-model verdict)
|
|
100
|
+
* Evaluate, ensure verdict=='worse' ──▶ Revert (queued rollbackTo the checkpoint)
|
|
101
|
+
*
|
|
102
|
+
* Inject the policy (open R&D):
|
|
103
|
+
* opts.hypothesize(graph, scope) — apply a self-mod; the added/patched fix MUST post
|
|
104
|
+
* `hypothesized: true` so Evaluate can fire after it lands
|
|
105
|
+
* (and `resolved: true` + `Stuck: null` if it solves it).
|
|
106
|
+
* opts.evaluate(graph, scope) — return 'better' | 'worse' (the supervisor judgment).
|
|
107
|
+
*
|
|
108
|
+
* NOTE (open R&D): the rollback checkpoint is `graph._lastSettledRev` — the last clean
|
|
109
|
+
* snapshot. A robust multi-attempt reactive loop (a checkpoint that holds the problem but
|
|
110
|
+
* not the attempt, + strategy memory so retries differ) is the next research step; this
|
|
111
|
+
* tree demonstrates the reactive wiring and the queued-rollback Revert path.
|
|
112
|
+
*/
|
|
113
|
+
function reactiveSupervisorTree() {
|
|
114
|
+
return { childConcepts: {
|
|
115
|
+
Supervise: { _id: 'Supervise', _name: 'Supervise', require: ['Stuck'], provider: ['Sup::propose'] },
|
|
116
|
+
Evaluate: { _id: 'Evaluate', _name: 'Evaluate', require: ['Supervise', 'hypothesized'], provider: ['Sup::judge'] },
|
|
117
|
+
Revert: { _id: 'Revert', _name: 'Revert', require: ['Evaluate'], ensure: ["$verdict=='worse'"], provider: ['Sup::revert'] }
|
|
118
|
+
} };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function makeSupervisorProviders( opts ) {
|
|
122
|
+
var hypothesize = opts.hypothesize, evaluate = opts.evaluate;
|
|
123
|
+
return { Sup: {
|
|
124
|
+
propose: function ( graph, concept, scope, argz, cb ) {
|
|
125
|
+
hypothesize(graph, scope);// a self-mod (queued); its fix posts `hypothesized:true`
|
|
126
|
+
cb(null, { $_id: '_parent', Supervise: true, supRev: graph._lastSettledRev });
|
|
127
|
+
},
|
|
128
|
+
judge: function ( graph, concept, scope, argz, cb ) {
|
|
129
|
+
cb(null, { $_id: '_parent', Evaluate: true, verdict: evaluate(graph, scope) });
|
|
130
|
+
},
|
|
131
|
+
revert: function ( graph, concept, scope, argz, cb ) {
|
|
132
|
+
graph.rollbackTo(scope._.supRev);// queued (#11.c.4) -> applied at the quiescent boundary
|
|
133
|
+
cb(null, { $_id: '_parent', Revert: true });
|
|
134
|
+
}
|
|
135
|
+
} };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = { supervise, nextStable, reactiveSupervisorTree, makeSupervisorProviders };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Nathanael Braun
|
|
3
|
+
* @author : Nathanael BRAUN <pp9ping@gmail.com>
|
|
4
|
+
*
|
|
5
|
+
* This program is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
* (at your option) any later version.
|
|
9
|
+
*
|
|
10
|
+
* This program is distributed in the hope that it will be useful,
|
|
11
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
* GNU Affero General Public License for more details.
|
|
14
|
+
*
|
|
15
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
'use strict';
|
|
19
|
+
/**
|
|
20
|
+
* The SUPPORT GRAMMAR (host-side authoring brick) — the support-grammar study made runnable
|
|
21
|
+
* (doc/WIP/studies/2026-06-25-grammaire-de-support-resolution-probleme.md). It composes two
|
|
22
|
+
* already-shipped pieces so a SMALL model only has to be locally competent on a bounded segment
|
|
23
|
+
* while the GRAPH holds the structure and runs the search:
|
|
24
|
+
*
|
|
25
|
+
* (structure, J1) the decompose loop (lib/authoring/loop.js): a Task DAG that splits a problem
|
|
26
|
+
* into segments and synthesizes bottom-up (reactive Rollup) — the whole is NEVER in the
|
|
27
|
+
* model's context, only `${label}` of one segment;
|
|
28
|
+
* (alternatives, J2) the per-segment SELECT cluster (lib/providers/semiring.js#selectConceptTree):
|
|
29
|
+
* an atomic segment GENERATES several candidate answers (`Propose`), the `pareto` semiring
|
|
30
|
+
* keeps the non-dominated trade-offs and picks one (`Select`), and `Adopt` makes the selected
|
|
31
|
+
* candidate the segment's answer. Multi-criteria selection is reified in the graph (not flattened
|
|
32
|
+
* by the model), keyed on DISCRETE bands (barrier-clean, deterministic).
|
|
33
|
+
*
|
|
34
|
+
* const { supportConceptTree, makeSupportProviders } = require('./support');
|
|
35
|
+
* const providers = makeSupportProviders({ evalFn, expandFn, proposeFn, rollupFn });
|
|
36
|
+
* register(Graph, [ providers ]); // or Graph._providers = {...}
|
|
37
|
+
* new Graph(seed, conf, { common: supportConceptTree({ criteria, lex }) });
|
|
38
|
+
*
|
|
39
|
+
* Content (eval/expand/propose/rollup) is INJECTED — deterministic in tests, an LLM in production.
|
|
40
|
+
* Zero core change: everything is concepts + providers over the existing engine.
|
|
41
|
+
*/
|
|
42
|
+
const { makeDecomposeProviders } = require('./loop.js');
|
|
43
|
+
const { createSemiring, selectConceptTree } = require('../providers');
|
|
44
|
+
const { rankOf } = require('../providers/semiring.js');
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The support-grammar concept set: the decompose scaffold + the per-segment Propose→Select→Adopt
|
|
48
|
+
* alternative-selection trio + reactive bottom-up synthesis.
|
|
49
|
+
* @param opts.criteria the SELECT comparison criteria (band lists or {dir}) — see semiring.js
|
|
50
|
+
* @param opts.lex criterion priority for the tie-break (default: criteria key order)
|
|
51
|
+
* @param opts.idKey candidate id key (default 'id')
|
|
52
|
+
*/
|
|
53
|
+
function supportConceptTree( opts ) {
|
|
54
|
+
opts = opts || {};
|
|
55
|
+
var sel = selectConceptTree({
|
|
56
|
+
criteria: opts.criteria || {}, lex: opts.lex, idKey: opts.idKey,
|
|
57
|
+
contribKey: 'candidates', require: ['Task', 'Atomic']
|
|
58
|
+
});
|
|
59
|
+
return { childConcepts: { Task: {
|
|
60
|
+
_id: 'Task', _name: 'Task', require: 'Segment',
|
|
61
|
+
childConcepts: {
|
|
62
|
+
// structure (J1): classify, and split a compound segment into sub-segments
|
|
63
|
+
EvalComplexity: { _id: 'EvalComplexity', _name: 'EvalComplexity', require: ['Task'], provider: ['AI::evalComplexity'] },
|
|
64
|
+
Expand : { _id: 'Expand', _name: 'Expand', require: ['Task', 'NeedsSplit'], provider: ['AI::expand'] },
|
|
65
|
+
// alternatives (J2): an atomic segment proposes candidates, the pareto SELECT picks one
|
|
66
|
+
Propose : { _id: 'Propose', _name: 'Propose', require: ['Task', 'Atomic'], provider: ['Support::propose'] },
|
|
67
|
+
Select : sel.childConcepts.Select,
|
|
68
|
+
Adopt : { _id: 'Adopt', _name: 'Adopt', require: ['Task', 'selectedId'], provider: ['Support::adopt'] },
|
|
69
|
+
// reactive synthesis: report up to the parent, roll up once all children answered
|
|
70
|
+
ReportUp : { _id: 'ReportUp', _name: 'ReportUp', require: ['Task', 'Answered', 'parentSeg'], provider: ['AI::reportUp'] },
|
|
71
|
+
Rollup : { _id: 'Rollup', _name: 'Rollup', require: ['Task', 'expandedInto'],
|
|
72
|
+
ensure: ['$answeredBy.length == $expandedInto.length'], provider: ['AI::rollup'] }
|
|
73
|
+
}
|
|
74
|
+
} } };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The support-grammar providers. Reuses the decompose providers (AI::evalComplexity/expand/reportUp/
|
|
79
|
+
* rollup, lib/authoring/loop.js) + the pareto reducer (Semiring::reduce) and adds the two tiny
|
|
80
|
+
* Support providers (propose/adopt) that bracket the SELECT.
|
|
81
|
+
* @param opts.proposeFn(scope) -> [{ id, ...criteria, content }] candidate alternatives for a segment
|
|
82
|
+
* @param opts.escalateFn(scope) -> [{ id, ...criteria, content }] J3: a BETTER-tier proposer, called
|
|
83
|
+
* ONLY for a segment whose own candidates all fall below the bar (Stuck). The better model is
|
|
84
|
+
* spent only where the small one is locally insufficient.
|
|
85
|
+
* @param opts.escalateBar { criterion, order, min } the quality gate: escalate iff no candidate's
|
|
86
|
+
* `criterion` band reaches `min` (band order worst→best). Omit to disable escalation.
|
|
87
|
+
* (+ evalFn/expandFn/answerFn/rollupFn/maxDepth/maxBranch passed through to makeDecomposeProviders)
|
|
88
|
+
*/
|
|
89
|
+
function makeSupportProviders( opts ) {
|
|
90
|
+
opts = opts || {};
|
|
91
|
+
var proposeFn = opts.proposeFn || function () { return []; },
|
|
92
|
+
escalateFn = opts.escalateFn,
|
|
93
|
+
bar = opts.escalateBar;
|
|
94
|
+
// does the candidate set already clear the quality bar? (a `min` band reached on `criterion`)
|
|
95
|
+
function clearsBar( cands ) {
|
|
96
|
+
if ( !bar ) return true;
|
|
97
|
+
var minRank = rankOf(bar.min, bar.order), best = -Infinity;
|
|
98
|
+
for ( var i = 0; i < cands.length; i++ ) best = Math.max(best, rankOf(cands[i][bar.criterion], bar.order));
|
|
99
|
+
return best >= minRank;
|
|
100
|
+
}
|
|
101
|
+
var support = { Support: {
|
|
102
|
+
// generate the segment's candidate alternatives (canned/LLM); if none clears the bar, ESCALATE
|
|
103
|
+
// to the better tier (J3) — recording the weak ones as memory — then arm the cardinality gate.
|
|
104
|
+
propose: function ( graph, concept, scope, argz, cb ) {
|
|
105
|
+
Promise.resolve(proposeFn(scope)).then(function ( cands ) {
|
|
106
|
+
cands = cands || [];
|
|
107
|
+
if ( escalateFn && bar && cands.length && !clearsBar(cands) ) {
|
|
108
|
+
return Promise.resolve(escalateFn(scope)).then(function ( better ) {
|
|
109
|
+
better = better || [];
|
|
110
|
+
var pool = cands.concat(better);
|
|
111
|
+
cb(null, { $_id: '_parent', Propose: true, Stuck: true, Escalated: true,
|
|
112
|
+
escalatedFrom: cands.map(function ( c ) { return c.id; }), // nogood memory of the dead-ends
|
|
113
|
+
candidates: pool, expected: pool.length });
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
cb(null, { $_id: '_parent', Propose: true, candidates: cands, expected: cands.length });
|
|
117
|
+
}).catch(function ( e ) {
|
|
118
|
+
cb(null, { $_id: '_parent', Propose: true, candidates: [], expected: 0, llmError: e.message });
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
// adopt the SELECTed candidate (by its discrete selectedId) as the segment's answer
|
|
122
|
+
adopt: function ( graph, concept, scope, argz, cb ) {
|
|
123
|
+
var id = scope._.selectedId, cands = scope._.candidates || [], sel = null;
|
|
124
|
+
for ( var i = 0; i < cands.length; i++ ) if ( cands[i].id === id ) { sel = cands[i]; break; }
|
|
125
|
+
cb(null, { $_id: '_parent', Adopt: true, Answered: true, answer: sel ? sel.content : '(none selected)' });
|
|
126
|
+
}
|
|
127
|
+
} };
|
|
128
|
+
return Object.assign({}, makeDecomposeProviders(opts), support, createSemiring(opts.semiringOpts));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = { supportConceptTree: supportConceptTree, makeSupportProviders: makeSupportProviders };
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
* task-mirror — the graph plan DRIVES the host agent's native task list (owner 2026-07-10). The graph instance is
|
|
10
|
+
* the authoritative multistep state (typed steps, needs/produces, frontier, JTMS); agent hosts (Claude Code,
|
|
11
|
+
* OpenCode, …) have their own task tools. MCP is host→server — a server cannot invoke host tools — so the correct
|
|
12
|
+
* realization is a TYPED DELTA the host applies VERBATIM to its own task system, AT ITS OWN CHOICE (SOFT lane,
|
|
13
|
+
* pull-based: the LLM calls the sync when it wants a mirror, or never).
|
|
14
|
+
*
|
|
15
|
+
* `diffPlanToTaskOps(plan, mirror)` → { ops, mirror } — pure, deterministic (id-ordered), idempotent (same state
|
|
16
|
+
* twice → empty delta). Neutral op vocabulary, host-agnostic:
|
|
17
|
+
* {op:'create', id, subject, blockedBy[]} a new step (blockedBy = its still-open needs)
|
|
18
|
+
* {op:'update', id, subject} the step title changed
|
|
19
|
+
* {op:'complete',id} the step reached done (a GATE-ADMITTED result, not a claim)
|
|
20
|
+
* {op:'reopen', id, reason} ⟐ the JTMS differentiator: a completed step whose premise DRIFTED
|
|
21
|
+
* was retracted → the mirrored task must reopen. No host does this.
|
|
22
|
+
* A step that VANISHES from the plan emits nothing (the mirror keeps it; the host decides its own hygiene).
|
|
23
|
+
*
|
|
24
|
+
* @param plan { steps: [{ id, title, needs: [ids], status: 'open'|'done'|'retracted', reason? }] }
|
|
25
|
+
* @param mirror the previous return's `mirror` (or null on first sync).
|
|
26
|
+
* @returns { ops: [...], mirror } — feed `mirror` back on the next call.
|
|
27
|
+
*/
|
|
28
|
+
function diffPlanToTaskOps( plan, mirror ) {
|
|
29
|
+
var steps = (plan && plan.steps || []).slice().sort(function ( a, b ) { return a.id < b.id ? -1 : 1; });
|
|
30
|
+
var prev = (mirror && mirror.tasks) || {};
|
|
31
|
+
var ops = [], tasks = {};
|
|
32
|
+
var open = {}; steps.forEach(function ( s ) { if ( s.status !== 'done' ) open[s.id] = true; });
|
|
33
|
+
steps.forEach(function ( s ) {
|
|
34
|
+
var p = prev[s.id];
|
|
35
|
+
var status = s.status === 'done' ? 'done' : 'open';
|
|
36
|
+
if ( !p ) {
|
|
37
|
+
ops.push({ op: 'create', id: s.id, subject: s.title, blockedBy: (s.needs || []).filter(function ( n ) { return open[n]; }) });
|
|
38
|
+
if ( s.status === 'done' ) ops.push({ op: 'complete', id: s.id });
|
|
39
|
+
} else {
|
|
40
|
+
if ( p.subject !== s.title ) ops.push({ op: 'update', id: s.id, subject: s.title });
|
|
41
|
+
if ( p.status !== 'done' && s.status === 'done' ) ops.push({ op: 'complete', id: s.id });
|
|
42
|
+
if ( p.status === 'done' && s.status !== 'done' ) ops.push({ op: 'reopen', id: s.id, reason: s.reason || 'premise drifted (JTMS retraction)' });
|
|
43
|
+
}
|
|
44
|
+
tasks[s.id] = { subject: s.title, status: status };
|
|
45
|
+
});
|
|
46
|
+
// steps gone from the plan stay in the mirror untouched (no phantom deletes pushed at the host).
|
|
47
|
+
Object.keys(prev).forEach(function ( id ) { if ( !tasks[id] ) tasks[id] = prev[id]; });
|
|
48
|
+
return { ops: ops, mirror: { tasks: tasks } };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { diffPlanToTaskOps: diffPlanToTaskOps };
|