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,310 @@
|
|
|
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
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
19
|
+
import { createRoot } from 'react-dom/client';
|
|
20
|
+
import { html } from 'htm/react';
|
|
21
|
+
import { connect } from '/ws.js';
|
|
22
|
+
import { GraphCanvas, LAYOUT_NAMES } from '/components/GraphCanvas.js';
|
|
23
|
+
import { Inspector } from '/components/Inspector.js';
|
|
24
|
+
import { Timeline, DiffPanel } from '/components/Timeline.js';
|
|
25
|
+
import { ConceptTree } from '/components/ConceptTree.js';
|
|
26
|
+
import { ConceptEditor } from '/components/ConceptEditor.js';
|
|
27
|
+
import { ForkTree } from '/components/ForkTree.js';
|
|
28
|
+
import { TilingOverlay } from '/components/TilingOverlay.js';
|
|
29
|
+
import { PromptConsole } from '/components/PromptConsole.js';
|
|
30
|
+
import { GrammarGraph } from '/components/GrammarGraph.js';
|
|
31
|
+
import { CorpusPanel } from '/components/CorpusPanel.js';
|
|
32
|
+
import { ProviderTrace } from '/components/ProviderTrace.js';
|
|
33
|
+
import { SessionSplit } from '/components/SessionSplit.js';
|
|
34
|
+
import { LearningPanel } from '/components/LearningPanel.js';
|
|
35
|
+
|
|
36
|
+
// a demo seed so you can watch a graph develop with one click
|
|
37
|
+
const DEMO_SEED = { conceptMaps: [
|
|
38
|
+
{ _id: 'a', Node: true, Position: { lat: 48.85, lng: 2.35 } },
|
|
39
|
+
{ _id: 'b', Node: true, Position: { lat: 1.35, lng: 103.8 } },
|
|
40
|
+
{ _id: 's', Segment: true, originNode: 'a', targetNode: 'b' }
|
|
41
|
+
] };
|
|
42
|
+
|
|
43
|
+
function App() {
|
|
44
|
+
const [status, setStatus] = useState('connecting');
|
|
45
|
+
const [corpora, setCorpora] = useState([]);
|
|
46
|
+
const [corpus, setCorpus] = useState('');
|
|
47
|
+
const [graph, setGraph] = useState({ objects: [], currentRev: 0, revCount: 0, revs: [] });
|
|
48
|
+
const [applies, setApplies] = useState([]);
|
|
49
|
+
const [lastApply, setLastApply] = useState(null);
|
|
50
|
+
const [selectedId, setSelectedId] = useState(null);
|
|
51
|
+
const [layout, setLayout] = useState('elk');
|
|
52
|
+
const [relayout, setRelayout] = useState(0);
|
|
53
|
+
const [diffResult, setDiffResult] = useState(null);
|
|
54
|
+
const [conceptTree, setConceptTree] = useState({});
|
|
55
|
+
const [editing, setEditing] = useState(null);
|
|
56
|
+
const valTimer = useRef(null); // debounce timer for in-editor live ref-checking
|
|
57
|
+
const [sessionId, setSessionId] = useState('root');
|
|
58
|
+
const [forks, setForks] = useState({ root: { id: 'root', parent: null } });
|
|
59
|
+
const [tiling, setTiling] = useState(null);
|
|
60
|
+
const [promptProgress, setPromptProgress] = useState([]);
|
|
61
|
+
const [promptAnswer, setPromptAnswer] = useState('');
|
|
62
|
+
const [promptRunning, setPromptRunning] = useState(false);
|
|
63
|
+
const [promptError, setPromptError] = useState('');
|
|
64
|
+
const [view, setView] = useState('data'); // 'data' (instance graph) | 'grammar' (concepts↔facts) | 'learning' (lattice registry)
|
|
65
|
+
const [grammar, setGrammar] = useState(null);
|
|
66
|
+
const [learning, setLearning] = useState(null); // { registry, rings } — the LearningPanel readout
|
|
67
|
+
const [learnVerdict, setLearnVerdict] = useState(null); // the last gate verdict { ok, text }
|
|
68
|
+
const [manifest, setManifest] = useState(null);
|
|
69
|
+
const [trace, setTrace] = useState([]);
|
|
70
|
+
const [lastRetract, setLastRetract] = useState(null);
|
|
71
|
+
const [split, setSplit] = useState(null); // { parentId, forkId, parentObjects, forkObjects, preview }
|
|
72
|
+
const api = useRef(null);
|
|
73
|
+
const sessionRef = useRef('root'); // the active session — events for others are ignored
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
const c = connect('ws://' + location.host, ( evt ) => {
|
|
77
|
+
if ( evt.type === 'forks' ) { setForks(evt.payload); return; }
|
|
78
|
+
if ( evt.sessionId && evt.sessionId !== sessionRef.current ) return; // not the active session
|
|
79
|
+
if ( evt.type === 'state' ) setGraph(evt.payload);
|
|
80
|
+
else if ( evt.type === 'conceptApply' ) { setApplies(( a ) => [...a, evt.payload]); setLastApply(evt.payload); }
|
|
81
|
+
else if ( evt.type === 'promptProgress' ) setPromptProgress(( p ) => [...p, evt.payload]);
|
|
82
|
+
else if ( evt.type === 'promptAnswer' ) { setPromptAnswer(evt.payload.answer); setPromptRunning(false); }
|
|
83
|
+
else if ( evt.type === 'retract' ) { setLastRetract(evt.payload); try { (window.__sgRetracts = window.__sgRetracts || []).push(evt.payload); } catch ( e ) {} }
|
|
84
|
+
});
|
|
85
|
+
c.onStatus(setStatus);
|
|
86
|
+
api.current = c;
|
|
87
|
+
try { window.__sgApi = c; } catch ( e ) {} // test hook (puppeteer)
|
|
88
|
+
c.call('listCorpora').then(setCorpora);
|
|
89
|
+
}, []);
|
|
90
|
+
|
|
91
|
+
const sid = () => sessionRef.current;
|
|
92
|
+
|
|
93
|
+
async function loadCorpus( name, seed ) {
|
|
94
|
+
const c = corpora.find(( x ) => x.name === name);
|
|
95
|
+
if ( !c ) return;
|
|
96
|
+
sessionRef.current = 'root'; setSessionId('root');
|
|
97
|
+
setForks({ root: { id: 'root', parent: null } });
|
|
98
|
+
setCorpus(name);
|
|
99
|
+
setApplies([]); setLastApply(null); setSelectedId(null);
|
|
100
|
+
await api.current.call('loadCorpus', { conceptsDir: c.dir, builtins: true, seed });
|
|
101
|
+
setGraph(await api.current.call('state', {}, 'root'));
|
|
102
|
+
setConceptTree(await api.current.call('conceptTree', {}, 'root'));
|
|
103
|
+
api.current.call('forkPlan', {}, 'root').then(setTiling).catch(() => setTiling(null));
|
|
104
|
+
refreshGrammar('root');
|
|
105
|
+
refreshTrace();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function refreshGrammar( id ) {
|
|
109
|
+
try {
|
|
110
|
+
setGrammar(await api.current.call('grammarGraph', {}, id || sid()));
|
|
111
|
+
setManifest(await api.current.call('corpusManifest', { meta: { name: corpus || id } }, id || sid()));
|
|
112
|
+
} catch ( e ) { setGrammar(null); setManifest(null); }
|
|
113
|
+
}
|
|
114
|
+
async function refreshTrace() {
|
|
115
|
+
try { setTrace(await api.current.call('providerTrace', { n: 80 }, sid())); } catch ( e ) { setTrace([]); }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// --- learning (the typed lattice registry — every alias goes through THE admission gate) ---
|
|
119
|
+
async function refreshLearning( id ) {
|
|
120
|
+
try { setLearning(await api.current.call('registry', {}, id || sid())); } catch ( e ) { setLearning(null); }
|
|
121
|
+
}
|
|
122
|
+
async function declareKey( key, en ) {
|
|
123
|
+
setLearning(await api.current.call('declareKey', { key, enum: en }, sid()));
|
|
124
|
+
setLearnVerdict(null);
|
|
125
|
+
}
|
|
126
|
+
async function proposeAlias( key, member, alias ) {
|
|
127
|
+
const r = await api.current.call('proposeAlias', { key, member, alias }, sid());
|
|
128
|
+
setLearnVerdict(r.admitted.length
|
|
129
|
+
? { ok: true, text: 'ADMITTED — ' + alias + ' → ' + member + ' (gate: member ∈ enum ∧ confluence)' }
|
|
130
|
+
: { ok: false, text: 'REJECTED — ' + (r.rejected[0] && r.rejected[0].reason || 'gate refused') });
|
|
131
|
+
try { window.__sgLastVerdict = r; } catch ( e ) {} // test hook (puppeteer)
|
|
132
|
+
await refreshLearning();
|
|
133
|
+
}
|
|
134
|
+
async function retractAlias( key, alias ) {
|
|
135
|
+
const r = await api.current.call('retractAlias', { key, alias }, sid());
|
|
136
|
+
setLearnVerdict({ ok: true, text: (r.retracted ? 'RETRACTED — ' : 'no-op — ') + alias + (r.member ? ' (was → ' + r.member + ')' : '') });
|
|
137
|
+
await refreshLearning();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// --- corpus exchange (.sgc) ---
|
|
141
|
+
async function exportCorpus() {
|
|
142
|
+
const bundle = await api.current.call('exportCorpus', { meta: { name: corpus || 'corpus', version: '1.0.0' } }, sid());
|
|
143
|
+
try { window.__sgLastBundle = bundle; } catch ( e ) {}
|
|
144
|
+
try {
|
|
145
|
+
const blob = new Blob([JSON.stringify(bundle, null, 2)], { type: 'application/json' });
|
|
146
|
+
const a = document.createElement('a');
|
|
147
|
+
a.href = URL.createObjectURL(blob); a.download = (bundle.manifest && bundle.manifest.name || 'corpus') + '.sgc';
|
|
148
|
+
a.click(); URL.revokeObjectURL(a.href);
|
|
149
|
+
} catch ( e ) {}
|
|
150
|
+
return bundle;
|
|
151
|
+
}
|
|
152
|
+
async function importCorpus( bundle, seed ) {
|
|
153
|
+
sessionRef.current = 'root'; setSessionId('root');
|
|
154
|
+
setForks({ root: { id: 'root', parent: null } });
|
|
155
|
+
setApplies([]); setLastApply(null); setSelectedId(null);
|
|
156
|
+
const res = await api.current.call('importCorpus', { bundle, opts: { builtins: true, seed } }, 'root');
|
|
157
|
+
if ( !res.ok ) { setPromptError('import failed: ' + (res.errors || []).map(( e ) => e.message).join('; ')); return res; }
|
|
158
|
+
setCorpus((bundle.manifest && bundle.manifest.name) || 'imported');
|
|
159
|
+
setGraph(await api.current.call('state', {}, 'root'));
|
|
160
|
+
setConceptTree(await api.current.call('conceptTree', {}, 'root'));
|
|
161
|
+
refreshGrammar('root');
|
|
162
|
+
return res;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// --- sub-graph split (parent ↔ fork side by side, with a merge preview) ---
|
|
166
|
+
async function openSplit() {
|
|
167
|
+
const forkId = sid();
|
|
168
|
+
if ( forkId === 'root' ) return;
|
|
169
|
+
const parentId = (forks[forkId] && forks[forkId].parent) || 'root';
|
|
170
|
+
const [parentObjects, forkState] = await Promise.all([
|
|
171
|
+
api.current.call('state', {}, parentId).then(( s ) => s.objects),
|
|
172
|
+
api.current.call('state', {}, forkId)
|
|
173
|
+
]);
|
|
174
|
+
const preview = await api.current.call('mergePreview', { template: { $$_id: forkId, forkResult: true }, opts: {} }, forkId).catch(() => null);
|
|
175
|
+
setSplit({ parentId, forkId, parentObjects, forkObjects: forkState.objects, preview });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function selectSession( id ) {
|
|
179
|
+
sessionRef.current = id; setSessionId(id);
|
|
180
|
+
setApplies([]); setLastApply(null); setSelectedId(null);
|
|
181
|
+
setGraph(await api.current.call('state', {}, id));
|
|
182
|
+
refreshGrammar(id);
|
|
183
|
+
}
|
|
184
|
+
async function doFork() {
|
|
185
|
+
const { childId } = await api.current.call('fork', {}, sid());
|
|
186
|
+
await selectSession(childId);
|
|
187
|
+
}
|
|
188
|
+
async function doMerge( childId ) {
|
|
189
|
+
const parent = (forks[childId] && forks[childId].parent) || 'root';
|
|
190
|
+
await api.current.call('merge', { childId, targetId: childId }, parent); // forkResult lands on a <childId> node
|
|
191
|
+
await selectSession(parent);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function runPrompt( text ) {
|
|
195
|
+
setPromptProgress([]); setPromptAnswer(''); setPromptError(''); setPromptRunning(true);
|
|
196
|
+
setApplies([]); setLastApply(null); setSelectedId(null);
|
|
197
|
+
try { await api.current.call('prompt', { text }, sid()); }
|
|
198
|
+
catch ( e ) { setPromptError(e.message); setPromptRunning(false); }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function openEditor( name ) {
|
|
202
|
+
const schema = await api.current.call('getConcept', { nameOrId: name }, sid());
|
|
203
|
+
setEditing({ name, text: JSON.stringify(schema || {}, null, 2), validation: null });
|
|
204
|
+
}
|
|
205
|
+
function editorChange( text ) {
|
|
206
|
+
setEditing(( e ) => ({ ...e, text, validation: null }));
|
|
207
|
+
// in-editor ref-checking (track 4): debounce-validate the schema live as the operator types — reuses the
|
|
208
|
+
// author-time validator (unknown-ref / prose-on-dependency / unparseable-expr checks) so mistakes surface inline.
|
|
209
|
+
if ( valTimer.current ) clearTimeout(valTimer.current);
|
|
210
|
+
valTimer.current = setTimeout(async () => {
|
|
211
|
+
let schema;
|
|
212
|
+
try { schema = JSON.parse(text); }
|
|
213
|
+
catch ( err ) { return setEditing(( e ) => e && { ...e, validation: { ok: false, errors: ['invalid JSON: ' + err.message] } }); }
|
|
214
|
+
const v = await api.current.call('validateConcept', { schema }, sid());
|
|
215
|
+
setEditing(( e ) => e && { ...e, validation: v });
|
|
216
|
+
}, 400);
|
|
217
|
+
}
|
|
218
|
+
async function editorValidate() {
|
|
219
|
+
let schema;
|
|
220
|
+
try { schema = JSON.parse(editing.text); }
|
|
221
|
+
catch ( err ) { return setEditing(( e ) => ({ ...e, validation: { ok: false, errors: ['invalid JSON: ' + err.message] } })); }
|
|
222
|
+
const v = await api.current.call('validateConcept', { schema }, sid());
|
|
223
|
+
setEditing(( e ) => ({ ...e, validation: v }));
|
|
224
|
+
}
|
|
225
|
+
async function editorApply() {
|
|
226
|
+
let schema;
|
|
227
|
+
try { schema = JSON.parse(editing.text); } catch ( err ) { return; }
|
|
228
|
+
await api.current.call('patchConcept', { nameOrId: editing.name, updates: schema }, sid());
|
|
229
|
+
setEditing(null);
|
|
230
|
+
setGraph(await api.current.call('state', {}, sid()));
|
|
231
|
+
}
|
|
232
|
+
// CRUD completion: remove the concept (+ its subtree); un-casts it everywhere, then refresh the tree + grammar.
|
|
233
|
+
async function editorDelete() {
|
|
234
|
+
if ( !editing ) return;
|
|
235
|
+
await api.current.call('deleteConcept', { nameOrId: editing.name }, sid());
|
|
236
|
+
setEditing(null);
|
|
237
|
+
setGraph(await api.current.call('state', {}, sid()));
|
|
238
|
+
setConceptTree(await api.current.call('conceptTree', {}, sid()));
|
|
239
|
+
refreshGrammar(sid());
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function rollback( rev ) {
|
|
243
|
+
await api.current.call('rollback', { rev }, sid());
|
|
244
|
+
setGraph(await api.current.call('state', {}, sid()));
|
|
245
|
+
}
|
|
246
|
+
async function showDiff( a, b ) {
|
|
247
|
+
setDiffResult({ a, b, d: await api.current.call('diff', { a, b }, sid()) });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const selected = selectedId && graph.objects.find(( o ) => o._id === selectedId);
|
|
251
|
+
|
|
252
|
+
return html`
|
|
253
|
+
<div class="studio">
|
|
254
|
+
<header class="toolbar">
|
|
255
|
+
<strong>sg studio</strong>
|
|
256
|
+
<select value=${corpus} onChange=${( e ) => loadCorpus(e.target.value)}>
|
|
257
|
+
<option value="" disabled>corpus…</option>
|
|
258
|
+
${corpora.map(( c ) => html`<option key=${c.name} value=${c.name}>${c.name}</option>`)}
|
|
259
|
+
</select>
|
|
260
|
+
<button disabled=${!corpus} onClick=${() => loadCorpus(corpus, DEMO_SEED)}>seed demo</button>
|
|
261
|
+
<button disabled=${!corpus} onClick=${() => loadCorpus(corpus)}>reset</button>
|
|
262
|
+
<span class="sep"></span>
|
|
263
|
+
<label class="lyt">layout
|
|
264
|
+
<select value=${layout} onChange=${( e ) => setLayout(e.target.value)}>
|
|
265
|
+
${LAYOUT_NAMES.map(( n ) => html`<option key=${n} value=${n}>${n}</option>`)}
|
|
266
|
+
</select>
|
|
267
|
+
</label>
|
|
268
|
+
<button title="re-run layout" onClick=${() => setRelayout(( r ) => r + 1)}>↻</button>
|
|
269
|
+
<span class="sep"></span>
|
|
270
|
+
<div class="viewtoggle">
|
|
271
|
+
<button class=${view === 'data' ? 'on' : ''} onClick=${() => setView('data')}>data</button>
|
|
272
|
+
<button class=${view === 'grammar' ? 'on' : ''} onClick=${() => { setView('grammar'); if ( !grammar ) refreshGrammar(); }}>grammar</button>
|
|
273
|
+
<button class=${view === 'learning' ? 'on' : ''} onClick=${() => { setView('learning'); refreshLearning(); }}>learning</button>
|
|
274
|
+
</div>
|
|
275
|
+
<button title="open two sub-graphs side by side" disabled=${sessionId === 'root'} onClick=${openSplit}>split</button>
|
|
276
|
+
<span class="status ${status}">● ${status}</span>
|
|
277
|
+
<span class="rev">${sessionId !== 'root' ? sessionId + ' · ' : ''}rev ${graph.currentRev}/${graph.revCount} · ${graph.objects.length} obj</span>
|
|
278
|
+
</header>
|
|
279
|
+
<${Timeline} revs=${graph.revs} currentRev=${graph.currentRev} onRollback=${rollback} onDiff=${showDiff} />
|
|
280
|
+
<main class="panels">
|
|
281
|
+
<${ConceptTree} tree=${conceptTree} onEdit=${openEditor} />
|
|
282
|
+
${view === 'learning'
|
|
283
|
+
? html`<${LearningPanel} learning=${learning} verdict=${learnVerdict} onDeclare=${declareKey} onPropose=${proposeAlias} onRetract=${retractAlias} />`
|
|
284
|
+
: view === 'grammar'
|
|
285
|
+
? html`<${GrammarGraph} grammar=${grammar} onSelect=${( id ) => setSelectedId(null)} />`
|
|
286
|
+
: html`<${GraphCanvas} objects=${graph.objects} lastApply=${lastApply} lastRetract=${lastRetract} onSelect=${setSelectedId} layout=${layout} relayout=${relayout} />`}
|
|
287
|
+
${view === 'grammar'
|
|
288
|
+
? html`<${CorpusPanel} manifest=${manifest} grammar=${grammar} onExport=${exportCorpus} onImport=${importCorpus} />`
|
|
289
|
+
: html`<${Inspector} object=${selected} applies=${applies} />`}
|
|
290
|
+
</main>
|
|
291
|
+
<footer class="bottom">
|
|
292
|
+
<div class="trace-wrap">
|
|
293
|
+
<h3>trace</h3>
|
|
294
|
+
<div class="tracerow">
|
|
295
|
+
${applies.slice(-40).map(( r, i ) => html`
|
|
296
|
+
<span key=${i} class="tchip"><b>${r.conceptName}</b>→${r.targetId}</span>`)}
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
<${ForkTree} forks=${forks} active=${sessionId} onFork=${doFork} onSelect=${selectSession} onMerge=${doMerge} />
|
|
300
|
+
<${TilingOverlay} plan=${tiling} />
|
|
301
|
+
<${ProviderTrace} records=${trace} onRefresh=${refreshTrace} />
|
|
302
|
+
</footer>
|
|
303
|
+
<${PromptConsole} onRun=${runPrompt} progress=${promptProgress} answer=${promptAnswer} running=${promptRunning} error=${promptError} />
|
|
304
|
+
<${ConceptEditor} editing=${editing} onChange=${editorChange} onValidate=${editorValidate} onApply=${editorApply} onDelete=${editorDelete} onClose=${() => setEditing(null)} />
|
|
305
|
+
<${DiffPanel} result=${diffResult} onClose=${() => setDiffResult(null)} />
|
|
306
|
+
<${SessionSplit} open=${!!split} parentId=${split && split.parentId} forkId=${split && split.forkId} parentObjects=${split && split.parentObjects} forkObjects=${split && split.forkObjects} preview=${split && split.preview} onClose=${() => setSplit(null)} onMerge=${async () => { if ( split ) { await doMerge(split.forkId); setSplit(null); } }} />
|
|
307
|
+
</div>`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
createRoot(document.getElementById('app')).render(html`<${App} />`);
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
import React from 'react';
|
|
19
|
+
import { html } from 'htm/react';
|
|
20
|
+
|
|
21
|
+
// modal to edit a concept's schema JSON, validate it (author-time), then patch it
|
|
22
|
+
// live — the re-eval cascade is visible on the canvas.
|
|
23
|
+
export function ConceptEditor( { editing, onChange, onValidate, onApply, onDelete, onClose } ) {
|
|
24
|
+
if ( !editing ) return null;
|
|
25
|
+
const { name, text, validation } = editing;
|
|
26
|
+
return html`
|
|
27
|
+
<div class="editor-modal" onClick=${( e ) => { if ( e.target.classList.contains('editor-modal') ) onClose(); }}>
|
|
28
|
+
<div class="em-box">
|
|
29
|
+
<div class="em-head">edit concept <b>${name}</b><button onClick=${onClose}>✕</button></div>
|
|
30
|
+
<textarea class="em-text" spellcheck="false" value=${text} onInput=${( e ) => onChange(e.target.value)}></textarea>
|
|
31
|
+
${validation ? html`
|
|
32
|
+
<div class="em-validation ${validation.ok ? 'ok' : 'bad'}">
|
|
33
|
+
${validation.ok ? '✓ valid' : '✗ ' + (validation.errors || []).join('; ')}
|
|
34
|
+
${(validation.warnings || []).length ? html`<div class="em-warn">⚠ ${validation.warnings.join('; ')}</div>` : null}
|
|
35
|
+
</div>` : null}
|
|
36
|
+
<div class="em-actions">
|
|
37
|
+
<button onClick=${onValidate}>validate</button>
|
|
38
|
+
${onDelete ? html`<button class="danger" onClick=${() => { if ( window.confirm('Delete concept "' + name + '" (and its subtree)? It will un-cast everywhere.') ) onDelete(); }}>delete</button>` : null}
|
|
39
|
+
<button class="primary" disabled=${validation && !validation.ok} onClick=${onApply}>apply (patch)</button>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>`;
|
|
43
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
import React from 'react';
|
|
19
|
+
import { html } from 'htm/react';
|
|
20
|
+
|
|
21
|
+
function children( node, onEdit, depth ) {
|
|
22
|
+
const kids = node && node.childConcepts;
|
|
23
|
+
if ( !kids ) return null;
|
|
24
|
+
return Object.keys(kids).map(( name ) => html`
|
|
25
|
+
<div key=${name} class="cnode">
|
|
26
|
+
<span class="cname" style=${{ paddingLeft: (depth * 12) + 'px' }} onClick=${() => onEdit(name)}>${name}</span>
|
|
27
|
+
${children(kids[name], onEdit, depth + 1)}
|
|
28
|
+
</div>`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// left panel: the loaded concept corpus (sets -> nested concepts); click to edit.
|
|
32
|
+
export function ConceptTree( { tree, onEdit } ) {
|
|
33
|
+
const sets = Object.keys(tree || {});
|
|
34
|
+
return html`
|
|
35
|
+
<aside class="concepts">
|
|
36
|
+
<h3>concepts</h3>
|
|
37
|
+
${sets.length === 0 ? html`<div class="empty">load a corpus</div>` : sets.map(( set ) => html`
|
|
38
|
+
<div key=${set} class="cset">
|
|
39
|
+
<div class="csetname">${set}</div>
|
|
40
|
+
${children(tree[set], onEdit, 0)}
|
|
41
|
+
</div>`)}
|
|
42
|
+
</aside>`;
|
|
43
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
import React, { useRef } from 'react';
|
|
19
|
+
import { html } from 'htm/react';
|
|
20
|
+
|
|
21
|
+
// The corpus side-panel for the grammar view: the derived manifest (produces/consumes alphabet,
|
|
22
|
+
// required providers), the cross-corpus links + silent writer-collisions, a polarity legend, and
|
|
23
|
+
// the .sgc import/export controls. `manifest`/`grammar` come from the corpusManifest/grammarGraph
|
|
24
|
+
// ops; onExport()/onImport(bundle) round-trip the portable bundle.
|
|
25
|
+
export function CorpusPanel( { manifest, grammar, onExport, onImport } ) {
|
|
26
|
+
const fileRef = useRef(null);
|
|
27
|
+
const g = grammar || {};
|
|
28
|
+
const m = manifest || {};
|
|
29
|
+
|
|
30
|
+
function pickFile( e ) {
|
|
31
|
+
const f = e.target.files && e.target.files[0];
|
|
32
|
+
if ( !f ) return;
|
|
33
|
+
const r = new FileReader();
|
|
34
|
+
r.onload = () => { try { onImport(JSON.parse(r.result)); } catch ( err ) { alert('bad .sgc: ' + err.message); } };
|
|
35
|
+
r.readAsText(f);
|
|
36
|
+
e.target.value = '';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return html`
|
|
40
|
+
<aside class="corpus-panel">
|
|
41
|
+
<div class="cp-actions">
|
|
42
|
+
<button class="cp-export" onClick=${onExport}>export .sgc</button>
|
|
43
|
+
<button class="cp-import" onClick=${() => fileRef.current && fileRef.current.click()}>import .sgc</button>
|
|
44
|
+
<input type="file" accept=".sgc,.json" ref=${fileRef} style=${{ display: 'none' }} onChange=${pickFile} />
|
|
45
|
+
</div>
|
|
46
|
+
<div class="cp-legend">
|
|
47
|
+
<span class="lg lg-w">writes</span><span class="lg lg-rp">reads +</span>
|
|
48
|
+
<span class="lg lg-rn">reads − (defeasance)</span><span class="lg lg-sep">separator</span>
|
|
49
|
+
</div>
|
|
50
|
+
${m.name ? html`
|
|
51
|
+
<div class="cp-manifest">
|
|
52
|
+
<div class="cp-row"><b>${m.name}</b> <span class="cp-v">v${m.version}</span> · ${m.conceptCount} concepts</div>
|
|
53
|
+
<div class="cp-row">providers: ${(m.providersRequired || []).length ? (m.providersRequired || []).map(( p ) => html`<span key=${p} class="cp-prov">${p}</span>`) : '∅'}</div>
|
|
54
|
+
<div class="cp-row">produces: <span class="cp-alpha">${(m.alphabet && m.alphabet.produces || []).join(', ')}</span></div>
|
|
55
|
+
<div class="cp-row">consumes: <span class="cp-alpha">${(m.alphabet && m.alphabet.consumes || []).join(', ') || '∅'}</span></div>
|
|
56
|
+
</div>` : null}
|
|
57
|
+
${(g.crossCorpus || []).length ? html`
|
|
58
|
+
<div class="cp-cross">
|
|
59
|
+
<h4>cross-corpus links</h4>
|
|
60
|
+
${g.crossCorpus.map(( l, i ) => html`<div key=${i} class="cp-link">${l.fromSet} <b>${l.fact}</b> → ${l.toSet}</div>`)}
|
|
61
|
+
</div>` : null}
|
|
62
|
+
${(g.collisions || []).length ? html`
|
|
63
|
+
<div class="cp-coll">
|
|
64
|
+
<h4>⚠ fact collisions</h4>
|
|
65
|
+
${g.collisions.map(( c, i ) => html`<div key=${i} class="cp-clash"><b>${c.fact}</b> written by ${c.sets.join(' & ')}</div>`)}
|
|
66
|
+
</div>` : null}
|
|
67
|
+
</aside>`;
|
|
68
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
import React from 'react';
|
|
19
|
+
import { html } from 'htm/react';
|
|
20
|
+
|
|
21
|
+
// the session tree: fork the active graph into an independent child (a sub-agent),
|
|
22
|
+
// switch between sessions, merge a fork's result back into its parent.
|
|
23
|
+
export function ForkTree( { forks, active, onFork, onSelect, onMerge } ) {
|
|
24
|
+
const ids = Object.keys(forks || {});
|
|
25
|
+
return html`
|
|
26
|
+
<div class="forks">
|
|
27
|
+
<div class="fk-head">forks<button onClick=${onFork} title="fork the active session">+ fork</button></div>
|
|
28
|
+
<div class="fk-list">
|
|
29
|
+
${ids.map(( id ) => html`
|
|
30
|
+
<div key=${id} class=${'fk-item' + (id === active ? ' active' : '')}>
|
|
31
|
+
<span class="fk-id" onClick=${() => onSelect(id)}>${id}${forks[id].parent ? '' : ' · root'}</span>
|
|
32
|
+
${forks[id].parent ? html`<button class="fk-merge" title="merge into parent" onClick=${() => onMerge(id)}>merge ↩</button>` : null}
|
|
33
|
+
</div>`)}
|
|
34
|
+
</div>
|
|
35
|
+
</div>`;
|
|
36
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
import React, { useRef, useEffect } from 'react';
|
|
19
|
+
import { html } from 'htm/react';
|
|
20
|
+
import cytoscape from 'cytoscape';
|
|
21
|
+
import fcose from 'cytoscape-fcose';
|
|
22
|
+
|
|
23
|
+
let _reg = false;
|
|
24
|
+
function register() { if ( _reg ) return; try { cytoscape.use(fcose); } catch ( e ) {} _reg = true; }
|
|
25
|
+
|
|
26
|
+
// per-set hue so concepts from different corpora are visually distinct
|
|
27
|
+
const SET_HUES = ['#6ea8fe', '#3ad29f', '#f5a524', '#c084fc', '#fb7185', '#22d3ee'];
|
|
28
|
+
function setColor( sets, set ) { const i = sets.indexOf(set); return SET_HUES[(i < 0 ? 0 : i) % SET_HUES.length]; }
|
|
29
|
+
|
|
30
|
+
// the second, orthogonal graph: concepts ↔ facts. A concept WRITES facts (green) and READS them
|
|
31
|
+
// (blue = positive support, red dashed = a negated / defeasance dependency). Separator facts (the
|
|
32
|
+
// tree-decomposition narrow waist) are highlighted as gold diamonds; entry-point facts are hollow.
|
|
33
|
+
function toElements( g ) {
|
|
34
|
+
const sets = [...new Set(g.concepts.map(( c ) => c.set))].sort();
|
|
35
|
+
const seps = new Set((g.tiling && g.tiling.separators) || []);
|
|
36
|
+
const entry = new Set(g.entryPoints || []);
|
|
37
|
+
const els = [];
|
|
38
|
+
for ( const c of g.concepts )
|
|
39
|
+
els.push({ data: { id: 'c:' + c.name, label: c.name, kind: c.kind, color: setColor(sets, c.set) }, classes: 'concept kind-' + c.kind });
|
|
40
|
+
for ( const f of g.facts ) {
|
|
41
|
+
const cls = ['fact'];
|
|
42
|
+
if ( seps.has(f.key) ) cls.push('separator');
|
|
43
|
+
if ( entry.has(f.key) ) cls.push('entry');
|
|
44
|
+
els.push({ data: { id: 'f:' + f.key, label: f.key }, classes: cls.join(' ') });
|
|
45
|
+
}
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
for ( const e of g.edges ) {
|
|
48
|
+
const a = e.kind === 'writes' ? 'c:' + e.concept : 'f:' + e.fact;
|
|
49
|
+
const b = e.kind === 'writes' ? 'f:' + e.fact : 'c:' + e.concept;
|
|
50
|
+
const cls = e.kind === 'writes' ? 'writes' : (e.polarity === '-' ? 'reads-neg' : 'reads-pos');
|
|
51
|
+
const id = cls + ':' + a + '>' + b;
|
|
52
|
+
if ( seen.has(id) ) continue; // collapse duplicate read edges (assert+ensure on same fact)
|
|
53
|
+
seen.add(id);
|
|
54
|
+
els.push({ data: { id, source: a, target: b }, classes: cls });
|
|
55
|
+
}
|
|
56
|
+
return els;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const STYLE = [
|
|
60
|
+
{ selector: 'node.concept', style: { 'background-color': 'data(color)', 'label': 'data(label)', 'shape': 'round-rectangle', 'color': '#0f1419', 'font-size': '10px', 'font-weight': 'bold', 'text-valign': 'center', 'text-halign': 'center', 'width': 'label', 'height': 22, 'padding': '6px' } },
|
|
61
|
+
{ selector: 'node.kind-llm', style: { 'border-width': 2, 'border-color': '#fde047', 'border-style': 'double' } },
|
|
62
|
+
{ selector: 'node.kind-provider', style: { 'border-width': 2, 'border-color': '#0f1419' } },
|
|
63
|
+
{ selector: 'node.fact', style: { 'background-color': '#1f2733', 'label': 'data(label)', 'shape': 'ellipse', 'color': '#cdd6e0', 'font-size': '9px', 'text-valign': 'center', 'text-halign': 'center', 'width': 'label', 'height': 18, 'padding': '4px', 'border-width': 1, 'border-color': '#3a4757' } },
|
|
64
|
+
{ selector: 'node.entry', style: { 'background-opacity': 0.15, 'border-style': 'dashed' } },
|
|
65
|
+
{ selector: 'node.separator', style: { 'background-color': '#f5a524', 'shape': 'diamond', 'color': '#0f1419', 'border-width': 2, 'border-color': '#fff7e6' } },
|
|
66
|
+
{ selector: 'node:selected', style: { 'border-color': '#fff', 'border-width': 3 } },
|
|
67
|
+
{ selector: 'edge', style: { 'curve-style': 'bezier', 'target-arrow-shape': 'triangle', 'width': 1.5, 'arrow-scale': 0.8 } },
|
|
68
|
+
{ selector: 'edge.writes', style: { 'line-color': '#3ad29f', 'target-arrow-color': '#3ad29f' } },
|
|
69
|
+
{ selector: 'edge.reads-pos', style: { 'line-color': '#6ea8fe', 'target-arrow-color': '#6ea8fe' } },
|
|
70
|
+
{ selector: 'edge.reads-neg', style: { 'line-color': '#fb7185', 'target-arrow-color': '#fb7185', 'line-style': 'dashed' } }
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const FCOSE = { name: 'fcose', animate: true, animationDuration: 350, randomize: true, padding: 40, nodeSeparation: 110, idealEdgeLength: 80 };
|
|
74
|
+
|
|
75
|
+
export function GrammarGraph( { grammar, onSelect } ) {
|
|
76
|
+
const ref = useRef(null);
|
|
77
|
+
const cyRef = useRef(null);
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
register();
|
|
81
|
+
const cy = cytoscape({ container: ref.current, style: STYLE, wheelSensitivity: 0.2 });
|
|
82
|
+
cy.on('tap', 'node', ( e ) => onSelect && onSelect(e.target.id()));
|
|
83
|
+
cyRef.current = cy;
|
|
84
|
+
try { window.__sgGrammarCy = cy; } catch ( e ) {} // puppeteer introspection hook
|
|
85
|
+
return () => cy.destroy();
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
const cy = cyRef.current;
|
|
90
|
+
if ( !cy ) return;
|
|
91
|
+
cy.elements().remove();
|
|
92
|
+
if ( grammar && grammar.concepts ) {
|
|
93
|
+
cy.add(toElements(grammar));
|
|
94
|
+
if ( cy.elements().length ) cy.layout(FCOSE).run();
|
|
95
|
+
}
|
|
96
|
+
}, [grammar]);
|
|
97
|
+
|
|
98
|
+
return html`<div class="cy grammar-cy" ref=${ref}></div>`;
|
|
99
|
+
}
|