@launchsecure/launch-kit 0.0.16 → 0.0.18
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/dist/chart-client/assets/index--120d9P9.css +1 -0
- package/dist/chart-client/assets/index-D7x8nz-H.js +441 -0
- package/dist/chart-client/index.html +2 -2
- package/dist/client/assets/index-Bf8zdL3x.css +32 -0
- package/dist/client/assets/index-Ds9UP_cj.js +291 -0
- package/dist/client/index.html +2 -2
- package/dist/council-client/assets/index-CofZh7pS.css +1 -0
- package/dist/council-client/assets/index-Dc41S-R2.js +198 -0
- package/dist/council-client/index.html +21 -0
- package/dist/deck-client/assets/_baseUniq-2gclQXo7.js +1 -0
- package/dist/deck-client/assets/arc-DcMY5Wm0.js +1 -0
- package/dist/deck-client/assets/architectureDiagram-Q4EWVU46-B8iirmmJ.js +36 -0
- package/dist/deck-client/assets/blockDiagram-DXYQGD6D-B4JBLjmJ.js +132 -0
- package/dist/deck-client/assets/c4Diagram-AHTNJAMY-CojrJAk8.js +10 -0
- package/dist/deck-client/assets/channel-ERh5jKXV.js +1 -0
- package/dist/deck-client/assets/chunk-4BX2VUAB-Bmb_BMDo.js +1 -0
- package/dist/deck-client/assets/chunk-4TB4RGXK-CumBy8qe.js +206 -0
- package/dist/deck-client/assets/chunk-55IACEB6-Ka8Hb1wD.js +1 -0
- package/dist/deck-client/assets/chunk-EDXVE4YY-B3sIPiQo.js +1 -0
- package/dist/deck-client/assets/chunk-FMBD7UC4-C1tYkaqu.js +15 -0
- package/dist/deck-client/assets/chunk-OYMX7WX6-D7Wacbky.js +231 -0
- package/dist/deck-client/assets/chunk-QZHKN3VN-ChXI0vO3.js +1 -0
- package/dist/deck-client/assets/chunk-YZCP3GAM-BXhiqf8u.js +1 -0
- package/dist/deck-client/assets/classDiagram-6PBFFD2Q-CMi1Gaev.js +1 -0
- package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-CMi1Gaev.js +1 -0
- package/dist/deck-client/assets/clone-DfWhlD4X.js +1 -0
- package/dist/deck-client/assets/cose-bilkent-S5V4N54A-Bqp3p68D.js +1 -0
- package/dist/deck-client/assets/cytoscape.esm-BQk4lpUV.js +331 -0
- package/dist/deck-client/assets/dagre-KV5264BT-BS-rtyhZ.js +4 -0
- package/dist/deck-client/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/deck-client/assets/diagram-5BDNPKRD-BIrj9YGI.js +10 -0
- package/dist/deck-client/assets/diagram-G4DWMVQ6-noHWPIg4.js +24 -0
- package/dist/deck-client/assets/diagram-MMDJMWI5-C2qHxvqV.js +43 -0
- package/dist/deck-client/assets/diagram-TYMM5635-BytnGQr-.js +24 -0
- package/dist/deck-client/assets/erDiagram-SMLLAGMA-BfK5m2YQ.js +85 -0
- package/dist/deck-client/assets/flowDiagram-DWJPFMVM-Cq925G1Z.js +162 -0
- package/dist/deck-client/assets/ganttDiagram-T4ZO3ILL-DhhHPAmj.js +292 -0
- package/dist/deck-client/assets/gitGraphDiagram-UUTBAWPF-B3Lc0h9q.js +106 -0
- package/dist/deck-client/assets/graph-RTawgVWm.js +1 -0
- package/dist/deck-client/assets/index-765AIQ9z.css +1 -0
- package/dist/deck-client/assets/index-BfIfJXmS.js +476 -0
- package/dist/deck-client/assets/infoDiagram-42DDH7IO-BlR584kX.js +2 -0
- package/dist/deck-client/assets/init-Gi6I4Gst.js +1 -0
- package/dist/deck-client/assets/ishikawaDiagram-UXIWVN3A-DygKoNGY.js +70 -0
- package/dist/deck-client/assets/journeyDiagram-VCZTEJTY-BnaiYp9N.js +139 -0
- package/dist/deck-client/assets/kanban-definition-6JOO6SKY-BQBUBzJC.js +89 -0
- package/dist/deck-client/assets/katex-DkKDou_j.js +257 -0
- package/dist/deck-client/assets/layout-DeZ8HI1T.js +1 -0
- package/dist/deck-client/assets/linear-C6roLi_9.js +1 -0
- package/dist/deck-client/assets/min-CbUksbuI.js +1 -0
- package/dist/deck-client/assets/mindmap-definition-QFDTVHPH-iNxV62yN.js +96 -0
- package/dist/deck-client/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/deck-client/assets/pieDiagram-DEJITSTG-DHVA0jaG.js +30 -0
- package/dist/deck-client/assets/quadrantDiagram-34T5L4WZ-DBeKKLUQ.js +7 -0
- package/dist/deck-client/assets/requirementDiagram-MS252O5E-CBwITx7p.js +84 -0
- package/dist/deck-client/assets/sankeyDiagram-XADWPNL6-BtE-1YTU.js +10 -0
- package/dist/deck-client/assets/sequenceDiagram-FGHM5R23-DN96yPP2.js +157 -0
- package/dist/deck-client/assets/stateDiagram-FHFEXIEX-VUkKC2uJ.js +1 -0
- package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-CA0IjulK.js +1 -0
- package/dist/deck-client/assets/timeline-definition-GMOUNBTQ-oUeZhRns.js +120 -0
- package/dist/deck-client/assets/vennDiagram-DHZGUBPP-D87fK90n.js +34 -0
- package/dist/deck-client/assets/wardley-RL74JXVD-DYbYcpDp.js +162 -0
- package/dist/deck-client/assets/wardleyDiagram-NUSXRM2D-Ca_i0QRA.js +20 -0
- package/dist/deck-client/assets/xychartDiagram-5P7HB3ND-CUOJVIvq.js +7 -0
- package/dist/deck-client/index.html +21 -0
- package/dist/server/chart-serve.js +258 -273
- package/dist/server/cli.js +305 -713
- package/dist/server/council-entry.js +1418 -0
- package/dist/server/council-serve.js +1039 -0
- package/dist/server/deck-mcp-entry.js +1789 -0
- package/dist/server/deck-serve.js +1275 -0
- package/dist/server/deck-server/deck-mcp-entry.js +1789 -0
- package/dist/server/deck-server/deck-serve.js +1275 -0
- package/dist/server/fb-wizard.js +0 -0
- package/dist/server/graph-mcp-entry.js +268 -701
- package/dist/server/server/chart-serve.js +4643 -0
- package/dist/server/server/cli.js +13360 -0
- package/dist/server/server/fb-wizard.js +136 -0
- package/dist/server/server/graph-mcp-entry.js +6776 -0
- package/package.json +25 -18
- package/dist/chart-client/assets/index-BpQPtTuo.js +0 -441
- package/dist/chart-client/assets/index-CbZ13AXL.css +0 -1
- package/dist/client/assets/index-3ENenBk-.js +0 -291
- package/dist/client/assets/index-BCYw64M7.css +0 -32
|
@@ -0,0 +1,1275 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/deck-server/deck-serve.ts
|
|
31
|
+
var deck_serve_exports = {};
|
|
32
|
+
__export(deck_serve_exports, {
|
|
33
|
+
broadcastToClients: () => broadcastToClients,
|
|
34
|
+
consumeRenderError: () => consumeRenderError,
|
|
35
|
+
createFeedbackWaiter: () => createFeedbackWaiter,
|
|
36
|
+
resolveFeedback: () => resolveFeedback,
|
|
37
|
+
runServeCli: () => runServeCli,
|
|
38
|
+
startDeckServer: () => startDeckServer
|
|
39
|
+
});
|
|
40
|
+
module.exports = __toCommonJS(deck_serve_exports);
|
|
41
|
+
var import_node_http = __toESM(require("node:http"));
|
|
42
|
+
var import_node_fs3 = __toESM(require("node:fs"));
|
|
43
|
+
var import_node_path3 = __toESM(require("node:path"));
|
|
44
|
+
var import_ws = require("ws");
|
|
45
|
+
|
|
46
|
+
// src/deck-server/lockfile.ts
|
|
47
|
+
var import_node_child_process = require("node:child_process");
|
|
48
|
+
var import_node_fs = require("node:fs");
|
|
49
|
+
var import_node_os = require("node:os");
|
|
50
|
+
var import_node_path = require("node:path");
|
|
51
|
+
function lockDir(projectRoot) {
|
|
52
|
+
if (projectRoot) {
|
|
53
|
+
return (0, import_node_path.join)(projectRoot, ".launchsecure");
|
|
54
|
+
}
|
|
55
|
+
return (0, import_node_path.join)((0, import_node_os.homedir)(), ".launchsecure");
|
|
56
|
+
}
|
|
57
|
+
function lockPath(projectRoot) {
|
|
58
|
+
return (0, import_node_path.join)(lockDir(projectRoot), "launch-deck.lock");
|
|
59
|
+
}
|
|
60
|
+
var _activeProjectRoot;
|
|
61
|
+
function readLock(projectRoot) {
|
|
62
|
+
const root = projectRoot ?? _activeProjectRoot;
|
|
63
|
+
const p = lockPath(root);
|
|
64
|
+
if (!(0, import_node_fs.existsSync)(p)) return null;
|
|
65
|
+
try {
|
|
66
|
+
const data = JSON.parse((0, import_node_fs.readFileSync)(p, "utf-8"));
|
|
67
|
+
if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
|
|
68
|
+
return data;
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function isPidAlive(pid) {
|
|
74
|
+
try {
|
|
75
|
+
process.kill(pid, 0);
|
|
76
|
+
return true;
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function getListenerPid(port) {
|
|
82
|
+
try {
|
|
83
|
+
const out = (0, import_node_child_process.execFileSync)("lsof", ["-nP", "-iTCP:" + port, "-sTCP:LISTEN", "-t"], {
|
|
84
|
+
encoding: "utf-8",
|
|
85
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
86
|
+
timeout: 500
|
|
87
|
+
}).trim();
|
|
88
|
+
if (!out) return null;
|
|
89
|
+
const pid = parseInt(out.split("\n")[0], 10);
|
|
90
|
+
return Number.isFinite(pid) ? pid : null;
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function getLiveLock(projectRoot) {
|
|
96
|
+
const root = projectRoot ?? _activeProjectRoot;
|
|
97
|
+
const lock = readLock(root);
|
|
98
|
+
if (!lock) return null;
|
|
99
|
+
const listenerPid = getListenerPid(lock.port);
|
|
100
|
+
const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
|
|
101
|
+
if (!live) {
|
|
102
|
+
try {
|
|
103
|
+
(0, import_node_fs.unlinkSync)(lockPath(root));
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
return lock;
|
|
109
|
+
}
|
|
110
|
+
function writeLock(data, projectRoot) {
|
|
111
|
+
const root = projectRoot ?? _activeProjectRoot;
|
|
112
|
+
(0, import_node_fs.mkdirSync)(lockDir(root), { recursive: true });
|
|
113
|
+
(0, import_node_fs.writeFileSync)(lockPath(root), JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
114
|
+
if (root) _activeProjectRoot = root;
|
|
115
|
+
}
|
|
116
|
+
function clearLock(projectRoot) {
|
|
117
|
+
const root = projectRoot ?? _activeProjectRoot;
|
|
118
|
+
try {
|
|
119
|
+
(0, import_node_fs.unlinkSync)(lockPath(root));
|
|
120
|
+
} catch {
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/deck-server/config.ts
|
|
125
|
+
var import_node_fs2 = require("node:fs");
|
|
126
|
+
var import_node_path2 = require("node:path");
|
|
127
|
+
var CONFIG_FILENAME = ".launchdeck.json";
|
|
128
|
+
function loadDeckConfig(rootDir) {
|
|
129
|
+
const configPath = (0, import_node_path2.join)(rootDir, CONFIG_FILENAME);
|
|
130
|
+
if (!(0, import_node_fs2.existsSync)(configPath)) return {};
|
|
131
|
+
try {
|
|
132
|
+
return JSON.parse((0, import_node_fs2.readFileSync)(configPath, "utf-8"));
|
|
133
|
+
} catch {
|
|
134
|
+
return {};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function saveDeckConfig(rootDir, config) {
|
|
138
|
+
const configPath = (0, import_node_path2.join)(rootDir, CONFIG_FILENAME);
|
|
139
|
+
(0, import_node_fs2.writeFileSync)(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/deck-server/blast-radius-render.ts
|
|
143
|
+
function generateBlastRadiusHtml(manifest, baseUrl) {
|
|
144
|
+
const data = JSON.stringify(manifest);
|
|
145
|
+
const dlBase = baseUrl ? baseUrl.replace("/deck-files/", "/deck-download/") : "";
|
|
146
|
+
return `<!DOCTYPE html>
|
|
147
|
+
<html lang="en">
|
|
148
|
+
<head>
|
|
149
|
+
<meta charset="UTF-8">
|
|
150
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
151
|
+
<title>Blast Radius \u2014 ${escHtml(manifest.title)}</title>
|
|
152
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
153
|
+
<script src="https://unpkg.com/lucide@latest"></script>
|
|
154
|
+
<style>
|
|
155
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
156
|
+
body{background:#08090e;color:#e2e8f0;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text','Segoe UI',system-ui,sans-serif;overflow:hidden;height:100vh;width:100vw}
|
|
157
|
+
.header{position:fixed;top:0;left:0;right:0;z-index:20;padding:16px 24px 12px;background:linear-gradient(180deg,rgba(8,9,14,.98) 60%,rgba(8,9,14,0) 100%);pointer-events:none}
|
|
158
|
+
.header>*{pointer-events:auto}
|
|
159
|
+
.header h1{font-size:10px;font-weight:600;color:#64748b;letter-spacing:1.2px;text-transform:uppercase}
|
|
160
|
+
.header h2{font-size:18px;font-weight:700;color:#f1f5f9;margin-top:4px}
|
|
161
|
+
.header h2 span{color:#b91c1c}
|
|
162
|
+
.stats{display:flex;gap:24px;margin-top:10px;flex-wrap:wrap;align-items:center}
|
|
163
|
+
.stat-val{font-size:22px;font-weight:700;line-height:1}
|
|
164
|
+
.stat-label{font-size:9px;color:#475569;text-transform:uppercase;letter-spacing:.6px;margin-top:2px}
|
|
165
|
+
.stat-sep{width:1px;height:28px;background:#334155}
|
|
166
|
+
.filter-panel{position:fixed;top:16px;right:24px;z-index:20;display:flex;flex-direction:column;gap:3px;background:rgba(8,9,14,.92);padding:14px;border-radius:12px;backdrop-filter:blur(12px);border:1px solid rgba(100,116,139,.12);min-width:160px}
|
|
167
|
+
.fp-section{font-size:8px;color:#475569;text-transform:uppercase;letter-spacing:1.2px;margin-top:10px;padding:0 6px}
|
|
168
|
+
.fp-section:first-child{margin-top:0}
|
|
169
|
+
.fp-btn{display:flex;align-items:center;gap:10px;font-size:12px;color:#64748b;background:none;border:1px solid transparent;border-radius:6px;padding:7px 10px;cursor:pointer;transition:all .15s;width:100%;text-align:left}
|
|
170
|
+
.fp-btn:hover{background:rgba(51,65,85,.5);color:#cbd5e1}
|
|
171
|
+
.fp-btn.active{background:rgba(99,102,241,.12);color:#e2e8f0;border-color:rgba(99,102,241,.2)}
|
|
172
|
+
.fp-toggle{display:flex;align-items:center;justify-content:space-between;font-size:12px;color:#64748b;background:rgba(30,41,59,.5);border:1px solid rgba(100,116,139,.15);border-radius:6px;padding:7px 10px;cursor:pointer;transition:all .15s;width:100%;text-align:left}
|
|
173
|
+
.fp-toggle:hover{background:rgba(51,65,85,.6);color:#cbd5e1}
|
|
174
|
+
.fp-toggle .toggle-dot{width:32px;height:16px;border-radius:8px;background:#334155;position:relative;transition:all .2s;flex-shrink:0}
|
|
175
|
+
.fp-toggle .toggle-dot::after{content:'';position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:50%;background:#64748b;transition:all .2s}
|
|
176
|
+
.fp-toggle.on .toggle-dot{background:rgba(99,102,241,.4)}
|
|
177
|
+
.fp-toggle.on .toggle-dot::after{left:18px;background:#818cf8}
|
|
178
|
+
.fp-toggle.on{color:#e2e8f0}
|
|
179
|
+
.fp-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
|
|
180
|
+
.fp-ring{width:10px;height:10px;border-radius:50%;border:2px solid;flex-shrink:0;background:transparent}
|
|
181
|
+
.fp-icon{width:16px;height:16px;flex-shrink:0;display:flex;align-items:center;justify-content:center}
|
|
182
|
+
.fp-icon svg{width:16px;height:16px}
|
|
183
|
+
.fp-edge-line{width:16px;height:2px;border-radius:1px;flex-shrink:0}
|
|
184
|
+
.detail-panel{position:fixed;right:24px;top:50%;transform:translateY(-50%);z-index:100;display:none;width:340px;max-height:70vh;overflow-y:auto;background:rgba(15,23,42,.97);border:1px solid rgba(100,116,139,.2);border-radius:12px;padding:0;backdrop-filter:blur(16px);box-shadow:0 16px 48px rgba(0,0,0,.7)}
|
|
185
|
+
.dp-header{padding:16px 18px 12px;border-bottom:1px solid rgba(100,116,139,.1)}
|
|
186
|
+
.dp-name{font-weight:700;font-size:16px}
|
|
187
|
+
.dp-badges{display:flex;gap:5px;margin-top:8px;flex-wrap:wrap}
|
|
188
|
+
.dp-badge{font-size:10px;padding:3px 9px;border-radius:4px;font-weight:600;letter-spacing:.3px}
|
|
189
|
+
.dp-body{padding:14px 18px 16px}
|
|
190
|
+
.dp-section{margin-top:12px}
|
|
191
|
+
.dp-section:first-child{margin-top:0}
|
|
192
|
+
.dp-section-label{font-size:9px;color:#475569;text-transform:uppercase;letter-spacing:1px;margin-bottom:4px;font-weight:600}
|
|
193
|
+
.dp-path{color:#64748b;font-size:11px;word-break:break-all;font-family:'SF Mono','Fira Code',monospace;background:rgba(100,116,139,.08);padding:6px 10px;border-radius:6px}
|
|
194
|
+
.dp-reason{color:#cbd5e1;font-size:12px;line-height:1.6}
|
|
195
|
+
.dp-connections{list-style:none;padding:0}
|
|
196
|
+
.dp-connections li{font-size:11px;color:#94a3b8;padding:3px 0;display:flex;align-items:center;gap:6px}
|
|
197
|
+
.dp-close{position:absolute;top:12px;right:12px;background:none;border:none;color:#64748b;cursor:pointer;font-size:18px;line-height:1;padding:4px}
|
|
198
|
+
.dp-close:hover{color:#e2e8f0}
|
|
199
|
+
.controls{display:none}
|
|
200
|
+
.ring-label{font-size:8px;fill:#475569;text-anchor:start;font-weight:600;letter-spacing:1.5px;text-transform:uppercase}
|
|
201
|
+
.edge-tooltip{position:fixed;z-index:200;pointer-events:none;background:rgba(15,23,42,.95);border:1px solid rgba(100,116,139,.25);border-radius:8px;padding:8px 12px;backdrop-filter:blur(8px);font-size:11px;line-height:1.5;max-width:260px;display:none}
|
|
202
|
+
.edge-tooltip .et-action{font-weight:700;font-size:10px;letter-spacing:.5px;text-transform:uppercase}
|
|
203
|
+
.edge-tooltip .et-nodes{color:#94a3b8}
|
|
204
|
+
.edge-tooltip .et-label{color:#cbd5e1;margin-top:2px;font-style:italic}
|
|
205
|
+
</style>
|
|
206
|
+
</head>
|
|
207
|
+
<body>
|
|
208
|
+
<div class="header">
|
|
209
|
+
<h1>${escHtml(manifest.subtitle || (manifest.mode === "structural" ? "Structural Blast Radius" : "Feature Blast Radius"))}</h1>
|
|
210
|
+
<h2><span>${escHtml(manifest.title)}</span></h2>
|
|
211
|
+
<div class="stats" id="stats"></div>
|
|
212
|
+
</div>
|
|
213
|
+
<div class="filter-panel" id="filterPanel"></div>
|
|
214
|
+
<div class="controls" id="controls"></div>
|
|
215
|
+
<div class="detail-panel" id="detail">
|
|
216
|
+
<button class="dp-close" id="dpClose">×</button>
|
|
217
|
+
<div class="dp-header">
|
|
218
|
+
<div class="dp-name" id="dpName"></div>
|
|
219
|
+
<div class="dp-badges" id="dpBadges"></div>
|
|
220
|
+
</div>
|
|
221
|
+
<div class="dp-body">
|
|
222
|
+
<div class="dp-section" id="dpPathSection">
|
|
223
|
+
<div class="dp-section-label">File</div>
|
|
224
|
+
<div class="dp-path" id="dpPath"></div>
|
|
225
|
+
</div>
|
|
226
|
+
<div class="dp-section" id="dpReasonSection">
|
|
227
|
+
<div class="dp-section-label">Why is this affected?</div>
|
|
228
|
+
<div class="dp-reason" id="dpReason"></div>
|
|
229
|
+
</div>
|
|
230
|
+
<div class="dp-section" id="dpAcceptanceSection">
|
|
231
|
+
<div class="dp-section-label">Acceptance Criteria</div>
|
|
232
|
+
<ul class="dp-connections" id="dpAcceptance"></ul>
|
|
233
|
+
</div>
|
|
234
|
+
<div class="dp-section" id="dpConnSection">
|
|
235
|
+
<div class="dp-section-label">Connected to</div>
|
|
236
|
+
<ul class="dp-connections" id="dpConn"></ul>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
<div class="edge-tooltip" id="edgeTooltip"></div>
|
|
241
|
+
<svg id="graph"></svg>
|
|
242
|
+
<script>
|
|
243
|
+
var M = ${data};
|
|
244
|
+
var DL_BASE = '${dlBase}';
|
|
245
|
+
|
|
246
|
+
var layerMap = {};
|
|
247
|
+
M.layers.forEach(function(l) { layerMap[l.id] = l; });
|
|
248
|
+
var ringMap = {};
|
|
249
|
+
M.rings.forEach(function(r) { ringMap[r.id] = r; });
|
|
250
|
+
var centerColor = '#b91c1c';
|
|
251
|
+
|
|
252
|
+
// \u2500\u2500 Edge Action Styles \u2500\u2500
|
|
253
|
+
var edgeActionStyles = {
|
|
254
|
+
create: {color:'#22c55e', dash:'', opacity:0.55, width:2},
|
|
255
|
+
modify: {color:'#f59e0b', dash:'7,4', opacity:0.55, width:2},
|
|
256
|
+
remove: {color:'#ef4444', dash:'4,4', opacity:0.5, width:1.8},
|
|
257
|
+
existing: {color:'#1e293b', dash:'', opacity:0.25, width:1}
|
|
258
|
+
};
|
|
259
|
+
var defaultEdgeStyle = edgeActionStyles.existing;
|
|
260
|
+
function edgeStyle(d){return edgeActionStyles[d.action]||defaultEdgeStyle;}
|
|
261
|
+
var hasActionEdges = M.edges.some(function(e){return e.action && e.action!=='existing';});
|
|
262
|
+
|
|
263
|
+
// \u2500\u2500 Stats \u2500\u2500
|
|
264
|
+
var statsEl = document.getElementById('stats');
|
|
265
|
+
M.rings.forEach(function(r) {
|
|
266
|
+
var count = M.nodes.filter(function(n){return n.ring===r.id}).length;
|
|
267
|
+
var d = document.createElement('div');
|
|
268
|
+
d.innerHTML = '<div class="stat-val" style="color:'+r.color+'">'+count+'</div><div class="stat-label">'+r.name+'</div>';
|
|
269
|
+
statsEl.appendChild(d);
|
|
270
|
+
});
|
|
271
|
+
(function(){var d=document.createElement('div');d.innerHTML='<div class="stat-val" style="color:#e2e8f0">'+M.layers.length+'</div><div class="stat-label">Layers</div>';statsEl.appendChild(d);})();
|
|
272
|
+
|
|
273
|
+
// Edge action stats (only when action edges exist)
|
|
274
|
+
if(hasActionEdges){
|
|
275
|
+
var sep=document.createElement('div');sep.className='stat-sep';statsEl.appendChild(sep);
|
|
276
|
+
['create','modify','remove'].forEach(function(a){
|
|
277
|
+
var count=M.edges.filter(function(e){return e.action===a;}).length;
|
|
278
|
+
if(count===0)return;
|
|
279
|
+
var s=edgeActionStyles[a];
|
|
280
|
+
var d=document.createElement('div');
|
|
281
|
+
d.innerHTML='<div class="stat-val" style="color:'+s.color+'">'+count+'</div><div class="stat-label">'+a.toUpperCase()+' edges</div>';
|
|
282
|
+
statsEl.appendChild(d);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// \u2500\u2500 Filter Panel (left vertical) \u2500\u2500
|
|
287
|
+
var fpEl = document.getElementById('filterPanel');
|
|
288
|
+
var fpHtml = '';
|
|
289
|
+
fpHtml += '<div class="fp-section">Toggles</div>';
|
|
290
|
+
fpHtml += '<button class="fp-toggle on" id="btnLabels">Aa Labels<div class="toggle-dot"></div></button>';
|
|
291
|
+
fpHtml += '<button class="fp-toggle on" id="btnRings">Ring colors<div class="toggle-dot"></div></button>';
|
|
292
|
+
fpHtml += '<div class="fp-section" style="margin-top:4px"></div>';
|
|
293
|
+
fpHtml += '<button class="fp-btn" id="btnReset">\\u21ba Reset zoom</button>';
|
|
294
|
+
fpHtml += '<div class="fp-section">Ring</div>';
|
|
295
|
+
fpHtml += '<button class="fp-btn active" id="rAll" data-ring="all"><div class="fp-dot" style="background:#94a3b8"></div> All</button>';
|
|
296
|
+
M.rings.forEach(function(r){fpHtml+='<button class="fp-btn" id="r_'+r.id+'" data-ring="'+r.id+'"><div class="fp-ring" style="border-color:'+r.color+'"></div> '+r.name+'</button>';});
|
|
297
|
+
fpHtml += '<div class="fp-section">Layer</div>';
|
|
298
|
+
fpHtml += '<button class="fp-btn active" id="lAll" data-layer="all"><div class="fp-dot" style="background:#94a3b8"></div> All</button>';
|
|
299
|
+
M.layers.forEach(function(l){fpHtml+='<button class="fp-btn" id="l_'+l.id+'" data-layer="'+l.id+'"><div class="fp-icon" id="fpi_'+l.id+'" style="color:'+l.color+'"></div> '+l.name+'</button>';});
|
|
300
|
+
|
|
301
|
+
// Edge Action filter (only when action edges exist)
|
|
302
|
+
if(hasActionEdges){
|
|
303
|
+
fpHtml += '<div class="fp-section">Edge Action</div>';
|
|
304
|
+
fpHtml += '<button class="fp-btn active" id="eAll" data-edge="all"><div class="fp-dot" style="background:#94a3b8"></div> All</button>';
|
|
305
|
+
['create','modify','remove','existing'].forEach(function(a){
|
|
306
|
+
var count=M.edges.filter(function(e){return(e.action||'existing')===a;}).length;
|
|
307
|
+
if(count===0)return;
|
|
308
|
+
var s=edgeActionStyles[a];
|
|
309
|
+
var dashStyle=s.dash?'background:repeating-linear-gradient(90deg,'+s.color+' 0,'+s.color+' 4px,transparent 4px,transparent 7px)':'background:'+s.color;
|
|
310
|
+
fpHtml+='<button class="fp-btn" id="e_'+a+'" data-edge="'+a+'"><div class="fp-edge-line" style="'+dashStyle+'"></div> '+a.charAt(0).toUpperCase()+a.slice(1)+' <span style="color:#475569;font-size:10px">('+count+')</span></button>';
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
fpEl.innerHTML = fpHtml;
|
|
315
|
+
|
|
316
|
+
var ringsOn = true;
|
|
317
|
+
|
|
318
|
+
// Attach filter panel handlers via event delegation
|
|
319
|
+
fpEl.addEventListener('click', function(e) {
|
|
320
|
+
var btn = e.target.closest('.fp-btn,.fp-toggle');
|
|
321
|
+
if (!btn) return;
|
|
322
|
+
if (btn.id === 'btnReset') { svg.transition().duration(400).call(zoomBehavior.transform, d3.zoomIdentity); return; }
|
|
323
|
+
if (btn.id === 'btnLabels') { labelsOn=!labelsOn; labelEls.transition().duration(200).attr('opacity',labelsOn?1:0); btn.classList.toggle('on',labelsOn); return; }
|
|
324
|
+
if (btn.id === 'btnRings') {
|
|
325
|
+
ringsOn=!ringsOn;
|
|
326
|
+
btn.classList.toggle('on',ringsOn);
|
|
327
|
+
// Hide/show node circles (not center)
|
|
328
|
+
nodeEls.select('.node-bg')
|
|
329
|
+
.transition().duration(300)
|
|
330
|
+
.attr('stroke',function(d){
|
|
331
|
+
if(d.hop===0) return centerColor;
|
|
332
|
+
return ringsOn?(ringMap[d.ring]?ringMap[d.ring].color:'#666'):'transparent';
|
|
333
|
+
})
|
|
334
|
+
.attr('fill',function(d){
|
|
335
|
+
if(d.hop===0) return '#1f0a0a';
|
|
336
|
+
return ringsOn?'rgba(8,9,14,.85)':'transparent';
|
|
337
|
+
})
|
|
338
|
+
.attr('stroke-width',function(d){if(d.hop===0) return 3; return ringsOn?3:0;});
|
|
339
|
+
// Concentric ring circles: colored when on, grey when off
|
|
340
|
+
g.selectAll('.ring-circle').transition().duration(300)
|
|
341
|
+
.attr('stroke',function(){return ringsOn?d3.select(this).attr('data-color'):'#334155';})
|
|
342
|
+
.attr('stroke-opacity',ringsOn?0.3:0.15);
|
|
343
|
+
// Hide/show edges \u2014 restore action-based opacity when turning back on
|
|
344
|
+
edgePaths.transition().duration(300).attr('opacity',function(d){return ringsOn?edgeStyle(d).opacity:0;});
|
|
345
|
+
// Scale icons (not center)
|
|
346
|
+
nodeEls.each(function(d){
|
|
347
|
+
if(d.hop===0) return;
|
|
348
|
+
var fo=d3.select(this).select('foreignObject');
|
|
349
|
+
if(fo.empty()) return;
|
|
350
|
+
var r=nR(d);
|
|
351
|
+
var s=ringsOn?Math.round(r*1.1):Math.round(r*2.5);
|
|
352
|
+
fo.attr('x',-s/2).attr('y',-s/2).attr('width',s).attr('height',s);
|
|
353
|
+
fo.select('div').style('width',s+'px').style('height',s+'px');
|
|
354
|
+
var icon=fo.select('svg');
|
|
355
|
+
if(!icon.empty()){
|
|
356
|
+
icon.attr('width',s).attr('height',s)
|
|
357
|
+
.style('width',s+'px').style('height',s+'px')
|
|
358
|
+
.style('color','#cbd5e1')
|
|
359
|
+
.style('fill',ringsOn?'rgba(203,213,225,0.15)':'rgba(203,213,225,0.3)')
|
|
360
|
+
.style('stroke-width',ringsOn?'1.5':'1');
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (btn.dataset && btn.dataset.ring !== undefined) { filterRing(btn.dataset.ring); return; }
|
|
366
|
+
if (btn.dataset && btn.dataset.layer !== undefined) { filterLayer(btn.dataset.layer); return; }
|
|
367
|
+
if (btn.dataset && btn.dataset.edge !== undefined) { filterEdgeAction(btn.dataset.edge); return; }
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// \u2500\u2500 Layout \u2500\u2500
|
|
371
|
+
var W = window.innerWidth, H = window.innerHeight;
|
|
372
|
+
var cx = W/2, cy = H/2 + 25;
|
|
373
|
+
var R = Math.min(W, H);
|
|
374
|
+
var ringIds = M.rings.map(function(r){return r.id});
|
|
375
|
+
var ringRadii = {};
|
|
376
|
+
ringIds.forEach(function(id, i){ ringRadii[id] = R * (0.14 + i * 0.13); });
|
|
377
|
+
|
|
378
|
+
var centerNode = {id:'__center__',name:M.center.name,layer:M.layers[0]?M.layers[0].id:'ui',ring:'__center__',path:'',reason:M.center.description,type:'feature',hop:0};
|
|
379
|
+
var allNodes = [centerNode].concat(M.nodes.map(function(n){var copy={};for(var k in n)copy[k]=n[k];copy.hop=ringIds.indexOf(n.ring)+1;return copy;}));
|
|
380
|
+
|
|
381
|
+
centerNode.fx = cx; centerNode.fy = cy;
|
|
382
|
+
ringIds.forEach(function(ringId){
|
|
383
|
+
var ring = allNodes.filter(function(n){return n.ring===ringId});
|
|
384
|
+
var layerOrder = {};
|
|
385
|
+
M.layers.forEach(function(l,i){layerOrder[l.id]=i;});
|
|
386
|
+
ring.sort(function(a,b){return(layerOrder[a.layer]||99)-(layerOrder[b.layer]||99);});
|
|
387
|
+
var r = ringRadii[ringId];
|
|
388
|
+
ring.forEach(function(n,i){var angle=-Math.PI/2+(i/ring.length)*Math.PI*2;n.fx=cx+Math.cos(angle)*r;n.fy=cy+Math.sin(angle)*r;});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// \u2500\u2500 D3 Render \u2500\u2500
|
|
392
|
+
var svg = d3.select('#graph').attr('width',W).attr('height',H);
|
|
393
|
+
var g = svg.append('g');
|
|
394
|
+
var zoomBehavior = d3.zoom().scaleExtent([0.3,5])
|
|
395
|
+
.filter(function(event){
|
|
396
|
+
// Don't zoom on clicks \u2014 only on wheel, drag, pinch
|
|
397
|
+
if (event.type === 'mousedown' || event.type === 'touchstart') return true;
|
|
398
|
+
if (event.type === 'wheel') return true;
|
|
399
|
+
if (event.type === 'dblclick') return true;
|
|
400
|
+
return true;
|
|
401
|
+
})
|
|
402
|
+
.on('zoom',function(e){g.attr('transform',e.transform);});
|
|
403
|
+
svg.call(zoomBehavior);
|
|
404
|
+
|
|
405
|
+
ringIds.forEach(function(id){
|
|
406
|
+
var r = ringRadii[id];
|
|
407
|
+
g.append('circle').attr('class','ring-circle').attr('data-color',ringMap[id].color).attr('cx',cx).attr('cy',cy).attr('r',r).attr('fill','none').attr('stroke',ringMap[id].color).attr('stroke-opacity',0.3).attr('stroke-width',1);
|
|
408
|
+
var angle = -Math.PI*0.18;
|
|
409
|
+
g.append('text').attr('class','ring-label').attr('x',cx+Math.cos(angle)*r+10).attr('y',cy+Math.sin(angle)*r).attr('fill',ringMap[id].color).attr('fill-opacity',1).text(ringMap[id].name.toUpperCase());
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
function nR(d){if(d.hop===0)return 30;return Math.max(18-d.hop*3,12);}
|
|
413
|
+
function isRemoveNode(d){return d.ring==='remove';}
|
|
414
|
+
|
|
415
|
+
var edgeGroup = g.append('g');
|
|
416
|
+
var edgePaths = edgeGroup.selectAll('path').data(M.edges).join('path')
|
|
417
|
+
.attr('fill','none')
|
|
418
|
+
.attr('stroke',function(d){return edgeStyle(d).color;})
|
|
419
|
+
.attr('stroke-width',function(d){return edgeStyle(d).width;})
|
|
420
|
+
.attr('opacity',function(d){return edgeStyle(d).opacity;})
|
|
421
|
+
.attr('stroke-dasharray',function(d){return edgeStyle(d).dash;})
|
|
422
|
+
.attr('d',function(d){
|
|
423
|
+
var src=allNodes.find(function(n){return n.id===(d.source==='center'?'__center__':d.source)});
|
|
424
|
+
var tgt=allNodes.find(function(n){return n.id===d.target});
|
|
425
|
+
if(!src||!tgt)return'';
|
|
426
|
+
var dx=tgt.fx-src.fx,dy=tgt.fy-src.fy,dist=Math.sqrt(dx*dx+dy*dy)||1;
|
|
427
|
+
var ux=dx/dist,uy=dy/dist;
|
|
428
|
+
return'M'+(src.fx+ux*nR(src))+','+(src.fy+uy*nR(src))+' A'+(dist*1.1)+','+(dist*1.1)+' 0 0,1 '+(tgt.fx-ux*nR(tgt))+','+(tgt.fy-uy*nR(tgt));
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// Edge hover tooltip
|
|
432
|
+
var edgeTooltipEl = document.getElementById('edgeTooltip');
|
|
433
|
+
edgePaths.on('mouseenter',function(event,d){
|
|
434
|
+
if(selectedNodeId)return;
|
|
435
|
+
var s=edgeStyle(d);
|
|
436
|
+
var srcId=d.source==='center'?'__center__':d.source;
|
|
437
|
+
var srcNode=allNodes.find(function(n){return n.id===srcId});
|
|
438
|
+
var tgtNode=allNodes.find(function(n){return n.id===d.target});
|
|
439
|
+
if(!srcNode||!tgtNode)return;
|
|
440
|
+
var action=(d.action||'existing').toUpperCase();
|
|
441
|
+
var html='<div class="et-action" style="color:'+s.color+'">'+action+'</div>';
|
|
442
|
+
html+='<div class="et-nodes">'+srcNode.name+' \\u2192 '+tgtNode.name+'</div>';
|
|
443
|
+
if(d.label)html+='<div class="et-label">'+d.label+'</div>';
|
|
444
|
+
edgeTooltipEl.innerHTML=html;
|
|
445
|
+
edgeTooltipEl.style.display='block';
|
|
446
|
+
edgeTooltipEl.style.left=(event.clientX+12)+'px';
|
|
447
|
+
edgeTooltipEl.style.top=(event.clientY-10)+'px';
|
|
448
|
+
// Highlight this edge
|
|
449
|
+
d3.select(this).attr('opacity',Math.min(s.opacity+0.3,1)).attr('stroke-width',s.width+1);
|
|
450
|
+
}).on('mousemove',function(event){
|
|
451
|
+
edgeTooltipEl.style.left=(event.clientX+12)+'px';
|
|
452
|
+
edgeTooltipEl.style.top=(event.clientY-10)+'px';
|
|
453
|
+
}).on('mouseleave',function(event,d){
|
|
454
|
+
edgeTooltipEl.style.display='none';
|
|
455
|
+
var s=edgeStyle(d);
|
|
456
|
+
d3.select(this).attr('opacity',s.opacity).attr('stroke-width',s.width);
|
|
457
|
+
});
|
|
458
|
+
// Make edges easier to hover
|
|
459
|
+
edgePaths.style('pointer-events','stroke').attr('cursor','pointer');
|
|
460
|
+
|
|
461
|
+
var nodeGroup = g.append('g');
|
|
462
|
+
var nodeEls = nodeGroup.selectAll('.node').data(allNodes).join('g')
|
|
463
|
+
.attr('class','node').attr('transform',function(d){return'translate('+d.fx+','+d.fy+')';}).style('cursor','pointer');
|
|
464
|
+
|
|
465
|
+
nodeEls.append('circle').attr('class','node-glow').attr('r',function(d){return nR(d)+12})
|
|
466
|
+
.attr('fill',function(d){return d.hop===0?centerColor:(ringMap[d.ring]?ringMap[d.ring].color:'#666')}).attr('opacity',0);
|
|
467
|
+
|
|
468
|
+
nodeEls.append('circle').attr('class','node-bg').attr('r',function(d){return nR(d)})
|
|
469
|
+
.attr('fill',function(d){if(d.hop===0)return'#1f0a0a';if(isRemoveNode(d))return'rgba(127,29,29,.6)';return'rgba(8,9,14,.85)';})
|
|
470
|
+
.attr('stroke',function(d){return d.hop===0?centerColor:(ringMap[d.ring]?ringMap[d.ring].color:'#666')})
|
|
471
|
+
.attr('stroke-width',function(d){return d.hop===0?3:3});
|
|
472
|
+
|
|
473
|
+
nodeEls.each(function(d){
|
|
474
|
+
var el=d3.select(this),r=nR(d);
|
|
475
|
+
if(d.hop===0){var cs=Math.round(r*1.3);var cfo=el.append('foreignObject').attr('x',-cs/2).attr('y',-cs/2).attr('width',cs).attr('height',cs).attr('style','overflow:visible;pointer-events:none;');cfo.append('xhtml:div').attr('style','width:'+cs+'px;height:'+cs+'px;display:flex;align-items:center;justify-content:center;').html('<i data-lucide="radiation" style="width:'+cs+'px;height:'+cs+'px;color:#fca5a5;fill:rgba(252,165,165,0.25);stroke-width:1.5;"></i>');return;}
|
|
476
|
+
var layer=layerMap[d.layer];if(!layer)return;
|
|
477
|
+
var s=Math.round(r*1.1);
|
|
478
|
+
var fo=el.append('foreignObject').attr('x',-s/2).attr('y',-s/2).attr('width',s).attr('height',s).attr('style','overflow:visible;pointer-events:none;');
|
|
479
|
+
var iconColor=isRemoveNode(d)?'#fca5a5':'#cbd5e1';
|
|
480
|
+
var iconFill=isRemoveNode(d)?'rgba(252,165,165,0.15)':'rgba(203,213,225,0.15)';
|
|
481
|
+
fo.append('xhtml:div').attr('style','width:'+s+'px;height:'+s+'px;display:flex;align-items:center;justify-content:center;')
|
|
482
|
+
.html('<i data-lucide="'+layer.icon+'" style="width:'+s+'px;height:'+s+'px;color:'+iconColor+';fill:'+iconFill+';stroke-width:1.5;"></i>');
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
var pulseEl=nodeEls.filter(function(d){return d.hop===0}).append('circle').attr('r',30).attr('fill','none').attr('stroke',centerColor).attr('stroke-width',1.5).attr('opacity',0.4);
|
|
486
|
+
(function pulse(){pulseEl.attr('r',30).attr('opacity',0.4).transition().duration(2200).ease(d3.easeCubicOut).attr('r',55).attr('opacity',0).on('end',pulse);})();
|
|
487
|
+
|
|
488
|
+
var labelsOn = true;
|
|
489
|
+
var labelEls = nodeEls.append('text').attr('text-anchor','middle').attr('y',function(d){return nR(d)+15})
|
|
490
|
+
.attr('font-size',function(d){return d.hop<=1?11:10}).attr('fill',function(d){if(d.hop===0)return centerColor;if(isRemoveNode(d))return'#f87171';return'#94a3b8';}).attr('font-weight',function(d){return d.hop===0?700:500})
|
|
491
|
+
.attr('text-decoration',function(d){return isRemoveNode(d)?'line-through':'none';})
|
|
492
|
+
.text(function(d){var m=d.hop>=2?16:20;return d.name.length>m?d.name.slice(0,m-1)+'\\u2026':d.name;});
|
|
493
|
+
|
|
494
|
+
lucide.createIcons();
|
|
495
|
+
M.layers.forEach(function(l){var el=document.getElementById('fpi_'+l.id);if(el){el.innerHTML='<i data-lucide="'+l.icon+'" style="width:12px;height:12px;color:#cbd5e1;fill:rgba(203,213,225,0.15);stroke-width:2;"></i>';}});
|
|
496
|
+
lucide.createIcons();
|
|
497
|
+
|
|
498
|
+
// \u2500\u2500 State \u2500\u2500
|
|
499
|
+
var activeRing = 'all', activeLayer = 'all', activeEdgeAction = 'all', selectedNodeId = null;
|
|
500
|
+
|
|
501
|
+
function isNodeVisible(d) {
|
|
502
|
+
if(d.hop===0)return true;
|
|
503
|
+
return(activeRing==='all'||d.ring===activeRing)&&(activeLayer==='all'||d.layer===activeLayer);
|
|
504
|
+
}
|
|
505
|
+
function isEdgeActionVisible(e) {
|
|
506
|
+
if(activeEdgeAction==='all')return true;
|
|
507
|
+
return(e.action||'existing')===activeEdgeAction;
|
|
508
|
+
}
|
|
509
|
+
function getRelated(nid) {
|
|
510
|
+
var s = {};s[nid]=true;
|
|
511
|
+
M.edges.forEach(function(e){var src=e.source==='center'?'__center__':e.source;if(src===nid||e.target===nid){s[src]=true;s[e.target]=true;}});
|
|
512
|
+
return s;
|
|
513
|
+
}
|
|
514
|
+
function getConnections(nid) {
|
|
515
|
+
var conns=[];
|
|
516
|
+
M.edges.forEach(function(e){
|
|
517
|
+
var src=e.source==='center'?'__center__':e.source;
|
|
518
|
+
if(src===nid){var t=allNodes.find(function(n){return n.id===e.target});if(t)conns.push({dir:'to',node:t,action:e.action,label:e.label});}
|
|
519
|
+
if(e.target===nid){var s=allNodes.find(function(n){return n.id===src});if(s)conns.push({dir:'from',node:s,action:e.action,label:e.label});}
|
|
520
|
+
});
|
|
521
|
+
return conns;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// \u2500\u2500 Hover: highlight edges only (no panel) \u2500\u2500
|
|
525
|
+
nodeEls.on('mouseenter',function(event,d){
|
|
526
|
+
if(selectedNodeId)return;
|
|
527
|
+
var related=d.hop===0?allNodes.reduce(function(o,n){o[n.id]=true;return o;},{}):getRelated(d.id);
|
|
528
|
+
nodeEls.transition().duration(150).style('opacity',function(n){
|
|
529
|
+
if(!isNodeVisible(n))return 0.06;
|
|
530
|
+
return related[n.id]?1:0.12;
|
|
531
|
+
});
|
|
532
|
+
edgePaths.transition().duration(150)
|
|
533
|
+
.attr('opacity',function(e){var s=e.source==='center'?'__center__':e.source;var sN=allNodes.find(function(n){return n.id===s});var tN=allNodes.find(function(n){return n.id===e.target});if(sN&&!isNodeVisible(sN)&&sN.hop!==0)return 0.02;if(tN&&!isNodeVisible(tN)&&tN.hop!==0)return 0.02;var isRelated=related[s]&&related[e.target];if(!isRelated)return 0.03;return Math.min(edgeStyle(e).opacity+0.25,0.85);})
|
|
534
|
+
.attr('stroke',function(e){var s=e.source==='center'?'__center__':e.source;var isRelated=related[s]&&related[e.target];return isRelated?edgeStyle(e).color:edgeStyle(e).color;})
|
|
535
|
+
.attr('stroke-width',function(e){var s=e.source==='center'?'__center__':e.source;var isRelated=related[s]&&related[e.target];return isRelated?edgeStyle(e).width+0.5:edgeStyle(e).width;});
|
|
536
|
+
d3.select(this).select('.node-glow').transition().duration(150).attr('opacity',0.15);
|
|
537
|
+
});
|
|
538
|
+
nodeEls.on('mouseleave',function(){
|
|
539
|
+
if(selectedNodeId)return;
|
|
540
|
+
applyFilters();
|
|
541
|
+
edgePaths.transition().duration(200)
|
|
542
|
+
.attr('stroke',function(d){return edgeStyle(d).color;})
|
|
543
|
+
.attr('stroke-width',function(d){return edgeStyle(d).width;});
|
|
544
|
+
d3.select(this).select('.node-glow').transition().duration(200).attr('opacity',0);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
// \u2500\u2500 Click: use pointerdown/pointerup with delta check to avoid zoom interference \u2500\u2500
|
|
548
|
+
var ptrDownPos = null;
|
|
549
|
+
nodeEls.on('pointerdown',function(event){
|
|
550
|
+
ptrDownPos = {x:event.clientX, y:event.clientY};
|
|
551
|
+
});
|
|
552
|
+
nodeEls.on('pointerup',function(event,d){
|
|
553
|
+
if(!ptrDownPos) return;
|
|
554
|
+
var dx=event.clientX-ptrDownPos.x, dy=event.clientY-ptrDownPos.y;
|
|
555
|
+
ptrDownPos = null;
|
|
556
|
+
if(Math.sqrt(dx*dx+dy*dy) > 5) return; // was a drag, not a click
|
|
557
|
+
|
|
558
|
+
event.stopPropagation();
|
|
559
|
+
selectedNodeId = d.id;
|
|
560
|
+
|
|
561
|
+
var related = d.hop===0 ? allNodes.reduce(function(o,n){o[n.id]=true;return o;},{}) : getRelated(d.id);
|
|
562
|
+
nodeEls.transition().duration(200).style('opacity',function(n){
|
|
563
|
+
if(!isNodeVisible(n))return 0.06;
|
|
564
|
+
return related[n.id]?1:0.08;
|
|
565
|
+
});
|
|
566
|
+
edgePaths.transition().duration(200)
|
|
567
|
+
.attr('opacity',function(e){var s=e.source==='center'?'__center__':e.source;var isRelated=related[s]&&related[e.target];return isRelated?Math.min(edgeStyle(e).opacity+0.3,0.9):0.03;})
|
|
568
|
+
.attr('stroke',function(e){var s=e.source==='center'?'__center__':e.source;var isRelated=related[s]&&related[e.target];return isRelated?edgeStyle(e).color:edgeStyle(e).color;})
|
|
569
|
+
.attr('stroke-width',function(e){var s=e.source==='center'?'__center__':e.source;var isRelated=related[s]&&related[e.target];return isRelated?edgeStyle(e).width+1:edgeStyle(e).width;});
|
|
570
|
+
nodeEls.select('.node-glow').attr('opacity',0);
|
|
571
|
+
d3.select(this).select('.node-glow').transition().duration(150).attr('opacity',0.25);
|
|
572
|
+
|
|
573
|
+
// Fill detail panel
|
|
574
|
+
var panel=document.getElementById('detail');
|
|
575
|
+
var nameEl=document.getElementById('dpName');
|
|
576
|
+
nameEl.textContent=d.name;
|
|
577
|
+
nameEl.style.color=d.hop===0?centerColor:(ringMap[d.ring]?ringMap[d.ring].color:'#e2e8f0');
|
|
578
|
+
|
|
579
|
+
var layer=layerMap[d.layer],ring=ringMap[d.ring];
|
|
580
|
+
document.getElementById('dpBadges').innerHTML=
|
|
581
|
+
(ring?'<span class="dp-badge" style="background:'+ring.color+'22;color:'+ring.color+'">'+ring.name+'</span>':'')+
|
|
582
|
+
(d.hop===0?'<span class="dp-badge" style="background:'+centerColor+'22;color:'+centerColor+'">Center</span>':'')+
|
|
583
|
+
(layer?'<span class="dp-badge" style="background:'+layer.color+'22;color:'+layer.color+';border:1px solid'+layer.color+'44">'+layer.name+'</span>':'')+
|
|
584
|
+
(d.type?'<span class="dp-badge" style="background:rgba(100,116,139,.12);color:#94a3b8">'+d.type+'</span>':'');
|
|
585
|
+
|
|
586
|
+
if(d.path){document.getElementById('dpPathSection').style.display='';document.getElementById('dpPath').textContent=d.path;}
|
|
587
|
+
else{document.getElementById('dpPathSection').style.display='none';}
|
|
588
|
+
|
|
589
|
+
if(d.reason){document.getElementById('dpReasonSection').style.display='';document.getElementById('dpReason').textContent=d.reason;}
|
|
590
|
+
else{document.getElementById('dpReasonSection').style.display='none';}
|
|
591
|
+
|
|
592
|
+
var accSection=document.getElementById('dpAcceptanceSection');
|
|
593
|
+
var accEl=document.getElementById('dpAcceptance');
|
|
594
|
+
if(d.acceptance&&d.acceptance.length>0){
|
|
595
|
+
accSection.style.display='';
|
|
596
|
+
accEl.innerHTML=d.acceptance.map(function(a){return '<li style="color:#10b981;font-size:11px;">\\u2713 '+a+'</li>';}).join('');
|
|
597
|
+
} else {accSection.style.display='none';}
|
|
598
|
+
|
|
599
|
+
var conns=getConnections(d.id);
|
|
600
|
+
var connSection=document.getElementById('dpConnSection');
|
|
601
|
+
var connEl=document.getElementById('dpConn');
|
|
602
|
+
if(conns.length>0){
|
|
603
|
+
connSection.style.display='';
|
|
604
|
+
connEl.innerHTML=conns.map(function(c){
|
|
605
|
+
var col=c.node.hop===0?centerColor:(ringMap[c.node.ring]?ringMap[c.node.ring].color:'#666');
|
|
606
|
+
var arrow=c.dir==='to'?'\\u2192':'\\u2190';
|
|
607
|
+
var actionBadge='';
|
|
608
|
+
if(c.action&&c.action!=='existing'){
|
|
609
|
+
var ac=edgeActionStyles[c.action];
|
|
610
|
+
actionBadge=' <span style="font-size:9px;padding:1px 5px;border-radius:3px;background:'+ac.color+'22;color:'+ac.color+';font-weight:600;letter-spacing:.3px">'+c.action.toUpperCase()+'</span>';
|
|
611
|
+
}
|
|
612
|
+
var labelText=c.label?' <span style="color:#64748b;font-style:italic">'+c.label+'</span>':'';
|
|
613
|
+
return '<li><span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:'+col+'"></span> '+arrow+' '+c.node.name+actionBadge+labelText+'</li>';
|
|
614
|
+
}).join('');
|
|
615
|
+
} else {connSection.style.display='none';}
|
|
616
|
+
|
|
617
|
+
panel.style.display='block';
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
// Close detail on SVG background click
|
|
621
|
+
svg.on('pointerup',function(event){
|
|
622
|
+
if(!ptrDownPos){closeDetail();}
|
|
623
|
+
ptrDownPos=null;
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
document.getElementById('dpClose').addEventListener('click',function(){closeDetail();});
|
|
627
|
+
|
|
628
|
+
function closeDetail(){
|
|
629
|
+
selectedNodeId=null;
|
|
630
|
+
document.getElementById('detail').style.display='none';
|
|
631
|
+
applyFilters();
|
|
632
|
+
edgePaths.transition().duration(200)
|
|
633
|
+
.attr('stroke',function(d){return edgeStyle(d).color;})
|
|
634
|
+
.attr('stroke-width',function(d){return edgeStyle(d).width;});
|
|
635
|
+
nodeEls.select('.node-glow').transition().duration(200).attr('opacity',0);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// \u2500\u2500 Filters \u2500\u2500
|
|
639
|
+
function applyFilters(){
|
|
640
|
+
nodeEls.transition().duration(300).style('opacity',function(d){
|
|
641
|
+
if(d.hop===0)return 1;
|
|
642
|
+
return isNodeVisible(d)?1:0.06;
|
|
643
|
+
});
|
|
644
|
+
edgePaths.transition().duration(300)
|
|
645
|
+
.attr('opacity',function(e){
|
|
646
|
+
if(!isEdgeActionVisible(e))return 0.02;
|
|
647
|
+
if(activeRing==='all'&&activeLayer==='all')return edgeStyle(e).opacity;
|
|
648
|
+
var srcId=e.source==='center'?'__center__':e.source;
|
|
649
|
+
var s=allNodes.find(function(n){return n.id===srcId}),t=allNodes.find(function(n){return n.id===e.target});
|
|
650
|
+
if(!s||!t)return 0.02;
|
|
651
|
+
return((s.hop===0||isNodeVisible(s))&&(t.hop===0||isNodeVisible(t)))?edgeStyle(e).opacity:0.02;
|
|
652
|
+
})
|
|
653
|
+
.attr('stroke',function(e){return edgeStyle(e).color;})
|
|
654
|
+
.attr('stroke-width',function(e){return edgeStyle(e).width;});
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function filterRing(id){
|
|
658
|
+
activeRing=id;closeDetail();
|
|
659
|
+
document.querySelectorAll('[data-ring]').forEach(function(b){b.classList.remove('active');});
|
|
660
|
+
var el=document.getElementById(id==='all'?'rAll':'r_'+id);if(el)el.classList.add('active');
|
|
661
|
+
applyFilters();
|
|
662
|
+
}
|
|
663
|
+
function filterLayer(id){
|
|
664
|
+
activeLayer=id;closeDetail();
|
|
665
|
+
document.querySelectorAll('[data-layer]').forEach(function(b){b.classList.remove('active');});
|
|
666
|
+
var el=document.getElementById(id==='all'?'lAll':'l_'+id);if(el)el.classList.add('active');
|
|
667
|
+
applyFilters();
|
|
668
|
+
}
|
|
669
|
+
function filterEdgeAction(id){
|
|
670
|
+
activeEdgeAction=id;closeDetail();
|
|
671
|
+
document.querySelectorAll('[data-edge]').forEach(function(b){b.classList.remove('active');});
|
|
672
|
+
var el=document.getElementById(id==='all'?'eAll':'e_'+id);if(el)el.classList.add('active');
|
|
673
|
+
applyFilters();
|
|
674
|
+
}
|
|
675
|
+
</script>
|
|
676
|
+
</body>
|
|
677
|
+
</html>`;
|
|
678
|
+
}
|
|
679
|
+
function generateBlastRadiusReport(manifest) {
|
|
680
|
+
const layerMap = {};
|
|
681
|
+
for (const l of manifest.layers) layerMap[l.id] = l.name;
|
|
682
|
+
let md = `# Blast Radius: ${manifest.title}
|
|
683
|
+
`;
|
|
684
|
+
md += `Mode: ${manifest.mode} | Layers: ${manifest.layers.length} | Total nodes: ${manifest.nodes.length}
|
|
685
|
+
|
|
686
|
+
`;
|
|
687
|
+
md += `## Center
|
|
688
|
+
`;
|
|
689
|
+
md += `**${manifest.center.name}** \u2014 ${manifest.center.description}
|
|
690
|
+
|
|
691
|
+
`;
|
|
692
|
+
for (const r of manifest.rings) {
|
|
693
|
+
const nodes = manifest.nodes.filter((n) => n.ring === r.id);
|
|
694
|
+
if (nodes.length === 0) continue;
|
|
695
|
+
md += `## ${r.name} (${nodes.length})
|
|
696
|
+
|
|
697
|
+
`;
|
|
698
|
+
md += `| Node | Layer | Type | Path | Why |
|
|
699
|
+
`;
|
|
700
|
+
md += `|------|-------|------|------|-----|
|
|
701
|
+
`;
|
|
702
|
+
for (const n of nodes) {
|
|
703
|
+
const ln = layerMap[n.layer] || n.layer;
|
|
704
|
+
const path2 = (n.path || "-").replace(/\|/g, "/");
|
|
705
|
+
const reason = (n.reason || "-").replace(/\|/g, "/");
|
|
706
|
+
md += `| ${n.name} | ${ln} | ${n.type || "-"} | ${path2} | ${reason} |
|
|
707
|
+
`;
|
|
708
|
+
}
|
|
709
|
+
md += `
|
|
710
|
+
`;
|
|
711
|
+
}
|
|
712
|
+
md += `## Edges
|
|
713
|
+
|
|
714
|
+
`;
|
|
715
|
+
for (const e of manifest.edges) {
|
|
716
|
+
const action = e.action ? ` [${e.action.toUpperCase()}]` : "";
|
|
717
|
+
const label = e.label ? ` \u2014 ${e.label}` : "";
|
|
718
|
+
md += `- ${e.source} \u2192 ${e.target}${action}${label}
|
|
719
|
+
`;
|
|
720
|
+
}
|
|
721
|
+
return md;
|
|
722
|
+
}
|
|
723
|
+
function escHtml(s) {
|
|
724
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// src/deck-shared/contract-generator.ts
|
|
728
|
+
function generateContract(manifest) {
|
|
729
|
+
const layerMap = {};
|
|
730
|
+
for (const l of manifest.layers) layerMap[l.id] = l.name;
|
|
731
|
+
const safeRingIds = manifest.rings.filter((r) => /safe|unchanged|no.?change/i.test(r.name) || /safe/i.test(r.id)).map((r) => r.id);
|
|
732
|
+
const actionableNodes = manifest.nodes.filter((n) => !safeRingIds.includes(n.ring));
|
|
733
|
+
const safeNodes = manifest.nodes.filter((n) => safeRingIds.includes(n.ring));
|
|
734
|
+
const createRingIds = manifest.rings.filter((r) => /create|new|build/i.test(r.name) || /create/i.test(r.id)).map((r) => r.id);
|
|
735
|
+
const items = actionableNodes.map((n, i) => ({
|
|
736
|
+
index: i + 1,
|
|
737
|
+
id: n.id,
|
|
738
|
+
name: n.name,
|
|
739
|
+
action: createRingIds.includes(n.ring) ? "CREATE" : "MODIFY",
|
|
740
|
+
layer: layerMap[n.layer] || n.layer,
|
|
741
|
+
layerId: n.layer,
|
|
742
|
+
type: n.type || "unknown",
|
|
743
|
+
file: n.path || null,
|
|
744
|
+
spec: n.reason || "No specification provided",
|
|
745
|
+
acceptance: n.acceptance || [],
|
|
746
|
+
dependsOn: manifest.edges.filter((e) => e.target === n.id && e.source !== "center").map((e) => e.source),
|
|
747
|
+
blocks: manifest.edges.filter((e) => e.source === n.id).map((e) => e.target),
|
|
748
|
+
status: "pending",
|
|
749
|
+
disposition: null
|
|
750
|
+
}));
|
|
751
|
+
const verify = safeNodes.map((n) => ({
|
|
752
|
+
id: n.id,
|
|
753
|
+
name: n.name,
|
|
754
|
+
layer: layerMap[n.layer] || n.layer,
|
|
755
|
+
type: n.type || "unknown",
|
|
756
|
+
note: "Classified as unchanged \u2014 verify after implementation"
|
|
757
|
+
}));
|
|
758
|
+
const layerIds = [...new Set(actionableNodes.map((n) => n.layer))];
|
|
759
|
+
return {
|
|
760
|
+
title: manifest.title,
|
|
761
|
+
description: manifest.center.description,
|
|
762
|
+
mode: manifest.mode,
|
|
763
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
764
|
+
summary: {
|
|
765
|
+
createCount: items.filter((i) => i.action === "CREATE").length,
|
|
766
|
+
modifyCount: items.filter((i) => i.action === "MODIFY").length,
|
|
767
|
+
totalItems: items.length,
|
|
768
|
+
verifyCount: verify.length,
|
|
769
|
+
layers: layerIds.map((id) => layerMap[id] || id)
|
|
770
|
+
},
|
|
771
|
+
items,
|
|
772
|
+
verify,
|
|
773
|
+
edges: manifest.edges
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
function contractToMarkdown(contract) {
|
|
777
|
+
let md = `# Contract: ${contract.title}
|
|
778
|
+
`;
|
|
779
|
+
md += `> ${contract.description}
|
|
780
|
+
|
|
781
|
+
`;
|
|
782
|
+
md += `Generated: ${contract.generatedAt}
|
|
783
|
+
|
|
784
|
+
`;
|
|
785
|
+
md += `## Summary
|
|
786
|
+
`;
|
|
787
|
+
md += `- **Create:** ${contract.summary.createCount}
|
|
788
|
+
`;
|
|
789
|
+
md += `- **Modify:** ${contract.summary.modifyCount}
|
|
790
|
+
`;
|
|
791
|
+
md += `- **Total items:** ${contract.summary.totalItems}
|
|
792
|
+
`;
|
|
793
|
+
md += `- **Verify unchanged:** ${contract.summary.verifyCount}
|
|
794
|
+
`;
|
|
795
|
+
md += `- **Layers:** ${contract.summary.layers.join(", ")}
|
|
796
|
+
|
|
797
|
+
`;
|
|
798
|
+
const byLayer = /* @__PURE__ */ new Map();
|
|
799
|
+
for (const item of contract.items) {
|
|
800
|
+
const group = byLayer.get(item.layer) || [];
|
|
801
|
+
group.push(item);
|
|
802
|
+
byLayer.set(item.layer, group);
|
|
803
|
+
}
|
|
804
|
+
md += `## Contract Items
|
|
805
|
+
|
|
806
|
+
`;
|
|
807
|
+
for (const [layer, items] of byLayer) {
|
|
808
|
+
md += `### ${layer}
|
|
809
|
+
|
|
810
|
+
`;
|
|
811
|
+
md += `| # | Action | Name | Type | Spec | Status |
|
|
812
|
+
`;
|
|
813
|
+
md += `|---|--------|------|------|------|--------|
|
|
814
|
+
`;
|
|
815
|
+
for (const item of items) {
|
|
816
|
+
const spec = item.spec.replace(/\|/g, "/").replace(/\n/g, " ");
|
|
817
|
+
md += `| ${item.index} | ${item.action} | ${item.name} | ${item.type} | ${spec} | [ ] ${item.status} |
|
|
818
|
+
`;
|
|
819
|
+
}
|
|
820
|
+
md += `
|
|
821
|
+
`;
|
|
822
|
+
const withAcceptance = items.filter((i) => i.acceptance.length > 0);
|
|
823
|
+
if (withAcceptance.length > 0) {
|
|
824
|
+
md += `#### Acceptance Criteria
|
|
825
|
+
|
|
826
|
+
`;
|
|
827
|
+
for (const item of withAcceptance) {
|
|
828
|
+
md += `**${item.index}. ${item.name}**
|
|
829
|
+
`;
|
|
830
|
+
for (const a of item.acceptance) {
|
|
831
|
+
md += `- [ ] ${a}
|
|
832
|
+
`;
|
|
833
|
+
}
|
|
834
|
+
md += `
|
|
835
|
+
`;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
md += `## Dependencies
|
|
840
|
+
|
|
841
|
+
`;
|
|
842
|
+
for (const item of contract.items) {
|
|
843
|
+
if (item.dependsOn.length > 0) {
|
|
844
|
+
md += `- **${item.name}** depends on: ${item.dependsOn.join(", ")}
|
|
845
|
+
`;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
md += `
|
|
849
|
+
`;
|
|
850
|
+
if (contract.verify.length > 0) {
|
|
851
|
+
md += `## Verify Unchanged
|
|
852
|
+
|
|
853
|
+
`;
|
|
854
|
+
md += `These items were classified as "no change." Scan for misclassifications after implementation.
|
|
855
|
+
|
|
856
|
+
`;
|
|
857
|
+
md += `| Name | Layer | Type | Status |
|
|
858
|
+
`;
|
|
859
|
+
md += `|------|-------|------|--------|
|
|
860
|
+
`;
|
|
861
|
+
for (const v of contract.verify) {
|
|
862
|
+
md += `| ${v.name} | ${v.layer} | ${v.type} | [ ] verified |
|
|
863
|
+
`;
|
|
864
|
+
}
|
|
865
|
+
md += `
|
|
866
|
+
`;
|
|
867
|
+
}
|
|
868
|
+
md += `## Disposition Legend
|
|
869
|
+
|
|
870
|
+
`;
|
|
871
|
+
md += `- \`pending\` \u2014 not started
|
|
872
|
+
`;
|
|
873
|
+
md += `- \`done\` \u2014 implemented and verified against acceptance criteria
|
|
874
|
+
`;
|
|
875
|
+
md += `- \`deferred\` \u2014 intentionally postponed (must include reason)
|
|
876
|
+
`;
|
|
877
|
+
md += `- \`cut\` \u2014 removed from scope (must include reason)
|
|
878
|
+
`;
|
|
879
|
+
md += `- \`superseded\` \u2014 replaced by different approach (must reference replacement)
|
|
880
|
+
`;
|
|
881
|
+
return md;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// src/deck-server/deck-serve.ts
|
|
885
|
+
var DEFAULT_PORT = 52829;
|
|
886
|
+
var MAX_PORT_SCAN = 3;
|
|
887
|
+
var MIME_TYPES = {
|
|
888
|
+
".html": "text/html; charset=utf-8",
|
|
889
|
+
".js": "application/javascript; charset=utf-8",
|
|
890
|
+
".css": "text/css; charset=utf-8",
|
|
891
|
+
".json": "application/json; charset=utf-8",
|
|
892
|
+
".png": "image/png",
|
|
893
|
+
".svg": "image/svg+xml",
|
|
894
|
+
".ico": "image/x-icon",
|
|
895
|
+
".woff": "font/woff",
|
|
896
|
+
".woff2": "font/woff2"
|
|
897
|
+
};
|
|
898
|
+
var pendingFeedback = /* @__PURE__ */ new Map();
|
|
899
|
+
var lastRenderError = null;
|
|
900
|
+
function consumeRenderError() {
|
|
901
|
+
const err = lastRenderError;
|
|
902
|
+
lastRenderError = null;
|
|
903
|
+
return err;
|
|
904
|
+
}
|
|
905
|
+
function createFeedbackWaiter(session) {
|
|
906
|
+
const existing = pendingFeedback.get(session);
|
|
907
|
+
if (existing) {
|
|
908
|
+
existing.reject(new Error("Superseded by new await_feedback call"));
|
|
909
|
+
}
|
|
910
|
+
let resolve;
|
|
911
|
+
let reject;
|
|
912
|
+
const promise = new Promise((res, rej) => {
|
|
913
|
+
resolve = res;
|
|
914
|
+
reject = rej;
|
|
915
|
+
});
|
|
916
|
+
pendingFeedback.set(session, { resolve, reject });
|
|
917
|
+
return promise;
|
|
918
|
+
}
|
|
919
|
+
function resolveFeedback(session, response) {
|
|
920
|
+
const waiter = pendingFeedback.get(session);
|
|
921
|
+
if (!waiter) return false;
|
|
922
|
+
pendingFeedback.delete(session);
|
|
923
|
+
waiter.resolve(response);
|
|
924
|
+
return true;
|
|
925
|
+
}
|
|
926
|
+
var wss = null;
|
|
927
|
+
function broadcastToClients(message) {
|
|
928
|
+
if (!wss) return;
|
|
929
|
+
const data = JSON.stringify(message);
|
|
930
|
+
for (const client of wss.clients) {
|
|
931
|
+
if (client.readyState === import_ws.WebSocket.OPEN) {
|
|
932
|
+
client.send(data);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
function serveStatic(res, filePath) {
|
|
937
|
+
if (!import_node_fs3.default.existsSync(filePath) || !import_node_fs3.default.statSync(filePath).isFile()) return false;
|
|
938
|
+
const ext = import_node_path3.default.extname(filePath).toLowerCase();
|
|
939
|
+
const mime = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
940
|
+
res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
|
|
941
|
+
import_node_fs3.default.createReadStream(filePath).pipe(res);
|
|
942
|
+
return true;
|
|
943
|
+
}
|
|
944
|
+
function serveIndex(res, clientDir) {
|
|
945
|
+
const indexPath = import_node_path3.default.join(clientDir, "index.html");
|
|
946
|
+
if (!import_node_fs3.default.existsSync(indexPath)) {
|
|
947
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
948
|
+
res.end(`LaunchDeck client bundle not found at ${clientDir}. Run 'npm run build:client'.`);
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
serveStatic(res, indexPath);
|
|
952
|
+
}
|
|
953
|
+
function readBody(req) {
|
|
954
|
+
return new Promise((resolve) => {
|
|
955
|
+
let body = "";
|
|
956
|
+
req.on("data", (chunk) => {
|
|
957
|
+
body += chunk.toString();
|
|
958
|
+
});
|
|
959
|
+
req.on("end", () => resolve(body));
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
function jsonResponse(res, status, data) {
|
|
963
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
964
|
+
res.end(JSON.stringify(data));
|
|
965
|
+
}
|
|
966
|
+
function tryListen(server, port) {
|
|
967
|
+
return new Promise((resolve, reject) => {
|
|
968
|
+
const onError = (err) => {
|
|
969
|
+
server.off("listening", onListening);
|
|
970
|
+
reject(err);
|
|
971
|
+
};
|
|
972
|
+
const onListening = () => {
|
|
973
|
+
server.off("error", onError);
|
|
974
|
+
resolve(port);
|
|
975
|
+
};
|
|
976
|
+
server.once("error", onError);
|
|
977
|
+
server.once("listening", onListening);
|
|
978
|
+
server.listen(port, "127.0.0.1");
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
async function bindWithFallback(server, startPort) {
|
|
982
|
+
let lastErr = null;
|
|
983
|
+
for (let i = 0; i < MAX_PORT_SCAN; i++) {
|
|
984
|
+
const port = startPort + i;
|
|
985
|
+
try {
|
|
986
|
+
return await tryListen(server, port);
|
|
987
|
+
} catch (err) {
|
|
988
|
+
const code = err.code;
|
|
989
|
+
if (code === "EADDRINUSE") {
|
|
990
|
+
lastErr = err;
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
throw err;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
throw lastErr ?? new Error("Failed to bind any port");
|
|
997
|
+
}
|
|
998
|
+
async function startDeckServer(opts = {}) {
|
|
999
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
1000
|
+
const existing = getLiveLock(cwd);
|
|
1001
|
+
if (existing) {
|
|
1002
|
+
if (!opts.quiet) {
|
|
1003
|
+
process.stderr.write(`[launch-deck] already running (pid ${existing.pid}) at ${existing.url}
|
|
1004
|
+
`);
|
|
1005
|
+
}
|
|
1006
|
+
return { port: existing.port, url: existing.url };
|
|
1007
|
+
}
|
|
1008
|
+
const clientDir = opts.clientDir ?? import_node_path3.default.join(__dirname, "..", "deck-client");
|
|
1009
|
+
const server = import_node_http.default.createServer(async (req, res) => {
|
|
1010
|
+
try {
|
|
1011
|
+
const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
1012
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1013
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1014
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
1015
|
+
if (req.method === "OPTIONS") {
|
|
1016
|
+
res.writeHead(204);
|
|
1017
|
+
res.end();
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
if (req.method === "GET" && url2.pathname === "/api/health") {
|
|
1021
|
+
jsonResponse(res, 200, { ok: true, tool: "launch-deck" });
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
if (req.method === "GET" && url2.pathname === "/api/deck/config") {
|
|
1025
|
+
const cfg = loadDeckConfig(cwd);
|
|
1026
|
+
jsonResponse(res, 200, { config: cfg });
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
if (req.method === "POST" && url2.pathname === "/api/deck/config") {
|
|
1030
|
+
try {
|
|
1031
|
+
const body = JSON.parse(await readBody(req));
|
|
1032
|
+
const existing2 = loadDeckConfig(cwd);
|
|
1033
|
+
const merged = { ...existing2, ...body };
|
|
1034
|
+
saveDeckConfig(cwd, merged);
|
|
1035
|
+
jsonResponse(res, 200, { ok: true });
|
|
1036
|
+
} catch (err) {
|
|
1037
|
+
jsonResponse(res, 400, { ok: false, error: String(err) });
|
|
1038
|
+
}
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
if (req.method === "POST" && url2.pathname === "/api/deck") {
|
|
1042
|
+
const body = JSON.parse(await readBody(req));
|
|
1043
|
+
for (const block of body.blocks) {
|
|
1044
|
+
if (block.type === "blast-radius" && block.manifest) {
|
|
1045
|
+
try {
|
|
1046
|
+
const sessionEncoded = encodeURIComponent(body.session);
|
|
1047
|
+
const baseUrl = `/deck-files/${sessionEncoded}`;
|
|
1048
|
+
const html = generateBlastRadiusHtml(block.manifest, baseUrl);
|
|
1049
|
+
const report = generateBlastRadiusReport(block.manifest);
|
|
1050
|
+
const dir = import_node_path3.default.join(cwd, ".launchsecure", "deck-files", body.session);
|
|
1051
|
+
import_node_fs3.default.mkdirSync(dir, { recursive: true });
|
|
1052
|
+
import_node_fs3.default.writeFileSync(import_node_path3.default.join(dir, "blast-radius.html"), html);
|
|
1053
|
+
import_node_fs3.default.writeFileSync(import_node_path3.default.join(dir, "blast-radius-report.md"), report);
|
|
1054
|
+
import_node_fs3.default.writeFileSync(import_node_path3.default.join(dir, "blast-radius-manifest.json"), JSON.stringify(block.manifest, null, 2));
|
|
1055
|
+
const contractData = generateContract(block.manifest);
|
|
1056
|
+
const contractMd = contractToMarkdown(contractData);
|
|
1057
|
+
import_node_fs3.default.writeFileSync(import_node_path3.default.join(dir, "contract.json"), JSON.stringify(contractData, null, 2));
|
|
1058
|
+
import_node_fs3.default.writeFileSync(import_node_path3.default.join(dir, "contract.md"), contractMd);
|
|
1059
|
+
block.type = "iframe";
|
|
1060
|
+
block.src = `${baseUrl}/blast-radius.html`;
|
|
1061
|
+
delete block.manifest;
|
|
1062
|
+
} catch (err) {
|
|
1063
|
+
console.error("Failed to generate blast radius HTML:", err);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
broadcastToClients({
|
|
1068
|
+
type: "session",
|
|
1069
|
+
session: body.session,
|
|
1070
|
+
mode: body.mode,
|
|
1071
|
+
blocks: body.blocks,
|
|
1072
|
+
prompt: body.prompt
|
|
1073
|
+
});
|
|
1074
|
+
jsonResponse(res, 200, { ok: true });
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
if (req.method === "POST" && url2.pathname === "/api/await-feedback") {
|
|
1078
|
+
const { session } = JSON.parse(await readBody(req));
|
|
1079
|
+
try {
|
|
1080
|
+
const feedback = await createFeedbackWaiter(session);
|
|
1081
|
+
jsonResponse(res, 200, feedback);
|
|
1082
|
+
} catch (err) {
|
|
1083
|
+
jsonResponse(res, 500, { error: String(err) });
|
|
1084
|
+
}
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
if (req.method === "POST" && url2.pathname === "/api/clear-session") {
|
|
1088
|
+
const { session } = JSON.parse(await readBody(req));
|
|
1089
|
+
broadcastToClients({
|
|
1090
|
+
type: session === "all" ? "clear_all" : "clear_session",
|
|
1091
|
+
session
|
|
1092
|
+
});
|
|
1093
|
+
if (session === "all") {
|
|
1094
|
+
for (const [id, waiter] of pendingFeedback) {
|
|
1095
|
+
waiter.resolve({ comment: "", closed: true });
|
|
1096
|
+
pendingFeedback.delete(id);
|
|
1097
|
+
}
|
|
1098
|
+
} else {
|
|
1099
|
+
resolveFeedback(session, { comment: "", closed: true });
|
|
1100
|
+
}
|
|
1101
|
+
try {
|
|
1102
|
+
const deckFilesBase = import_node_path3.default.join(cwd, ".launchsecure", "deck-files");
|
|
1103
|
+
if (session === "all") {
|
|
1104
|
+
if (import_node_fs3.default.existsSync(deckFilesBase)) {
|
|
1105
|
+
import_node_fs3.default.rmSync(deckFilesBase, { recursive: true, force: true });
|
|
1106
|
+
}
|
|
1107
|
+
} else {
|
|
1108
|
+
const sessionDir = import_node_path3.default.join(deckFilesBase, session);
|
|
1109
|
+
if (import_node_fs3.default.existsSync(sessionDir)) {
|
|
1110
|
+
import_node_fs3.default.rmSync(sessionDir, { recursive: true, force: true });
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
} catch {
|
|
1114
|
+
}
|
|
1115
|
+
jsonResponse(res, 200, { ok: true });
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
if (req.method === "GET" && url2.pathname === "/api/render-errors") {
|
|
1119
|
+
const err = consumeRenderError();
|
|
1120
|
+
jsonResponse(res, 200, err ?? null);
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
if (req.method === "GET" && url2.pathname.startsWith("/deck-files/")) {
|
|
1124
|
+
const relative = decodeURIComponent(url2.pathname.slice("/deck-files/".length));
|
|
1125
|
+
if (relative.includes("..") || relative.startsWith("/")) {
|
|
1126
|
+
res.writeHead(403);
|
|
1127
|
+
res.end("Forbidden");
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
const filePath = import_node_path3.default.join(cwd, ".launchsecure", "deck-files", relative);
|
|
1131
|
+
if (serveStatic(res, filePath)) return;
|
|
1132
|
+
res.writeHead(404);
|
|
1133
|
+
res.end("Not found");
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
if (req.method === "GET" && url2.pathname.startsWith("/deck-download/")) {
|
|
1137
|
+
const relative = decodeURIComponent(url2.pathname.slice("/deck-download/".length));
|
|
1138
|
+
if (relative.includes("..") || relative.startsWith("/")) {
|
|
1139
|
+
res.writeHead(403);
|
|
1140
|
+
res.end("Forbidden");
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
const filePath = import_node_path3.default.join(cwd, ".launchsecure", "deck-files", relative);
|
|
1144
|
+
if (!import_node_fs3.default.existsSync(filePath) || !import_node_fs3.default.statSync(filePath).isFile()) {
|
|
1145
|
+
res.writeHead(404);
|
|
1146
|
+
res.end("Not found");
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
const ext = import_node_path3.default.extname(filePath).toLowerCase();
|
|
1150
|
+
const mime = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
1151
|
+
const filename = import_node_path3.default.basename(filePath);
|
|
1152
|
+
res.writeHead(200, {
|
|
1153
|
+
"Content-Type": mime,
|
|
1154
|
+
"Content-Disposition": `attachment; filename="${filename}"`,
|
|
1155
|
+
"Cache-Control": "no-cache"
|
|
1156
|
+
});
|
|
1157
|
+
import_node_fs3.default.createReadStream(filePath).pipe(res);
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
if (url2.pathname !== "/") {
|
|
1161
|
+
const staticPath = import_node_path3.default.join(clientDir, url2.pathname);
|
|
1162
|
+
if (serveStatic(res, staticPath)) return;
|
|
1163
|
+
}
|
|
1164
|
+
serveIndex(res, clientDir);
|
|
1165
|
+
} catch (err) {
|
|
1166
|
+
jsonResponse(res, 500, { error: String(err) });
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
wss = new import_ws.WebSocketServer({ server });
|
|
1170
|
+
wss.on("connection", (ws) => {
|
|
1171
|
+
if (!opts.quiet) {
|
|
1172
|
+
process.stderr.write("[launch-deck] browser connected\n");
|
|
1173
|
+
}
|
|
1174
|
+
ws.on("message", (raw) => {
|
|
1175
|
+
try {
|
|
1176
|
+
const msg = JSON.parse(String(raw));
|
|
1177
|
+
if (msg.type === "feedback" && msg.session) {
|
|
1178
|
+
resolveFeedback(msg.session, {
|
|
1179
|
+
comment: msg.comment ?? "",
|
|
1180
|
+
selections: msg.selections
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
if (msg.type === "session_closed" && msg.session) {
|
|
1184
|
+
resolveFeedback(msg.session, {
|
|
1185
|
+
comment: "",
|
|
1186
|
+
closed: true
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
if (msg.type === "render_error") {
|
|
1190
|
+
lastRenderError = {
|
|
1191
|
+
error: msg.error ?? "Unknown render error",
|
|
1192
|
+
source: msg.source ?? ""
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
} catch {
|
|
1196
|
+
}
|
|
1197
|
+
});
|
|
1198
|
+
ws.on("close", () => {
|
|
1199
|
+
if (!opts.quiet) {
|
|
1200
|
+
process.stderr.write("[launch-deck] browser disconnected\n");
|
|
1201
|
+
}
|
|
1202
|
+
const hasClients = [...wss.clients].some((c) => c.readyState === import_ws.WebSocket.OPEN);
|
|
1203
|
+
if (!hasClients && pendingFeedback.size > 0) {
|
|
1204
|
+
for (const [id, waiter] of pendingFeedback) {
|
|
1205
|
+
waiter.reject(new Error("Browser disconnected \u2014 no clients available"));
|
|
1206
|
+
pendingFeedback.delete(id);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
});
|
|
1211
|
+
const config = loadDeckConfig(cwd);
|
|
1212
|
+
const startPort = opts.port ?? config.port ?? DEFAULT_PORT;
|
|
1213
|
+
const port = await bindWithFallback(server, startPort);
|
|
1214
|
+
const url = `http://localhost:${port}`;
|
|
1215
|
+
writeLock({
|
|
1216
|
+
pid: process.pid,
|
|
1217
|
+
port,
|
|
1218
|
+
cwd,
|
|
1219
|
+
url,
|
|
1220
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1221
|
+
}, cwd);
|
|
1222
|
+
const cleanup = () => {
|
|
1223
|
+
for (const [id, waiter] of pendingFeedback) {
|
|
1224
|
+
waiter.reject(new Error("LaunchDeck server shutting down"));
|
|
1225
|
+
pendingFeedback.delete(id);
|
|
1226
|
+
}
|
|
1227
|
+
try {
|
|
1228
|
+
const deckFilesBase = import_node_path3.default.join(cwd, ".launchsecure", "deck-files");
|
|
1229
|
+
if (import_node_fs3.default.existsSync(deckFilesBase)) {
|
|
1230
|
+
import_node_fs3.default.rmSync(deckFilesBase, { recursive: true, force: true });
|
|
1231
|
+
}
|
|
1232
|
+
} catch {
|
|
1233
|
+
}
|
|
1234
|
+
clearLock(cwd);
|
|
1235
|
+
server.close();
|
|
1236
|
+
};
|
|
1237
|
+
process.once("SIGINT", () => {
|
|
1238
|
+
cleanup();
|
|
1239
|
+
process.exit(0);
|
|
1240
|
+
});
|
|
1241
|
+
process.once("SIGTERM", () => {
|
|
1242
|
+
cleanup();
|
|
1243
|
+
process.exit(0);
|
|
1244
|
+
});
|
|
1245
|
+
process.once("exit", cleanup);
|
|
1246
|
+
if (!opts.quiet) {
|
|
1247
|
+
process.stderr.write(`[launch-deck] serving ${url}
|
|
1248
|
+
`);
|
|
1249
|
+
}
|
|
1250
|
+
return { port, url };
|
|
1251
|
+
}
|
|
1252
|
+
function runServeCli(argv) {
|
|
1253
|
+
let port;
|
|
1254
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1255
|
+
if (argv[i] === "--port" && argv[i + 1]) {
|
|
1256
|
+
port = parseInt(argv[++i], 10);
|
|
1257
|
+
} else if (argv[i].startsWith("--port=")) {
|
|
1258
|
+
port = parseInt(argv[i].slice("--port=".length), 10);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
startDeckServer({ port }).catch((err) => {
|
|
1262
|
+
process.stderr.write(`[launch-deck] failed to start: ${err}
|
|
1263
|
+
`);
|
|
1264
|
+
process.exit(1);
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1268
|
+
0 && (module.exports = {
|
|
1269
|
+
broadcastToClients,
|
|
1270
|
+
consumeRenderError,
|
|
1271
|
+
createFeedbackWaiter,
|
|
1272
|
+
resolveFeedback,
|
|
1273
|
+
runServeCli,
|
|
1274
|
+
startDeckServer
|
|
1275
|
+
});
|