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,476 @@
|
|
|
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
|
+
* contract — C-contract: the DEFEASIBLE SEPARATION TRIPLE checker (design doc §2 / §9.1 / §11.6, the "central
|
|
10
|
+
* hole"). A learned METHOD carries a typed contract `{ read, write, pre, post, effect }` (the shape `lintMethod`
|
|
11
|
+
* already lints for the static frame); this module adds the two things that make COMPOSITION-WITHOUT-OPENING-THE-
|
|
12
|
+
* BOX sound:
|
|
13
|
+
*
|
|
14
|
+
* 1. COMPOSE-TIME ⊨ (`checkCompose`) — for every shared fact f ∈ write(M1) ∩ read(M2), is post(M1) ⊨ pre(M2)
|
|
15
|
+
* over f? Decided by PER-KEY ABSTRACT-DOMAIN entailment (interval + finite-domain, [Cousot-Cousot 1977];
|
|
16
|
+
* symbolic-heap-style precise footprints, [Berdine-Calcagno-O'Hearn 2004]) — NOT atom-by-atom (which misses
|
|
17
|
+
* `x≥5 ∧ x≤5 ⊨ x==5` and the integer gap `x>3 ∧ x<5 ⊨ x==4`). SOUND + DECIDABLE on the monadic, ground
|
|
18
|
+
* fragment; everything outside it (disjunction → QF-coNP; two-key/relational → difference logic; non-ground/
|
|
19
|
+
* ref-walked footprints → aliasing) is REFUSED → 'escalate' (a micro-LLM open-box step, the §0.1 cost
|
|
20
|
+
* gradient). The checker NEVER false-accepts: in doubt it escalates.
|
|
21
|
+
*
|
|
22
|
+
* 2. RUNTIME post-assert + BLAME (`assertPost`) — the post is an INDUCED hypothesis (sound on observed cases,
|
|
23
|
+
* maybe wrong on the next), so it is ASSERTED at settle (contracts-with-blame, [Findler-Felleisen 2002;
|
|
24
|
+
* Wadler-Findler 2009]; gradual verification, [Bader-Aldrich-Tanter 2018]) ⇒ EVENTUAL, not static,
|
|
25
|
+
* soundness. The adversary's three real holes are closed here, not by the ⊨ check:
|
|
26
|
+
* · G1 FRAME-COMPLETENESS — the keys the body actually TOUCHED ⊆ the declared write (an under-declared
|
|
27
|
+
* frame is the silent unsoundness; the only check that closes it is the runtime touched-vs-declared diff);
|
|
28
|
+
* · G2 EFFECT-TAG DISCIPLINE — an `external`/`irreversible` method's post may NOT be discharged by an
|
|
29
|
+
* internal recorded fact; it must be confirmed by a ground-truth ORACLE (a clean-but-incomplete post on
|
|
30
|
+
* an irreversible effect is the most dangerous failure — it caches as fully-typed, elides the oracle,
|
|
31
|
+
* commits real damage, and is BLESSED sound);
|
|
32
|
+
* · G3 FOOTPRINT-CYCLE rejection (`footprintCycles`) — two retractable methods that are each other's
|
|
33
|
+
* premise through coupled facts OSCILLATE to the apply-cap (`divergent`); reject the cycle at compose-time.
|
|
34
|
+
*
|
|
35
|
+
* Blame on a violated INDUCED post = contract REVISION (specialize the pre with the counterexample's discriminating
|
|
36
|
+
* atom — CEGIS/ICE, [Solar-Lezama 2006; Garg et al. 2014]), not method removal (which oscillates). `reviseOnBlame`.
|
|
37
|
+
*
|
|
38
|
+
* HONEST CLAIM (the only one the literature supports): eventual soundness via a runtime monitor over a SOUND-but-
|
|
39
|
+
* INCOMPLETE compose gate; the typed-coverage fraction is MEASURED (`acceptRate`), not proven — deciding whether an
|
|
40
|
+
* arbitrary learned method's true contract lands in ANY finite typed alphabet is undecidable (Rice). So the runtime
|
|
41
|
+
* assert is LOAD-BEARING; "compose without opening the box" is never an unconditional claim.
|
|
42
|
+
*
|
|
43
|
+
* ZERO-CORE: pure host logic over the typed contract; reuses `frameKeys` (method.js) + `expr.js` for evaluation.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
const { compileExpression } = require('../graph/expr.js');
|
|
47
|
+
|
|
48
|
+
// ───────────────────────────────────────────────────────── atom parsing → per-key abstract constraints ────────
|
|
49
|
+
|
|
50
|
+
const REL_OPS = ['==', '!=', '<=', '>=', '<', '>'];
|
|
51
|
+
|
|
52
|
+
// Parse ONE atom string into { key, op, value } or { refuse:<reason> }. An atom keys ONE fact on ONE constant
|
|
53
|
+
// (monadic, ground). `a && b` is split by the caller; `||` (disjunction), a two-key comparison (relational), and
|
|
54
|
+
// a function call leave the decidable fragment → refuse.
|
|
55
|
+
function parseAtom( s ) {
|
|
56
|
+
const t = String(s).trim();
|
|
57
|
+
if ( !t ) return null;
|
|
58
|
+
if ( t.indexOf('||') >= 0 ) return { refuse: 'disjunction' };
|
|
59
|
+
if ( /[a-zA-Z_]\w*\s*\(/.test(t) ) return { refuse: 'function-call' };
|
|
60
|
+
// `key in [..]`
|
|
61
|
+
let m = t.match(/^\$?\$?([A-Za-z_][\w.:]*)\s+in\s+\[(.*)\]$/);
|
|
62
|
+
if ( m ) {
|
|
63
|
+
const vals = m[2].split(',').map(( v ) => v.trim()).filter(( v ) => v.length).map(parseValue);
|
|
64
|
+
// a bare id INSIDE an in-list is an unambiguous enum literal (no two-key ambiguity) → accept its value.
|
|
65
|
+
if ( vals.some(( v ) => v.refuse && v.refuse !== 'bare-ref' ) ) return { refuse: 'in-list-value' };
|
|
66
|
+
return { key: m[1], op: 'in', value: vals.map(( v ) => v.value) };
|
|
67
|
+
}
|
|
68
|
+
for ( const op of REL_OPS ) { // order matters: <=/>= before </>
|
|
69
|
+
const i = t.indexOf(op);
|
|
70
|
+
if ( i < 0 ) continue;
|
|
71
|
+
const lhs = t.slice(0, i).trim().replace(/^\$\$?/, ''), rhs = t.slice(i + op.length).trim();
|
|
72
|
+
if ( !/^[A-Za-z_][\w.:]*$/.test(lhs) ) return { refuse: 'non-key-lhs' };
|
|
73
|
+
const v = parseValue(rhs);
|
|
74
|
+
if ( v.refuse === 'bare-ref' ) return { refuse: 'relational' }; // rhs is another key (two-key compare)
|
|
75
|
+
if ( v.refuse ) return { refuse: v.refuse };
|
|
76
|
+
return { key: lhs, op, value: v.value, int: v.int }; // carry int so integer-gap reasoning survives
|
|
77
|
+
}
|
|
78
|
+
return { refuse: 'unparseable' };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Parse an atom RHS literal. A QUOTED string / number / bool / bracketed-list is a constant; a BARE identifier is
|
|
82
|
+
// treated as an enum/id constant (the K1 vocabulary is symbolic) UNLESS it would alias a fact key in a comparison
|
|
83
|
+
// — that ambiguity is handled by the caller (a bare id on the rhs of a comparison = relational → refuse).
|
|
84
|
+
function parseValue( s ) {
|
|
85
|
+
const t = String(s).trim();
|
|
86
|
+
if ( /^'.*'$/.test(t) || /^".*"$/.test(t) ) return { value: t.slice(1, -1), num: false };
|
|
87
|
+
if ( t === 'true' ) return { value: true, num: false };
|
|
88
|
+
if ( t === 'false' ) return { value: false, num: false };
|
|
89
|
+
if ( /^-?\d+$/.test(t) ) return { value: parseInt(t, 10), num: true, int: true };
|
|
90
|
+
if ( /^-?\d*\.\d+$/.test(t) ) return { value: parseFloat(t), num: true, int: false };
|
|
91
|
+
if ( /^\$\$?[A-Za-z_][\w.:]*$/.test(t) ) return { refuse: 'bare-ref', value: t.replace(/^\$\$?/, '') }; // a $ref rhs = two-key compare
|
|
92
|
+
if ( /^[A-Za-z_][\w-]*$/.test(t) ) return { refuse: 'bare-ref', value: t }; // ambiguous: a key or an enum literal
|
|
93
|
+
return { refuse: 'unparseable-value' };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Fold a list of atom STRINGS (a pre/post; `&&` inside an entry is split) into per-key constraints + a refusal
|
|
97
|
+
// list. A constraint is { kind:'num', lo, loInc, hi, hiInc, int, ne:Set } or { kind:'cat', allow:Set|null, ne:Set }.
|
|
98
|
+
function normalize( entries ) {
|
|
99
|
+
const byKey = {}, refuse = [];
|
|
100
|
+
const atoms = [];
|
|
101
|
+
for ( const e of (entries || []) ) {
|
|
102
|
+
if ( typeof e !== 'string' ) { refuse.push({ reason: 'non-string-atom' }); continue; }
|
|
103
|
+
for ( const part of e.split('&&') ) { const a = parseAtom(part); if ( a ) atoms.push(a); }
|
|
104
|
+
}
|
|
105
|
+
for ( const a of atoms ) {
|
|
106
|
+
if ( a.refuse ) { refuse.push({ reason: a.refuse }); continue; }
|
|
107
|
+
const numeric = (a.op !== 'in' && a.op !== '==' && a.op !== '!=') || (typeof a.value === 'number');
|
|
108
|
+
let c = byKey[a.key];
|
|
109
|
+
if ( !c ) c = byKey[a.key] = numeric ? { kind: 'num', lo: -Infinity, loInc: true, hi: Infinity, hiInc: true, int: true, ne: new Set() }
|
|
110
|
+
: { kind: 'cat', allow: null, ne: new Set() };
|
|
111
|
+
// a key seen as both numeric and categorical → out of the monadic fragment for that key
|
|
112
|
+
if ( numeric && c.kind !== 'num' ) { refuse.push({ reason: 'mixed-domain', key: a.key }); continue; }
|
|
113
|
+
if ( !numeric && c.kind !== 'cat' ) { refuse.push({ reason: 'mixed-domain', key: a.key }); continue; }
|
|
114
|
+
applyAtom(c, a);
|
|
115
|
+
}
|
|
116
|
+
return { byKey, refuse };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function applyAtom( c, a ) {
|
|
120
|
+
if ( c.kind === 'num' ) {
|
|
121
|
+
if ( typeof a.value !== 'number' && a.op !== 'in' ) { c._broken = 'non-numeric-on-num-key'; return; }
|
|
122
|
+
if ( a.int === false ) c.int = false; // a rational literal demotes the key off integer reasoning
|
|
123
|
+
switch ( a.op ) {
|
|
124
|
+
case '==': c.lo = c.hi = a.value; c.loInc = c.hiInc = true; break;
|
|
125
|
+
case '!=': c.ne.add(a.value); break;
|
|
126
|
+
case '>': if ( a.value > c.lo || (a.value === c.lo && !c.loInc) ) { c.lo = a.value; c.loInc = false; } break;
|
|
127
|
+
case '>=': if ( a.value > c.lo ) { c.lo = a.value; c.loInc = true; } break;
|
|
128
|
+
case '<': if ( a.value < c.hi || (a.value === c.hi && !c.hiInc) ) { c.hi = a.value; c.hiInc = false; } break;
|
|
129
|
+
case '<=': if ( a.value < c.hi ) { c.hi = a.value; c.hiInc = true; } break;
|
|
130
|
+
case 'in': c.kind = 'cat'; c.allow = new Set(a.value); break; // an `in` over numbers → an allow-set
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
switch ( a.op ) {
|
|
134
|
+
case '==': c.allow = c.allow ? intersect(c.allow, [a.value]) : new Set([a.value]); break;
|
|
135
|
+
case '!=': c.ne.add(a.value); break;
|
|
136
|
+
case 'in': c.allow = c.allow ? intersect(c.allow, a.value) : new Set(a.value); break;
|
|
137
|
+
default: c._broken = 'order-op-on-cat-key';
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function intersect( set, vals ) { const out = new Set(); for ( const v of vals ) if ( set.has(v) ) out.add(v); return out; }
|
|
143
|
+
|
|
144
|
+
// integer-normalise open bounds: x>3 ⇒ x>=4 ; x<5 ⇒ x<=4 (so [>3,<5] over ints == {4}).
|
|
145
|
+
function intNorm( c ) {
|
|
146
|
+
if ( c.kind !== 'num' || !c.int ) return c;
|
|
147
|
+
const d = { lo: c.lo, loInc: c.loInc, hi: c.hi, hiInc: c.hiInc, int: true, ne: c.ne, kind: 'num' };
|
|
148
|
+
if ( d.lo > -Infinity && !d.loInc ) { d.lo = d.lo + 1; d.loInc = true; }
|
|
149
|
+
if ( d.hi < Infinity && !d.hiInc ) { d.hi = d.hi - 1; d.hiInc = true; }
|
|
150
|
+
return d;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ───────────────────────────────────────────────────────── per-key entailment: post ⊆ pre ────────────────────
|
|
154
|
+
|
|
155
|
+
// Does the POST constraint entail (⊆) the PRE constraint for one key? 'yes' (sound) | 'no' (a counterexample
|
|
156
|
+
// exists — post admits a value pre forbids) | 'unknown' (can't decide in-fragment → escalate). NEVER false-'yes'.
|
|
157
|
+
function entailsKey( postC, preC ) {
|
|
158
|
+
if ( !preC ) return 'yes'; // pre constrains nothing on this key → trivially ⊨
|
|
159
|
+
if ( !postC ) return 'unknown'; // the UNDER-DETERMINED gap (post leaves it free) → escalate
|
|
160
|
+
if ( postC._broken || preC._broken ) return 'unknown';
|
|
161
|
+
if ( postC.kind !== preC.kind ) return 'unknown'; // different domains → can't compare in-fragment
|
|
162
|
+
if ( postC.kind === 'num' ) {
|
|
163
|
+
const p = intNorm(postC), q = intNorm(preC);
|
|
164
|
+
if ( p.lo > p.hi || (p.lo === p.hi && !(p.loInc && p.hiInc)) ) return 'yes'; // post is EMPTY ⇒ vacuously ⊆
|
|
165
|
+
const loOk = p.lo > q.lo || (p.lo === q.lo && (q.loInc || !p.loInc));
|
|
166
|
+
const hiOk = p.hi < q.hi || (p.hi === q.hi && (q.hiInc || !p.hiInc));
|
|
167
|
+
if ( !loOk || !hiOk ) return 'no'; // post's interval pokes outside pre's
|
|
168
|
+
for ( const n of q.ne ) if ( admitsNum(p, n) ) return 'no'; // pre excludes n but post admits it
|
|
169
|
+
return 'yes';
|
|
170
|
+
}
|
|
171
|
+
// categorical
|
|
172
|
+
if ( postC.allow ) { // post admits a FINITE set → check each is pre-admitted
|
|
173
|
+
for ( const v of postC.allow ) if ( !catAdmits(preC, v) ) return 'no';
|
|
174
|
+
return 'yes';
|
|
175
|
+
}
|
|
176
|
+
// post is allow=any minus ne (an INFINITE admit set) → ⊆ pre only if pre is also any-minus-ne with ne ⊆ post.ne
|
|
177
|
+
if ( preC.allow ) return 'no'; // post infinite, pre finite → post ⊄ pre
|
|
178
|
+
for ( const n of preC.ne ) if ( !postC.ne.has(n) ) return 'no'; // pre excludes n that post admits
|
|
179
|
+
return 'yes';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function admitsNum( c, n ) {
|
|
183
|
+
if ( c.ne.has(n) ) return false;
|
|
184
|
+
const aboveLo = n > c.lo || (n === c.lo && c.loInc);
|
|
185
|
+
const belowHi = n < c.hi || (n === c.hi && c.hiInc);
|
|
186
|
+
return aboveLo && belowHi;
|
|
187
|
+
}
|
|
188
|
+
function catAdmits( c, v ) { if ( c.ne.has(v) ) return false; return c.allow ? c.allow.has(v) : true; }
|
|
189
|
+
|
|
190
|
+
// ───────────────────────────────────────────────────────── the COMPOSE-TIME gate ─────────────────────────────
|
|
191
|
+
|
|
192
|
+
const EFFECTING = new Set(['external', 'irreversible']);
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* checkCompose(m1, m2, opts) — is the composition M1→M2 sound on the typed contracts alone (box CLOSED)?
|
|
196
|
+
* @param m1,m2 { name?, contract:{ read, write, pre, post, effect } }
|
|
197
|
+
* @param opts { oracle?: (m1, key) => bool } — a ground-truth probe for an effecting post (G2)
|
|
198
|
+
* @returns { verdict:'sound'|'unsound'|'escalate', shared:[keys], perKey:{key:'yes'|'no'|'unknown'}, reasons:[],
|
|
199
|
+
* needsOracle:bool }
|
|
200
|
+
*/
|
|
201
|
+
function checkCompose( m1, m2, opts ) {
|
|
202
|
+
opts = opts || {};
|
|
203
|
+
const c1 = (m1 && m1.contract) || {}, c2 = (m2 && m2.contract) || {};
|
|
204
|
+
const write1 = new Set(c1.write || []), read2 = new Set(c2.read || []);
|
|
205
|
+
const shared = [...write1].filter(( k ) => read2.has(k));
|
|
206
|
+
const reasons = [], perKey = {};
|
|
207
|
+
if ( !c1.post && !c1.write ) { reasons.push('m1 uncontracted'); return { verdict: 'escalate', shared, perKey, reasons, needsOracle: false }; }
|
|
208
|
+
|
|
209
|
+
const post1 = normalize(c1.post), pre2 = normalize(c2.pre);
|
|
210
|
+
for ( const r of post1.refuse ) reasons.push('post(m1) out-of-fragment: ' + r.reason);
|
|
211
|
+
for ( const r of pre2.refuse ) reasons.push('pre(m2) out-of-fragment: ' + r.reason);
|
|
212
|
+
const fragmentClean = post1.refuse.length === 0 && pre2.refuse.length === 0;
|
|
213
|
+
|
|
214
|
+
let anyNo = false, anyUnknown = !fragmentClean;
|
|
215
|
+
for ( const k of shared ) {
|
|
216
|
+
const v = entailsKey(post1.byKey[k], pre2.byKey[k]);
|
|
217
|
+
perKey[k] = v;
|
|
218
|
+
if ( v === 'no' ) anyNo = true; else if ( v === 'unknown' ) anyUnknown = true;
|
|
219
|
+
}
|
|
220
|
+
// a pre(m2) key that M1 WRITES but is not constrained by post(m1) = the under-determined gap (already 'unknown'
|
|
221
|
+
// above via entailsKey(undefined, preC)); a pre(m2) key M1 does NOT write is framed-through (M2's own concern).
|
|
222
|
+
|
|
223
|
+
// G2 — effect-tag discipline: an effecting M1's post is about a real-world effect the internal fact can't vouch
|
|
224
|
+
// for; it must be confirmed by a ground-truth oracle, else escalate (never silently bless).
|
|
225
|
+
let needsOracle = false;
|
|
226
|
+
if ( EFFECTING.has(c1.effect) && shared.length ) {
|
|
227
|
+
needsOracle = true;
|
|
228
|
+
const oracleOk = opts.oracle && shared.every(( k ) => opts.oracle(m1, k));
|
|
229
|
+
if ( !oracleOk ) { reasons.push('effecting M1 (' + c1.effect + ') post needs a ground-truth oracle (G2)'); anyUnknown = true; }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let verdict;
|
|
233
|
+
if ( anyNo ) verdict = 'unsound';
|
|
234
|
+
else if ( anyUnknown ) verdict = 'escalate';
|
|
235
|
+
else verdict = 'sound';
|
|
236
|
+
return { verdict, shared, perKey, reasons, needsOracle };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ───────────────────────────────────────────────────────── G3 — footprint-cycle rejection ────────────────────
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* footprintCycles(methods) — a directed edge M_a → M_b iff write(M_a) ∩ read(M_b) ≠ ∅. A cycle of RETRACTABLE
|
|
243
|
+
* (non-`pure`) methods = mutual premises through coupled facts → JTMS oscillation to the apply-cap. Returns the
|
|
244
|
+
* cycles (arrays of method names) so the supervisor rejects / priority-orders them (Tarjan SCC, size > 1, or a self-loop).
|
|
245
|
+
*/
|
|
246
|
+
function footprintCycles( methods ) {
|
|
247
|
+
const ms = methods || [], idx = {}, retract = {};
|
|
248
|
+
ms.forEach(( m, i ) => { idx[m.name == null ? i : m.name] = m; retract[m.name == null ? i : m.name] = (m.contract || {}).effect !== 'pure'; });
|
|
249
|
+
const adj = {};
|
|
250
|
+
for ( const a of ms ) {
|
|
251
|
+
const an = a.name, wa = new Set((a.contract || {}).write || []);
|
|
252
|
+
adj[an] = [];
|
|
253
|
+
for ( const b of ms ) {
|
|
254
|
+
const rb = (b.contract || {}).read || [];
|
|
255
|
+
if ( rb.some(( k ) => wa.has(k) ) ) adj[an].push(b.name);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// Tarjan SCC
|
|
259
|
+
let id = 0; const ids = {}, low = {}, onStack = {}, stack = [], sccs = [];
|
|
260
|
+
function dfs( at ) {
|
|
261
|
+
ids[at] = low[at] = id++; stack.push(at); onStack[at] = true;
|
|
262
|
+
for ( const to of adj[at] || [] ) {
|
|
263
|
+
if ( ids[to] === undefined ) { dfs(to); low[at] = Math.min(low[at], low[to]); }
|
|
264
|
+
else if ( onStack[to] ) low[at] = Math.min(low[at], ids[to]);
|
|
265
|
+
}
|
|
266
|
+
if ( low[at] === ids[at] ) {
|
|
267
|
+
const comp = []; let w;
|
|
268
|
+
do { w = stack.pop(); onStack[w] = false; comp.push(w); } while ( w !== at );
|
|
269
|
+
sccs.push(comp);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
for ( const m of ms ) if ( ids[m.name] === undefined ) dfs(m.name);
|
|
273
|
+
const cycles = [];
|
|
274
|
+
for ( const comp of sccs ) {
|
|
275
|
+
const selfLoop = comp.length === 1 && (adj[comp[0]] || []).indexOf(comp[0]) >= 0;
|
|
276
|
+
if ( (comp.length > 1 || selfLoop) && comp.some(( n ) => retract[n]) ) cycles.push(comp);
|
|
277
|
+
}
|
|
278
|
+
return cycles;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ───────────────────────────────────────────────────────── the RUNTIME post-assert + blame ───────────────────
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* assertPost(contract, factsAfter, touchedKeys, opts) — the runtime monitor (settle-time). Closes the holes the
|
|
285
|
+
* compose-time ⊨ structurally cannot: G1 frame-completeness + G2 effect-tag oracle.
|
|
286
|
+
* @param contract { write, post, effect }
|
|
287
|
+
* @param factsAfter the realized facts (a flat object) the body produced
|
|
288
|
+
* @param touchedKeys the keys the body ACTUALLY wrote (from the engine's sequenced mutation) — for G1
|
|
289
|
+
* @param opts { oracle?: (contract, factsAfter) => bool }
|
|
290
|
+
* @returns { ok, violations:[{kind,detail}], blame:{by,kind,...}|null }
|
|
291
|
+
*/
|
|
292
|
+
function assertPost( contract, factsAfter, touchedKeys, opts ) {
|
|
293
|
+
opts = opts || {};
|
|
294
|
+
const violations = [];
|
|
295
|
+
const write = new Set(contract.write || []);
|
|
296
|
+
|
|
297
|
+
// G1 — FRAME COMPLETENESS: every key the body touched must be declared (an under-declared write is the silent
|
|
298
|
+
// frame hole the ⊨ check ranges right past — the only check that closes it is this touched-vs-declared diff).
|
|
299
|
+
for ( const k of (touchedKeys || []) ) if ( !write.has(k) )
|
|
300
|
+
violations.push({ kind: 'undeclared-write', detail: k });
|
|
301
|
+
|
|
302
|
+
// the post must actually HOLD on the realized facts (the induced hypothesis, asserted). Evaluated from the
|
|
303
|
+
// PARSED atoms (the same monadic fragment as the ⊨ check — no `$`-syntax dependence); an out-of-fragment post
|
|
304
|
+
// atom falls back to the engine's `expr.js` (the `$ref` convention).
|
|
305
|
+
for ( const atom of (contract.post || []) ) {
|
|
306
|
+
if ( typeof atom !== 'string' ) continue;
|
|
307
|
+
if ( !holdsAtoms(factsAfter, atom) ) violations.push({ kind: 'post-violated', detail: atom });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// G2 — EFFECTING post must be confirmed by a ground-truth oracle, not the internal fact (the most dangerous
|
|
311
|
+
// hole: a clean post HOLDS on what we recorded while the world disagrees).
|
|
312
|
+
if ( EFFECTING.has(contract.effect) ) {
|
|
313
|
+
if ( !opts.oracle ) violations.push({ kind: 'effecting-post-unverified', detail: contract.effect });
|
|
314
|
+
else if ( !opts.oracle(contract, factsAfter) ) violations.push({ kind: 'oracle-disagrees', detail: contract.effect });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const ok = violations.length === 0;
|
|
318
|
+
return { ok, violations, blame: ok ? null : { by: 'post', kind: violations[0].kind, violations } };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function resolve( facts, ref ) {
|
|
322
|
+
const r = String(ref).replace(/^\$\$?/, '');
|
|
323
|
+
if ( r.indexOf('.') < 0 ) return facts == null ? undefined : facts[r];
|
|
324
|
+
return r.split('.').reduce(( o, k ) => (o == null ? undefined : o[k]), facts);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// does a post atom-string (a `&&`-conjunction) HOLD on the realized facts? Simple monadic atoms eval directly;
|
|
328
|
+
// an out-of-fragment atom (`||`/relational/function) falls back to the engine's safe `expr.js`. The fallback must
|
|
329
|
+
// resolve BOTH `$ref` tokens (via `resolve`, 1st arg) AND BARE fact keys (via `names`, 2nd arg = `facts`) — the
|
|
330
|
+
// contract DSL writes bare keys (`decision=="approve"`, `a < b`), which expr.js resolves through `names`, not the
|
|
331
|
+
// `$ref` path. (BUG FIX: previously `names` was omitted → bare keys in a refused atom resolved to `undefined` →
|
|
332
|
+
// the post silently mis-evaluated, e.g. a `||` implication post passed when it should have failed.)
|
|
333
|
+
function holdsAtoms( facts, atomStr ) {
|
|
334
|
+
for ( const part of String(atomStr).split('&&') ) {
|
|
335
|
+
const a = parseAtom(part);
|
|
336
|
+
if ( !a ) continue; // an EMPTY conjunct (trailing `&&`) is vacuously true
|
|
337
|
+
if ( a.refuse ) { const fn = compileExpression(part, { empty: false }); if ( !fn(( ref ) => resolve(facts, ref), facts) ) return false; continue; }
|
|
338
|
+
if ( !evalAtom(facts, a) ) return false;
|
|
339
|
+
}
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function evalAtom( facts, a ) {
|
|
344
|
+
const v = resolve(facts, a.key);
|
|
345
|
+
switch ( a.op ) {
|
|
346
|
+
case '==': return v === a.value;
|
|
347
|
+
case '!=': return v !== a.value;
|
|
348
|
+
case '<': return v < a.value;
|
|
349
|
+
case '<=': return v <= a.value;
|
|
350
|
+
case '>': return v > a.value;
|
|
351
|
+
case '>=': return v >= a.value;
|
|
352
|
+
case 'in': return (a.value || []).indexOf(v) >= 0;
|
|
353
|
+
}
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* reviseOnBlame(contract, counterexample) — a violated INDUCED post means the hypothesis OVER-GENERALIZED; specialize
|
|
359
|
+
* the PRECONDITION with the counterexample's discriminating atom (CEGIS/ICE) so the method no longer claims the case
|
|
360
|
+
* it failed — NOT remove the method (which oscillates). Returns a NEW contract (versioned by the caller / B8).
|
|
361
|
+
* @param contract the lying contract
|
|
362
|
+
* @param counterexample { key, value } a discrete fact that distinguishes the failing case
|
|
363
|
+
*/
|
|
364
|
+
function reviseOnBlame( contract, counterexample ) {
|
|
365
|
+
const ce = counterexample || {};
|
|
366
|
+
const pre = (contract.pre || []).slice();
|
|
367
|
+
if ( ce.key != null && ce.value !== undefined ) {
|
|
368
|
+
const atom = '$' + ce.key + "!=" + (typeof ce.value === 'string' ? "'" + ce.value + "'" : ce.value);
|
|
369
|
+
if ( pre.indexOf(atom) < 0 ) pre.push(atom); // exclude the failing case from applicability
|
|
370
|
+
}
|
|
371
|
+
const read = (contract.read || []).slice();
|
|
372
|
+
if ( ce.key != null && read.indexOf(ce.key) < 0 ) read.push(ce.key);
|
|
373
|
+
return Object.assign({}, contract, { pre, read });
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Does a case satisfy a pre/atom list (is the method APPLICABLE to it)? The selection-side dual of assertPost —
|
|
377
|
+
// the supervisor uses it to admit/exclude a method per case (e.g. after `reviseOnBlame` specialized the pre).
|
|
378
|
+
function satisfies( atoms, facts ) {
|
|
379
|
+
for ( const a of (atoms || []) ) if ( typeof a === 'string' && !holdsAtoms(facts, a) ) return false;
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ───────────────────────────────────────────────────────── §6.4 — the WIDEN (S-boundary climb) ────────────────
|
|
384
|
+
|
|
385
|
+
const fmtVal = ( v ) => typeof v === 'string' ? "'" + v + "'" : String(v);
|
|
386
|
+
const unq = ( s ) => { const t = String(s).trim(); if ( /^'.*'$/.test(t) || /^".*"$/.test(t) ) return t.slice(1, -1);
|
|
387
|
+
if ( /^-?\d+$/.test(t) ) return parseInt(t, 10); if ( /^-?\d*\.\d+$/.test(t) ) return parseFloat(t); return t; };
|
|
388
|
+
// is `atom` the discriminator's POSITIVE-constraint — the ONE atom widen relaxes? Recognises `$disc==v`, `$disc in [..]`,
|
|
389
|
+
// AND a parenthesized membership disjunction `($disc==a || $disc==b)` (so a re-widen folds the prior widen). A `!=` is NOT.
|
|
390
|
+
function isDiscPositive( atom, disc ) {
|
|
391
|
+
const s = String(atom).trim();
|
|
392
|
+
if ( new RegExp('^\\$?\\$?' + disc + '\\s*==').test(s) ) return true;
|
|
393
|
+
if ( new RegExp('^\\$?\\$?' + disc + '\\s+in\\s*\\[').test(s) ) return true;
|
|
394
|
+
return /^\(/.test(s) && new RegExp('\\$?\\$?' + disc + '\\s*==').test(s) && !new RegExp('\\$?\\$?' + disc + '\\s*!=').test(s);
|
|
395
|
+
}
|
|
396
|
+
// the values a discriminator's positive-constraint atoms currently admit (`==v` → [v]; `in [a,b]` → [a,b]; the
|
|
397
|
+
// `(==a || ==b)` membership disjunction → [a,b]). Only reads disc-POSITIVE atoms (a `!=` nogood is never pulled in).
|
|
398
|
+
function priorAllowed( pre, disc ) {
|
|
399
|
+
const out = [];
|
|
400
|
+
for ( const a of (pre || []) ) {
|
|
401
|
+
const s = String(a);
|
|
402
|
+
if ( !isDiscPositive(s, disc) ) continue;
|
|
403
|
+
const ml = s.match(new RegExp('^\\$?\\$?' + disc + '\\s+in\\s*\\[(.*)\\]\\s*$'));
|
|
404
|
+
if ( ml ) { ml[1].split(',').map(( v ) => v.trim()).filter(( v ) => v.length).forEach(( v ) => out.push(unq(v))); continue; }
|
|
405
|
+
const re = new RegExp('\\$?\\$?' + disc + '\\s*==\\s*([^\\s|)]+)', 'g'); // every `disc == v` (covers == and the || group)
|
|
406
|
+
let mm; while ( (mm = re.exec(s)) ) out.push(unq(mm[1]));
|
|
407
|
+
}
|
|
408
|
+
return out;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* widenOnVerified(contract, positives, opts) — the S-BOUNDARY CLIMB, the symmetric dual of `reviseOnBlame`
|
|
413
|
+
* (the G-descent). On ≥k VERIFIED POSITIVES, GENERALIZE the discriminator's pre to the LGG (Plotkin 1970) of the
|
|
414
|
+
* OBSERVED values (candidate elimination, Mitchell 1982). Refined by the 2026-06-30 confront — all ZERO-CORE:
|
|
415
|
+
* • ADDITIVE (pt2, the FREE clamp): relax ONLY the discriminator's positive-constraint (`==`/`in`) atom; PRESERVE
|
|
416
|
+
* every surviving `!=` blame nogood + every other atom. `region in [EU,JP] ∧ region!='EU'` mechanically admits
|
|
417
|
+
* only JP — "never cross G" is the existing `normalize` `allow∩ne` intersection, not a new check.
|
|
418
|
+
* • OBSERVED UNION (pt1): the new allow-set is the UNION of observed values (enum or numeric), NOT a hull — a hull
|
|
419
|
+
* admits the unobserved gap, sound only under `assertPost`, which FROZEN elides. `opts.numeric:'hull'` is opt-in
|
|
420
|
+
* and sets `forceInstance` (the caller must pin INSTANCE — never FROZEN).
|
|
421
|
+
* • METHOD-IDENTITY (pt5): a positive must carry a `methodId` GOVERNING the widened value — `∈ target ∪ siblings`
|
|
422
|
+
* (the sibling-merge / guarded-speculative-admit signals). A cross-method positive (a §6.2 `M_new` success fed for
|
|
423
|
+
* `M_donor`) is REJECTED (a positive must be an instance of the TARGET concept) — no borrowing across a contract.
|
|
424
|
+
* @param positives [{ value, methodId }] the verified-positive observations on the discriminator.
|
|
425
|
+
* @param opts { discriminator, target?, siblings?:[], numeric?:'union'|'hull' }
|
|
426
|
+
* @returns { contract, ensure, widenAtom, accepted:[values], rejected:[positives], clamped:[values], forceInstance }
|
|
427
|
+
*/
|
|
428
|
+
function widenOnVerified( contract, positives, opts ) {
|
|
429
|
+
opts = opts || {};
|
|
430
|
+
const disc = opts.discriminator;
|
|
431
|
+
const govern = new Set([opts.target].concat(opts.siblings || []).filter(( x ) => x != null));
|
|
432
|
+
const pre = (contract.pre || []).slice();
|
|
433
|
+
|
|
434
|
+
// pt5 — accept only positives whose methodId GOVERNS the widen target (target ∪ declared siblings).
|
|
435
|
+
const accepted = [], rejected = [];
|
|
436
|
+
for ( const p of (positives || []) ) {
|
|
437
|
+
if ( opts.target != null && p && p.methodId != null && !govern.has(p.methodId) ) { rejected.push(p); continue; }
|
|
438
|
+
if ( p && p.value !== undefined ) accepted.push(p.value);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const kept = pre.filter(( a ) => !isDiscPositive(a, disc)); // pt2 — preserve `!=` nogoods + every other atom
|
|
442
|
+
const union = []; const seen = new Set();
|
|
443
|
+
for ( const v of priorAllowed(pre, disc).concat(accepted) ) { const k = fmtVal(v); if ( !seen.has(k) ) { seen.add(k); union.push(v); } }
|
|
444
|
+
|
|
445
|
+
// the membership is a PARENTHESIZED DISJUNCTION of `==` — NOT an `in`-list: the engine gate (`expr.js`, jsep) does
|
|
446
|
+
// NOT support `in [..]` (verified — it raises "Unclosed ["), but `||`/`==` compile, and `holdsAtoms` evaluates the
|
|
447
|
+
// disjunction via its `expr.js` fallback. (The contract `pre` thus leaves the monadic fragment → `checkCompose`
|
|
448
|
+
// ESCALATEs a widened method — the honest eventual-soundness line: assume-at-compose gives way to assert-at-runtime.)
|
|
449
|
+
let widenAtom, forceInstance = false;
|
|
450
|
+
if ( opts.numeric === 'hull' && union.length && union.every(( v ) => typeof v === 'number' ) ) {
|
|
451
|
+
const lo = Math.min.apply(null, union), hi = Math.max.apply(null, union);
|
|
452
|
+
widenAtom = '$' + disc + '>=' + lo + ' && $' + disc + '<=' + hi;
|
|
453
|
+
forceInstance = true; // pt1 — a hull admits the gap → assertPost must guard every case → never FROZEN
|
|
454
|
+
} else {
|
|
455
|
+
const sorted = union.slice().sort(( a, b ) => typeof a === 'number' && typeof b === 'number' ? a - b : String(a).localeCompare(String(b)));
|
|
456
|
+
widenAtom = '(' + sorted.map(( v ) => '$' + disc + '==' + fmtVal(v)).join(' || ') + ')';
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const newPre = kept.concat([widenAtom]);
|
|
460
|
+
const read = (contract.read || []).slice();
|
|
461
|
+
if ( disc != null && read.indexOf(disc) < 0 ) read.push(disc);
|
|
462
|
+
// pt2 — report any accepted value still EXCLUDED by a surviving `!=` nogood (G wins; the clamp is `normalize`'s `ne`).
|
|
463
|
+
const clamped = accepted.filter(( v ) => kept.some(( a ) => String(a).replace(/\s/g, '') === ('$' + disc + '!=' + fmtVal(v)).replace(/\s/g, '')));
|
|
464
|
+
return { contract: Object.assign({}, contract, { pre: newPre, read }), ensure: newPre, widenAtom, accepted, rejected, clamped, forceInstance };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// the typed-coverage fraction of a workload (the §11 #5/#6 currency — MEASURED, never proven).
|
|
468
|
+
function acceptRate( results ) {
|
|
469
|
+
const xs = results || []; if ( !xs.length ) return { sound: 0, escalate: 0, unsound: 0, n: 0, rate: 0 };
|
|
470
|
+
const tally = { sound: 0, escalate: 0, unsound: 0 };
|
|
471
|
+
for ( const v of xs ) tally[v] = (tally[v] || 0) + 1;
|
|
472
|
+
return Object.assign(tally, { n: xs.length, rate: tally.sound / xs.length });
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
module.exports = { parseAtom, parseValue, normalize, entailsKey, checkCompose, footprintCycles, assertPost,
|
|
476
|
+
reviseOnBlame, widenOnVerified, satisfies, acceptRate, holdsAtoms };
|
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
* Corpus exchange (host-side, fs-free) — a portable `.sgc` bundle for moving a concept corpus
|
|
21
|
+
* (a grammar) between hosts/instances, with a DERIVED manifest so the receiver knows what the
|
|
22
|
+
* corpus needs and crosses.
|
|
23
|
+
*
|
|
24
|
+
* The worker boundary already proves a JSON concept map is a viable wire form (lib/runtime ships
|
|
25
|
+
* exactly that). What was missing for real portability is the METADATA — what providers the
|
|
26
|
+
* corpus calls, what facts it produces vs. consumes (its narrow-waist alphabet = the assume-
|
|
27
|
+
* guarantee contract), and what other sets it links to. `deriveManifest` computes all of that
|
|
28
|
+
* from the tree (via grammar-graph.js), so nothing has to be hand-maintained.
|
|
29
|
+
*
|
|
30
|
+
* const { packCorpus, unpackCorpus } = require('./corpus-pack');
|
|
31
|
+
* const bundle = packCorpus({ common: tree }, { name: 'common', version: '1.0.0' }); // -> .sgc JSON
|
|
32
|
+
* const { conceptMap, manifest, validation } = unpackCorpus(bundle, { validate: true });
|
|
33
|
+
*
|
|
34
|
+
* The on-disk JSONC tree stays canonical for editing (lib/load.js#exportConceptsToDir is the
|
|
35
|
+
* disk round-trip); the `.sgc` bundle is the single-file exchange artifact.
|
|
36
|
+
*/
|
|
37
|
+
const { conceptFactGraph } = require('./grammar-graph');
|
|
38
|
+
const { validateConceptTree, eachConcept } = require('./validate');
|
|
39
|
+
|
|
40
|
+
// Accept either a concept map ({ set: tree }) or a single bare tree (-> { name: tree }).
|
|
41
|
+
function asSetMap( input, name ) {
|
|
42
|
+
if ( input && Object.prototype.hasOwnProperty.call(input, 'childConcepts') )
|
|
43
|
+
return { [name || 'corpus']: input };
|
|
44
|
+
return input || {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Every distinct `Ns::fn` provider string the corpus calls.
|
|
48
|
+
function providersOf( conceptMap ) {
|
|
49
|
+
const provs = new Set();
|
|
50
|
+
for ( const set of Object.keys(conceptMap) )
|
|
51
|
+
eachConcept(conceptMap[set], ( c ) => {
|
|
52
|
+
const schema = (c && c._schema) || c || {};
|
|
53
|
+
const p = schema.provider;
|
|
54
|
+
if ( !p ) return;
|
|
55
|
+
const name = Array.isArray(p) ? p[0] : p;
|
|
56
|
+
if ( typeof name === 'string' ) provs.add(name);
|
|
57
|
+
});
|
|
58
|
+
return [...provs].sort();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Derive the corpus manifest from its concept map (or single tree). All fields are computed —
|
|
63
|
+
* the only inputs are the optional name/version/description.
|
|
64
|
+
* @returns {{name,version,description,conceptSets,conceptCount,providersRequired,
|
|
65
|
+
* alphabet:{produces,consumes},crossCorpus,collisions,extends}}
|
|
66
|
+
*/
|
|
67
|
+
function deriveManifest( conceptMap, meta ) {
|
|
68
|
+
meta = meta || {};
|
|
69
|
+
const map = asSetMap(conceptMap, meta.name);
|
|
70
|
+
const g = conceptFactGraph(map);
|
|
71
|
+
return {
|
|
72
|
+
name : meta.name || Object.keys(map).join('+'),
|
|
73
|
+
version : meta.version || '0.0.0',
|
|
74
|
+
description : meta.description || '',
|
|
75
|
+
conceptSets : Object.keys(map).sort(),
|
|
76
|
+
conceptCount : g.concepts.length,
|
|
77
|
+
providersRequired: providersOf(map),
|
|
78
|
+
// the narrow-waist alphabet: facts the corpus WRITES vs. external facts it READS
|
|
79
|
+
alphabet : {
|
|
80
|
+
produces: g.facts.filter(( f ) => f.producedBy.length).map(( f ) => f.key).sort(),
|
|
81
|
+
consumes: g.entryPoints.slice().sort()
|
|
82
|
+
},
|
|
83
|
+
crossCorpus : g.crossCorpus,
|
|
84
|
+
collisions : g.collisions,
|
|
85
|
+
extends : [...new Set(g.crossCorpus.map(( l ) => l.fromSet))].sort()
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Pack a corpus into a portable `.sgc` bundle (plain JSON).
|
|
91
|
+
* @param input concept map ({ set: tree }) or a single tree
|
|
92
|
+
* @param opts { name, version, description, seed }
|
|
93
|
+
*/
|
|
94
|
+
function packCorpus( input, opts ) {
|
|
95
|
+
opts = opts || {};
|
|
96
|
+
const map = asSetMap(input, opts.name);
|
|
97
|
+
const bundle = {
|
|
98
|
+
format : 'sgc',
|
|
99
|
+
sgcVersion: 1,
|
|
100
|
+
manifest : deriveManifest(map, opts),
|
|
101
|
+
conceptMap: JSON.parse(JSON.stringify(map))
|
|
102
|
+
};
|
|
103
|
+
if ( opts.seed !== undefined ) bundle.seed = opts.seed;
|
|
104
|
+
return bundle;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Unpack a `.sgc` bundle. With `{ validate: true }` it runs validateConceptTree per set and
|
|
109
|
+
* returns the records (it never throws on a grammar error — the caller decides severity).
|
|
110
|
+
* @returns {{ conceptMap, manifest, seed, validation }}
|
|
111
|
+
*/
|
|
112
|
+
function unpackCorpus( bundle, opts ) {
|
|
113
|
+
opts = opts || {};
|
|
114
|
+
if ( !bundle || bundle.format !== 'sgc' ) throw new Error('not an .sgc bundle');
|
|
115
|
+
const conceptMap = bundle.conceptMap || {};
|
|
116
|
+
let validation = null;
|
|
117
|
+
if ( opts.validate )
|
|
118
|
+
validation = Object.keys(conceptMap).map(( set ) => {
|
|
119
|
+
const r = validateConceptTree(conceptMap[set], opts.validateOpts || {});
|
|
120
|
+
return { set, errors: r.errors, warnings: r.warnings };
|
|
121
|
+
});
|
|
122
|
+
return { conceptMap, manifest: bundle.manifest, seed: bundle.seed, validation };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = { deriveManifest, packCorpus, unpackCorpus };
|