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,343 @@
|
|
|
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
|
+
* Tree-decomposition pass on the concept-dependency graph (host-side authoring tool,
|
|
21
|
+
* zero core change) — the Mixture-of-Reasoners "tiling" brick (doc/WIP/HANDOFF.md §7
|
|
22
|
+
* Tier 1; SOTA subgraph-grammars brick iii; experiment E7).
|
|
23
|
+
*
|
|
24
|
+
* From a concept tree it DERIVES — does not guess — (a) the interface alphabet (the
|
|
25
|
+
* SEPARATOR facts a fork frontier should cross) and (b) the hierarchical tiling (the
|
|
26
|
+
* candidate forks), and reports the TREEWIDTH as the inference cost bound: a low
|
|
27
|
+
* treewidth with thin separators means partitioning pays (cheap merge traffic, the
|
|
28
|
+
* #P-avoidance made operational); a dense corpus with high treewidth and no thin cut
|
|
29
|
+
* means it does not. It feeds fork creation (`Graph.fork`/`merge` with a snapped
|
|
30
|
+
* frontier — see lib/providers/merge-consistency.js for the C1 cross-boundary contract).
|
|
31
|
+
*
|
|
32
|
+
* const { treeDecomposition } = require('./decompose');
|
|
33
|
+
* const { treewidth, separators, tiles, partitionPays } = treeDecomposition(tree);
|
|
34
|
+
*
|
|
35
|
+
* Model (static analysis — no engine runtime):
|
|
36
|
+
* - PRIMAL graph: facts = vertices; each concept's referenced fact-set is a clique
|
|
37
|
+
* (a factor over those facts). `conceptCliques(tree)` extracts the fact-sets.
|
|
38
|
+
* - MIN-FILL elimination -> treewidth (exact treewidth is NP-hard, Bodlaender 1996,
|
|
39
|
+
* so this is the standard min-fill heuristic — an upper bound).
|
|
40
|
+
* - ARTICULATION points (Tarjan) -> candidate separator facts = the interface alphabet.
|
|
41
|
+
* - COMPONENTS after removing the separators -> the tiles (forks).
|
|
42
|
+
*
|
|
43
|
+
* Fact extraction note: this keys a cross-object walk ref (`a:b`) on its WALK BASE
|
|
44
|
+
* (the fact named first), which is the vertex the dependency hangs off in the primal
|
|
45
|
+
* graph — deliberately the same heuristic that recovered the planted structure in E7
|
|
46
|
+
* (synthetic -> {cost,risk}; real `common` -> [Distance,Stay]). validate.js keys the
|
|
47
|
+
* walk TARGET instead (it judges the resolved fact, a different question); the two are
|
|
48
|
+
* intentionally not unified.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
// ---- fact extraction: the cliques (one fact-set per concept) ----
|
|
52
|
+
|
|
53
|
+
const asArray = (v) => (v == null ? [] : Array.isArray(v) ? v : [v]);
|
|
54
|
+
// $ref / $$ref token in an assert/ensure expression (no ':' in the class -> a cross-walk
|
|
55
|
+
// ref is captured at its base, e.g. `$$clock:tick` -> `clock`).
|
|
56
|
+
const EXPR_REF_RE = /\$\$?[A-Za-z_][\w.]*/g;
|
|
57
|
+
|
|
58
|
+
// The fact keys one concept node references — its self-flag, its require LHS facts, the
|
|
59
|
+
// refs inside its assert/ensure, and the keys its applyMutations template writes.
|
|
60
|
+
function conceptFacts( node ) {
|
|
61
|
+
const schema = (node && node._schema) || node || {};
|
|
62
|
+
const f = new Set();
|
|
63
|
+
if ( node && node._name ) f.add(node._name);
|
|
64
|
+
for ( const r of asArray(schema.require) )
|
|
65
|
+
if ( typeof r === 'string' ) f.add(r.split(':').pop()); // walk base of a require LHS
|
|
66
|
+
for ( const e of asArray(schema.assert).concat(asArray(schema.ensure)) ) {
|
|
67
|
+
if ( typeof e !== 'string' ) continue;
|
|
68
|
+
for ( const t of (e.match(EXPR_REF_RE) || []) )
|
|
69
|
+
f.add(t.replace(/^\$+/, '').split('.')[0].split(':').pop()); // strip $, drop .member, walk base
|
|
70
|
+
}
|
|
71
|
+
for ( const mu of asArray(schema.applyMutations) )
|
|
72
|
+
if ( mu && typeof mu === 'object' )
|
|
73
|
+
for ( const k of Object.keys(mu) ) if ( !k.startsWith('$') && k !== '_id' ) f.add(k);
|
|
74
|
+
return [...f];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Walk the concept tree (same node semantics as validate.js#eachConcept: a node is a
|
|
78
|
+
// concept iff it is a child entry or carries a `_name`; the synthetic root container is
|
|
79
|
+
// not) and collect one non-empty clique per concept.
|
|
80
|
+
function conceptCliques( tree ) {
|
|
81
|
+
const cliques = [];
|
|
82
|
+
const walk = ( node, isChild ) => {
|
|
83
|
+
if ( !node || typeof node !== 'object' ) return;
|
|
84
|
+
if ( isChild || node._name ) {
|
|
85
|
+
const c = conceptFacts(node);
|
|
86
|
+
if ( c.length ) cliques.push(c);
|
|
87
|
+
}
|
|
88
|
+
const kids = node.childConcepts;
|
|
89
|
+
if ( kids ) for ( const k of Object.keys(kids) ) walk(kids[k], true);
|
|
90
|
+
};
|
|
91
|
+
walk(tree, false);
|
|
92
|
+
return cliques;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---- graph algorithms ----
|
|
96
|
+
|
|
97
|
+
function primalAdj( cliques ) {
|
|
98
|
+
const adj = new Map();
|
|
99
|
+
const add = (a, b) => { if ( !adj.has(a) ) adj.set(a, new Set()); if ( a !== b ) adj.get(a).add(b); };
|
|
100
|
+
for ( const cl of cliques ) for ( const a of cl ) { add(a, a); for ( const b of cl ) add(a, b); }
|
|
101
|
+
return adj;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Min-fill elimination ordering -> a tree decomposition; bag sizes give the treewidth
|
|
105
|
+
// (max bag - 1). Heuristic: pick the vertex whose elimination adds the fewest fill edges.
|
|
106
|
+
// ALSO returns the bag-intersection SEPARATORS: when vertex v is eliminated, its higher-neighbour set
|
|
107
|
+
// nb(v) is exactly B_v ∩ parent(B_v) in the resulting tree decomposition (Robertson-Seymour 1986) — the
|
|
108
|
+
// REAL Σ_sep family (k-aware minimal separators of the chordal completion, monotone under edge addition,
|
|
109
|
+
// Dirac 1961), NOT the size-1 articulation cuts. `treewidth` is a SCALAR PROJECTION that loses this.
|
|
110
|
+
function minFillTreewidth( adj ) {
|
|
111
|
+
const G = new Map([...adj].map(([k, s]) => [k, new Set(s)]));
|
|
112
|
+
const remaining = new Set(G.keys());
|
|
113
|
+
const bags = [], separators = [];
|
|
114
|
+
while ( remaining.size ) {
|
|
115
|
+
let best = null, bestFill = Infinity;
|
|
116
|
+
for ( const v of remaining ) {
|
|
117
|
+
const nb = [...G.get(v)].filter((x) => remaining.has(x));
|
|
118
|
+
let fill = 0;
|
|
119
|
+
for ( let i = 0; i < nb.length; i++ )
|
|
120
|
+
for ( let j = i + 1; j < nb.length; j++ )
|
|
121
|
+
if ( !G.get(nb[i]).has(nb[j]) ) fill++;
|
|
122
|
+
if ( fill < bestFill ) { bestFill = fill; best = v; }
|
|
123
|
+
}
|
|
124
|
+
const nb = [...G.get(best)].filter((x) => remaining.has(x));
|
|
125
|
+
bags.push(new Set([best, ...nb]));
|
|
126
|
+
if ( nb.length ) separators.push(new Set(nb)); // B_best ∩ parent = the bag-intersection separator
|
|
127
|
+
for ( let i = 0; i < nb.length; i++ ) // make the neighbourhood a clique (fill in)
|
|
128
|
+
for ( let j = i + 1; j < nb.length; j++ ) { G.get(nb[i]).add(nb[j]); G.get(nb[j]).add(nb[i]); }
|
|
129
|
+
remaining.delete(best);
|
|
130
|
+
}
|
|
131
|
+
return { bags, treewidth: Math.max(0, ...bags.map((b) => b.size)) - 1, separators };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// A bag-intersection separator `s` SPLITS the corpus iff removing it leaves ≥2 components. It is MINIMAL
|
|
135
|
+
// iff no single-element-smaller subset also splits (a sufficient, conservative minimality test — Robertson-
|
|
136
|
+
// Seymour minimal separators). The MINIMAL splitting separators are the thin cross-tile interfaces (the
|
|
137
|
+
// articulation points are exactly their size-1 case); their union is the cross-tile separator alphabet Σ_sep.
|
|
138
|
+
function isMinimalSplit( adj, s ) {
|
|
139
|
+
if ( componentsWithout(adj, s).length < 2 ) return false;
|
|
140
|
+
for ( const v of s ) {
|
|
141
|
+
const sub = new Set([...s].filter(( x ) => x !== v));
|
|
142
|
+
if ( sub.size && componentsWithout(adj, sub).length >= 2 ) return false; // a proper subset already splits → not minimal
|
|
143
|
+
}
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The bounded-context HORIZON of a primal graph (E7/Σ_sep) — the OBJECT the scalar treewidth + the size-1
|
|
149
|
+
* articulation `separators` both PROJECT AWAY. Reconstructs the bag-intersection separators from the min-fill
|
|
150
|
+
* elimination and reports:
|
|
151
|
+
* - `sigmaSep` the cross-tile separator alphabet = ∪ of the MINIMAL splitting separators (the thin
|
|
152
|
+
* interface facts; generalises articulation points to size-k cuts);
|
|
153
|
+
* - `minimalInterface` the THINNEST splitting separator `{ size, sep }` — the horizon WIDTH. A grammar refactor
|
|
154
|
+
* that grows it (a size-1 cut → a size-2 cut) regressed the bound even when the scalar
|
|
155
|
+
* treewidth is unchanged AND no new articulation key appeared (the killer case).
|
|
156
|
+
* - `indivisible` true if NO bag-separator splits (one blob); then the "width" is the whole bag (treewidth+1).
|
|
157
|
+
* - `treewidth` kept as a DIAGNOSTIC only (min-fill is an uncoupled upper bound → unsound as a scalar gate).
|
|
158
|
+
*/
|
|
159
|
+
function bagInterface( cliques ) {
|
|
160
|
+
const adj = primalAdj(cliques);
|
|
161
|
+
const { treewidth, separators } = minFillTreewidth(adj);
|
|
162
|
+
const sigma = new Set();
|
|
163
|
+
let min = null;
|
|
164
|
+
for ( const s of separators ) {
|
|
165
|
+
if ( !isMinimalSplit(adj, s) ) continue;
|
|
166
|
+
for ( const f of s ) sigma.add(f);
|
|
167
|
+
if ( !min || s.size < min.size ) min = { size: s.size, sep: [...s].sort() };
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
treewidth,
|
|
171
|
+
bagSeparators: separators.map(( s ) => [...s].sort()),
|
|
172
|
+
sigmaSep: [...sigma].sort(),
|
|
173
|
+
minimalInterface: min || { size: Math.max(1, treewidth + 1), sep: [...adj.keys()].sort() },
|
|
174
|
+
indivisible: !min,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Tarjan articulation points — the cut vertices whose removal disconnects the primal
|
|
179
|
+
// graph = the candidate separator facts (the interface alphabet between tiles).
|
|
180
|
+
function articulationPoints( adj ) {
|
|
181
|
+
const disc = new Map(), low = new Map(), ap = new Set();
|
|
182
|
+
let timer = 0;
|
|
183
|
+
const dfs = (u, parent) => {
|
|
184
|
+
disc.set(u, ++timer); low.set(u, timer);
|
|
185
|
+
let children = 0;
|
|
186
|
+
for ( const v of adj.get(u) ) {
|
|
187
|
+
if ( !disc.has(v) ) {
|
|
188
|
+
children++; dfs(v, u);
|
|
189
|
+
low.set(u, Math.min(low.get(u), low.get(v)));
|
|
190
|
+
if ( parent !== null && low.get(v) >= disc.get(u) ) ap.add(u);
|
|
191
|
+
} else if ( v !== parent ) {
|
|
192
|
+
low.set(u, Math.min(low.get(u), disc.get(v)));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if ( parent === null && children > 1 ) ap.add(u);
|
|
196
|
+
};
|
|
197
|
+
for ( const v of adj.keys() ) if ( !disc.has(v) ) dfs(v, null);
|
|
198
|
+
return ap;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Connected components of the primal graph with the `removed` vertices (separators) cut out.
|
|
202
|
+
function componentsWithout( adj, removed ) {
|
|
203
|
+
const seen = new Set(), comps = [];
|
|
204
|
+
for ( const s of adj.keys() ) {
|
|
205
|
+
if ( seen.has(s) || removed.has(s) ) continue;
|
|
206
|
+
const comp = [], stack = [s]; seen.add(s);
|
|
207
|
+
while ( stack.length ) {
|
|
208
|
+
const u = stack.pop(); comp.push(u);
|
|
209
|
+
for ( const v of adj.get(u) ) if ( !seen.has(v) && !removed.has(v) ) { seen.add(v); stack.push(v); }
|
|
210
|
+
}
|
|
211
|
+
comps.push(comp);
|
|
212
|
+
}
|
|
213
|
+
return comps;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Decompose a primal graph given directly as cliques (fact-name arrays). Use this when
|
|
218
|
+
* you already have the fact-sets; `treeDecomposition` extracts them from a concept tree.
|
|
219
|
+
* @returns { nFacts, nConcepts, treewidth, separators, tiles, nTiles, bags, partitionPays }
|
|
220
|
+
*/
|
|
221
|
+
function decomposeCliques( cliques ) {
|
|
222
|
+
const adj = primalAdj(cliques);
|
|
223
|
+
const { bags, treewidth } = minFillTreewidth(adj);
|
|
224
|
+
const sepSet = articulationPoints(adj);
|
|
225
|
+
const separators = [...sepSet].sort();
|
|
226
|
+
const tiles = componentsWithout(adj, sepSet)
|
|
227
|
+
.filter((c) => c.length)
|
|
228
|
+
.map((c) => c.sort())
|
|
229
|
+
.sort((a, b) => b.length - a.length);
|
|
230
|
+
return {
|
|
231
|
+
nFacts: adj.size,
|
|
232
|
+
nConcepts: cliques.length,
|
|
233
|
+
treewidth,
|
|
234
|
+
separators,
|
|
235
|
+
tiles,
|
|
236
|
+
nTiles: tiles.length,
|
|
237
|
+
bags: bags.map((b) => [...b].sort()),
|
|
238
|
+
// partition pays when there is a thin cut AND it actually splits the corpus
|
|
239
|
+
partitionPays: separators.length > 0 && tiles.length > 1
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Run the tree-decomposition pass over a concept tree.
|
|
245
|
+
* @param tree the nested concept tree (root container or a concept node)
|
|
246
|
+
* @returns the `decomposeCliques` result for the derived cliques.
|
|
247
|
+
*/
|
|
248
|
+
function treeDecomposition( tree ) {
|
|
249
|
+
return decomposeCliques(conceptCliques(tree));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Walk the tree collecting { name, facts:Set } per concept (the named form of conceptCliques).
|
|
253
|
+
function conceptsWithFacts( tree ) {
|
|
254
|
+
const out = [];
|
|
255
|
+
const walk = ( node, isChild ) => {
|
|
256
|
+
if ( !node || typeof node !== 'object' ) return;
|
|
257
|
+
if ( isChild || node._name ) {
|
|
258
|
+
const f = conceptFacts(node);
|
|
259
|
+
if ( f.length && node._name ) out.push({ name: node._name, facts: new Set(f) });
|
|
260
|
+
}
|
|
261
|
+
const kids = node.childConcepts;
|
|
262
|
+
if ( kids ) for ( const k of Object.keys(kids) ) walk(kids[k], true);
|
|
263
|
+
};
|
|
264
|
+
walk(tree, false);
|
|
265
|
+
return out;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Derive a FORK PLAN from the tree-decomposition — the "interface derivation" the SOTA synthesis
|
|
270
|
+
* flagged as the killer deliverable: assign each concept to a tile/fork (by where its non-separator
|
|
271
|
+
* facts fall) and derive each fork's FRONTIER ALPHABET (the separator facts its concepts touch =
|
|
272
|
+
* what crosses that fork boundary). Feeds `Graph.fork`/`merge` AND the `validateMergeProjection`
|
|
273
|
+
* frontier alphabet. Hub concepts (only separator facts) are reported as `interface` glue, not tiled.
|
|
274
|
+
* @returns { treewidth, separators, partitionPays, forks:[{ facts, concepts, frontier }], interface:[names] }
|
|
275
|
+
*/
|
|
276
|
+
function forkPlan( tree ) {
|
|
277
|
+
const concepts = conceptsWithFacts(tree);
|
|
278
|
+
const decomp = decomposeCliques(concepts.map((c) => [...c.facts]));
|
|
279
|
+
const sepSet = new Set(decomp.separators);
|
|
280
|
+
|
|
281
|
+
// fact -> tile index (non-separator facts only; tiles are the post-cut components)
|
|
282
|
+
const tileOf = new Map();
|
|
283
|
+
decomp.tiles.forEach((t, i) => t.forEach((f) => tileOf.set(f, i)));
|
|
284
|
+
|
|
285
|
+
const forks = decomp.tiles.map((tile) => ({ facts: tile, concepts: [], frontier: new Set() }));
|
|
286
|
+
const iface = [];
|
|
287
|
+
for ( const c of concepts ) {
|
|
288
|
+
const votes = {};
|
|
289
|
+
const seps = [];
|
|
290
|
+
for ( const f of c.facts ) {
|
|
291
|
+
if ( sepSet.has(f) ) seps.push(f);
|
|
292
|
+
else if ( tileOf.has(f) ) votes[tileOf.get(f)] = (votes[tileOf.get(f)] || 0) + 1;
|
|
293
|
+
}
|
|
294
|
+
const best = Object.keys(votes).sort((a, b) => votes[b] - votes[a])[0];
|
|
295
|
+
if ( best == null ) { iface.push(c.name); continue; } // only separator facts -> interface glue
|
|
296
|
+
forks[best].concepts.push(c.name);
|
|
297
|
+
seps.forEach((s) => forks[best].frontier.add(s));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
treewidth: decomp.treewidth,
|
|
302
|
+
separators: decomp.separators,
|
|
303
|
+
partitionPays: decomp.partitionPays,
|
|
304
|
+
forks: forks.map((p) => ({ facts: p.facts, concepts: p.concepts.sort(), frontier: [...p.frontier].sort() })),
|
|
305
|
+
interface: iface.sort()
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* §6.3 separatorGate — the SEPARATOR HORIZON check (the §3.3 obligation `params ∪ appConditions ∪ requiredFacts
|
|
311
|
+
* ⊆ Σ_sep`). A proposed ancestry projection (the facts the ancestry oracle would promote to runtime params / fold
|
|
312
|
+
* into the memo digest KEY) is sound iff every projected fact is a CROSS-TILE SEPARATOR fact (on the horizon) —
|
|
313
|
+
* never a fact above the separator (in another tile's interior), which a runtime read would reach past the horizon.
|
|
314
|
+
* • Σ_sep = the bag-intersection minimal splitting separators (NOT the global articulation points: those are the
|
|
315
|
+
* size-1 cut family only, and a global cut-vertex elsewhere is NOT on THIS projection's horizon — `bagInterface`).
|
|
316
|
+
* • a projected fact ∉ Σ_sep is "above-separator" → REFUSED (the spec compiles it to a baked constant / forged hole).
|
|
317
|
+
* Pairs with the memo-key MONOTONICITY (a finer key never false-hits): key-enlargement is safe for memo correctness,
|
|
318
|
+
* but ONLY this gate makes it bound-safe (the digest read at dispatch is itself a `requiredFact`).
|
|
319
|
+
* @returns { ok, above:[facts ∉ Σ_sep], sigmaSep }
|
|
320
|
+
*/
|
|
321
|
+
function separatorGate( tree, projectionFacts ) {
|
|
322
|
+
const iface = bagInterface(conceptCliques(tree));
|
|
323
|
+
const sigma = new Set(iface.sigmaSep);
|
|
324
|
+
const above = (projectionFacts || []).filter(( f ) => !sigma.has(f));
|
|
325
|
+
return { ok: above.length === 0, above, sigmaSep: iface.sigmaSep };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
module.exports = {
|
|
329
|
+
treeDecomposition,
|
|
330
|
+
forkPlan,
|
|
331
|
+
decomposeCliques,
|
|
332
|
+
conceptCliques,
|
|
333
|
+
conceptFacts,
|
|
334
|
+
// §6.3 — the bag-intersection separator horizon (the real Σ_sep) + the gate
|
|
335
|
+
bagInterface,
|
|
336
|
+
separatorGate,
|
|
337
|
+
isMinimalSplit,
|
|
338
|
+
// lower-level graph primitives (exported for reuse / testing)
|
|
339
|
+
primalAdj,
|
|
340
|
+
minFillTreewidth,
|
|
341
|
+
articulationPoints,
|
|
342
|
+
componentsWithout
|
|
343
|
+
};
|
|
@@ -0,0 +1,236 @@
|
|
|
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
|
+
* emittability — the SIGNATURE-STABILITY / paraphrase-consistency profiler (roadmap STAGE-0 Grammar-P1
|
|
10
|
+
* instrument, 2026-07-01). Read-only measurement: given the intake `FactsDigest` a small model produces
|
|
11
|
+
* for many PARAPHRASES of the same latent task, does the typed signature stay STABLE? Everything
|
|
12
|
+
* downstream (dispatch, cache, call-elision, method reuse) keys on that digest; if paraphrases fragment
|
|
13
|
+
* the key, the memo never hits (K1 fragmentation) — so this is the component SCREEN feeding the
|
|
14
|
+
* end-to-end call-count gate (paper-dll arms). It is NOT itself the make-or-break gate (Laurie confront).
|
|
15
|
+
*
|
|
16
|
+
* TWO failure modes, not one (Laurie confront — the load-bearing correction):
|
|
17
|
+
* • FRAGMENTATION (cheap): paraphrases of ONE task scatter into several digests → wasted calls.
|
|
18
|
+
* Measured by within-task agreement (`collisionProb`, the unbiased Simpson estimator).
|
|
19
|
+
* • COLLISION (correctness-fatal): TWO DIFFERENT tasks land on the SAME digest → the memo returns the
|
|
20
|
+
* WRONG method. Measured by cross-task HOMOGENEITY of the pooled partition (V-measure).
|
|
21
|
+
* A scheme that maps EVERYTHING to one tuple scores perfect within-task agreement and is catastrophic;
|
|
22
|
+
* only the pooled two-partition view (reference=task vs induced=digest) catches that. So we report BOTH,
|
|
23
|
+
* with a HIGHER bar on homogeneity than on completeness.
|
|
24
|
+
*
|
|
25
|
+
* Chance correction (the vacuousness alarm): raw agreement can't tell "stable" from "always emits the same
|
|
26
|
+
* enum". Fleiss' κ (1971) subtracts the pooled-marginal chance agreement → κ→0 under mode collapse even as
|
|
27
|
+
* raw agreement→1 (the κ paradox under skew is here a FEATURE). NOTE for the memo, chance agreement is REAL
|
|
28
|
+
* value (a hit is a hit), so `collisionProb` is the OPERATIONAL number and κ is the VALIDITY/discrimination
|
|
29
|
+
* number — reported side by side, never conflated.
|
|
30
|
+
*
|
|
31
|
+
* The "by construction" split (the crux): grammar-constrained decoding guarantees FORMAT VALIDITY + VOCAB
|
|
32
|
+
* CLOSURE (digest drawn from a finite known set → H(D|T) bounded). It does NOT guarantee SEMANTIC
|
|
33
|
+
* consistency (which enum member → H(D|T)≈0, nuisance-invariance in the Information-Bottleneck sense) — and
|
|
34
|
+
* can DISTORT it (Park et al. 2024, Grammar-Aligned Decoding). So to measure the semantic half you must NET
|
|
35
|
+
* OUT format: compare arms only on inputs where BOTH emitted an in-vocab label (`crossArmAgreement`).
|
|
36
|
+
*
|
|
37
|
+
* `untyped` is NOT dropped (survivorship bias) — it is its own digest class ⊥; `typedRate` is reported
|
|
38
|
+
* separately from the digest agreement. Refs: Simpson 1949; Fleiss 1971; Rosenberg & Hirschberg 2007
|
|
39
|
+
* (V-measure); Hubert & Arabie 1985 (ARI); Errica et al. 2025 (sensitivity/consistency of LM labels).
|
|
40
|
+
*
|
|
41
|
+
* Pure + synchronous (unit-tested). No engine coupling — it consumes plain intake-result records.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
var UNTYPED = '⊥'; // ⊥ — the digest class for a non-typed (out-of-vocab / errored) intake.
|
|
45
|
+
|
|
46
|
+
// the digest CLASS of one intake result: its FactsDigest when typed, else ⊥.
|
|
47
|
+
function signatureClass( r ) {
|
|
48
|
+
return (r && r.status === 'typed' && r.digest != null) ? String(r.digest) : UNTYPED;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function countBy( items, keyOf ) {
|
|
52
|
+
var m = new Map();
|
|
53
|
+
items.forEach(function ( x ) { var k = keyOf(x); m.set(k, (m.get(k) || 0) + 1); });
|
|
54
|
+
return m;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function log2( x ) { return Math.log(x) / Math.LN2; }
|
|
58
|
+
|
|
59
|
+
// Shannon entropy (bits) of a count map over N items.
|
|
60
|
+
function entropyOf( counts, N ) {
|
|
61
|
+
if ( !N ) return 0;
|
|
62
|
+
var h = 0;
|
|
63
|
+
counts.forEach(function ( n ) { if ( n > 0 ) { var p = n / N; h -= p * log2(p); } });
|
|
64
|
+
return h;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Within-task statistics for ONE task's paraphrase results.
|
|
69
|
+
* @param results [{status, digest}] the K paraphrase intake results for one latent task.
|
|
70
|
+
* @param classOf (optional) override the class projection (for per-field marginals).
|
|
71
|
+
* @returns { K, typedRate, collisionProb, modalCoverage, effectiveSignatures, numClasses, counts }
|
|
72
|
+
* collisionProb — UNBIASED Simpson estimator Σ n_i(n_i-1)/[K(K-1)] = P(two paraphrases collide on the
|
|
73
|
+
* same digest) = the memo-hit event. null when K<2. PRIMARY.
|
|
74
|
+
* modalCoverage — max n_i / K. SECONDARY (upward-biased at small K — flag, don't lead with it).
|
|
75
|
+
* effectiveSignatures — 1/Σ p_i^2 (plug-in Hill-2): how many memo keys one task splinters into.
|
|
76
|
+
*/
|
|
77
|
+
function perTaskStats( results, classOf ) {
|
|
78
|
+
classOf = classOf || signatureClass;
|
|
79
|
+
var K = results.length;
|
|
80
|
+
var typed = results.filter(function ( r ) { return r && r.status === 'typed'; }).length;
|
|
81
|
+
var counts = countBy(results, classOf);
|
|
82
|
+
var sumNiNi1 = 0, sumP2 = 0, modal = 0;
|
|
83
|
+
counts.forEach(function ( n ) { sumNiNi1 += n * (n - 1); sumP2 += (n / K) * (n / K); if ( n > modal ) modal = n; });
|
|
84
|
+
return {
|
|
85
|
+
K: K,
|
|
86
|
+
typedRate: K ? typed / K : 0,
|
|
87
|
+
collisionProb: K > 1 ? sumNiNi1 / (K * (K - 1)) : null,
|
|
88
|
+
modalCoverage: K ? modal / K : 0,
|
|
89
|
+
effectiveSignatures: sumP2 > 0 ? 1 / sumP2 : 0,
|
|
90
|
+
numClasses: counts.size,
|
|
91
|
+
counts: counts,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Pooled two-partition agreement over ALL tasks (reference partition = taskId, induced = digest class).
|
|
97
|
+
* @param groups [{ taskId, results:[{status,digest}] }]
|
|
98
|
+
* @returns { N, homogeneity, completeness, vMeasure, ari, miBits, hClass, hCluster }
|
|
99
|
+
* homogeneity — 1 - H(task|digest)/H(task): each digest holds ONE task = COLLISION-FREE (the safety
|
|
100
|
+
* direction; bar it HIGH).
|
|
101
|
+
* completeness — 1 - H(digest|task)/H(digest): each task's paraphrases share ONE digest = consistency.
|
|
102
|
+
* vMeasure — harmonic mean (the rate-distortion pair collapsed to one number; report H & C too).
|
|
103
|
+
* ari — Adjusted Rand Index (chance-corrected pooled agreement, Hubert & Arabie 1985).
|
|
104
|
+
*/
|
|
105
|
+
function poolAgreement( groups, classOf ) {
|
|
106
|
+
classOf = classOf || signatureClass;
|
|
107
|
+
var points = [];
|
|
108
|
+
groups.forEach(function ( g ) { g.results.forEach(function ( r ) { points.push({ cls: g.taskId, clu: classOf(r) }); }); });
|
|
109
|
+
var N = points.length;
|
|
110
|
+
if ( !N ) return { N: 0, homogeneity: 1, completeness: 1, vMeasure: 1, ari: 1, miBits: 0, hClass: 0, hCluster: 0 };
|
|
111
|
+
|
|
112
|
+
var classCounts = countBy(points, function ( p ) { return p.cls; });
|
|
113
|
+
var cluCounts = countBy(points, function ( p ) { return p.clu; });
|
|
114
|
+
// joint contingency table keyed "clsclu"
|
|
115
|
+
var joint = countBy(points, function ( p ) { return p.cls + '' + p.clu; });
|
|
116
|
+
|
|
117
|
+
var hClass = entropyOf(classCounts, N), hCluster = entropyOf(cluCounts, N);
|
|
118
|
+
// H(class|cluster) = Σ_k (|k|/N) H(class within k) = H(class,cluster) - H(cluster)
|
|
119
|
+
var hJoint = entropyOf(joint, N);
|
|
120
|
+
var hClassGivenClu = hJoint - hCluster; // ≥0 (numerical clamp below)
|
|
121
|
+
var hCluGivenClass = hJoint - hClass;
|
|
122
|
+
if ( hClassGivenClu < 0 ) hClassGivenClu = 0;
|
|
123
|
+
if ( hCluGivenClass < 0 ) hCluGivenClass = 0;
|
|
124
|
+
|
|
125
|
+
var homogeneity = hClass === 0 ? 1 : 1 - hClassGivenClu / hClass;
|
|
126
|
+
var completeness = hCluster === 0 ? 1 : 1 - hCluGivenClass / hCluster;
|
|
127
|
+
var vMeasure = (homogeneity + completeness) === 0 ? 0 : 2 * homogeneity * completeness / (homogeneity + completeness);
|
|
128
|
+
var miBits = hCluster - hCluGivenClass; // I(class;cluster)
|
|
129
|
+
|
|
130
|
+
// ── Adjusted Rand Index (Hubert & Arabie 1985) ──
|
|
131
|
+
var choose2 = function ( n ) { return n * (n - 1) / 2; };
|
|
132
|
+
var sumJoint = 0; joint.forEach(function ( n ) { sumJoint += choose2(n); });
|
|
133
|
+
var sumA = 0; classCounts.forEach(function ( n ) { sumA += choose2(n); });
|
|
134
|
+
var sumB = 0; cluCounts.forEach(function ( n ) { sumB += choose2(n); });
|
|
135
|
+
var totPairs = choose2(N);
|
|
136
|
+
var expected = totPairs ? (sumA * sumB) / totPairs : 0;
|
|
137
|
+
var maxIndex = (sumA + sumB) / 2;
|
|
138
|
+
var ari = (maxIndex - expected) === 0 ? 1 : (sumJoint - expected) / (maxIndex - expected);
|
|
139
|
+
|
|
140
|
+
return { N: N, homogeneity: homogeneity, completeness: completeness, vMeasure: vMeasure, ari: ari, miBits: miBits, hClass: hClass, hCluster: hCluster };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Fleiss' κ (1971) over tasks: items = tasks, ratings = the K paraphrase digest-classes, categories = the
|
|
145
|
+
* union of digest classes seen. The vacuousness alarm: mode collapse → Pe→1 → κ→0 (or vacuous). Handles
|
|
146
|
+
* per-item K_i (generalized). @returns { kappa, Pbar, Pe, vacuous }
|
|
147
|
+
*/
|
|
148
|
+
function fleissKappa( groups, classOf ) {
|
|
149
|
+
classOf = classOf || signatureClass;
|
|
150
|
+
var perItem = groups.map(function ( g ) {
|
|
151
|
+
var K = g.results.length;
|
|
152
|
+
var counts = countBy(g.results, classOf);
|
|
153
|
+
var sumSq = 0; counts.forEach(function ( n ) { sumSq += n * n; });
|
|
154
|
+
var Pi = K > 1 ? (sumSq - K) / (K * (K - 1)) : null; // = the unbiased Simpson for this item
|
|
155
|
+
return { counts: counts, K: K, Pi: Pi };
|
|
156
|
+
}).filter(function ( it ) { return it.Pi != null; });
|
|
157
|
+
if ( !perItem.length ) return { kappa: null, Pbar: null, Pe: null, vacuous: true };
|
|
158
|
+
|
|
159
|
+
var Pbar = perItem.reduce(function ( s, it ) { return s + it.Pi; }, 0) / perItem.length;
|
|
160
|
+
var totalRatings = perItem.reduce(function ( s, it ) { return s + it.K; }, 0);
|
|
161
|
+
var catTotals = new Map();
|
|
162
|
+
perItem.forEach(function ( it ) { it.counts.forEach(function ( n, cat ) { catTotals.set(cat, (catTotals.get(cat) || 0) + n); }); });
|
|
163
|
+
var Pe = 0; catTotals.forEach(function ( n ) { var p = n / totalRatings; Pe += p * p; });
|
|
164
|
+
var vacuous = (1 - Pe) < 1e-12;
|
|
165
|
+
return { kappa: vacuous ? null : (Pbar - Pe) / (1 - Pe), Pbar: Pbar, Pe: Pe, vacuous: vacuous };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Cross-arm SEMANTIC agreement, format NETTED OUT (the (a)/(b)-isolating comparison — Laurie + SOTA).
|
|
170
|
+
* Given two arms' results ALIGNED by input index, restrict to inputs where BOTH are `typed`
|
|
171
|
+
* (both in-vocab) and report the fraction whose digest AGREES. This is what tells whether the grammar
|
|
172
|
+
* constraint changes the SEMANTIC choice vs a format-guaranteed control — could be ≤0 (Park et al. 2024).
|
|
173
|
+
* @param a,b aligned arrays [{status,digest}] (same inputs, same order).
|
|
174
|
+
* @returns { nBothTyped, agree, agreeFraction, aTypedRate, bTypedRate }
|
|
175
|
+
*/
|
|
176
|
+
function crossArmAgreement( a, b ) {
|
|
177
|
+
var n = Math.min(a.length, b.length), both = 0, agree = 0, at = 0, bt = 0;
|
|
178
|
+
for ( var i = 0; i < n; i++ ) {
|
|
179
|
+
var ta = a[i] && a[i].status === 'typed', tb = b[i] && b[i].status === 'typed';
|
|
180
|
+
if ( ta ) at++; if ( tb ) bt++;
|
|
181
|
+
if ( ta && tb ) { both++; if ( String(a[i].digest) === String(b[i].digest) ) agree++; }
|
|
182
|
+
}
|
|
183
|
+
return { nBothTyped: both, agree: agree, agreeFraction: both ? agree / both : null, aTypedRate: n ? at / n : 0, bTypedRate: n ? bt / n : 0 };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Aggregate profile for one arm over many paraphrase-task groups.
|
|
188
|
+
* @param groups [{ taskId, results:[{status,digest,facts?}] }]
|
|
189
|
+
* @param opts.fields optional [fieldName...] → also report per-field MARGINAL stability (a low marginal
|
|
190
|
+
* localizes the instability + flags a field that should be DEMOTED off the tracked digest).
|
|
191
|
+
* @returns aggregate (per-task array, means with the replication unit = TASK, pooled agreement, κ, marginals)
|
|
192
|
+
*/
|
|
193
|
+
function profile( groups, opts ) {
|
|
194
|
+
opts = opts || {};
|
|
195
|
+
var perTask = groups.map(function ( g ) {
|
|
196
|
+
var s = perTaskStats(g.results);
|
|
197
|
+
return { taskId: g.taskId, K: s.K, typedRate: s.typedRate, collisionProb: s.collisionProb, modalCoverage: s.modalCoverage, effectiveSignatures: s.effectiveSignatures, numClasses: s.numClasses };
|
|
198
|
+
});
|
|
199
|
+
var withCollision = perTask.filter(function ( t ) { return t.collisionProb != null; });
|
|
200
|
+
var mean = function ( arr, f ) { return arr.length ? arr.reduce(function ( s, x ) { return s + f(x); }, 0) / arr.length : null; };
|
|
201
|
+
|
|
202
|
+
var out = {
|
|
203
|
+
nTasks: groups.length, // the replication unit for population claims
|
|
204
|
+
perTask: perTask,
|
|
205
|
+
meanCollisionProb: mean(withCollision, function ( t ) { return t.collisionProb; }),
|
|
206
|
+
meanModalCoverage: mean(perTask, function ( t ) { return t.modalCoverage; }),
|
|
207
|
+
meanTypedRate: mean(perTask, function ( t ) { return t.typedRate; }),
|
|
208
|
+
minCollisionProb: withCollision.length ? Math.min.apply(null, withCollision.map(function ( t ) { return t.collisionProb; })) : null,
|
|
209
|
+
pool: poolAgreement(groups),
|
|
210
|
+
fleiss: fleissKappa(groups),
|
|
211
|
+
};
|
|
212
|
+
if ( opts.fields ) {
|
|
213
|
+
out.marginals = {};
|
|
214
|
+
opts.fields.forEach(function ( field ) {
|
|
215
|
+
var proj = function ( r ) {
|
|
216
|
+
if ( !r || r.status !== 'typed' || !r.facts || r.facts[field] == null ) return UNTYPED;
|
|
217
|
+
return field + '=' + String(r.facts[field]);
|
|
218
|
+
};
|
|
219
|
+
var pt = groups.map(function ( g ) { var s = perTaskStats(g.results, proj); return { taskId: g.taskId, collisionProb: s.collisionProb, modalCoverage: s.modalCoverage }; });
|
|
220
|
+
var wc = pt.filter(function ( t ) { return t.collisionProb != null; });
|
|
221
|
+
out.marginals[field] = { perTask: pt, meanCollisionProb: mean(wc, function ( t ) { return t.collisionProb; }), pool: poolAgreement(groups, proj) };
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = {
|
|
228
|
+
UNTYPED: UNTYPED,
|
|
229
|
+
signatureClass: signatureClass,
|
|
230
|
+
perTaskStats: perTaskStats,
|
|
231
|
+
poolAgreement: poolAgreement,
|
|
232
|
+
fleissKappa: fleissKappa,
|
|
233
|
+
crossArmAgreement: crossArmAgreement,
|
|
234
|
+
profile: profile,
|
|
235
|
+
_entropyOf: entropyOf, // exported for the unit test
|
|
236
|
+
};
|