fux-engine 0.1.0__py3-none-any.whl
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.
- fux/__init__.py +6 -0
- fux/__main__.py +4 -0
- fux/assets/fux-icon.svg +61 -0
- fux/assets/fux-lockup.svg +69 -0
- fux/assets/fux-mark.svg +26 -0
- fux/assets/graph_boot.js +214 -0
- fux/assets/graph_template.html +111 -0
- fux/astextract.py +410 -0
- fux/bench.py +34 -0
- fux/build.py +29 -0
- fux/capture.py +99 -0
- fux/check.py +143 -0
- fux/cli.py +139 -0
- fux/clicmds.py +162 -0
- fux/cligraph.py +69 -0
- fux/cliquery.py +157 -0
- fux/cliutil.py +13 -0
- fux/community.py +50 -0
- fux/config.py +85 -0
- fux/context.py +31 -0
- fux/costledger.py +119 -0
- fux/coverage.py +43 -0
- fux/data/global/README.md +37 -0
- fux/data/global/rules/async-everywhere.md +22 -0
- fux/data/global/rules/doc-per-code-change.md +23 -0
- fux/data/global/rules/files-max-100-lines.md +20 -0
- fux/data/global/rules/no-secrets-in-vcs.md +21 -0
- fux/data/hooks/_common.sh +12 -0
- fux/data/hooks/post_tool_use.sh +7 -0
- fux/data/hooks/session_start.sh +7 -0
- fux/data/hooks/stop.sh +7 -0
- fux/data/hooks/user_prompt_submit.sh +7 -0
- fux/data/packs/indian-markets-tax/pack.toml +6 -0
- fux/data/packs/indian-markets-tax/rules/capital-gains-equity.md +22 -0
- fux/data/packs/indian-markets-tax/rules/market-hours-nse.md +21 -0
- fux/data/schema.json +48 -0
- fux/data/skills/adr/SKILL.md +37 -0
- fux/data/skills/distill/SKILL.md +66 -0
- fux/data/skills/fetch-rules/SKILL.md +147 -0
- fux/data/skills/fux/SKILL.md +154 -0
- fux/data/skills/plan/SKILL.md +67 -0
- fux/data/skills/savings/SKILL.md +56 -0
- fux/data/skills/trace/SKILL.md +38 -0
- fux/drift.py +27 -0
- fux/embed.py +67 -0
- fux/explain.py +64 -0
- fux/fetchrules.py +122 -0
- fux/findings.py +26 -0
- fux/fix.py +54 -0
- fux/fmwrite.py +46 -0
- fux/frontmatter.py +88 -0
- fux/gate.py +65 -0
- fux/gitutil.py +69 -0
- fux/globs.py +38 -0
- fux/governance.py +55 -0
- fux/graph.py +137 -0
- fux/graphhtml.py +14 -0
- fux/graphquery.py +113 -0
- fux/hookio.py +34 -0
- fux/hooks.py +99 -0
- fux/hybrid.py +78 -0
- fux/importer.py +86 -0
- fux/index.py +45 -0
- fux/initcmd.py +58 -0
- fux/lint.py +96 -0
- fux/loader.py +62 -0
- fux/mcpserver.py +179 -0
- fux/mine.py +96 -0
- fux/model.py +82 -0
- fux/narrative.py +35 -0
- fux/pack.py +53 -0
- fux/parity.py +156 -0
- fux/paths.py +85 -0
- fux/recall.py +131 -0
- fux/report.py +63 -0
- fux/savings.py +192 -0
- fux/scaffold.py +58 -0
- fux/scalars.py +32 -0
- fux/schema.py +54 -0
- fux/seal.py +142 -0
- fux/serve.py +77 -0
- fux/settings.py +52 -0
- fux/stats.py +132 -0
- fux/templates/formula.md +22 -0
- fux/templates/spec.md +29 -0
- fux/touch.py +37 -0
- fux/tour.py +34 -0
- fux/usage.py +60 -0
- fux/verify.py +75 -0
- fux/vexamples.py +144 -0
- fux_engine-0.1.0.dist-info/METADATA +186 -0
- fux_engine-0.1.0.dist-info/RECORD +96 -0
- fux_engine-0.1.0.dist-info/WHEEL +5 -0
- fux_engine-0.1.0.dist-info/entry_points.txt +2 -0
- fux_engine-0.1.0.dist-info/licenses/LICENSE +21 -0
- fux_engine-0.1.0.dist-info/top_level.txt +1 -0
fux/__init__.py
ADDED
fux/__main__.py
ADDED
fux/assets/fux-icon.svg
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
|
|
2
|
+
<defs>
|
|
3
|
+
<!-- tile backgrounds -->
|
|
4
|
+
<linearGradient id="ic-bg" x1="0" y1="0" x2=".6" y2="1">
|
|
5
|
+
<stop offset="0%" stop-color="#16140f"/>
|
|
6
|
+
<stop offset="100%" stop-color="#0a0908"/>
|
|
7
|
+
</linearGradient>
|
|
8
|
+
<radialGradient id="ic-glow" cx="50%" cy="18%" r="70%">
|
|
9
|
+
<stop offset="0%" stop-color="#9ed94a1a"/>
|
|
10
|
+
<stop offset="100%" stop-color="rgba(0,0,0,0)"/>
|
|
11
|
+
</radialGradient>
|
|
12
|
+
<linearGradient id="ic-shine" x1="0" y1="0" x2="0" y2="1">
|
|
13
|
+
<stop offset="0%" stop-color="rgba(255,255,255,.06)"/>
|
|
14
|
+
<stop offset="100%" stop-color="rgba(255,255,255,0)"/>
|
|
15
|
+
</linearGradient>
|
|
16
|
+
<!-- mark gradient -->
|
|
17
|
+
<linearGradient id="ic-mg" x1="0" y1="0" x2="1" y2="1">
|
|
18
|
+
<stop offset="0%" stop-color="#D2F58F"/>
|
|
19
|
+
<stop offset="55%" stop-color="#8FD13F"/>
|
|
20
|
+
<stop offset="100%" stop-color="#4C8A1B"/>
|
|
21
|
+
</linearGradient>
|
|
22
|
+
<clipPath id="ic-mc">
|
|
23
|
+
<rect x="12" y="10" width="40" height="44" rx="10"/>
|
|
24
|
+
</clipPath>
|
|
25
|
+
<filter id="ic-drop" x="-30%" y="-30%" width="160%" height="160%">
|
|
26
|
+
<feGaussianBlur in="SourceGraphic" stdDeviation="2.5" result="blur"/>
|
|
27
|
+
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
|
28
|
+
</filter>
|
|
29
|
+
</defs>
|
|
30
|
+
|
|
31
|
+
<!-- tile base -->
|
|
32
|
+
<rect width="120" height="120" rx="28" fill="url(#ic-bg)" stroke="rgba(255,255,255,.08)" stroke-width="1"/>
|
|
33
|
+
<!-- accent glow overlay -->
|
|
34
|
+
<rect width="120" height="120" rx="28" fill="url(#ic-glow)"/>
|
|
35
|
+
<!-- top-edge shine -->
|
|
36
|
+
<rect width="120" height="60" rx="28" fill="url(#ic-shine)"/>
|
|
37
|
+
|
|
38
|
+
<!-- mark: size=78, centred → offset=(120-78)/2=21 -->
|
|
39
|
+
<svg x="21" y="21" width="78" height="78" viewBox="0 0 64 64">
|
|
40
|
+
<defs>
|
|
41
|
+
<linearGradient id="ic-mg2" x1="0" y1="0" x2="1" y2="1">
|
|
42
|
+
<stop offset="0%" stop-color="#D2F58F"/>
|
|
43
|
+
<stop offset="55%" stop-color="#8FD13F"/>
|
|
44
|
+
<stop offset="100%" stop-color="#4C8A1B"/>
|
|
45
|
+
</linearGradient>
|
|
46
|
+
<clipPath id="ic-mc2">
|
|
47
|
+
<rect x="12" y="10" width="40" height="44" rx="10"/>
|
|
48
|
+
</clipPath>
|
|
49
|
+
</defs>
|
|
50
|
+
<g filter="url(#ic-drop)">
|
|
51
|
+
<rect x="12" y="10" width="40" height="44" rx="10" fill="url(#ic-mg2)"/>
|
|
52
|
+
<g clip-path="url(#ic-mc2)">
|
|
53
|
+
<polygon points="12,10 32,10 12,30" fill="#fff" opacity=".15"/>
|
|
54
|
+
<polygon points="52,54 52,34 32,54" fill="#000" opacity=".17"/>
|
|
55
|
+
</g>
|
|
56
|
+
<path d="M26 10 L38 10 L38 33 L32 28 L26 33 Z" fill="#000" opacity=".26"/>
|
|
57
|
+
<rect x="19" y="42" width="26" height="3" rx="1.5" fill="#000" opacity=".24"/>
|
|
58
|
+
<rect x="19" y="48" width="17" height="3" rx="1.5" fill="#000" opacity=".24"/>
|
|
59
|
+
</g>
|
|
60
|
+
</svg>
|
|
61
|
+
</svg>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 440 110" width="440" height="110">
|
|
2
|
+
<defs>
|
|
3
|
+
<!-- background -->
|
|
4
|
+
<linearGradient id="lk-bg" x1="0" y1="0" x2="0" y2="1">
|
|
5
|
+
<stop offset="0%" stop-color="#121110"/>
|
|
6
|
+
<stop offset="100%" stop-color="#0a0a09"/>
|
|
7
|
+
</linearGradient>
|
|
8
|
+
<!-- vertical rule -->
|
|
9
|
+
<linearGradient id="lk-rule" x1="0" y1="0" x2="0" y2="1">
|
|
10
|
+
<stop offset="0%" stop-color="rgba(255,255,255,0)"/>
|
|
11
|
+
<stop offset="50%" stop-color="rgba(255,255,255,.14)"/>
|
|
12
|
+
<stop offset="100%" stop-color="rgba(255,255,255,0)"/>
|
|
13
|
+
</linearGradient>
|
|
14
|
+
<!-- mark gradient -->
|
|
15
|
+
<linearGradient id="lk-mg" x1="0" y1="0" x2="1" y2="1">
|
|
16
|
+
<stop offset="0%" stop-color="#D2F58F"/>
|
|
17
|
+
<stop offset="55%" stop-color="#8FD13F"/>
|
|
18
|
+
<stop offset="100%" stop-color="#4C8A1B"/>
|
|
19
|
+
</linearGradient>
|
|
20
|
+
<clipPath id="lk-mc">
|
|
21
|
+
<rect x="12" y="10" width="40" height="44" rx="10"/>
|
|
22
|
+
</clipPath>
|
|
23
|
+
<filter id="lk-glow" x="-40%" y="-40%" width="180%" height="180%">
|
|
24
|
+
<feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur"/>
|
|
25
|
+
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
|
26
|
+
</filter>
|
|
27
|
+
<style>
|
|
28
|
+
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@700&family=Space+Mono&display=swap');
|
|
29
|
+
.lk-eyebrow { font: 8.5px 'Space Mono', 'Courier New', monospace; letter-spacing: .34em; fill: #7a756b; }
|
|
30
|
+
.lk-name { font: 700 30px 'Space Grotesk', 'Helvetica Neue', Arial, sans-serif; letter-spacing: .16em; fill: #f2efe9; }
|
|
31
|
+
.lk-role { font: 10px 'Space Mono', 'Courier New', monospace; letter-spacing: .26em; fill: #9ed94a; }
|
|
32
|
+
</style>
|
|
33
|
+
</defs>
|
|
34
|
+
|
|
35
|
+
<!-- plate -->
|
|
36
|
+
<rect width="440" height="110" rx="14" fill="url(#lk-bg)" stroke="rgba(255,255,255,.07)" stroke-width="1"/>
|
|
37
|
+
|
|
38
|
+
<!-- mark: 74×74, vertically centred → y=18 -->
|
|
39
|
+
<svg x="34" y="18" width="74" height="74" viewBox="0 0 64 64" overflow="visible">
|
|
40
|
+
<defs>
|
|
41
|
+
<linearGradient id="lk-mg2" x1="0" y1="0" x2="1" y2="1">
|
|
42
|
+
<stop offset="0%" stop-color="#D2F58F"/>
|
|
43
|
+
<stop offset="55%" stop-color="#8FD13F"/>
|
|
44
|
+
<stop offset="100%" stop-color="#4C8A1B"/>
|
|
45
|
+
</linearGradient>
|
|
46
|
+
<clipPath id="lk-mc2">
|
|
47
|
+
<rect x="12" y="10" width="40" height="44" rx="10"/>
|
|
48
|
+
</clipPath>
|
|
49
|
+
</defs>
|
|
50
|
+
<g filter="url(#lk-glow)">
|
|
51
|
+
<rect x="12" y="10" width="40" height="44" rx="10" fill="url(#lk-mg2)"/>
|
|
52
|
+
<g clip-path="url(#lk-mc2)">
|
|
53
|
+
<polygon points="12,10 32,10 12,30" fill="#fff" opacity=".15"/>
|
|
54
|
+
<polygon points="52,54 52,34 32,54" fill="#000" opacity=".17"/>
|
|
55
|
+
</g>
|
|
56
|
+
<path d="M26 10 L38 10 L38 33 L32 28 L26 33 Z" fill="#000" opacity=".26"/>
|
|
57
|
+
<rect x="19" y="42" width="26" height="3" rx="1.5" fill="#000" opacity=".24"/>
|
|
58
|
+
<rect x="19" y="48" width="17" height="3" rx="1.5" fill="#000" opacity=".24"/>
|
|
59
|
+
</g>
|
|
60
|
+
</svg>
|
|
61
|
+
|
|
62
|
+
<!-- vertical rule: x=130, margin 22px top+bottom → y=22..88 -->
|
|
63
|
+
<rect x="130" y="22" width="1" height="66" fill="url(#lk-rule)"/>
|
|
64
|
+
|
|
65
|
+
<!-- text block: x=153, centred on y=55 -->
|
|
66
|
+
<text x="153" y="30" class="lk-eyebrow">ALPHA FORGE</text>
|
|
67
|
+
<text x="153" y="68" class="lk-name">FUX</text>
|
|
68
|
+
<text x="153" y="88" class="lk-role">KNOWLEDGE INDEX</text>
|
|
69
|
+
</svg>
|
fux/assets/fux-mark.svg
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="fux-g" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0%" stop-color="#D2F58F"/>
|
|
5
|
+
<stop offset="55%" stop-color="#8FD13F"/>
|
|
6
|
+
<stop offset="100%" stop-color="#4C8A1B"/>
|
|
7
|
+
</linearGradient>
|
|
8
|
+
<clipPath id="fux-c">
|
|
9
|
+
<rect x="12" y="10" width="40" height="44" rx="10"/>
|
|
10
|
+
</clipPath>
|
|
11
|
+
<filter id="fux-glow" x="-30%" y="-30%" width="160%" height="160%">
|
|
12
|
+
<feGaussianBlur in="SourceGraphic" stdDeviation="2.5" result="blur"/>
|
|
13
|
+
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
|
14
|
+
</filter>
|
|
15
|
+
</defs>
|
|
16
|
+
<g filter="url(#fux-glow)">
|
|
17
|
+
<rect x="12" y="10" width="40" height="44" rx="10" fill="url(#fux-g)"/>
|
|
18
|
+
<g clip-path="url(#fux-c)">
|
|
19
|
+
<polygon points="12,10 32,10 12,30" fill="#fff" opacity=".15"/>
|
|
20
|
+
<polygon points="52,54 52,34 32,54" fill="#000" opacity=".17"/>
|
|
21
|
+
</g>
|
|
22
|
+
<path d="M26 10 L38 10 L38 33 L32 28 L26 33 Z" fill="#000" opacity=".26"/>
|
|
23
|
+
<rect x="19" y="42" width="26" height="3" rx="1.5" fill="#000" opacity=".24"/>
|
|
24
|
+
<rect x="19" y="48" width="17" height="3" rx="1.5" fill="#000" opacity=".24"/>
|
|
25
|
+
</g>
|
|
26
|
+
</svg>
|
fux/assets/graph_boot.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// Fux graph — vanilla canvas force viewer. Offline, dependency-free.
|
|
2
|
+
const cv = document.getElementById("cv"), ctx = cv.getContext("2d");
|
|
3
|
+
const tip = document.getElementById("tip"), $ = id => document.getElementById(id);
|
|
4
|
+
const SIDE = 290;
|
|
5
|
+
|
|
6
|
+
const nodes = DATA.nodes.map(n => ({ ...n, x: Math.random()*900-450, y: Math.random()*900-450, vx:0, vy:0 }));
|
|
7
|
+
const byId = Object.fromEntries(nodes.map(n => [n.id, n]));
|
|
8
|
+
const edges = DATA.edges.filter(e => byId[e.source] && byId[e.target]);
|
|
9
|
+
const deg = {}; edges.forEach(e => { deg[e.source]=(deg[e.source]||0)+1; deg[e.target]=(deg[e.target]||0)+1; });
|
|
10
|
+
const maxDeg = Math.max(1, ...Object.values(deg));
|
|
11
|
+
const adj = {}; nodes.forEach(n => adj[n.id] = []);
|
|
12
|
+
edges.forEach(e => { adj[e.source].push([e.target,e,1]); adj[e.target].push([e.source,e,-1]); });
|
|
13
|
+
|
|
14
|
+
let view = { x: 0, y: 0, k: 1 }, hidden = new Set(), hiddenE = new Set();
|
|
15
|
+
let selected = null, hover = null, query = "", colorMode = "type";
|
|
16
|
+
let running = true, showLabels = true, focusSet = null;
|
|
17
|
+
let linkDist = 70, charge = 900;
|
|
18
|
+
|
|
19
|
+
// ---- colour modes -------------------------------------------------------
|
|
20
|
+
const ccolor = c => "hsl(" + (((c||0)*67)%360) + ",62%,55%)";
|
|
21
|
+
const heat = d => { const t = (deg[d]||0)/maxDeg; return `hsl(${(1-t)*210},80%,${35+t*25}%)`; };
|
|
22
|
+
function nodeColor(n){
|
|
23
|
+
if (colorMode === "community") return ccolor(n.community);
|
|
24
|
+
if (colorMode === "layer") return LAYER_COLORS[n.layer] || color(n.type);
|
|
25
|
+
if (colorMode === "degree") return heat(n.id);
|
|
26
|
+
return color(n.type);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---- sidebar: stats, filters, controls ----------------------------------
|
|
30
|
+
$("stats").textContent =
|
|
31
|
+
`${nodes.length} nodes · ${edges.length} edges · ${DATA.meta.code_files} files · `+
|
|
32
|
+
`${DATA.meta.rules} rules · ${DATA.meta.communities} communities`;
|
|
33
|
+
|
|
34
|
+
const typeCounts = {}; nodes.forEach(n => typeCounts[n.type]=(typeCounts[n.type]||0)+1);
|
|
35
|
+
const types = Object.keys(typeCounts).sort();
|
|
36
|
+
$("filters").innerHTML = types.map(t =>
|
|
37
|
+
`<label><span class="sw" style="background:${color(t)}"></span>
|
|
38
|
+
<input type="checkbox" data-t="${t}" checked> ${t}<span class="ct">${typeCounts[t]}</span></label>`).join("");
|
|
39
|
+
document.querySelectorAll("[data-t]").forEach(cb => cb.onchange = () => {
|
|
40
|
+
cb.checked ? hidden.delete(cb.dataset.t) : hidden.add(cb.dataset.t); });
|
|
41
|
+
|
|
42
|
+
const edgeCounts = {}; edges.forEach(e => edgeCounts[e.type]=(edgeCounts[e.type]||0)+1);
|
|
43
|
+
const etypes = Object.keys(edgeCounts).sort();
|
|
44
|
+
$("efilters").innerHTML = etypes.map(t =>
|
|
45
|
+
`<label><span class="ln" style="border-color:${edgeColor(t)}"></span>
|
|
46
|
+
<input type="checkbox" data-e="${t}" checked> ${t}<span class="ct">${edgeCounts[t]}</span></label>`).join("");
|
|
47
|
+
document.querySelectorAll("[data-e]").forEach(cb => cb.onchange = () => {
|
|
48
|
+
cb.checked ? hiddenE.delete(cb.dataset.e) : hiddenE.add(cb.dataset.e); });
|
|
49
|
+
|
|
50
|
+
$("ntoggle").onclick = () => toggleAll("[data-t]", hidden, t=>t.dataset.t);
|
|
51
|
+
$("etoggle").onclick = () => toggleAll("[data-e]", hiddenE, t=>t.dataset.e);
|
|
52
|
+
function toggleAll(sel, set, key){ const boxes=[...document.querySelectorAll(sel)];
|
|
53
|
+
const any=boxes.some(b=>b.checked); boxes.forEach(b=>{ b.checked=!any;
|
|
54
|
+
!any ? set.delete(key(b)) : set.add(key(b)); }); }
|
|
55
|
+
|
|
56
|
+
$("cmode").onchange = e => colorMode = e.target.value;
|
|
57
|
+
$("q").oninput = e => { query = e.target.value.toLowerCase(); updateHits(); };
|
|
58
|
+
$("slink").oninput = e => linkDist = +e.target.value;
|
|
59
|
+
$("scharge").oninput = e => charge = +e.target.value;
|
|
60
|
+
$("bpause").onclick = () => { running=!running; $("bpause").textContent = running?"Pause":"Resume";
|
|
61
|
+
$("bpause").classList.toggle("on",!running); if(running) draw(); };
|
|
62
|
+
$("blabels").onclick = () => { showLabels=!showLabels; $("blabels").classList.toggle("on",showLabels); };
|
|
63
|
+
$("bfit").onclick = fit; $("breset").onclick = () => { view={x:0,y:0,k:1}; };
|
|
64
|
+
$("bfocus").onclick = () => { if(selected) setFocus(selected); };
|
|
65
|
+
$("bclear").onclick = clearFocus;
|
|
66
|
+
$("bcopy").onclick = () => selected && copy(nodeMarkdown(byId[selected]), "node copied");
|
|
67
|
+
$("bexport").onclick = () => copy(graphMarkdown(), "visible graph copied");
|
|
68
|
+
|
|
69
|
+
function updateHits(){ if(!query){ $("qhits").textContent=""; return; }
|
|
70
|
+
const m = nodes.filter(n => match(n)).length; $("qhits").textContent = `${m} match`; }
|
|
71
|
+
const match = n => query && (n.label.toLowerCase().includes(query) || n.id.toLowerCase().includes(query));
|
|
72
|
+
|
|
73
|
+
// ---- layout & geometry --------------------------------------------------
|
|
74
|
+
function resize(){ cv.width = innerWidth-SIDE; cv.height = innerHeight; } resize(); onresize = resize;
|
|
75
|
+
const inFocus = n => !focusSet || focusSet.has(n.id);
|
|
76
|
+
const visible = n => !hidden.has(n.type) && inFocus(n);
|
|
77
|
+
const eVisible = e => !hiddenE.has(e.type) && visible(byId[e.source]) && visible(byId[e.target]);
|
|
78
|
+
|
|
79
|
+
// Physics runs every PHYS_STRIDE render frames so rendering stays at ≥30 fps
|
|
80
|
+
// even when the simulation is expensive (large graphs).
|
|
81
|
+
const PHYS_STRIDE = nodes.length > 600 ? 2 : 1;
|
|
82
|
+
let _drawFrame = 0;
|
|
83
|
+
|
|
84
|
+
function step(){
|
|
85
|
+
const vis = nodes.filter(visible);
|
|
86
|
+
// Process each pair once (i<j) and apply equal-and-opposite forces to both
|
|
87
|
+
// nodes — halves the O(n²) work vs the naive double-loop.
|
|
88
|
+
for (let i=0; i<vis.length; i++){
|
|
89
|
+
const a=vis[i];
|
|
90
|
+
for (let j=i+1; j<vis.length; j++){
|
|
91
|
+
const b=vis[j];
|
|
92
|
+
let dx=a.x-b.x, dy=a.y-b.y, d=Math.hypot(dx,dy)||1;
|
|
93
|
+
if(d<280){ const f=charge/(d*d*d);
|
|
94
|
+
a.vx+=dx*f; a.vy+=dy*f; b.vx-=dx*f; b.vy-=dy*f; } } }
|
|
95
|
+
for (const e of edges){ if(!eVisible(e)) continue; const a=byId[e.source], b=byId[e.target];
|
|
96
|
+
let dx=b.x-a.x, dy=b.y-a.y, d=Math.hypot(dx,dy)||1, f=(d-linkDist)*0.01;
|
|
97
|
+
a.vx+=dx/d*f; a.vy+=dy/d*f; b.vx-=dx/d*f; b.vy-=dy/d*f; }
|
|
98
|
+
for (const n of vis){ n.vx*=.85; n.vy*=.85; n.x+=n.vx*0.5; n.y+=n.vy*0.5;
|
|
99
|
+
n.vx-=n.x*0.0007; n.vy-=n.y*0.0007; }
|
|
100
|
+
}
|
|
101
|
+
const T = n => ({ x: n.x*view.k+cv.width/2+view.x, y: n.y*view.k+cv.height/2+view.y });
|
|
102
|
+
const radius = n => Math.min(4+(deg[n.id]||0)*0.8, 16)*view.k;
|
|
103
|
+
const neighbors = id => new Set(adj[id].map(([t])=>t));
|
|
104
|
+
|
|
105
|
+
function fit(){ const vis = nodes.filter(visible); if(!vis.length) return;
|
|
106
|
+
const xs=vis.map(n=>n.x), ys=vis.map(n=>n.y);
|
|
107
|
+
const minX=Math.min(...xs),maxX=Math.max(...xs),minY=Math.min(...ys),maxY=Math.max(...ys);
|
|
108
|
+
const w=maxX-minX||1, h=maxY-minY||1;
|
|
109
|
+
view.k = Math.max(0.2, Math.min(2.5, 0.85*Math.min(cv.width/w, cv.height/h)));
|
|
110
|
+
view.x = -(minX+maxX)/2*view.k; view.y = -(minY+maxY)/2*view.k; }
|
|
111
|
+
|
|
112
|
+
function setFocus(id){ focusSet = new Set([id, ...neighbors(id)]); }
|
|
113
|
+
function clearFocus(){ focusSet = null; }
|
|
114
|
+
|
|
115
|
+
// ---- render -------------------------------------------------------------
|
|
116
|
+
function draw(){
|
|
117
|
+
_drawFrame++;
|
|
118
|
+
if(running && _drawFrame % PHYS_STRIDE === 0) step();
|
|
119
|
+
ctx.clearRect(0,0,cv.width,cv.height);
|
|
120
|
+
const near = selected ? neighbors(selected) : (hover ? neighbors(hover) : null);
|
|
121
|
+
const anchor = selected || hover;
|
|
122
|
+
for (const e of edges){ if(!eVisible(e)) continue; const a=byId[e.source], b=byId[e.target];
|
|
123
|
+
const on = !anchor || e.source===anchor || e.target===anchor;
|
|
124
|
+
ctx.globalAlpha = on ? 0.9 : 0.12; ctx.strokeStyle = edgeColor(e.type);
|
|
125
|
+
ctx.lineWidth = on ? 1.4 : 0.8;
|
|
126
|
+
const p=T(a), q=T(b); ctx.beginPath(); ctx.moveTo(p.x,p.y); ctx.lineTo(q.x,q.y); ctx.stroke();
|
|
127
|
+
if(on && view.k>0.7) arrow(p,q,radius(b)); }
|
|
128
|
+
ctx.globalAlpha=1;
|
|
129
|
+
for (const n of nodes){ if(!visible(n)) continue; const p=T(n), r=radius(n);
|
|
130
|
+
const dim = (anchor && near && !near.has(n.id) && n.id!==anchor) || (query && !match(n));
|
|
131
|
+
ctx.globalAlpha = dim ? 0.18 : 1; ctx.fillStyle = nodeColor(n);
|
|
132
|
+
ctx.beginPath(); ctx.arc(p.x,p.y,r,0,7); ctx.fill();
|
|
133
|
+
if(n.id===selected){ ctx.strokeStyle="#fff"; ctx.lineWidth=2; ctx.stroke(); }
|
|
134
|
+
else if(query && match(n)){ ctx.strokeStyle="#ffd33d"; ctx.lineWidth=2; ctx.stroke(); }
|
|
135
|
+
if(showLabels && (view.k>1.1 || n.id===anchor || (near&&near.has(n.id)) || (query&&match(n)))){
|
|
136
|
+
ctx.globalAlpha = dim?0.2:0.95; ctx.fillStyle="#c9d1d9"; ctx.font="10px ui-sans-serif";
|
|
137
|
+
ctx.fillText(n.label, p.x+r+3, p.y+3); } }
|
|
138
|
+
ctx.globalAlpha=1; requestAnimationFrame(draw);
|
|
139
|
+
}
|
|
140
|
+
function arrow(p,q,rad){ const a=Math.atan2(q.y-p.y,q.x-p.x), ex=q.x-Math.cos(a)*rad, ey=q.y-Math.sin(a)*rad;
|
|
141
|
+
ctx.beginPath(); ctx.moveTo(ex,ey);
|
|
142
|
+
ctx.lineTo(ex-Math.cos(a-0.4)*6, ey-Math.sin(a-0.4)*6);
|
|
143
|
+
ctx.lineTo(ex-Math.cos(a+0.4)*6, ey-Math.sin(a+0.4)*6); ctx.closePath();
|
|
144
|
+
ctx.fillStyle=ctx.strokeStyle; ctx.fill(); }
|
|
145
|
+
|
|
146
|
+
// ---- interaction --------------------------------------------------------
|
|
147
|
+
function hit(mx,my){ for(const n of nodes){ if(!visible(n)) continue; const p=T(n);
|
|
148
|
+
if(Math.hypot(mx-p.x,my-p.y) < radius(n)+3) return n; } }
|
|
149
|
+
let drag=null, pan=false, last=null;
|
|
150
|
+
cv.onmousedown = e => { const n=hit(e.offsetX,e.offsetY);
|
|
151
|
+
if(n){ drag=n; selected=n.id; showDetail(n); } else { pan=true; selected=null; clearDetail(); }
|
|
152
|
+
last={x:e.offsetX,y:e.offsetY}; };
|
|
153
|
+
cv.onmousemove = e => { const n=hit(e.offsetX,e.offsetY); hover = n?n.id:null;
|
|
154
|
+
if(n && !drag){ tip.style.display="block"; tip.style.left=(e.clientX+12)+"px"; tip.style.top=(e.clientY+12)+"px";
|
|
155
|
+
tip.innerHTML = `<b>${esc(n.label)}</b> · ${n.type}`+(n.file?`<br>${esc(n.file)}${n.line?":"+n.line:""}`:"")+
|
|
156
|
+
`<br>${deg[n.id]||0} edges`; }
|
|
157
|
+
else if(!drag) tip.style.display="none";
|
|
158
|
+
if(drag){ drag.x+=(e.offsetX-last.x)/view.k; drag.y+=(e.offsetY-last.y)/view.k; drag.vx=drag.vy=0; }
|
|
159
|
+
else if(pan){ view.x+=e.offsetX-last.x; view.y+=e.offsetY-last.y; }
|
|
160
|
+
last={x:e.offsetX,y:e.offsetY}; };
|
|
161
|
+
onmouseup = () => { drag=null; pan=false; };
|
|
162
|
+
cv.ondblclick = e => { const n=hit(e.offsetX,e.offsetY); if(n){ selected=n.id; setFocus(n.id); showDetail(n); } };
|
|
163
|
+
cv.onwheel = e => { e.preventDefault(); const f=e.deltaY<0?1.1:0.9;
|
|
164
|
+
const mx=e.offsetX-cv.width/2-view.x, my=e.offsetY-cv.height/2-view.y;
|
|
165
|
+
view.k=Math.max(0.15,Math.min(5,view.k*f)); view.x-=mx*(f-1); view.y-=my*(f-1); };
|
|
166
|
+
|
|
167
|
+
onkeydown = e => { if(e.target.tagName==="INPUT"||e.target.tagName==="SELECT"){ if(e.key==="Escape")e.target.blur(); return; }
|
|
168
|
+
const k=e.key.toLowerCase();
|
|
169
|
+
if(k==="/"){ e.preventDefault(); $("q").focus(); }
|
|
170
|
+
else if(k==="f") fit(); else if(k==="r") view={x:0,y:0,k:1};
|
|
171
|
+
else if(k===" "){ e.preventDefault(); $("bpause").click(); }
|
|
172
|
+
else if(k==="e"){ if(selected) setFocus(selected); }
|
|
173
|
+
else if(k==="l") $("blabels").click();
|
|
174
|
+
else if(k==="escape"){ clearFocus(); selected=null; clearDetail(); } };
|
|
175
|
+
|
|
176
|
+
// ---- detail panel + agent export ---------------------------------------
|
|
177
|
+
function showDetail(n){ $("agentrow").style.display="flex";
|
|
178
|
+
const groups={}; for(const [tid,e,dir] of adj[n.id]){ (groups[e.type]=groups[e.type]||[])
|
|
179
|
+
.push({id:tid,dir,sym:dir>0?"→":"←"}); }
|
|
180
|
+
let meta = `<b>${esc(n.label)}</b><br><span class="muted">${n.type}`+
|
|
181
|
+
(n.file?` · ${esc(n.file)}${n.line?":"+n.line:""}`:"")+`</span><br>`;
|
|
182
|
+
const pills=[]; if(n.domain)pills.push(n.domain); if(n.layer)pills.push(n.layer);
|
|
183
|
+
if(n.status)pills.push(n.status); if(n.community!=null)pills.push("community "+n.community);
|
|
184
|
+
pills.push((deg[n.id]||0)+" edges");
|
|
185
|
+
meta += pills.map(p=>`<span class="pill">${esc(p)}</span>`).join("")+"<br>";
|
|
186
|
+
for(const t of Object.keys(groups).sort()){ meta += `<br><span class="muted">${t}</span><br>`+
|
|
187
|
+
groups[t].slice(0,40).map(x=>`<span class="nb" data-go="${esc(x.id)}">${x.sym} ${esc(byId[x.id].label)}</span>`).join("<br>"); }
|
|
188
|
+
$("detail").innerHTML = meta;
|
|
189
|
+
$("detail").querySelectorAll("[data-go]").forEach(el => el.onclick = () => {
|
|
190
|
+
selected = el.dataset.go; showDetail(byId[selected]);
|
|
191
|
+
const p=byId[selected]; view.x=-p.x*view.k; view.y=-p.y*view.k; }); }
|
|
192
|
+
function clearDetail(){ $("agentrow").style.display="none";
|
|
193
|
+
$("detail").innerHTML="Click a node. Double-click to focus its neighbourhood."; }
|
|
194
|
+
|
|
195
|
+
function nodeMarkdown(n){ let s=`### ${n.label} (${n.type})\n`;
|
|
196
|
+
if(n.file)s+=`- file: ${n.file}${n.line?":"+n.line:""}\n`;
|
|
197
|
+
for(const f of ["domain","layer","status","community"]) if(n[f]!=null) s+=`- ${f}: ${n[f]}\n`;
|
|
198
|
+
s+=`- degree: ${deg[n.id]||0}\n\n**Connections**\n`;
|
|
199
|
+
for(const [tid,e,dir] of adj[n.id]) s+=`- ${e.type} ${dir>0?"→":"←"} ${byId[tid].label} (${byId[tid].type})\n`;
|
|
200
|
+
return s; }
|
|
201
|
+
function graphMarkdown(){ const vis=nodes.filter(visible);
|
|
202
|
+
let s=`# Fux graph (visible subset)\n${vis.length} nodes, `+
|
|
203
|
+
edges.filter(eVisible).length+` edges\n\n## Nodes by type\n`;
|
|
204
|
+
const byT={}; vis.forEach(n=>(byT[n.type]=byT[n.type]||[]).push(n.label));
|
|
205
|
+
for(const t of Object.keys(byT).sort()) s+=`- **${t}** (${byT[t].length}): ${byT[t].slice(0,30).join(", ")}\n`;
|
|
206
|
+
s+=`\n## Edges\n`; edges.filter(eVisible).slice(0,200).forEach(e=>
|
|
207
|
+
s+=`- ${byId[e.source].label} —${e.type}→ ${byId[e.target].label}\n`);
|
|
208
|
+
return s; }
|
|
209
|
+
function copy(text,msg){ navigator.clipboard?.writeText(text).then(()=>toast(msg),()=>toast("copy blocked")); }
|
|
210
|
+
function toast(m){ const t=$("toast"); t.textContent=m; t.style.display="block";
|
|
211
|
+
clearTimeout(t._h); t._h=setTimeout(()=>t.style.display="none",1500); }
|
|
212
|
+
const esc = s => String(s).replace(/[&<>]/g,c=>({"&":"&","<":"<",">":">"}[c]));
|
|
213
|
+
|
|
214
|
+
fit(); draw();
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Fux — knowledge graph</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { color-scheme: dark; --bg:#0e1116; --panel:#161b22; --line:#30363d;
|
|
9
|
+
--fg:#e6edf3; --muted:#8b949e; --accent:#58a6ff; }
|
|
10
|
+
* { box-sizing: border-box; }
|
|
11
|
+
body { margin: 0; font: 13px/1.45 ui-sans-serif, system-ui, sans-serif;
|
|
12
|
+
background: var(--bg); color: var(--fg); overflow: hidden; }
|
|
13
|
+
#side { position: fixed; top: 0; left: 0; width: 290px; height: 100%;
|
|
14
|
+
background: var(--panel); border-right: 1px solid var(--line); padding: 14px 14px 40px;
|
|
15
|
+
overflow-y: auto; scrollbar-width: thin; }
|
|
16
|
+
h1 { font-size: 15px; margin: 0 0 2px; display:flex; align-items:center; gap:7px; }
|
|
17
|
+
h2 { font-size: 11px; text-transform: uppercase; letter-spacing:.06em;
|
|
18
|
+
color: var(--muted); margin: 16px 0 6px; border-bottom:1px solid var(--line); padding-bottom:3px; }
|
|
19
|
+
.muted { color: var(--muted); font-size: 11px; }
|
|
20
|
+
input[type=search], select { width: 100%; padding: 6px 8px; background: var(--bg);
|
|
21
|
+
color: var(--fg); border: 1px solid var(--line); border-radius: 6px; font: inherit; }
|
|
22
|
+
input[type=search] { margin: 10px 0 4px; }
|
|
23
|
+
.row { display:flex; gap:6px; flex-wrap:wrap; margin:6px 0; }
|
|
24
|
+
button { flex:1; min-width:0; padding:5px 6px; background: var(--bg); color: var(--fg);
|
|
25
|
+
border:1px solid var(--line); border-radius:6px; cursor:pointer; font: inherit; white-space:nowrap; }
|
|
26
|
+
button:hover { border-color: var(--accent); color: var(--accent); }
|
|
27
|
+
button.on { background:#1f6feb33; border-color:var(--accent); color:var(--accent); }
|
|
28
|
+
label { display: flex; align-items: center; gap: 6px; padding: 1px 0; cursor: pointer; font-size:12px; }
|
|
29
|
+
label .ct { margin-left:auto; color:var(--muted); font-size:10px; }
|
|
30
|
+
.sw { width: 11px; height: 11px; border-radius: 3px; flex:none; }
|
|
31
|
+
.ln { width: 14px; height: 0; border-top:2px solid; flex:none; }
|
|
32
|
+
.slider { display:flex; align-items:center; gap:8px; margin:4px 0; font-size:11px; color:var(--muted); }
|
|
33
|
+
.slider input { flex:1; }
|
|
34
|
+
#detail b { color: var(--accent); }
|
|
35
|
+
#detail .nb { cursor:pointer; } #detail .nb:hover { color: var(--accent); }
|
|
36
|
+
#detail .pill { display:inline-block; padding:1px 6px; border:1px solid var(--line);
|
|
37
|
+
border-radius:10px; margin:0 4px 4px 0; font-size:10px; color:var(--muted); }
|
|
38
|
+
canvas { position: fixed; left: 290px; top: 0; cursor: grab; }
|
|
39
|
+
canvas:active { cursor: grabbing; }
|
|
40
|
+
#tip { position: fixed; pointer-events: none; background: #161b22ee; max-width:320px;
|
|
41
|
+
border: 1px solid var(--line); border-radius: 6px; padding: 5px 9px;
|
|
42
|
+
font-size: 11px; display: none; z-index:5; }
|
|
43
|
+
#toast { position: fixed; bottom: 16px; left: 306px; background:#1f6feb; color:#fff;
|
|
44
|
+
padding:7px 12px; border-radius:6px; font-size:12px; display:none; z-index:6; }
|
|
45
|
+
#help { position: fixed; right: 14px; top: 12px; background:#161b22ee; border:1px solid var(--line);
|
|
46
|
+
border-radius:8px; padding:10px 14px; font-size:11px; color:var(--muted); z-index:5; }
|
|
47
|
+
#help b { color: var(--fg); }
|
|
48
|
+
kbd { background:var(--bg); border:1px solid var(--line); border-radius:4px; padding:0 4px; color:var(--fg); }
|
|
49
|
+
</style>
|
|
50
|
+
</head>
|
|
51
|
+
<body>
|
|
52
|
+
<div id="side">
|
|
53
|
+
<h1><svg width="20" height="20" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" style="display:block;flex:none;filter:drop-shadow(0 0 6px #9ed94a55)"><defs><linearGradient id="hg" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#D2F58F"/><stop offset="55%" stop-color="#8FD13F"/><stop offset="100%" stop-color="#4C8A1B"/></linearGradient><clipPath id="hc"><rect x="12" y="10" width="40" height="44" rx="10"/></clipPath></defs><rect x="12" y="10" width="40" height="44" rx="10" fill="url(#hg)"/><g clip-path="url(#hc)"><polygon points="12,10 32,10 12,30" fill="#fff" opacity=".15"/><polygon points="52,54 52,34 32,54" fill="#000" opacity=".17"/></g><path d="M26 10 L38 10 L38 33 L32 28 L26 33 Z" fill="#000" opacity=".26"/><rect x="19" y="42" width="26" height="3" rx="1.5" fill="#000" opacity=".24"/><rect x="19" y="48" width="17" height="3" rx="1.5" fill="#000" opacity=".24"/></svg>Fux graph</h1>
|
|
54
|
+
<div class="muted" id="stats"></div>
|
|
55
|
+
<input type="search" id="q" placeholder="search nodes… ( / )" />
|
|
56
|
+
<div class="muted" id="qhits"></div>
|
|
57
|
+
|
|
58
|
+
<h2>Colour by</h2>
|
|
59
|
+
<select id="cmode">
|
|
60
|
+
<option value="type">node type</option>
|
|
61
|
+
<option value="community">community</option>
|
|
62
|
+
<option value="layer">rule layer</option>
|
|
63
|
+
<option value="degree">degree (heat)</option>
|
|
64
|
+
</select>
|
|
65
|
+
|
|
66
|
+
<div class="row">
|
|
67
|
+
<button id="bfit" title="Fit all (f)">Fit</button>
|
|
68
|
+
<button id="breset" title="Reset view (r)">Reset</button>
|
|
69
|
+
<button id="bpause" title="Pause layout (space)">Pause</button>
|
|
70
|
+
<button id="blabels" class="on" title="Toggle labels (l)">Labels</button>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="row">
|
|
73
|
+
<button id="bfocus" title="Isolate selection + neighbours (e)">Focus sel.</button>
|
|
74
|
+
<button id="bclear" title="Clear focus (Esc)">Unfocus</button>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="slider"><span>link</span><input type="range" id="slink" min="30" max="200" value="70"></div>
|
|
77
|
+
<div class="slider"><span>charge</span><input type="range" id="scharge" min="200" max="2000" value="900"></div>
|
|
78
|
+
|
|
79
|
+
<h2>Node types <span class="muted" id="ntoggle" style="cursor:pointer;float:right">all/none</span></h2>
|
|
80
|
+
<div id="filters"></div>
|
|
81
|
+
<h2>Edge types <span class="muted" id="etoggle" style="cursor:pointer;float:right">all/none</span></h2>
|
|
82
|
+
<div id="efilters"></div>
|
|
83
|
+
|
|
84
|
+
<h2>Inspect</h2>
|
|
85
|
+
<div id="detail" class="muted">Click a node. Double-click to focus its neighbourhood.</div>
|
|
86
|
+
<div class="row" id="agentrow" style="display:none">
|
|
87
|
+
<button id="bcopy" title="Copy this node + neighbours as markdown for an agent">Copy node ⧉</button>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="row"><button id="bexport" title="Copy the visible sub-graph as markdown">Copy visible graph ⧉</button></div>
|
|
90
|
+
</div>
|
|
91
|
+
<canvas id="cv"></canvas>
|
|
92
|
+
<div id="tip"></div>
|
|
93
|
+
<div id="toast"></div>
|
|
94
|
+
<div id="help"><b>keys</b> <kbd>/</kbd> search · <kbd>f</kbd> fit · <kbd>r</kbd> reset ·
|
|
95
|
+
<kbd>space</kbd> pause · <kbd>e</kbd> focus · <kbd>Esc</kbd> clear · <kbd>l</kbd> labels</div>
|
|
96
|
+
<script>
|
|
97
|
+
const DATA = __GRAPH_DATA__;
|
|
98
|
+
const COLORS = { "code-file":"#7d8590","function":"#3fb950","class":"#a371f7",
|
|
99
|
+
"rule":"#58a6ff","formula":"#d29922","glossary":"#56d4dd","invariant":"#f85149",
|
|
100
|
+
"adr":"#db61a2","edge-case":"#e3b341","convention":"#79c0ff","regulatory":"#ff7b72",
|
|
101
|
+
"runbook":"#3fb950","narrative":"#bc8cff","memory":"#f0883e","spec":"#a5d6ff","task":"#ffa657" };
|
|
102
|
+
const EDGE_COLORS = { "contains":"#30363d","calls":"#3fb950","references":"#6e7681",
|
|
103
|
+
"governs":"#58a6ff","related":"#8957e5","depends-on":"#d29922","supersedes":"#db61a2",
|
|
104
|
+
"contradicts":"#f85149","implements":"#56d4dd" };
|
|
105
|
+
const LAYER_COLORS = { "project":"#58a6ff","global":"#3fb950" };
|
|
106
|
+
const color = t => COLORS[t] || "#8b949e";
|
|
107
|
+
const edgeColor = t => EDGE_COLORS[t] || "#6e7681";
|
|
108
|
+
__BOOT__
|
|
109
|
+
</script>
|
|
110
|
+
</body>
|
|
111
|
+
</html>
|