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,314 @@
|
|
|
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
|
+
* compose-hotspot — the COMPOSITIONAL-recurrence go/no-go DETECTOR (roadmap STAGE-0 gate; the depth-≥2
|
|
10
|
+
* analog of `hotspot.js`). It answers the one shared front kill-gate of the composed roadmap: *does the
|
|
11
|
+
* workload COMPOSE?* — before building the (substantial, still-absent) `compress.js` operator.
|
|
12
|
+
*
|
|
13
|
+
* Why a pre-test (the honest off-ramp, mirroring hotspot): the flat LEAF/whole-task memo ALREADY elides a
|
|
14
|
+
* recurrent typed slice at 0 calls. So minting a persisted depth-≥2 "method-of-methods" buys NOTHING unless a
|
|
15
|
+
* SUB-composite recurs across DISTINCT whole-tasks — i.e. the shared sub-work that whole-task crystallization
|
|
16
|
+
* cannot cover (X=A∘B∘C and Y=A∘B∘D both contain A∘B, but X≠Y so neither's crystallized method serves the other).
|
|
17
|
+
* That is the precise "compress pays" condition; the detector separates it from "already-flat-covered".
|
|
18
|
+
*
|
|
19
|
+
* Signal = RE-PAIR (Larsson & Moffat 2000): repeatedly replace the most-frequent adjacent method-pair with a new
|
|
20
|
+
* symbol → recovers the composition TREE/hierarchy and the MDL saving cheaply. A recurring composite tree
|
|
21
|
+
* linearizes to a recurring subsequence, so a contiguous-pair tally has NO false negatives for the GATE (spurious
|
|
22
|
+
* pairs are filtered by the cross-task + utility gates downstream — the operator-grade SEQUITUR/Stitch/DreamCoder
|
|
23
|
+
* corpus-MDL is `compress.js`'s job, not the gate's).
|
|
24
|
+
*
|
|
25
|
+
* The four-way verdict per candidate composite S (analog of hotspot's four):
|
|
26
|
+
* too-rare — count(S) < minCount (not enough volume)
|
|
27
|
+
* already-flat-covered— frequent BUT distinctTasks(S) < minDistinctTasks (only inside ONE repeated whole-task →
|
|
28
|
+
* whole-task memo already serves it; compression is redundant — THE off-ramp)
|
|
29
|
+
* unstable — frequent ∧ cross-task BUT the composite's result is NOT a function of its typed input
|
|
30
|
+
* (K1-insufficient at the composite level; only checked when result keys are provided)
|
|
31
|
+
* compose-candidate — FREQUENT ∧ CROSS-TASK (≥ minDistinctTasks) ∧ COMPRESSIBLE (savedCalls > 0)
|
|
32
|
+
* Utility is in CALLS (Minton): savedCalls(S) ≈ distinctTasks(S) − 1 forge-calls elided over the whole-task
|
|
33
|
+
* baseline, minus library bloat. `anyComposeCandidate(rows)` is the go/no-go boolean for `compress.js`.
|
|
34
|
+
*
|
|
35
|
+
* HONEST SCOPE: on a DESIGNED workload the planted composite is found by construction — a positive result
|
|
36
|
+
* validates the INSTRUMENT + QUANTIFIES the saving, it does NOT establish that real domains compose. Run the
|
|
37
|
+
* detector on a REAL dispatch trace to answer that (the instrument is the deliverable; cf. hotspot's "no
|
|
38
|
+
* candidate on today's workload"). Pure + synchronous.
|
|
39
|
+
*
|
|
40
|
+
* ─────────────────────────── PROVENANCE layer (confront-reshaped 2026-07-01, Laurie Q2/Q4 + babble/Stitch SOTA) ──
|
|
41
|
+
* The flat tally above keys on CONTIGUOUS method-id adjacency in a per-task EMISSION-order sequence. But the engine's
|
|
42
|
+
* stabilize loop is a fixpoint over `_unstable`, so the emission order of data-flow-INDEPENDENT firings is NOT
|
|
43
|
+
* deterministic: on a multi-subproblem task a real composite A→B→C surfaces INTERLEAVED as [A,D,B,E,C] and the
|
|
44
|
+
* contiguous tally MISSES it (a false NEGATIVE — the EXPENSIVE gate error: it would wrongly FILE compress.js when it
|
|
45
|
+
* would pay). The fix: tally producer→consumer PROVENANCE edges (interleave-robust) and extract the DATA-FLOW CHAINS,
|
|
46
|
+
* then RE-PAIR those clean chains — so a composite is recognised by its data-flow, regardless of interleaving.
|
|
47
|
+
* `provenanceEdges` is the tally, `provenanceChains` the extraction, `trackFromFirings` feeds `composeHotspots`.
|
|
48
|
+
*
|
|
49
|
+
* The poly / iso-free GUARDS (Laurie — dropping any one re-summons a named NP-hardness):
|
|
50
|
+
* G-a candidates are PATHS: an internal chain node has in-degree = out-degree = 1 WITHIN the task DAG. A fork/join
|
|
51
|
+
* (fan-in/out > 1) is a chain BOUNDARY. Fork-join composites are OUT of scope for the poly gate (SUBDUE-beam
|
|
52
|
+
* territory, not poly / not exact) — a DOCUMENTED CEILING, not a silent drop. [drop → frequent-subgraph mining,
|
|
53
|
+
* subgraph-iso NP-complete: Yan-Han gSpan ICDM'02, Cook'71]
|
|
54
|
+
* G-b cross-task grouping is the RE-PAIR canonical-STRING recurrence over the path's concept sequence (a path's
|
|
55
|
+
* topological order is unique) — never a pairwise graph-iso test. [drop → graph canonicalization / GI]
|
|
56
|
+
* G-c edges come from PROVENANCE (a producer WROTE a key the consumer READ, SAME target, earlier rev) via a
|
|
57
|
+
* (target,key) last-writer hash-join — never value-matching (a shared value across many objects = a needless
|
|
58
|
+
* m×n blowup + semantically spurious edges).
|
|
59
|
+
* Q3 SAME-TARGET only (single relativization base). Cross-linked-target provenance (a producer on segment X
|
|
60
|
+
* feeding a child segment Y) is MULTI-BASE (needs a third `⟦@internal⟧` hole kind) and belongs to compress.js.
|
|
61
|
+
* The gate stops at DETECTION (G-d): "does ANY sound cross-task PATH-composite recur?". The antiUnify CERTIFICATION
|
|
62
|
+
* (`mine.js#mineMethods` lifted to the composite) + the MDL non-overlap SELECTION (MWIS/B&B) are compress.js's front
|
|
63
|
+
* half (babble-generator + Stitch-selector, minus the e-graph), NOT the gate — a false GO is cheap (compress.js
|
|
64
|
+
* refuses the unstable composite via its own `signatureDetermined`/leak guards), a false NO-GO is expensive.
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
var SEP = '\u001f';
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A tally you feed one entry per task: the ordered SEQUENCE of method ids it dispatched, its whole-task
|
|
71
|
+
* signature (so cross-task recurrence is distinguishable from a repeated whole-task), and optionally a
|
|
72
|
+
* per-occurrence result key (for the composite-level K1-stability gate).
|
|
73
|
+
*/
|
|
74
|
+
function trackCompositions() {
|
|
75
|
+
var tasks = []; // [{ taskSig, seq:[methodId...], resultBySpan?:{ "i:j": resultKey } }]
|
|
76
|
+
return {
|
|
77
|
+
tasks: tasks,
|
|
78
|
+
/** @param e { taskSig, seq:[id...], resultKeyOf?(i,j)->key } — resultKeyOf spans the composite seq[i..j] */
|
|
79
|
+
observe: function ( e ) {
|
|
80
|
+
tasks.push({ taskSig: e.taskSig != null ? String(e.taskSig) : (e.seq || []).join('>'),
|
|
81
|
+
seq: (e.seq || []).map(String), resultKeyOf: e.resultKeyOf || null });
|
|
82
|
+
},
|
|
83
|
+
reset: function () { tasks.length = 0; },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// count adjacent pairs across the corpus; return the most frequent (count ≥ 2) with its distinct-task set.
|
|
88
|
+
function bestPair( corpus ) {
|
|
89
|
+
var count = new Map(), tasksOf = new Map();
|
|
90
|
+
for ( var t = 0; t < corpus.length; t++ ) {
|
|
91
|
+
var seq = corpus[t].seq, sig = corpus[t].taskSig;
|
|
92
|
+
for ( var i = 0; i < seq.length - 1; i++ ) {
|
|
93
|
+
var p = seq[i] + SEP + seq[i + 1];
|
|
94
|
+
count.set(p, (count.get(p) || 0) + 1);
|
|
95
|
+
var s = tasksOf.get(p); if ( !s ) { s = new Set(); tasksOf.set(p, s); } s.add(sig);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
var best = null, bc = 1;
|
|
99
|
+
count.forEach(function ( c, p ) { if ( c > bc ) { bc = c; best = p; } });
|
|
100
|
+
return best ? { pair: best, count: bc, tasks: tasksOf.get(best) } : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// replace every non-overlapping occurrence of (a,b) with `sym` in each sequence (left-to-right, greedy).
|
|
104
|
+
function replacePair( corpus, a, b, sym ) {
|
|
105
|
+
for ( var t = 0; t < corpus.length; t++ ) {
|
|
106
|
+
var seq = corpus[t].seq, out = [];
|
|
107
|
+
for ( var i = 0; i < seq.length; i++ ) {
|
|
108
|
+
if ( i < seq.length - 1 && seq[i] === a && seq[i + 1] === b ) { out.push(sym); i++; }
|
|
109
|
+
else out.push(seq[i]);
|
|
110
|
+
}
|
|
111
|
+
corpus[t].seq = out;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// expand a composite symbol back to its primitive-method leaf sequence (the flattened tree).
|
|
116
|
+
function expand( sym, rules ) {
|
|
117
|
+
var r = rules[sym];
|
|
118
|
+
if ( !r ) return [sym];
|
|
119
|
+
return expand(r.expansion[0], rules).concat(expand(r.expansion[1], rules));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Detect compositional recurrence. Pure (no engine, no model).
|
|
124
|
+
* @param tracker a trackCompositions() collector (or { tasks })
|
|
125
|
+
* @param opts.minCount volume floor on a composite's occurrences (default 3)
|
|
126
|
+
* @param opts.minDistinctTasks cross-task floor: # DISTINCT whole-tasks a composite must span (default 2)
|
|
127
|
+
* @returns [{ composite:[leafId...], size, count, distinctTasks, savedCalls, mdlSymbols, stable, verdict, reason }]
|
|
128
|
+
* sorted compose-candidates first, then by savedCalls. verdict ∈
|
|
129
|
+
* { 'compose-candidate', 'unstable', 'already-flat-covered', 'too-rare' }.
|
|
130
|
+
*/
|
|
131
|
+
function composeHotspots( tracker, opts ) {
|
|
132
|
+
opts = opts || {};
|
|
133
|
+
var minCount = opts.minCount != null ? opts.minCount : 3;
|
|
134
|
+
var minDist = opts.minDistinctTasks != null ? opts.minDistinctTasks : 2;
|
|
135
|
+
var tasks = (tracker && tracker.tasks) || tracker || [];
|
|
136
|
+
|
|
137
|
+
// working copy (RE-PAIR mutates the sequences) + a parallel copy for the stability span lookup.
|
|
138
|
+
var corpus = tasks.map(function ( t ) { return { taskSig: t.taskSig, seq: t.seq.slice(), resultKeyOf: t.resultKeyOf }; });
|
|
139
|
+
var rules = {}; // sym -> { expansion:[a,b], count, tasks:Set }
|
|
140
|
+
var nextSym = 0;
|
|
141
|
+
|
|
142
|
+
// RE-PAIR: greedily fold the most-frequent adjacent pair into a fresh symbol, recording the rule.
|
|
143
|
+
while ( true ) {
|
|
144
|
+
var bp = bestPair(corpus);
|
|
145
|
+
if ( !bp || bp.count < 2 ) break;
|
|
146
|
+
var parts = bp.pair.split(SEP);
|
|
147
|
+
var sym = '§' + (nextSym++);
|
|
148
|
+
rules[sym] = { expansion: [parts[0], parts[1]], count: bp.count, tasks: bp.tasks };
|
|
149
|
+
replacePair(corpus, parts[0], parts[1], sym);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// stability (optional): a composite is K1-UNSTABLE if, for its occurrences sharing an input, results diverge.
|
|
153
|
+
// v0 proxy via caller-provided resultKeyOf over the composite's leaf span: two occurrences of the SAME leaf
|
|
154
|
+
// sequence whose result keys differ ⇒ the composite result is not a function of the (structural) input.
|
|
155
|
+
function stabilityOf( leafSeq ) {
|
|
156
|
+
var seen = null, saw = false;
|
|
157
|
+
for ( var t = 0; t < tasks.length; t++ ) {
|
|
158
|
+
var seq = tasks[t].seq, rk = tasks[t].resultKeyOf; if ( !rk ) continue;
|
|
159
|
+
for ( var i = 0; i + leafSeq.length <= seq.length; i++ ) {
|
|
160
|
+
var match = true;
|
|
161
|
+
for ( var j = 0; j < leafSeq.length; j++ ) if ( seq[i + j] !== leafSeq[j] ) { match = false; break; }
|
|
162
|
+
if ( !match ) continue;
|
|
163
|
+
var key = rk(i, i + leafSeq.length - 1); if ( key == null ) continue;
|
|
164
|
+
saw = true;
|
|
165
|
+
if ( seen == null ) seen = key; else if ( seen !== key ) return false; // diverged → unstable
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return saw ? true : null; // null = unconfirmed (no result data)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
var rows = Object.keys(rules).map(function ( sym ) {
|
|
172
|
+
var r = rules[sym];
|
|
173
|
+
var leaf = expand(sym, rules);
|
|
174
|
+
var distinctTasks = r.tasks.size;
|
|
175
|
+
var savedCalls = Math.max(0, distinctTasks - 1); // forge-calls elided over the whole-task baseline
|
|
176
|
+
var mdlSymbols = (r.count - 1) * (leaf.length - 1) - 1; // RE-PAIR description-length saving (symbols)
|
|
177
|
+
var stable = stabilityOf(leaf);
|
|
178
|
+
var verdict, reason;
|
|
179
|
+
if ( r.count < minCount ) {
|
|
180
|
+
verdict = 'too-rare'; reason = 'count ' + r.count + ' < minCount ' + minCount;
|
|
181
|
+
} else if ( distinctTasks < minDist ) {
|
|
182
|
+
verdict = 'already-flat-covered';
|
|
183
|
+
reason = 'distinctTasks ' + distinctTasks + ' < ' + minDist + ' — only inside one repeated whole-task; whole-task memo already serves it';
|
|
184
|
+
} else if ( stable === false ) {
|
|
185
|
+
verdict = 'unstable'; reason = 'the composite result is not a function of its typed input (K1-insufficient at the composite level)';
|
|
186
|
+
} else {
|
|
187
|
+
verdict = 'compose-candidate';
|
|
188
|
+
reason = 'frequent ∧ cross-task (' + distinctTasks + ' distinct tasks) ∧ compressible (saves ~' + savedCalls + ' forge-calls)'
|
|
189
|
+
+ (stable == null ? '; stability UNCONFIRMED (no result data)' : '');
|
|
190
|
+
}
|
|
191
|
+
return { composite: leaf, size: leaf.length, count: r.count, distinctTasks: distinctTasks,
|
|
192
|
+
savedCalls: savedCalls, mdlSymbols: mdlSymbols, stable: stable, verdict: verdict, reason: reason };
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
var rank = { 'compose-candidate': 0, 'unstable': 1, 'already-flat-covered': 2, 'too-rare': 3 };
|
|
196
|
+
rows.sort(function ( a, b ) { return (rank[a.verdict] - rank[b.verdict]) || (b.savedCalls - a.savedCalls) || (b.count - a.count); });
|
|
197
|
+
return rows;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ─────────────────────────── PROVENANCE tally + DATA-FLOW chain extraction (the interleave-robust signal) ──────────
|
|
201
|
+
//
|
|
202
|
+
// A FIRING record (one per dispatched method / concept apply):
|
|
203
|
+
// { task, rev, concept, target, reads:[key…], writes:[key…] }
|
|
204
|
+
// `task` = the whole-task signature (so cross-DISTINCT-task recurrence is distinguishable — the off-ramp); `rev` = the
|
|
205
|
+
// engine's monotonic apply order (the total order, from the onConceptApply record); `concept` = the method-id SYMBOL
|
|
206
|
+
// (two firings of one crystallized method share it — the gate keys on the method, not its content, per G-d liberal
|
|
207
|
+
// gate); `reads`/`writes` = the require/premise keys consumed and the produced-fact keys (self-flag + template keys).
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The PROVENANCE tally (G-c): same-target producer→consumer edges via an (target,key) LAST-WRITER hash-join. A
|
|
211
|
+
* consumer C's read of key K binds to the most-recent EARLIER firing that wrote K on the SAME target — provenance,
|
|
212
|
+
* never value-matching. Pure, O(Σ reads). @returns [{ from, to, via, fromConcept, toConcept, task, target }] (from/to
|
|
213
|
+
* are the producer/consumer `rev`s).
|
|
214
|
+
*/
|
|
215
|
+
function provenanceEdges( firings ) {
|
|
216
|
+
var byTask = new Map();
|
|
217
|
+
for ( var i = 0; i < (firings || []).length; i++ ) {
|
|
218
|
+
var f = firings[i]; if ( !f || f.concept == null ) continue;
|
|
219
|
+
if ( !byTask.has(f.task) ) byTask.set(f.task, []);
|
|
220
|
+
byTask.get(f.task).push(f);
|
|
221
|
+
}
|
|
222
|
+
var edges = [];
|
|
223
|
+
byTask.forEach(function ( fs, task ) {
|
|
224
|
+
fs = fs.slice().sort(function ( a, b ) { return (a.rev || 0) - (b.rev || 0); });
|
|
225
|
+
var lastWriter = new Map(); // "target SEP key" -> the most-recent writer firing
|
|
226
|
+
for ( var j = 0; j < fs.length; j++ ) {
|
|
227
|
+
var f = fs[j];
|
|
228
|
+
for ( var r = 0; r < (f.reads || []).length; r++ ) { // resolve reads against PRIOR writers (before recording this firing's writes)
|
|
229
|
+
var k = f.reads[r], w = lastWriter.get(f.target + SEP + k);
|
|
230
|
+
if ( w && w !== f ) edges.push({ from: w.rev, to: f.rev, via: k, fromConcept: w.concept, toConcept: f.concept, task: task, target: f.target });
|
|
231
|
+
}
|
|
232
|
+
for ( var wI = 0; wI < (f.writes || []).length; wI++ ) lastWriter.set(f.target + SEP + f.writes[wI], f);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
return edges;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Extract per-task maximal DATA-FLOW CHAINS (G-a paths) from the provenance edges. An edge from→to is a CHAIN EDGE iff
|
|
240
|
+
* outDeg(from)==1 ∧ inDeg(to)==1 in the task DAG (both endpoints linearly connected); chain edges form vertex-disjoint
|
|
241
|
+
* simple paths (rev-monotone ⇒ acyclic). A fork/join breaks the chain (the fan node is a boundary — G-a ceiling). Each
|
|
242
|
+
* maximal chain of ≥2 firings is a candidate composite. @returns [{ task, seq:[concept…], firings:[rev…], vias:[[key…]…] }].
|
|
243
|
+
*/
|
|
244
|
+
function provenanceChains( firings, opts ) {
|
|
245
|
+
opts = opts || {};
|
|
246
|
+
var edges = provenanceEdges(firings);
|
|
247
|
+
var conceptByRev = new Map(), taskEdges = new Map();
|
|
248
|
+
for ( var i = 0; i < (firings || []).length; i++ ) { var f = firings[i]; if ( f && f.concept != null ) conceptByRev.set(f.task + SEP + f.rev, f.concept); }
|
|
249
|
+
for ( var e = 0; e < edges.length; e++ ) { var ed = edges[e]; if ( !taskEdges.has(ed.task) ) taskEdges.set(ed.task, []); taskEdges.get(ed.task).push(ed); }
|
|
250
|
+
|
|
251
|
+
var chains = [];
|
|
252
|
+
taskEdges.forEach(function ( es, task ) {
|
|
253
|
+
// collapse parallel multi-key edges between the SAME (from,to) into one (a diamond via two keys is still ONE edge).
|
|
254
|
+
var byPair = new Map();
|
|
255
|
+
for ( var i = 0; i < es.length; i++ ) { var ed = es[i], pk = ed.from + '>' + ed.to;
|
|
256
|
+
if ( !byPair.has(pk) ) byPair.set(pk, { from: ed.from, to: ed.to, vias: [] }); byPair.get(pk).vias.push(ed.via); }
|
|
257
|
+
var uedges = Array.from(byPair.values());
|
|
258
|
+
var outDeg = new Map(), inDeg = new Map();
|
|
259
|
+
uedges.forEach(function ( ed ) { outDeg.set(ed.from, (outDeg.get(ed.from) || 0) + 1); inDeg.set(ed.to, (inDeg.get(ed.to) || 0) + 1); });
|
|
260
|
+
var chainSucc = new Map(), isChainTo = new Set();
|
|
261
|
+
uedges.forEach(function ( ed ) { if ( outDeg.get(ed.from) === 1 && inDeg.get(ed.to) === 1 ) { chainSucc.set(ed.from, ed); isChainTo.add(ed.to); } });
|
|
262
|
+
chainSucc.forEach(function ( _e, from ) {
|
|
263
|
+
if ( isChainTo.has(from) ) return; // not a start (mid-chain node)
|
|
264
|
+
var revs = [from], vias = [], cur = from;
|
|
265
|
+
while ( chainSucc.has(cur) ) { var ce = chainSucc.get(cur); revs.push(ce.to); vias.push(ce.vias); cur = ce.to; }
|
|
266
|
+
if ( revs.length < 2 ) return;
|
|
267
|
+
chains.push({ task: task, seq: revs.map(function ( rv ) { return conceptByRev.get(task + SEP + rv); }), firings: revs, vias: vias });
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
return chains;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Feed an enriched FIRING trace through the provenance layer into a `trackCompositions()` collector, so `composeHotspots`
|
|
275
|
+
* runs the four-way verdict over the INTERLEAVE-ROBUST data-flow chains (not the raw emission order). This is the honest
|
|
276
|
+
* structural go/no-go. @param opts.stability if a firing carries an `outKey` (a canonical digest of the composite's
|
|
277
|
+
* observed output), thread a `resultKeyOf` so the `unstable` verdict still fires (the cheap K1 PROXY — NOT the full
|
|
278
|
+
* antiUnify certification, which is compress.js's job).
|
|
279
|
+
*/
|
|
280
|
+
function trackFromFirings( firings, opts ) {
|
|
281
|
+
opts = opts || {};
|
|
282
|
+
var t = trackCompositions();
|
|
283
|
+
var chains = provenanceChains(firings, opts);
|
|
284
|
+
var byRev = new Map();
|
|
285
|
+
for ( var i = 0; i < (firings || []).length; i++ ) { var f = firings[i]; if ( f && f.concept != null ) byRev.set(f.task + SEP + f.rev, f); }
|
|
286
|
+
for ( var c = 0; c < chains.length; c++ ) {
|
|
287
|
+
var ch = chains[c], entry = { taskSig: ch.task, seq: ch.seq };
|
|
288
|
+
if ( opts.stability ) entry.resultKeyOf = (function ( chain ) {
|
|
289
|
+
return function ( _i, j ) { var f = byRev.get(chain.task + SEP + chain.firings[j]); return f && f.outKey != null ? String(f.outKey) : null; };
|
|
290
|
+
})(ch);
|
|
291
|
+
t.observe(entry);
|
|
292
|
+
}
|
|
293
|
+
return t;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// the go/no-go boolean: is there any compositional recurrence worth compressing?
|
|
297
|
+
function anyComposeCandidate( rows ) { return (rows || []).some(function ( r ) { return r.verdict === 'compose-candidate'; }); }
|
|
298
|
+
|
|
299
|
+
// total forge-calls a fully-built compress.js could elide (upper bound) across all candidates.
|
|
300
|
+
function potentialSavedCalls( rows ) {
|
|
301
|
+
return (rows || []).reduce(function ( s, r ) { return s + (r.verdict === 'compose-candidate' ? r.savedCalls : 0); }, 0);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
module.exports = {
|
|
305
|
+
trackCompositions: trackCompositions,
|
|
306
|
+
composeHotspots: composeHotspots,
|
|
307
|
+
anyComposeCandidate: anyComposeCandidate,
|
|
308
|
+
potentialSavedCalls: potentialSavedCalls,
|
|
309
|
+
expand: expand,
|
|
310
|
+
// the PROVENANCE layer (interleave-robust structural go/no-go)
|
|
311
|
+
provenanceEdges: provenanceEdges,
|
|
312
|
+
provenanceChains: provenanceChains,
|
|
313
|
+
trackFromFirings: trackFromFirings,
|
|
314
|
+
};
|
|
@@ -0,0 +1,279 @@
|
|
|
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
|
+
* compress — the DIGRAM-grain AFFINITY MINER (the long-filed compress.js, finally built on evidence).
|
|
10
|
+
*
|
|
11
|
+
* WHY NOW. RUN 8 (e2e-fidelity) bounded whole-structure composition by whole-structure decode invariance:
|
|
12
|
+
* on external/cross-domain prose a task class holds 4-5 decompose shapes (mode 38%) → no whole composite ever
|
|
13
|
+
* forms → `synthesizeByBlend` has nothing to blend. The 2026-07-03 kill-gate (experiments/2026-07-03-digram-
|
|
14
|
+
* invariance) measured the SAME captured shapes at DIGRAM grain — recurring pairs of ADJACENT typed steps —
|
|
15
|
+
* and returned GO: top-digram support 1.0/0.875, coverage 0.98/0.92, net MDL gain 20/12% exactly where the
|
|
16
|
+
* whole-structure read 0. This module mines that grain: the owner's affinity mechanism « AB qualifié · BC
|
|
17
|
+
* adjacent ont une affinité → composer AC qualifié qui devient un slot » — a SEQUITUR/SUBDUE binary production
|
|
18
|
+
* (nonterminal invention), affinity = MDL gain on OBSERVED digrams (linear in the trace, never |lattice|²).
|
|
19
|
+
*
|
|
20
|
+
* WHAT IT MINES (input = decompose shape-trees: `[{k:<stepKind>, c:[…children]}]` per task — the typed-loop
|
|
21
|
+
* trace grain, also the RUN-8 diag-dump grain):
|
|
22
|
+
* - `mineDigrams` recurring adjacent sibling pairs (support = tasks, multiplicity-weighted);
|
|
23
|
+
* - `subExpansionIndex` the DISPATCHABLE form: (parentKind→kind | kind) → unique observed expansion body,
|
|
24
|
+
* under the online K1 rule (conflicting bodies → undetermined, never first-wins) —
|
|
25
|
+
* the ctx grain splits classes the kind grain conflates (the granularity lever at
|
|
26
|
+
* the LEVEL grain instead of the root);
|
|
27
|
+
* - `foldSubpaths` admission under the mdl.js ΔL objective (its step-alphabet face: savedBits −
|
|
28
|
+
* encodeBits − taxBits, same three terms — mdl.js states the concept-schema face);
|
|
29
|
+
* - `toExpandPatch` mint a typed-loop-PARITY expand patch from a mined body (the mount/guard
|
|
30
|
+
* conventions ride the template: stamped EvalComplexity + Atomic|NeedsSplit, the
|
|
31
|
+
* plan-state mid nodes, the `_s`/`_m` id scheme) — a mined subpath mounts EXACTLY
|
|
32
|
+
* like a live expand, so the #33 re-fire guard discipline is preserved;
|
|
33
|
+
* - `mineForest` the method-FOREST skeleton: per class, the observed variant multiset + the digrams
|
|
34
|
+
* shared across ≥2 variants (a concept-method is a LIBRARY of alternative subpaths
|
|
35
|
+
* for one abstract problem — the concept-method design (the lattice preprint); the variants are its body,
|
|
36
|
+
* the shared digrams its common sub-methods).
|
|
37
|
+
*
|
|
38
|
+
* Fail-closed: a node with no typed `k` mines as the literal kind `∅` — it supports nothing typed and a body
|
|
39
|
+
* containing it stays mintable-but-quarantined (the caller sees the ∅). Kinds are NEVER invented.
|
|
40
|
+
*
|
|
41
|
+
* NON-GOAL (measured, not assumed): whether dispatching mined sub-expansions nets out ELIDED calls against
|
|
42
|
+
* the on-demand `synthesizeByBlend`+`indexMethod` baseline is the replay experiment's question
|
|
43
|
+
* (experiments/2026-07-03-digram-miner-elision), per the cont.⁶/⁷ baseline discipline — MDL gain is a
|
|
44
|
+
* compression floor, not an elision.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
const NIL = '∅'; // fail-closed kind for a malformed (k-less) node
|
|
48
|
+
const TOP = '⊤'; // the synthetic parent of a task root
|
|
49
|
+
|
|
50
|
+
const kindOf = ( node ) => (node && typeof node.k === 'string') ? node.k : NIL;
|
|
51
|
+
const childrenOf = ( node ) => (node && Array.isArray(node.c)) ? node.c : [];
|
|
52
|
+
const isAtomic = ( node ) => childrenOf(node).length === 0;
|
|
53
|
+
const digramKey = ( a, b ) => a + '→' + b;
|
|
54
|
+
|
|
55
|
+
/** The canonical BODY of one expansion: the child step-list with atomicity flags — the exact per-level memo
|
|
56
|
+
* canon of the fidelity harness (a dispatch must predict the kinds AND the split-vs-atomic verdicts). */
|
|
57
|
+
const bodyOf = ( children ) => children.map(( ch ) => ({ k: kindOf(ch), a: isAtomic(ch) }));
|
|
58
|
+
const bodyKey = ( body ) => JSON.stringify(body);
|
|
59
|
+
|
|
60
|
+
/** Every horizontal sibling sequence of a tree (as kind arrays): the top-level array + each node's children. */
|
|
61
|
+
function siblingSequences( topArr ) {
|
|
62
|
+
const seqs = [];
|
|
63
|
+
const stack = [topArr];
|
|
64
|
+
while ( stack.length ) {
|
|
65
|
+
const arr = stack.pop();
|
|
66
|
+
seqs.push(arr.map(kindOf));
|
|
67
|
+
for ( const node of arr ) { const kids = childrenOf(node); if ( kids.length ) stack.push(kids); }
|
|
68
|
+
}
|
|
69
|
+
return seqs;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Mine recurring adjacent digrams over a shape corpus.
|
|
74
|
+
* @param entries [{ cls, mult=1, tree }] — tree = the task root's expansion, `[{k,c}]`
|
|
75
|
+
* @returns { digrams:[{a,b,key,support,occurrences}], tasks, pairOccurrences } — support = # tasks
|
|
76
|
+
* (multiplicity-weighted) containing the digram ≥ once; occurrences overlap (raw adjacency count).
|
|
77
|
+
*/
|
|
78
|
+
function mineDigrams( entries, opts ) {
|
|
79
|
+
opts = opts || {};
|
|
80
|
+
const support = new Map(), occurrences = new Map();
|
|
81
|
+
let tasks = 0, pairOccurrences = 0;
|
|
82
|
+
for ( const e of entries ) {
|
|
83
|
+
const mult = e.mult == null ? 1 : e.mult;
|
|
84
|
+
tasks += mult;
|
|
85
|
+
const present = new Set();
|
|
86
|
+
for ( const seq of siblingSequences(e.tree) )
|
|
87
|
+
for ( let i = 0; i + 1 < seq.length; i++ ) {
|
|
88
|
+
const k = digramKey(seq[i], seq[i + 1]);
|
|
89
|
+
present.add(k);
|
|
90
|
+
occurrences.set(k, (occurrences.get(k) || 0) + mult);
|
|
91
|
+
pairOccurrences += mult;
|
|
92
|
+
}
|
|
93
|
+
for ( const k of present ) support.set(k, (support.get(k) || 0) + mult);
|
|
94
|
+
}
|
|
95
|
+
const digrams = [...support.entries()]
|
|
96
|
+
.map(( [key, sup] ) => { const i = key.indexOf('→'); return { a: key.slice(0, i), b: key.slice(i + 1), key, support: sup, occurrences: occurrences.get(key) }; })
|
|
97
|
+
.sort(( x, y ) => (y.support - x.support) || (x.key < y.key ? -1 : 1));
|
|
98
|
+
return { digrams, tasks, pairOccurrences };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The DISPATCHABLE index of observed sub-expansions, at two key grains:
|
|
103
|
+
* kind grain — key = the expanding node's stepKind (≈ the fidelity harness's per-level memo);
|
|
104
|
+
* ctx grain — key = parentKind→stepKind (splits classes the kind grain conflates).
|
|
105
|
+
* Online K1 rule at dispatch: a key maps to a body ONLY if exactly one distinct body was ever observed for it
|
|
106
|
+
* (conflict → undetermined, refuse — never first-wins/majority) AND its support ≥ minSupport.
|
|
107
|
+
*/
|
|
108
|
+
function subExpansionIndex( opts ) {
|
|
109
|
+
opts = opts || {};
|
|
110
|
+
const minSupport = opts.minSupport == null ? 2 : opts.minSupport;
|
|
111
|
+
const byKind = new Map(), byCtx = new Map(); // key → Map(bodyKey → {body, support})
|
|
112
|
+
const bump = ( map, key, body, mult ) => {
|
|
113
|
+
let bodies = map.get(key);
|
|
114
|
+
if ( !bodies ) map.set(key, bodies = new Map());
|
|
115
|
+
const bk = bodyKey(body);
|
|
116
|
+
const cur = bodies.get(bk);
|
|
117
|
+
if ( cur ) cur.support += mult; else bodies.set(bk, { body, support: mult });
|
|
118
|
+
};
|
|
119
|
+
const walk = ( arr, parentKind, mult ) => {
|
|
120
|
+
for ( const node of arr ) {
|
|
121
|
+
const kids = childrenOf(node);
|
|
122
|
+
if ( !kids.length ) continue;
|
|
123
|
+
const body = bodyOf(kids), k = kindOf(node);
|
|
124
|
+
bump(byKind, k, body, mult);
|
|
125
|
+
bump(byCtx, digramKey(parentKind, k), body, mult);
|
|
126
|
+
walk(kids, k, mult);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
return {
|
|
130
|
+
/** Observe ONE production (a single paid expand: parentKind→kind emitted `body`) — the STREAMING grain:
|
|
131
|
+
* a live/replay loop must accrue exactly what was derived, never a whole tree at once (future leak). */
|
|
132
|
+
observeProduction( parentKind, kind, body, mult ) {
|
|
133
|
+
mult = mult == null ? 1 : mult;
|
|
134
|
+
bump(byKind, kind, body, mult);
|
|
135
|
+
bump(byCtx, digramKey(parentKind == null ? TOP : parentKind, kind), body, mult);
|
|
136
|
+
},
|
|
137
|
+
/** Observe one task's shape. site = { rootKind, mult=1 } — the root's own expansion is indexed too
|
|
138
|
+
* (kind = rootKind, ctx = ⊤→rootKind). */
|
|
139
|
+
observe( tree, site ) {
|
|
140
|
+
site = site || {};
|
|
141
|
+
const mult = site.mult == null ? 1 : site.mult;
|
|
142
|
+
const rootKind = site.rootKind || TOP;
|
|
143
|
+
bump(byKind, rootKind, bodyOf(tree), mult);
|
|
144
|
+
bump(byCtx, digramKey(TOP, rootKind), bodyOf(tree), mult);
|
|
145
|
+
walk(tree, rootKind, mult);
|
|
146
|
+
},
|
|
147
|
+
/** parentKind null → kind grain; else ctx grain. → {body, support} | null (undetermined/unseen/under-support). */
|
|
148
|
+
dispatch( parentKind, kind ) {
|
|
149
|
+
const bodies = parentKind == null ? byKind.get(kind) : byCtx.get(digramKey(parentKind, kind));
|
|
150
|
+
if ( !bodies || bodies.size !== 1 ) return null; // unseen, or K1-undetermined (≥2 bodies)
|
|
151
|
+
const only = bodies.values().next().value;
|
|
152
|
+
return only.support >= minSupport ? { body: only.body, support: only.support } : null;
|
|
153
|
+
},
|
|
154
|
+
/** The RUN-8 false-hit discipline: a dispatched body contradicted by ground truth INVALIDATES exactly
|
|
155
|
+
* the used key at the used grain (parentKind null = kind grain) — it goes undetermined, never re-fires. */
|
|
156
|
+
invalidate( parentKind, kind ) {
|
|
157
|
+
const map = parentKind == null ? byKind : byCtx;
|
|
158
|
+
const key = parentKind == null ? kind : digramKey(parentKind, kind);
|
|
159
|
+
const bodies = map.get(key);
|
|
160
|
+
if ( bodies ) bodies.set('⊥', { body: null, support: 0 }); // a 2nd distinct entry ⇒ K1-undetermined
|
|
161
|
+
},
|
|
162
|
+
stats() {
|
|
163
|
+
const summarize = ( map ) => { let determined = 0, undetermined = 0;
|
|
164
|
+
for ( const bodies of map.values() ) (bodies.size === 1 ? determined++ : undetermined++);
|
|
165
|
+
return { keys: map.size, determined, undetermined }; };
|
|
166
|
+
return { kind: summarize(byKind), ctx: summarize(byCtx), minSupport };
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Fold admission under the mdl.js ΔL objective, step-alphabet face (same three terms as mdl.js — which states
|
|
173
|
+
* the concept-schema face; one objective, two alphabets):
|
|
174
|
+
* savedBits = folds·log2(N) each non-overlapping fold names 1 nonterminal instead of 2 symbols
|
|
175
|
+
* encodeBits = 2·log2(N) the nonterminal's body, paid once (the dictionary)
|
|
176
|
+
* taxBits = R·(log2(N+1)−log2(N)) the match-cost tax: +1 alphabet symbol raises EVERY parse
|
|
177
|
+
* ΔL = encodeBits + taxBits − savedBits ; admit iff ΔL < 0 ∧ support ≥ minSupport
|
|
178
|
+
* Folds are counted greedily (most-frequent-by-occurrence first, non-overlapping within a sibling sequence) —
|
|
179
|
+
* the SEQUITUR single pass; dlAfter = dlBefore + Σ_admitted ΔL (a ranking proxy, documented as such).
|
|
180
|
+
*/
|
|
181
|
+
function foldSubpaths( entries, opts ) {
|
|
182
|
+
opts = opts || {};
|
|
183
|
+
const minSupport = opts.minSupport == null ? 2 : opts.minSupport;
|
|
184
|
+
const { digrams } = mineDigrams(entries);
|
|
185
|
+
|
|
186
|
+
// alphabet + corpus size (weighted symbol count)
|
|
187
|
+
const alphabet = new Set();
|
|
188
|
+
let R = 0;
|
|
189
|
+
const work = []; // flat {kinds, mult, consumed} sequence instances for the greedy fold
|
|
190
|
+
for ( const e of entries ) {
|
|
191
|
+
const mult = e.mult == null ? 1 : e.mult;
|
|
192
|
+
for ( const seq of siblingSequences(e.tree) ) {
|
|
193
|
+
for ( const k of seq ) { alphabet.add(k); R += mult; }
|
|
194
|
+
if ( seq.length >= 2 ) work.push({ kinds: seq, mult, consumed: new Array(seq.length).fill(false) });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const N = Math.max(alphabet.size, 2);
|
|
198
|
+
const log2 = ( x ) => Math.log(x) / Math.LN2;
|
|
199
|
+
const dlBefore = R * log2(N);
|
|
200
|
+
const taxBits = R * (log2(N + 1) - log2(N));
|
|
201
|
+
|
|
202
|
+
const ranked = digrams.slice().sort(( x, y ) => (y.occurrences - x.occurrences) || (x.key < y.key ? -1 : 1));
|
|
203
|
+
const subpaths = [];
|
|
204
|
+
let dlAfter = dlBefore;
|
|
205
|
+
for ( const d of ranked ) {
|
|
206
|
+
let folds = 0;
|
|
207
|
+
for ( const w of work ) {
|
|
208
|
+
const K = w.kinds, C = w.consumed;
|
|
209
|
+
for ( let i = 0; i + 1 < K.length; i++ )
|
|
210
|
+
if ( !C[i] && !C[i + 1] && K[i] === d.a && K[i + 1] === d.b ) { C[i] = C[i + 1] = true; folds += w.mult; i++; }
|
|
211
|
+
}
|
|
212
|
+
const saved = folds * log2(N), encode = 2 * log2(N);
|
|
213
|
+
const delta = encode + taxBits - saved;
|
|
214
|
+
const admitted = delta < 0 && d.support >= minSupport;
|
|
215
|
+
if ( admitted ) dlAfter += delta;
|
|
216
|
+
subpaths.push(Object.assign({}, d, { folds, dl: { saved, encode, tax: taxBits, delta }, admitted }));
|
|
217
|
+
}
|
|
218
|
+
return { subpaths, dlBefore, dlAfter, alphabet: [...alphabet].sort() };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Mint the typed-loop-PARITY expand patch for a mined body — byte-for-byte what `makeTypedDecomposeProviders`'
|
|
223
|
+
* expand emits for the same steps at the same site (minus prose, which a mined subpath never carries): the
|
|
224
|
+
* `$_id:'_parent'` header with `Expand:true + expandedInto`, per-step Segment children carrying the STAMPED
|
|
225
|
+
* `EvalComplexity` + `Atomic|NeedsSplit` guards (#33 — a mounted subpath must never re-fire the providers),
|
|
226
|
+
* the typed `state:'plan-<kind>'` mid nodes, and the `_s`/`_m` id scheme `mountTemplate` grounds.
|
|
227
|
+
* @param steps [{kind, atomic=true}] @param site {baseId, origin, target, depth=1}
|
|
228
|
+
*/
|
|
229
|
+
function toExpandPatch( steps, site ) {
|
|
230
|
+
const baseId = site.baseId, depth = site.depth == null ? 1 : site.depth;
|
|
231
|
+
const childIds = steps.map(( _, i ) => baseId + '_s' + i);
|
|
232
|
+
const tpl = [{ $_id: '_parent', Expand: true, expandedInto: childIds }];
|
|
233
|
+
let prev = site.origin;
|
|
234
|
+
steps.forEach(( st, i ) => {
|
|
235
|
+
const last = i === steps.length - 1;
|
|
236
|
+
const tnode = last ? site.target : baseId + '_m' + i;
|
|
237
|
+
const child = { _id: childIds[i], Segment: true, originNode: prev, targetNode: tnode,
|
|
238
|
+
depth, parentSeg: baseId, stepIndex: i, EvalComplexity: true };
|
|
239
|
+
if ( st.atomic !== false ) child.Atomic = true; else child.NeedsSplit = true;
|
|
240
|
+
child.stepKind = st.kind;
|
|
241
|
+
if ( !last ) tpl.push({ _id: tnode, Node: true, state: 'plan-' + st.kind });
|
|
242
|
+
tpl.push(child);
|
|
243
|
+
prev = tnode;
|
|
244
|
+
});
|
|
245
|
+
return tpl;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The method-FOREST skeleton per class: the observed variant multiset (the alternative subpaths that ARE the
|
|
250
|
+
* concept-method's body — the concept-method design (the lattice preprint)) + the digrams shared across ≥2 distinct variants (the
|
|
251
|
+
* common sub-methods the miner would fold).
|
|
252
|
+
* @returns { classes: { <cls>: { variants:[{tree,mult,canon}], shared:[digramKey] } } }
|
|
253
|
+
*/
|
|
254
|
+
function mineForest( entries ) {
|
|
255
|
+
const classes = {};
|
|
256
|
+
for ( const e of entries ) {
|
|
257
|
+
const mult = e.mult == null ? 1 : e.mult;
|
|
258
|
+
const cls = classes[e.cls] = classes[e.cls] || { variants: [], _byCanon: new Map() };
|
|
259
|
+
const canon = JSON.stringify(e.tree);
|
|
260
|
+
const cur = cls._byCanon.get(canon);
|
|
261
|
+
if ( cur ) cur.mult += mult;
|
|
262
|
+
else { const v = { tree: e.tree, mult, canon }; cls._byCanon.set(canon, v); cls.variants.push(v); }
|
|
263
|
+
}
|
|
264
|
+
for ( const cls of Object.values(classes) ) {
|
|
265
|
+
const inVariants = new Map(); // digramKey → # distinct variants containing it
|
|
266
|
+
for ( const v of cls.variants ) {
|
|
267
|
+
const present = new Set();
|
|
268
|
+
for ( const seq of siblingSequences(v.tree) )
|
|
269
|
+
for ( let i = 0; i + 1 < seq.length; i++ ) present.add(digramKey(seq[i], seq[i + 1]));
|
|
270
|
+
for ( const k of present ) inVariants.set(k, (inVariants.get(k) || 0) + 1);
|
|
271
|
+
}
|
|
272
|
+
cls.shared = [...inVariants.entries()].filter(( [, n] ) => n >= 2).map(( [k] ) => k).sort();
|
|
273
|
+
delete cls._byCanon;
|
|
274
|
+
}
|
|
275
|
+
return { classes };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports = { mineDigrams, subExpansionIndex, foldSubpaths, toExpandPatch, mineForest,
|
|
279
|
+
NIL, TOP, bodyOf, siblingSequences };
|