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,569 @@
|
|
|
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
|
+
* Author-time concept validator (host-side tool, zero core change) — the enforcement
|
|
21
|
+
* half of the typed-fact spine / canonicalization barrier (doc/MODELISATION.md §6.5,
|
|
22
|
+
* roadmap #1) and the safety gate for declarative AI-authoring (#10).
|
|
23
|
+
*
|
|
24
|
+
* Standing directive: VALIDATE STRUCTURE, NEVER THE EXPRESSION GRAMMAR. We check field
|
|
25
|
+
* presence/types, that `assert`/`ensure` *parse* under the real engine evaluator
|
|
26
|
+
* (App/expr.js), that `provider` is in a host-vetted palette, and — the valuable one —
|
|
27
|
+
* REF SOUNDNESS: a `require`/`ensure`/`assert` edge must not key on a PROSE fact (that
|
|
28
|
+
* is the K1 footgun — a prose dependency fragments the memo every run). We never cap
|
|
29
|
+
* what an expression may *compute*.
|
|
30
|
+
*
|
|
31
|
+
* const { validateConceptTree } = require('./validate');
|
|
32
|
+
* const { errors, warnings } = validateConceptTree(tree, { palette: ['LLM::complete'] });
|
|
33
|
+
*
|
|
34
|
+
* Returns `{ errors, warnings }`, each an array of `{ concept, kind, message, ref? }`.
|
|
35
|
+
* `validateOrThrow(tree, opts)` throws on the first error (use in an authoring loop).
|
|
36
|
+
*/
|
|
37
|
+
const { compileExpression, parseExpression, REF_FN } = require('../graph/expr');
|
|
38
|
+
const { compileEnumMap } = require('../providers/canonicalize'); // G-1: reuse the barrier's confluence check for synonym rings
|
|
39
|
+
|
|
40
|
+
// $ref / $$ref token — IDENTICAL capture to the engine regex (App/expr.js:45) so we
|
|
41
|
+
// classify exactly the references the evaluator will resolve.
|
|
42
|
+
const REF_RE = /\$(\$?[A-Za-z_][\w.:$]*)/g;
|
|
43
|
+
const BLOCKED = /(?:^|[^\w$])(constructor|__proto__|prototype)(?![\w$])/;
|
|
44
|
+
|
|
45
|
+
// Comparison operators that form a numeric GATE (the K1 continuous-gate footgun, A2).
|
|
46
|
+
const COMPARE_OPS = { '<': 1, '>': 1, '<=': 1, '>=': 1, '==': 1, '!=': 1, '===': 1, '!==': 1 };
|
|
47
|
+
// Fact-key suffixes that are conventionally a SNAPPED discrete grain (a fractional compare
|
|
48
|
+
// on one is not the footgun) — keeps the continuous-gate check from false-flagging ranks.
|
|
49
|
+
const DISCRETE_SUFFIX = /(?:Rank|Bucket|Band|Idx|Index|Digest)$/;
|
|
50
|
+
|
|
51
|
+
// A `$ref` AST node is the injected `__ref("key")` CallExpression — return its key, else null.
|
|
52
|
+
function astRefKey( n ) {
|
|
53
|
+
if ( n && n.type === 'CallExpression' && n.callee && n.callee.name === REF_FN && n.arguments && n.arguments[0] )
|
|
54
|
+
return String(n.arguments[0].value).replace(/^\$+/, '').split(':').pop().split('.')[0];
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
// A non-integer numeric literal (incl. a unary-minus on one), else null.
|
|
58
|
+
function astFractionalLiteral( n ) {
|
|
59
|
+
if ( n && n.type === 'Literal' && typeof n.value === 'number' && !Number.isInteger(n.value) ) return n.value;
|
|
60
|
+
if ( n && n.type === 'UnaryExpression' && n.operator === '-' && n.argument && n.argument.type === 'Literal'
|
|
61
|
+
&& typeof n.argument.value === 'number' && !Number.isInteger(n.argument.value) ) return -n.argument.value;
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
// Walk an expression AST for `$ref <cmp> <fractional-literal>` comparisons (either operand
|
|
65
|
+
// order) — the raw-continuous-on-a-gate signature. Returns [{ refKey, literal }].
|
|
66
|
+
function continuousGateHits( ast ) {
|
|
67
|
+
const hits = [];
|
|
68
|
+
(function walk( n ) {
|
|
69
|
+
if ( !n || typeof n !== 'object' ) return;
|
|
70
|
+
if ( (n.type === 'BinaryExpression') && COMPARE_OPS[n.operator] ) {
|
|
71
|
+
const lk = astRefKey(n.left), rk = astRefKey(n.right),
|
|
72
|
+
lf = astFractionalLiteral(n.left), rf = astFractionalLiteral(n.right);
|
|
73
|
+
if ( lk != null && rf != null ) hits.push({ refKey: lk, literal: rf });
|
|
74
|
+
else if ( rk != null && lf != null ) hits.push({ refKey: rk, literal: lf });
|
|
75
|
+
}
|
|
76
|
+
for ( const k in n ) {
|
|
77
|
+
const c = n[k];
|
|
78
|
+
if ( c && typeof c === 'object' ) Array.isArray(c) ? c.forEach(walk) : walk(c);
|
|
79
|
+
}
|
|
80
|
+
})(ast);
|
|
81
|
+
return hits;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// G-1 interface coherence: walk for `$key ==|=== '<string>'` (either operand order) and `$key in ['a',…]` — the enum
|
|
85
|
+
// VALUE gate a consumer keys on. Returns [{ key, value }] (an `in` expands to one hit per array string member). Only
|
|
86
|
+
// EQUALITY/membership (a `!=` gate is satisfiable by other values → not a never-fire). Reuses `astRefKey`.
|
|
87
|
+
function enumValueGates( ast ) {
|
|
88
|
+
const hits = [], strLit = ( n ) => (n && n.type === 'Literal' && typeof n.value === 'string') ? n.value : null;
|
|
89
|
+
(function walk( n ) {
|
|
90
|
+
if ( !n || typeof n !== 'object' ) return;
|
|
91
|
+
if ( n.type === 'BinaryExpression' && (n.operator === '==' || n.operator === '===') ) {
|
|
92
|
+
const lk = astRefKey(n.left), rk = astRefKey(n.right), ls = strLit(n.left), rs = strLit(n.right);
|
|
93
|
+
if ( lk != null && rs != null ) hits.push({ key: lk, value: rs });
|
|
94
|
+
else if ( rk != null && ls != null ) hits.push({ key: rk, value: ls });
|
|
95
|
+
}
|
|
96
|
+
if ( n.type === 'BinaryExpression' && n.operator === 'in' && astRefKey(n.left) != null && n.right && n.right.type === 'ArrayExpression' )
|
|
97
|
+
for ( const el of (n.right.elements || []) ) { const s = strLit(el); if ( s != null ) hits.push({ key: astRefKey(n.left), value: s }); }
|
|
98
|
+
for ( const k in n ) { const c = n[k]; if ( c && typeof c === 'object' ) Array.isArray(c) ? c.forEach(walk) : walk(c); }
|
|
99
|
+
})(ast);
|
|
100
|
+
return hits;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// the literal STRING values an applyMutations template WRITES per key (a template write of a closed-vocab value — so a
|
|
104
|
+
// consumer gating on it is reachable even when no prompt.facts enum lists it). A bare `key: 'v'` writes v; a `$`-prefixed
|
|
105
|
+
// key is a ref (not a literal write); a value that is itself a `$`-ref token is not a literal. Conservative (reduces
|
|
106
|
+
// false positives in the never-fire check).
|
|
107
|
+
function templateStringValues( tpl, out ) {
|
|
108
|
+
out = out || {};
|
|
109
|
+
(function walk( n ) {
|
|
110
|
+
if ( Array.isArray(n) ) return n.forEach(walk);
|
|
111
|
+
if ( n && typeof n === 'object' ) {
|
|
112
|
+
for ( const k in n ) {
|
|
113
|
+
const v = n[k];
|
|
114
|
+
if ( typeof v === 'string' && k[0] !== '$' && k !== '_id' && v[0] !== '$' ) (out[k] = out[k] || new Set()).add(v);
|
|
115
|
+
else if ( v && typeof v === 'object' ) walk(v);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
})(tpl);
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Collect every `$ref` key in a subtree (for "everything under a `!` is negated").
|
|
123
|
+
function collectRefKeys( n, out ) {
|
|
124
|
+
const rk = astRefKey(n);
|
|
125
|
+
if ( rk ) out.add(rk);
|
|
126
|
+
if ( n && typeof n === 'object' )
|
|
127
|
+
for ( const k in n ) {
|
|
128
|
+
const c = n[k];
|
|
129
|
+
if ( c && typeof c === 'object' ) Array.isArray(c) ? c.forEach((x) => collectRefKeys(x, out)) : collectRefKeys(c, out);
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
const isFalsyLit = (n) => n && n.type === 'Literal' && (n.value === false || n.value === 0 || n.value === null || n.value === 'false');
|
|
134
|
+
const isTrueLit = (n) => n && n.type === 'Literal' && (n.value === true || n.value === 'true');
|
|
135
|
+
// The ref keys that appear NEGATED in an expression AST: anything under a unary `!`, plus
|
|
136
|
+
// `$F==false|0|null` and `$F!=true` (the "non-cast condition" patterns). Heuristic — it does
|
|
137
|
+
// not unfold De Morgan; it catches the common defeasance idioms (`!$skip`, `$sat==false`).
|
|
138
|
+
function negatedRefKeys( ast ) {
|
|
139
|
+
const neg = new Set();
|
|
140
|
+
(function walk( n ) {
|
|
141
|
+
if ( !n || typeof n !== 'object' ) return;
|
|
142
|
+
if ( n.type === 'UnaryExpression' && n.operator === '!' ) collectRefKeys(n.argument, neg);
|
|
143
|
+
if ( n.type === 'BinaryExpression' ) {
|
|
144
|
+
const lk = astRefKey(n.left), rk = astRefKey(n.right);
|
|
145
|
+
if ( n.operator === '==' || n.operator === '===' ) {
|
|
146
|
+
if ( lk && isFalsyLit(n.right) ) neg.add(lk);
|
|
147
|
+
if ( rk && isFalsyLit(n.left) ) neg.add(rk);
|
|
148
|
+
}
|
|
149
|
+
if ( n.operator === '!=' || n.operator === '!==' ) {
|
|
150
|
+
if ( lk && isTrueLit(n.right) ) neg.add(lk);
|
|
151
|
+
if ( rk && isTrueLit(n.left) ) neg.add(rk);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
for ( const k in n ) {
|
|
155
|
+
const c = n[k];
|
|
156
|
+
if ( c && typeof c === 'object' ) Array.isArray(c) ? c.forEach(walk) : walk(c);
|
|
157
|
+
}
|
|
158
|
+
})(ast);
|
|
159
|
+
return neg;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Tarjan strongly-connected components over an adjacency map (name -> [toName]).
|
|
163
|
+
function tarjanSCC( nodes, adj ) {
|
|
164
|
+
const index = new Map(), low = new Map(), onStack = new Set(), stack = [], out = [];
|
|
165
|
+
let idx = 0;
|
|
166
|
+
function strongConnect( v ) {
|
|
167
|
+
index.set(v, idx); low.set(v, idx); idx++;
|
|
168
|
+
stack.push(v); onStack.add(v);
|
|
169
|
+
for ( const w of (adj.get(v) || []) ) {
|
|
170
|
+
if ( !index.has(w) ) { strongConnect(w); low.set(v, Math.min(low.get(v), low.get(w))); }
|
|
171
|
+
else if ( onStack.has(w) ) low.set(v, Math.min(low.get(v), index.get(w)));
|
|
172
|
+
}
|
|
173
|
+
if ( low.get(v) === index.get(v) ) {
|
|
174
|
+
const comp = [];
|
|
175
|
+
let w;
|
|
176
|
+
do { w = stack.pop(); onStack.delete(w); comp.push(w); } while ( w !== v );
|
|
177
|
+
out.push(comp);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
for ( const v of nodes ) if ( !index.has(v) ) strongConnect(v);
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Stratification lint (#5.3 / Tier 3): a recursion-through-negation in the concept-dependency
|
|
186
|
+
* graph is unstratified and may OSCILLATE (K7 — a live risk once `ensure`-gated verdicts can
|
|
187
|
+
* retract). Build directed edges D->C when D's require/ensure/assert references a fact C
|
|
188
|
+
* *statically* produces (its self-flag or an applyMutations key — provider-written facts are
|
|
189
|
+
* not tracked, which keeps monotone patterns like the nogood store from false-flagging), tag
|
|
190
|
+
* the edge negative when the dependency is negated, and warn on any SCC that contains a
|
|
191
|
+
* negative edge. Advisory: a flagged cycle is a POSSIBLE oscillation to verify, not a proof.
|
|
192
|
+
* @returns array of `{ concept, kind:'unstratified-cycle', message, cycle:[names] }`
|
|
193
|
+
*/
|
|
194
|
+
function stratificationWarnings( tree ) {
|
|
195
|
+
const concepts = [];
|
|
196
|
+
eachConcept(tree, (c) => { if ( c._name ) concepts.push(c); });
|
|
197
|
+
const names = concepts.map((c) => c._name);
|
|
198
|
+
|
|
199
|
+
// fact -> concepts that statically produce it
|
|
200
|
+
const producers = new Map();
|
|
201
|
+
for ( const c of concepts ) {
|
|
202
|
+
const schema = c._schema || c;
|
|
203
|
+
const prod = new Set([c._name, ...templateKeys(schema.applyMutations)]);
|
|
204
|
+
for ( const f of prod ) { if ( !producers.has(f) ) producers.set(f, []); producers.get(f).push(c._name); }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// directed dependency edges with polarity
|
|
208
|
+
const adj = new Map(); // name -> [toName] (for SCC)
|
|
209
|
+
const negEdges = new Set(); // "from->to" marked negative
|
|
210
|
+
for ( const c of concepts ) {
|
|
211
|
+
const schema = c._schema || c, from = c._name, pos = new Set(), neg = new Set();
|
|
212
|
+
for ( const r of refsOf(schema.require, false) ) pos.add(refKeyOf(r).key);
|
|
213
|
+
for ( const fld of ['ensure', 'assert'] )
|
|
214
|
+
for ( const e of asArray(schema[fld]) ) {
|
|
215
|
+
if ( typeof e !== 'string' ) continue;
|
|
216
|
+
let ast; try { ast = parseExpression(e); } catch ( _e ) { ast = null; }
|
|
217
|
+
const negs = negatedRefKeys(ast);
|
|
218
|
+
for ( const r of refsOf(e, true) ) { const k = refKeyOf(r).key; (negs.has(k) ? neg : pos).add(k); }
|
|
219
|
+
}
|
|
220
|
+
const edges = adj.get(from) || [];
|
|
221
|
+
const link = (keys, isNeg) => {
|
|
222
|
+
for ( const k of keys ) for ( const p of (producers.get(k) || []) ) if ( p !== from ) {
|
|
223
|
+
edges.push(p);
|
|
224
|
+
if ( isNeg ) negEdges.add(from + '->' + p);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
link(pos, false); link(neg, true);
|
|
228
|
+
adj.set(from, edges);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const out = [];
|
|
232
|
+
for ( const comp of tarjanSCC(names, adj) ) {
|
|
233
|
+
if ( comp.length < 2 ) {
|
|
234
|
+
// a self-loop counts only if the concept negatively depends on its own product
|
|
235
|
+
const v = comp[0];
|
|
236
|
+
if ( !(adj.get(v) || []).includes(v) || !negEdges.has(v + '->' + v) ) continue;
|
|
237
|
+
}
|
|
238
|
+
const inComp = new Set(comp);
|
|
239
|
+
// is there a negative edge WITHIN the SCC?
|
|
240
|
+
let hasNeg = false;
|
|
241
|
+
for ( const from of comp ) for ( const to of (adj.get(from) || []) )
|
|
242
|
+
if ( inComp.has(to) && negEdges.has(from + '->' + to) ) hasNeg = true;
|
|
243
|
+
if ( !hasNeg ) continue; // a purely-positive cycle is monotone mutual support — fine
|
|
244
|
+
out.push({
|
|
245
|
+
concept: comp[0], kind: 'unstratified-cycle',
|
|
246
|
+
message: `concepts {${comp.join(', ')}} form a dependency cycle through a NEGATED edge — unstratified, may oscillate (K7); break the negation out of the cycle or stage it across revisions`,
|
|
247
|
+
cycle: comp.slice()
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const asArray = (v) => (v == null ? [] : Array.isArray(v) ? v : [v]);
|
|
254
|
+
const providerName = (p) => (Array.isArray(p) ? p[0] : p);
|
|
255
|
+
|
|
256
|
+
// Structural keys the engine always resolves — never "unknown" regardless of the
|
|
257
|
+
// declared ref alphabet. (Domain facts are declared by the host via opts.knownFacts.)
|
|
258
|
+
const BUILTIN_KEYS = new Set(['_parent', '_id', '_name', '_incoming', '_outgoing', 'originNode', 'targetNode']);
|
|
259
|
+
|
|
260
|
+
// The fact keys a mutation template WRITES (so a dependency on one is sound). A
|
|
261
|
+
// template is an object (or array of objects); we collect its plain data keys,
|
|
262
|
+
// dropping `$`-control markers ($_id/$$_id/$ref) and structural nesting keys.
|
|
263
|
+
function templateKeys( tpl, out ) {
|
|
264
|
+
out = out || [];
|
|
265
|
+
for ( const obj of asArray(tpl) ) {
|
|
266
|
+
if ( !obj || typeof obj !== 'object' ) continue;
|
|
267
|
+
for ( const raw of Object.keys(obj) ) {
|
|
268
|
+
const k = raw.replace(/^\$+/, ''); // `$key`/`$$key` -> key
|
|
269
|
+
if ( k === '_incoming' || k === '_outgoing' ) { // nested child segments — recurse
|
|
270
|
+
templateKeys(obj[raw], out);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if ( BUILTIN_KEYS.has(k) || k === '_id' ) continue;
|
|
274
|
+
out.push(k);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// The fact key a ref path actually reads + whether a `.member` (e.g. `.length`) is
|
|
281
|
+
// applied. `$$budget:spent.length` -> { key:'spent', hasMember:true }; 'Task' -> { key:'Task' }.
|
|
282
|
+
function refKeyOf( path ) {
|
|
283
|
+
let body = String(path).replace(/^\$+/, ''); // drop $ / $$ ref markers
|
|
284
|
+
const segs = body.split(':'); // cross-object walk
|
|
285
|
+
const last = segs[segs.length - 1];
|
|
286
|
+
const dot = last.split('.'); // member access on the final object
|
|
287
|
+
return { key: dot[0], hasMember: dot.length > 1 };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Pull every ref path out of an expression-or-ref field.
|
|
291
|
+
// require items are bare ref paths ('Task', '_parent:originNode');
|
|
292
|
+
// assert/ensure are expressions whose refs are `$...` tokens.
|
|
293
|
+
function refsOf( field, isExpr ) {
|
|
294
|
+
const out = [];
|
|
295
|
+
for ( const item of asArray(field) ) {
|
|
296
|
+
if ( typeof item !== 'string' ) continue;
|
|
297
|
+
if ( isExpr ) {
|
|
298
|
+
let m;
|
|
299
|
+
REF_RE.lastIndex = 0;
|
|
300
|
+
while ( (m = REF_RE.exec(item)) ) out.push(m[1]);
|
|
301
|
+
} else {
|
|
302
|
+
out.push(item);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Walk the tree; call fn(concept, key) for every concept node. A node is a concept iff
|
|
309
|
+
// it is listed in a parent's `childConcepts` (so a missing `_name` IS still visited and
|
|
310
|
+
// flagged) OR it is a bare concept passed as the root. The synthetic root *container*
|
|
311
|
+
// (only `childConcepts`, no `_name`) is the one node that is never a concept.
|
|
312
|
+
function eachConcept( node, fn, key, isChild ) {
|
|
313
|
+
if ( !node || typeof node !== 'object' ) return;
|
|
314
|
+
if ( isChild || node._name ) fn(node, key);
|
|
315
|
+
const kids = node.childConcepts;
|
|
316
|
+
if ( kids ) for ( const k of Object.keys(kids) ) eachConcept(kids[k], fn, k, true);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* @param tree the nested concept tree (root container or a concept node)
|
|
321
|
+
* @param opts.palette allowed `Ns::fn` provider strings (advisory: a non-palette
|
|
322
|
+
* provider is a WARNING, or an ERROR when `opts.strict`)
|
|
323
|
+
* @param opts.collectionKeys keys known to hold child sets — a bare (non-`.length`)
|
|
324
|
+
* dependency on one is the "all-children" aggregation footgun
|
|
325
|
+
* @param opts.flagContinuousGates opt-in (#P4): flag an `assert`/`ensure` comparing a fact
|
|
326
|
+
* against a FRACTIONAL literal (`$x>=0.7`) — a raw continuous value
|
|
327
|
+
* on a defeasant gate churns the memo (K1/A2). Heuristic -> a
|
|
328
|
+
* WARNING (ERROR when `strict`); skips declared-discrete facts and
|
|
329
|
+
* …Rank/Bucket/Band/Idx/Index/Digest conventions.
|
|
330
|
+
* @param opts.continuousExempt fact keys to whitelist for the above (discrete-but-fractional,
|
|
331
|
+
* e.g. a k-of-n `confidence`)
|
|
332
|
+
* @param opts.strict promote palette warnings to errors
|
|
333
|
+
*/
|
|
334
|
+
function validateConceptTree( tree, opts ) {
|
|
335
|
+
opts = opts || {};
|
|
336
|
+
const palette = opts.palette ? new Set(opts.palette) : null;
|
|
337
|
+
const collectionKeys = new Set(opts.collectionKeys || ['expandedInto', 'answeredBy', 'children', 'steps']);
|
|
338
|
+
const errors = [], warnings = [];
|
|
339
|
+
const err = (concept, kind, message, ref) => errors.push({ concept, kind, message, ref });
|
|
340
|
+
const warn = (concept, kind, message, ref) => warnings.push({ concept, kind, message, ref });
|
|
341
|
+
|
|
342
|
+
// --- pass 1: classify every declared fact key as discrete (tracked) or prose (untracked) ---
|
|
343
|
+
const discrete = new Set(); // a require/ensure may depend on these
|
|
344
|
+
const prose = new Set(); // a require/ensure must NOT depend on these (K1)
|
|
345
|
+
eachConcept(tree, (c) => {
|
|
346
|
+
if ( !c._name ) return; // a nameless node declares nothing (flagged in pass 2)
|
|
347
|
+
discrete.add(c._name); // a self-flag is a discrete boolean fact
|
|
348
|
+
const schema = c._schema || c;
|
|
349
|
+
const prompt = schema.prompt;
|
|
350
|
+
if ( prompt && prompt.facts ) {
|
|
351
|
+
for ( const k of Object.keys(prompt.facts) ) {
|
|
352
|
+
discrete.add(k);
|
|
353
|
+
// G-1: a curated synonym ring must be CONFLUENT — single-valued ∧ disjoint ∧ members valid. Compile it at
|
|
354
|
+
// author-time (the critical-pair check) so a malformed ring is a validation ERROR here, not a runtime throw.
|
|
355
|
+
const spec = prompt.facts[k];
|
|
356
|
+
if ( spec && spec.enum && spec.synonyms ) {
|
|
357
|
+
try { compileEnumMap(spec); }
|
|
358
|
+
catch ( e ) { err(c._name, 'synonym-ring', `fact "${k}": ${e.message}`); }
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
discrete.add(c._name + 'FactsDigest');
|
|
362
|
+
prose.add(prompt.prose || (c._name + 'Prose'));
|
|
363
|
+
prose.add(c._name + 'CanonMiss');
|
|
364
|
+
}
|
|
365
|
+
// facts written by the concept's applyMutations template are produced (sound to depend on)
|
|
366
|
+
for ( const k of templateKeys(schema.applyMutations) ) discrete.add(k);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// ref-soundness layer 3 (#10): only active when the host declares its ref alphabet
|
|
370
|
+
// (provider-emitted facts + seed/free-node keys it knows exist outside the tree).
|
|
371
|
+
// Without it we cannot soundly judge an "unknown" ref — so the check stays off.
|
|
372
|
+
const checkRefs = !!opts.knownFacts;
|
|
373
|
+
const known = new Set([...discrete, ...BUILTIN_KEYS, ...(opts.knownFacts || [])]);
|
|
374
|
+
|
|
375
|
+
// continuous-vs-snapped axis (#P4) is opt-in (a heuristic — see check 3b). `exempt`
|
|
376
|
+
// lets a host whitelist a discrete-but-fractional fact (e.g. a k-of-n confidence).
|
|
377
|
+
const checkContinuous = !!opts.flagContinuousGates;
|
|
378
|
+
const exempt = new Set(opts.continuousExempt || []);
|
|
379
|
+
|
|
380
|
+
// --- pass 2: per-concept structural + expression + ref-soundness checks ---
|
|
381
|
+
eachConcept(tree, (c, key) => {
|
|
382
|
+
const name = c._name;
|
|
383
|
+
const schema = c._schema || c;
|
|
384
|
+
|
|
385
|
+
// (1) structural — _name present (the self-flag the provider writes; without a
|
|
386
|
+
// name to flag, the engine re-fires the concept forever — loop.js:88).
|
|
387
|
+
if ( !name ) { err(c._id || key || '(anonymous)', 'no-name', 'concept has no _name (self-flag) — it will re-fire forever'); return; }
|
|
388
|
+
|
|
389
|
+
// (2) provider in the vetted palette (advisory)
|
|
390
|
+
const prov = schema.provider != null ? providerName(schema.provider) : null;
|
|
391
|
+
if ( prov && palette && !palette.has(prov) ) {
|
|
392
|
+
(opts.strict ? err : warn)(name, 'provider-not-in-palette', `provider "${prov}" is not in the vetted palette`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// (3) expression well-formedness — assert/ensure must PARSE under the engine
|
|
396
|
+
// evaluator, and must not poke the prototype chain. Optionally (opt-in) flag a
|
|
397
|
+
// raw-continuous threshold on a gate (the K1/A2 footgun — see below).
|
|
398
|
+
for ( const fld of ['assert', 'ensure'] ) {
|
|
399
|
+
for ( const expr of asArray(schema[fld]) ) {
|
|
400
|
+
if ( typeof expr !== 'string' ) continue;
|
|
401
|
+
if ( BLOCKED.test(expr) ) err(name, 'blocked-prop', `${fld} touches constructor/__proto__/prototype`, expr);
|
|
402
|
+
try { compileExpression(expr, { empty: true }); }
|
|
403
|
+
catch ( e ) { err(name, 'unparseable', `${fld} does not parse: ${e.message}`, expr); continue; }
|
|
404
|
+
|
|
405
|
+
// (3b) continuous-vs-snapped axis (#P4) — only when the host opts in (it is a
|
|
406
|
+
// heuristic: a discrete-but-fractional fact like a k-of-n confidence is fine).
|
|
407
|
+
// A defeasant gate (`ensure`) that compares a fact against a FRACTIONAL literal
|
|
408
|
+
// (`$x>=0.7`) churns the memo every run under sub-threshold noise (A2: 5 re-runs
|
|
409
|
+
// vs 1). Snap the fact to a grain/rank and gate on that. NOTE: a "produced" fact
|
|
410
|
+
// (in `discrete`) is NOT necessarily discrete-VALUED — a provider/template can
|
|
411
|
+
// write a raw float (pHat:0.5) — so we do NOT skip on `discrete`; only on a
|
|
412
|
+
// conventionally-snapped name (…Rank/Bucket/…) or the host's exempt list.
|
|
413
|
+
if ( checkContinuous ) {
|
|
414
|
+
let ast;
|
|
415
|
+
try { ast = parseExpression(expr); } catch ( e ) { ast = null; }
|
|
416
|
+
for ( const hit of continuousGateHits(ast) ) {
|
|
417
|
+
if ( DISCRETE_SUFFIX.test(hit.refKey) || exempt.has(hit.refKey) ) continue;
|
|
418
|
+
(opts.strict ? err : warn)(name, 'continuous-gate',
|
|
419
|
+
`${fld} compares "${hit.refKey}" against fractional ${hit.literal} — a raw continuous value on a defeasant gate churns the memo every run (K1/A2); snap it to a discrete grain/rank and gate on that`, expr);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// (4) ref soundness — the valuable check. A dependency edge keyed on PROSE
|
|
426
|
+
// fragments the memo (K1); a bare dependency on a child-set is the
|
|
427
|
+
// "all-children-answered" aggregation footgun (getRef has no quantifier).
|
|
428
|
+
const edges = [
|
|
429
|
+
...refsOf(schema.require, false).map((r) => ({ r, fld: 'require' })),
|
|
430
|
+
...refsOf(schema.ensure, true).map((r) => ({ r, fld: 'ensure' })),
|
|
431
|
+
...refsOf(schema.assert, true).map((r) => ({ r, fld: 'assert' }))
|
|
432
|
+
];
|
|
433
|
+
for ( const { r, fld } of edges ) {
|
|
434
|
+
const { key, hasMember } = refKeyOf(r);
|
|
435
|
+
const isCrossWalk = /:/.test(String(r).replace(/^\$+/, '')); // a:b walk — resolves on another object
|
|
436
|
+
if ( prose.has(key) )
|
|
437
|
+
err(name, 'prose-dependency', `${fld} depends on prose key "${key}" — fragments the memo every run (K1); key on a discrete fact instead`, r);
|
|
438
|
+
else if ( collectionKeys.has(key) && !hasMember )
|
|
439
|
+
warn(name, 'aggregating-dependency', `${fld} depends on child-set "${key}" without .length — getRef cannot quantify; use a {__push}+\`.length\` completion gate`, r);
|
|
440
|
+
else if ( checkRefs && !isCrossWalk && !known.has(key) )
|
|
441
|
+
(opts.strict ? err : warn)(name, 'unknown-ref', `${fld} keys on "${key}" — no concept produces it and the ref-alphabet does not declare it; this dependency may never resolve (silent never-fires)`, r);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// --- pass 3: stratification (whole-tree) — a dependency cycle through a NEGATED edge is
|
|
446
|
+
// unstratified and may oscillate (K7). Structural + sound (static facts only), so a
|
|
447
|
+
// default warning (strict -> error). Opt out with opts.skipStratification.
|
|
448
|
+
if ( !opts.skipStratification )
|
|
449
|
+
for ( const s of stratificationWarnings(tree) )
|
|
450
|
+
(opts.strict ? errors : warnings).push(s); // s = { concept, kind, message, cycle }
|
|
451
|
+
|
|
452
|
+
// --- pass 4 (G-1): cross-method INTERFACE COHERENCE over enum VALUES (the STAGE-1 keystone soundness core). The
|
|
453
|
+
// key-level never-fire is already caught above (unknown-ref); this adds the VALUE level. Two checks:
|
|
454
|
+
// (a) NEVER-FIRE — a consumer gates on `$key=='V'` but NO producer writes V for that enum-interface key → a
|
|
455
|
+
// silent dead edge (producer writes {intransit,delivered}; consumer gates =='shipped'). WARN (strict→err).
|
|
456
|
+
// (b) SYNONYM INTENT-COLLISION — the SAME alias maps to DIFFERENT members for one key across concepts: the
|
|
457
|
+
// shared vocabulary is non-confluent across the method graph (Laurie condition 3 — cross-method critical pair).
|
|
458
|
+
// Opt out with opts.skipInterfaceCoherence.
|
|
459
|
+
if ( !opts.skipInterfaceCoherence ) {
|
|
460
|
+
const producedValues = {}; // key -> Set(values any concept writes: enum members + template literals)
|
|
461
|
+
const enumInterfaceKeys = new Set(); // keys that carry a prompt.facts enum (a typed closed vocab)
|
|
462
|
+
const synonymBy = {}; // key -> { normAlias -> { member -> [concept...] } }
|
|
463
|
+
eachConcept(tree, (c) => {
|
|
464
|
+
if ( !c._name ) return;
|
|
465
|
+
const schema = c._schema || c, facts = schema.prompt && schema.prompt.facts;
|
|
466
|
+
if ( facts ) for ( const key of Object.keys(facts) ) {
|
|
467
|
+
const spec = facts[key];
|
|
468
|
+
if ( !spec || !spec.enum ) continue;
|
|
469
|
+
enumInterfaceKeys.add(key);
|
|
470
|
+
for ( const m of spec.enum ) (producedValues[key] = producedValues[key] || new Set()).add(m);
|
|
471
|
+
if ( spec.synonyms ) for ( const member of Object.keys(spec.synonyms) )
|
|
472
|
+
for ( const alias of (spec.synonyms[member] || []) ) {
|
|
473
|
+
const a = String(alias).trim().toLowerCase().replace(/\s+/g, ' ');
|
|
474
|
+
const mm = ((synonymBy[key] = synonymBy[key] || {})[a] = (synonymBy[key] || {})[a] || {});
|
|
475
|
+
(mm[member] = mm[member] || []).push(c._name);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const tv = templateStringValues(schema.applyMutations); // template literal writes broaden the reachable set
|
|
479
|
+
for ( const key of Object.keys(tv) ) for ( const v of tv[key] ) (producedValues[key] = producedValues[key] || new Set()).add(v);
|
|
480
|
+
});
|
|
481
|
+
// (b) synonym intent-collision (cross-method confluence)
|
|
482
|
+
for ( const key of Object.keys(synonymBy) )
|
|
483
|
+
for ( const alias of Object.keys(synonymBy[key]) ) {
|
|
484
|
+
const members = Object.keys(synonymBy[key][alias]);
|
|
485
|
+
if ( members.length > 1 ) {
|
|
486
|
+
const involved = [...new Set(members.flatMap((m) => synonymBy[key][alias][m]))];
|
|
487
|
+
err(involved.join(','), 'synonym-intent-collision',
|
|
488
|
+
`alias "${alias}" for "${key}" maps to DIFFERENT members across concepts (${members.join(' vs ')}) — the shared vocabulary is non-confluent across the method graph; use per-concept intent-scoped rings or reconcile the mapping`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
// (a) value-level never-fire
|
|
492
|
+
eachConcept(tree, (c) => {
|
|
493
|
+
if ( !c._name ) return;
|
|
494
|
+
const schema = c._schema || c;
|
|
495
|
+
for ( const fld of ['ensure', 'assert'] )
|
|
496
|
+
for ( const expr of asArray(schema[fld]) ) {
|
|
497
|
+
if ( typeof expr !== 'string' ) continue;
|
|
498
|
+
let ast; try { ast = parseExpression(expr); } catch ( e ) { continue; }
|
|
499
|
+
for ( const g of enumValueGates(ast) ) {
|
|
500
|
+
if ( !enumInterfaceKeys.has(g.key) ) continue; // not a typed enum interface key → can't judge soundly
|
|
501
|
+
const produced = producedValues[g.key] || new Set();
|
|
502
|
+
if ( !produced.has(g.value) )
|
|
503
|
+
(opts.strict ? err : warn)(c._name, 'interface-never-fire',
|
|
504
|
+
`${fld} gates on ${g.key}=='${g.value}' but no concept writes that value for the enum interface "${g.key}" (produced: {${[...produced].join(', ')}}) — a silent never-fire; align the producer/consumer vocabulary or add "${g.value}" to the enum`, expr);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return { errors, warnings };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function validateOrThrow( tree, opts ) {
|
|
514
|
+
const res = validateConceptTree(tree, opts);
|
|
515
|
+
if ( res.errors.length ) {
|
|
516
|
+
const e = res.errors[0];
|
|
517
|
+
throw new Error(`concept "${e.concept}" — ${e.kind}: ${e.message}` + (e.ref ? ` [${e.ref}]` : ''));
|
|
518
|
+
}
|
|
519
|
+
return res;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Merge-projection contract validator (#P4 part b) — makes the fork/merge frontier a CHECKED
|
|
524
|
+
* assume-guarantee contract. `validate.js` validates a concept tree but never sees `merge`'s
|
|
525
|
+
* `project` function (opaque JS), so a projection that crosses a raw/undeclared fact onto the
|
|
526
|
+
* parent passes everything (the D-experiment gap). Call this on the TEMPLATE the projection
|
|
527
|
+
* returns, with the host-declared frontier alphabet (the keys permitted to cross a sub-graph
|
|
528
|
+
* boundary — the snapped contract keys), to catch an undeclared crossing.
|
|
529
|
+
*
|
|
530
|
+
* const { errors, warnings } = validateMergeProjection(
|
|
531
|
+
* { $$_id: 'belief', ellA: 0.6 }, { frontierAlphabet: ['ellA', 'ellB'], flagContinuous: true });
|
|
532
|
+
*
|
|
533
|
+
* @param template the projection object (or array of them); `$$_id`/`$_id` are control keys.
|
|
534
|
+
* @param opts.frontierAlphabet keys permitted to cross (the check is inactive without it — like
|
|
535
|
+
* the tree validator's `knownFacts`, we only judge what's declared).
|
|
536
|
+
* @param opts.flagContinuous advisory: also warn on a raw-fractional value crossing — fine ONLY
|
|
537
|
+
* if the parent snaps before gating (C1/E4); a raw-gated parent is K1.
|
|
538
|
+
* @param opts.strict promote the undeclared-crossing warning to an error.
|
|
539
|
+
* @returns { errors, warnings } records shaped like validateConceptTree's.
|
|
540
|
+
*/
|
|
541
|
+
function validateMergeProjection( template, opts ) {
|
|
542
|
+
opts = opts || {};
|
|
543
|
+
const errors = [], warnings = [];
|
|
544
|
+
const alpha = opts.frontierAlphabet ? new Set(opts.frontierAlphabet) : null;
|
|
545
|
+
for ( const tpl of asArray(template) ) {
|
|
546
|
+
if ( !tpl || typeof tpl !== 'object' ) continue;
|
|
547
|
+
for ( const raw of Object.keys(tpl) ) {
|
|
548
|
+
const k = raw.replace(/^\$+/, '');
|
|
549
|
+
if ( BUILTIN_KEYS.has(k) || k === '_id' ) continue; // $$_id / $_id select the target
|
|
550
|
+
if ( alpha && !alpha.has(k) )
|
|
551
|
+
(opts.strict ? errors : warnings).push({ concept: '(merge)', kind: 'frontier-leak',
|
|
552
|
+
message: `merge projection crosses "${k}" — not in the declared frontier alphabet; only the snapped contract keys may cross a sub-graph boundary`, ref: k });
|
|
553
|
+
if ( opts.flagContinuous ) {
|
|
554
|
+
const v = tpl[raw];
|
|
555
|
+
if ( typeof v === 'number' && !Number.isInteger(v) )
|
|
556
|
+
warnings.push({ concept: '(merge)', kind: 'continuous-crossing',
|
|
557
|
+
message: `merge projection crosses raw continuous "${k}"=${v} — sound ONLY if the parent snaps before gating (cross-continuous-then-snap, C1/E4); a parent that gates on it raw is the K1 breach`, ref: k });
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return { errors, warnings };
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
module.exports = {
|
|
565
|
+
validateConceptTree, validateOrThrow, stratificationWarnings, validateMergeProjection,
|
|
566
|
+
// low-level extraction helpers — exported so the grammar-graph derivation (lib/authoring/
|
|
567
|
+
// grammar-graph.js) reuses the SAME ref/polarity/produced-fact logic (single source of truth).
|
|
568
|
+
eachConcept, refsOf, refKeyOf, templateKeys, negatedRefKeys
|
|
569
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
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
|
+
* widen — the STANDING / autonomous WIDEN loop (the S-boundary CLIMB; host-side, ZERO-CORE). The reactive SIBLING of
|
|
10
|
+
* `relearn.js` (the narrow loop): where blame → `reviseOnBlame` → patch NARROWS a method on a retraction, ≥k verified
|
|
11
|
+
* positives → `widenOnVerified` → patch WIDENS it — candidate elimination's two boundaries, both engine-driven.
|
|
12
|
+
*
|
|
13
|
+
* Mirrors `relearn.js#{relearnTree, makeRelearnProviders}`:
|
|
14
|
+
* - the upstream guarded-speculative-admit / sibling-merge signal `{__push}`es a verified positive `{value, methodId}`
|
|
15
|
+
* onto the method's library node `lib:<Method>` and, at ≥k, deposits a discrete `widenReady` fact (a clean `require`
|
|
16
|
+
* trigger — #22-safe, keyed on a fact APPEARING, not a value change), exactly as blame deposits `blamed`;
|
|
17
|
+
* - a `Widen` meta-concept (`require:['widenReady'], ensure:['!$widened']`) fires;
|
|
18
|
+
* - `Lib::widen` runs `widenOnVerified` (additive enum-union, methodId-gated, `!=`-nogood preserving) AND patches the
|
|
19
|
+
* engine gate to match — with the G2 ORDERING the §6.4 confront fixed: `recordWiden` DEMOTES FROZEN→INSTANCE
|
|
20
|
+
* SYNCHRONOUSLY *before* the gate-relax `patchConcept` (which is queued to the quiescent #11.a boundary → lands
|
|
21
|
+
* strictly after), so the relaxed gate never admits a newly-widened case under a frozen, assertPost-eliding regime.
|
|
22
|
+
*
|
|
23
|
+
* Wiring (symmetric to contract-relearn.js):
|
|
24
|
+
* const { makeWidenProviders, widenTree } = require('./widen');
|
|
25
|
+
* Graph._providers = Object.assign({ App:{serve} }, makeWidenProviders({ registry, mount }));
|
|
26
|
+
* // tree: the over-specific method + widenTree(); seed a `lib:<Method>` node with { method, discriminator, positives, widenReady }.
|
|
27
|
+
*/
|
|
28
|
+
const { widenOnVerified } = require('./contract.js');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The reactive widen flow as a concept tree. One meta-concept:
|
|
32
|
+
* widenReady ──require──▶ Widen (ensure !widened) ──▶ Lib::widen widens the library contract + the gate.
|
|
33
|
+
* @param opts.name the meta-concept id/_name (default 'Widen')
|
|
34
|
+
*/
|
|
35
|
+
function widenTree( opts ) {
|
|
36
|
+
opts = opts || {};
|
|
37
|
+
var name = opts.name || 'Widen';
|
|
38
|
+
var def = { _id: name, _name: name, require: ['widenReady'], ensure: ['!$widened'], provider: ['Lib::widen'] };
|
|
39
|
+
var tree = { childConcepts: {} };
|
|
40
|
+
tree.childConcepts[name] = def;
|
|
41
|
+
return tree;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The Lib widen provider (host opt-in, like makeRelearnProviders).
|
|
46
|
+
* @param opts.registry { <Method>: contract } — the library's typed contracts; widen mutates the entry to the new
|
|
47
|
+
* (versioned) contract. Pass copies for B8.
|
|
48
|
+
* @param opts.mount optional mount controller (`mount.js`) — `recordWiden` demotes FROZEN→INSTANCE + spends the
|
|
49
|
+
* megamorphic WIDTH budget (NOT μ). Called BEFORE the gate patch (the G2 ordering).
|
|
50
|
+
* @returns { Lib: { widen } }
|
|
51
|
+
*/
|
|
52
|
+
function makeWidenProviders( opts ) {
|
|
53
|
+
opts = opts || {};
|
|
54
|
+
var registry = opts.registry || {};
|
|
55
|
+
var mount = opts.mount;
|
|
56
|
+
|
|
57
|
+
return { Lib: {
|
|
58
|
+
// the standing widen: read the accumulated positives → widen the library contract + the engine gate, autonomously.
|
|
59
|
+
widen: function ( graph, concept, scope, argz, cb ) {
|
|
60
|
+
var e = (scope && scope._) || {};
|
|
61
|
+
var M = e.method;
|
|
62
|
+
var positives = e.positives || [];
|
|
63
|
+
var target = graph.getConceptByName(M);
|
|
64
|
+
|
|
65
|
+
// 1 — the contract to widen: the registry entry, else seed from the live gate.
|
|
66
|
+
var contract = registry[M] || { read: e.discriminator ? [e.discriminator] : [], pre: (target && target._schema && target._schema.ensure || []).slice(), effect: 'pure' };
|
|
67
|
+
var out = widenOnVerified(contract, positives, { discriminator: e.discriminator, target: M, siblings: e.siblings, numeric: e.numeric });
|
|
68
|
+
if ( registry[M] != null ) registry[M] = out.contract;
|
|
69
|
+
|
|
70
|
+
// 2 — the G2 ORDERING (Laurie confront pt3): demote FROZEN→INSTANCE SYNCHRONOUSLY, BEFORE the gate-relax
|
|
71
|
+
// patchConcept (which the engine queues to the quiescent #11.a boundary → strictly after). So a
|
|
72
|
+
// newly-admitted case can never apply under a frozen, assertPost-eliding regime.
|
|
73
|
+
var demoted = (mount && mount.recordWiden) ? mount.recordWiden(M) : null;
|
|
74
|
+
|
|
75
|
+
// 3 — widen the engine gate to match. Concept.patch REPLACES arrays → pass the FULL ensure (queued, #11.a).
|
|
76
|
+
graph.patchConcept(M, { ensure: out.ensure });
|
|
77
|
+
|
|
78
|
+
// 4 — own cast marker (#33) + `widened` re-fire guard; record the widened pre on lib:<Method> (typed atoms,
|
|
79
|
+
// not prose) so rollbackTo (N6) restores both the concept and the recorded pre coherently.
|
|
80
|
+
cb(null, { $_id: '_parent', Widen: true, widened: true,
|
|
81
|
+
widenedEnsure: out.ensure, widenedPre: out.contract && out.contract.pre,
|
|
82
|
+
demotedFrom: demoted && demoted.demotedFrom, widenCount: demoted && demoted.widenCount,
|
|
83
|
+
rejected: out.rejected && out.rejected.length ? out.rejected.length : 0, clamped: out.clamped });
|
|
84
|
+
}
|
|
85
|
+
} };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { makeWidenProviders: makeWidenProviders, widenTree: widenTree };
|