@hegemonart/get-design-done 1.54.0 → 1.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +92 -0
- package/README.md +6 -0
- package/SKILL.md +1 -0
- package/agents/design-fixer.md +16 -0
- package/bin/gdd-dashboard +91 -0
- package/dist/claude-code/.claude/skills/override/SKILL.md +86 -0
- package/hooks/gdd-decision-injector.js +58 -0
- package/hooks/gdd-fact-force.js +345 -0
- package/hooks/gdd-risk-gate.js +406 -0
- package/hooks/hooks.json +18 -0
- package/package.json +2 -1
- package/reference/schemas/events.schema.json +61 -1
- package/reference/skill-graph.md +2 -1
- package/scripts/lib/dashboard/graph-html.cjs +0 -0
- package/scripts/lib/health-mirror/index.cjs +146 -1
- package/scripts/lib/manifest/skills.json +8 -0
- package/scripts/lib/risk/calibration.cjs +385 -0
- package/scripts/lib/risk/compute-risk.cjs +229 -0
- package/scripts/lib/risk/consumers.cjs +211 -0
- package/scripts/lib/risk/override.cjs +87 -0
- package/scripts/lib/risk/route.cjs +59 -0
- package/scripts/lib/risk/tables.cjs +221 -0
- package/sdk/cli/commands/dashboard.ts +419 -0
- package/sdk/cli/index.js +253 -2
- package/sdk/cli/index.ts +7 -0
- package/sdk/dashboard/data/_pkg-root.cjs +92 -0
- package/sdk/dashboard/data/cost-aggregator.cjs +187 -0
- package/sdk/dashboard/data/discovery.cjs +297 -0
- package/sdk/dashboard/data/risk-surface.cjs +136 -0
- package/sdk/dashboard/data/source.cjs +576 -0
- package/sdk/dashboard/tui/ansi.cjs +355 -0
- package/sdk/dashboard/tui/index.cjs +778 -0
- package/sdk/mcp/gdd-mcp/server.js +70 -0
- package/skills/override/SKILL.md +86 -0
package/sdk/cli/index.js
CHANGED
|
@@ -9582,6 +9582,252 @@ ${BUILD_USAGE}`);
|
|
|
9582
9582
|
return typeof res.status === "number" ? res.status : 3;
|
|
9583
9583
|
}
|
|
9584
9584
|
|
|
9585
|
+
// sdk/cli/commands/dashboard.ts
|
|
9586
|
+
var import_node_child_process2 = require("node:child_process");
|
|
9587
|
+
var import_node_module4 = require("node:module");
|
|
9588
|
+
var import_node_http = require("node:http");
|
|
9589
|
+
var import_node_fs23 = require("node:fs");
|
|
9590
|
+
var import_node_path21 = require("node:path");
|
|
9591
|
+
var DASHBOARD_FLAGS = [
|
|
9592
|
+
...COMMON_FLAGS,
|
|
9593
|
+
{ name: "web", type: "boolean", default: false },
|
|
9594
|
+
{ name: "once", type: "boolean", default: false },
|
|
9595
|
+
{ name: "no-open", type: "boolean", default: false },
|
|
9596
|
+
{ name: "root", type: "string" }
|
|
9597
|
+
];
|
|
9598
|
+
var DASHBOARD_USAGE = `gdd-sdk dashboard [flags]
|
|
9599
|
+
|
|
9600
|
+
Open the GDD dashboard. Read-only. Dep-free (Node builtins only).
|
|
9601
|
+
|
|
9602
|
+
Default (no --web) launches the terminal UI (bin/gdd-dashboard). With --web it
|
|
9603
|
+
emits a self-contained HTML graph of the design-context, serves it on an ephemeral
|
|
9604
|
+
local port, and opens your browser.
|
|
9605
|
+
|
|
9606
|
+
Flags:
|
|
9607
|
+
--web Web mode: build + serve the design-context graph as HTML.
|
|
9608
|
+
--once Write the HTML to .design/dashboard.html and exit (no server). Implies --web.
|
|
9609
|
+
--no-open Web mode: serve + print the URL but do NOT open a browser (headless/CI).
|
|
9610
|
+
--root <dir> Project root to read .design/ from (default: GDD_PROJECT_ROOT or walk-up).
|
|
9611
|
+
-h, --help Show this help.
|
|
9612
|
+
|
|
9613
|
+
Exit codes: 0 ok \xB7 3 arg error / TUI not found \xB7 (TUI exit code forwarded otherwise)
|
|
9614
|
+
`;
|
|
9615
|
+
function anchorDirs() {
|
|
9616
|
+
const out = [];
|
|
9617
|
+
const entry = process.argv[1];
|
|
9618
|
+
if (typeof entry === "string" && entry.length > 0) out.push((0, import_node_path21.dirname)(entry));
|
|
9619
|
+
out.push(process.cwd());
|
|
9620
|
+
return out;
|
|
9621
|
+
}
|
|
9622
|
+
function climbToMarker(startDir) {
|
|
9623
|
+
const req = (0, import_node_module4.createRequire)((0, import_node_path21.join)(startDir, "noop.js"));
|
|
9624
|
+
let dir = startDir;
|
|
9625
|
+
let firstWithPkg = null;
|
|
9626
|
+
for (let i = 0; i < 12; i++) {
|
|
9627
|
+
const pkgPath = (0, import_node_path21.join)(dir, "package.json");
|
|
9628
|
+
if ((0, import_node_fs23.existsSync)(pkgPath)) {
|
|
9629
|
+
if (firstWithPkg === null) firstWithPkg = dir;
|
|
9630
|
+
try {
|
|
9631
|
+
const pkg = req(pkgPath);
|
|
9632
|
+
if (pkg && pkg.name === "get-design-done") return { root: dir, firstWithPkg };
|
|
9633
|
+
} catch {
|
|
9634
|
+
}
|
|
9635
|
+
}
|
|
9636
|
+
const parent = (0, import_node_path21.dirname)(dir);
|
|
9637
|
+
if (parent === dir) break;
|
|
9638
|
+
dir = parent;
|
|
9639
|
+
}
|
|
9640
|
+
return { root: null, firstWithPkg };
|
|
9641
|
+
}
|
|
9642
|
+
var _cachedPkgRoot = null;
|
|
9643
|
+
function findPackageRoot() {
|
|
9644
|
+
if (_cachedPkgRoot !== null) return _cachedPkgRoot;
|
|
9645
|
+
let fallback = null;
|
|
9646
|
+
for (const anchor of anchorDirs()) {
|
|
9647
|
+
const { root, firstWithPkg } = climbToMarker(anchor);
|
|
9648
|
+
if (root) {
|
|
9649
|
+
_cachedPkgRoot = root;
|
|
9650
|
+
return root;
|
|
9651
|
+
}
|
|
9652
|
+
if (fallback === null && firstWithPkg !== null) fallback = firstWithPkg;
|
|
9653
|
+
}
|
|
9654
|
+
_cachedPkgRoot = fallback ?? process.cwd();
|
|
9655
|
+
return _cachedPkgRoot;
|
|
9656
|
+
}
|
|
9657
|
+
function requireFromRoot(relPath) {
|
|
9658
|
+
const root = findPackageRoot();
|
|
9659
|
+
const req = (0, import_node_module4.createRequire)((0, import_node_path21.join)(root, "noop.js"));
|
|
9660
|
+
return req((0, import_node_path21.join)(root, relPath));
|
|
9661
|
+
}
|
|
9662
|
+
function resolveRoot(deps, flags) {
|
|
9663
|
+
if (typeof deps.root === "string" && deps.root.length > 0) return deps.root;
|
|
9664
|
+
const flagRoot = flags["root"];
|
|
9665
|
+
if (typeof flagRoot === "string" && flagRoot.length > 0) return flagRoot;
|
|
9666
|
+
if (process.env["GDD_PROJECT_ROOT"]) return process.env["GDD_PROJECT_ROOT"];
|
|
9667
|
+
return findPackageRoot();
|
|
9668
|
+
}
|
|
9669
|
+
function loadGraphGraceful(root, stderr) {
|
|
9670
|
+
const graphPath = (0, import_node_path21.join)(root, ".design", "context-graph.json");
|
|
9671
|
+
try {
|
|
9672
|
+
const query = requireFromRoot("scripts/lib/design-context-query.cjs");
|
|
9673
|
+
if (typeof query.load === "function") return query.load(graphPath);
|
|
9674
|
+
} catch (err) {
|
|
9675
|
+
stderr.write(
|
|
9676
|
+
`gdd-sdk dashboard: no design-context graph at ${graphPath} (${errMsg(err)}); rendering an empty graph.
|
|
9677
|
+
`
|
|
9678
|
+
);
|
|
9679
|
+
}
|
|
9680
|
+
return { nodes: [], edges: [] };
|
|
9681
|
+
}
|
|
9682
|
+
function buildDashboardHtml(root, stderr) {
|
|
9683
|
+
const graph = loadGraphGraceful(root, stderr);
|
|
9684
|
+
const htmlLib = requireFromRoot("scripts/lib/dashboard/graph-html.cjs");
|
|
9685
|
+
return htmlLib.buildGraphHtml(graph, { title: "GDD Design Context Graph" });
|
|
9686
|
+
}
|
|
9687
|
+
function isHeadless(deps, flags) {
|
|
9688
|
+
if (typeof deps.headless === "boolean") return deps.headless;
|
|
9689
|
+
if (flags["no-open"] === true) return true;
|
|
9690
|
+
if (process.env["CI"]) return true;
|
|
9691
|
+
if (process.platform === "linux") {
|
|
9692
|
+
return !process.env["DISPLAY"] && !process.env["WAYLAND_DISPLAY"];
|
|
9693
|
+
}
|
|
9694
|
+
return false;
|
|
9695
|
+
}
|
|
9696
|
+
function defaultOpenBrowser(url) {
|
|
9697
|
+
try {
|
|
9698
|
+
if (process.platform === "darwin") {
|
|
9699
|
+
(0, import_node_child_process2.spawn)("open", [url], { stdio: "ignore", detached: true }).unref();
|
|
9700
|
+
} else if (process.platform === "win32") {
|
|
9701
|
+
(0, import_node_child_process2.spawn)("cmd", ["/c", "start", "", url], { stdio: "ignore", detached: true }).unref();
|
|
9702
|
+
} else {
|
|
9703
|
+
(0, import_node_child_process2.spawn)("xdg-open", [url], { stdio: "ignore", detached: true }).unref();
|
|
9704
|
+
}
|
|
9705
|
+
return true;
|
|
9706
|
+
} catch {
|
|
9707
|
+
return false;
|
|
9708
|
+
}
|
|
9709
|
+
}
|
|
9710
|
+
function serveHtml(html) {
|
|
9711
|
+
return new Promise((resolve11, reject) => {
|
|
9712
|
+
const server = (0, import_node_http.createServer)((_req, res) => {
|
|
9713
|
+
res.writeHead(200, {
|
|
9714
|
+
"content-type": "text/html; charset=utf-8",
|
|
9715
|
+
"cache-control": "no-store"
|
|
9716
|
+
});
|
|
9717
|
+
res.end(html);
|
|
9718
|
+
});
|
|
9719
|
+
server.on("error", reject);
|
|
9720
|
+
server.listen(0, "127.0.0.1", () => {
|
|
9721
|
+
const addr = server.address();
|
|
9722
|
+
if (addr === null || typeof addr === "string") {
|
|
9723
|
+
server.close();
|
|
9724
|
+
reject(new Error("could not determine the ephemeral server port"));
|
|
9725
|
+
return;
|
|
9726
|
+
}
|
|
9727
|
+
const port = addr.port;
|
|
9728
|
+
resolve11({ server, port, url: `http://127.0.0.1:${port}/` });
|
|
9729
|
+
});
|
|
9730
|
+
});
|
|
9731
|
+
}
|
|
9732
|
+
function errMsg(err) {
|
|
9733
|
+
if (err instanceof Error) return err.message;
|
|
9734
|
+
return String(err);
|
|
9735
|
+
}
|
|
9736
|
+
async function dashboardCommand(parsed, deps = {}) {
|
|
9737
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
9738
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
9739
|
+
if (parsed.flags["help"] === true || parsed.flags["h"] === true) {
|
|
9740
|
+
stdout.write(DASHBOARD_USAGE);
|
|
9741
|
+
return 0;
|
|
9742
|
+
}
|
|
9743
|
+
let flags;
|
|
9744
|
+
try {
|
|
9745
|
+
flags = coerceFlags(parsed, DASHBOARD_FLAGS);
|
|
9746
|
+
} catch {
|
|
9747
|
+
stderr.write(`gdd-sdk dashboard: invalid flags
|
|
9748
|
+
${DASHBOARD_USAGE}`);
|
|
9749
|
+
return 3;
|
|
9750
|
+
}
|
|
9751
|
+
const once = flags["once"] === true;
|
|
9752
|
+
const web = flags["web"] === true || once;
|
|
9753
|
+
if (!web) {
|
|
9754
|
+
return runTui(deps, stdout, stderr);
|
|
9755
|
+
}
|
|
9756
|
+
const root = resolveRoot(deps, flags);
|
|
9757
|
+
const html = buildDashboardHtml(root, stderr);
|
|
9758
|
+
if (once) {
|
|
9759
|
+
const designDir = (0, import_node_path21.join)(root, ".design");
|
|
9760
|
+
try {
|
|
9761
|
+
(0, import_node_fs23.mkdirSync)(designDir, { recursive: true });
|
|
9762
|
+
} catch {
|
|
9763
|
+
}
|
|
9764
|
+
const outFile = (0, import_node_path21.join)(designDir, "dashboard.html");
|
|
9765
|
+
try {
|
|
9766
|
+
(0, import_node_fs23.writeFileSync)(outFile, html, "utf8");
|
|
9767
|
+
} catch (err) {
|
|
9768
|
+
stderr.write(`gdd-sdk dashboard: could not write ${outFile}: ${errMsg(err)}
|
|
9769
|
+
`);
|
|
9770
|
+
return 3;
|
|
9771
|
+
}
|
|
9772
|
+
stdout.write(`Wrote dashboard HTML to ${outFile}
|
|
9773
|
+
`);
|
|
9774
|
+
return 0;
|
|
9775
|
+
}
|
|
9776
|
+
let served;
|
|
9777
|
+
try {
|
|
9778
|
+
served = await serveHtml(html);
|
|
9779
|
+
} catch (err) {
|
|
9780
|
+
stderr.write(`gdd-sdk dashboard: could not start the web server: ${errMsg(err)}
|
|
9781
|
+
`);
|
|
9782
|
+
return 3;
|
|
9783
|
+
}
|
|
9784
|
+
const headless = isHeadless(deps, flags);
|
|
9785
|
+
const opener = deps.openBrowser ?? defaultOpenBrowser;
|
|
9786
|
+
stdout.write(`GDD dashboard serving at ${served.url}
|
|
9787
|
+
`);
|
|
9788
|
+
if (headless) {
|
|
9789
|
+
stdout.write("Headless environment detected \u2014 open the URL above in a browser.\n");
|
|
9790
|
+
stdout.write("Press Ctrl+C to stop the server.\n");
|
|
9791
|
+
} else {
|
|
9792
|
+
const launched = opener(served.url);
|
|
9793
|
+
if (!launched) {
|
|
9794
|
+
stdout.write("Could not auto-open a browser \u2014 open the URL above manually.\n");
|
|
9795
|
+
}
|
|
9796
|
+
stdout.write("Press Ctrl+C to stop the server.\n");
|
|
9797
|
+
}
|
|
9798
|
+
await new Promise((resolve11) => {
|
|
9799
|
+
const shutdown = () => {
|
|
9800
|
+
served.server.close(() => resolve11());
|
|
9801
|
+
};
|
|
9802
|
+
process.once("SIGINT", shutdown);
|
|
9803
|
+
process.once("SIGTERM", shutdown);
|
|
9804
|
+
served.server.on("close", () => resolve11());
|
|
9805
|
+
});
|
|
9806
|
+
return 0;
|
|
9807
|
+
}
|
|
9808
|
+
function runTui(deps, _stdout, stderr) {
|
|
9809
|
+
let bin = deps.tuiBin;
|
|
9810
|
+
if (!bin) {
|
|
9811
|
+
const root = findPackageRoot();
|
|
9812
|
+
const candidate = (0, import_node_path21.join)(root, "bin", "gdd-dashboard");
|
|
9813
|
+
bin = (0, import_node_fs23.existsSync)(candidate) ? candidate : void 0;
|
|
9814
|
+
}
|
|
9815
|
+
if (!bin || !(0, import_node_fs23.existsSync)(bin)) {
|
|
9816
|
+
stderr.write(
|
|
9817
|
+
"gdd-sdk dashboard: could not locate bin/gdd-dashboard (the terminal UI).\nTry `gdd dashboard --web` for the browser graph instead.\n"
|
|
9818
|
+
);
|
|
9819
|
+
return 3;
|
|
9820
|
+
}
|
|
9821
|
+
const stdio = deps.tuiStdio ?? "inherit";
|
|
9822
|
+
const res = (0, import_node_child_process2.spawnSync)(process.execPath, [bin], { stdio });
|
|
9823
|
+
if (res.error) {
|
|
9824
|
+
stderr.write(`gdd-sdk dashboard: failed to launch the TUI: ${res.error.message}
|
|
9825
|
+
`);
|
|
9826
|
+
return 3;
|
|
9827
|
+
}
|
|
9828
|
+
return typeof res.status === "number" ? res.status : 0;
|
|
9829
|
+
}
|
|
9830
|
+
|
|
9585
9831
|
// sdk/cli/index.ts
|
|
9586
9832
|
var USAGE6 = `gdd-sdk <command> [flags]
|
|
9587
9833
|
|
|
@@ -9592,6 +9838,7 @@ Commands:
|
|
|
9592
9838
|
audit Probe connections + dry-run verify.
|
|
9593
9839
|
init Bootstrap a new project.
|
|
9594
9840
|
build skills Compile per-harness skill bundles from source/skills/.
|
|
9841
|
+
dashboard Open the GDD dashboard (TUI; --web for the browser graph).
|
|
9595
9842
|
|
|
9596
9843
|
Use 'gdd-sdk <command> -h' for command-specific flags.
|
|
9597
9844
|
|
|
@@ -9610,7 +9857,8 @@ async function dispatch(parsed, deps = {}) {
|
|
|
9610
9857
|
query: deps.commands?.query ?? queryCommand,
|
|
9611
9858
|
audit: deps.commands?.audit ?? auditCommand,
|
|
9612
9859
|
init: deps.commands?.init ?? initCommand,
|
|
9613
|
-
build: deps.commands?.build ?? buildCommand
|
|
9860
|
+
build: deps.commands?.build ?? buildCommand,
|
|
9861
|
+
dashboard: deps.commands?.dashboard ?? dashboardCommand
|
|
9614
9862
|
};
|
|
9615
9863
|
if (parsed.subcommand === null) {
|
|
9616
9864
|
stdout.write(USAGE6);
|
|
@@ -9636,6 +9884,8 @@ async function dispatch(parsed, deps = {}) {
|
|
|
9636
9884
|
return await commands.init(parsed, { stdout, stderr });
|
|
9637
9885
|
case "build":
|
|
9638
9886
|
return await commands.build(parsed, { stdout, stderr });
|
|
9887
|
+
case "dashboard":
|
|
9888
|
+
return await commands.dashboard(parsed, { stdout, stderr });
|
|
9639
9889
|
default:
|
|
9640
9890
|
stderr.write(
|
|
9641
9891
|
`gdd-sdk: unknown subcommand "${parsed.subcommand}"
|
|
@@ -9650,7 +9900,8 @@ var KNOWN_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
|
9650
9900
|
"query",
|
|
9651
9901
|
"audit",
|
|
9652
9902
|
"init",
|
|
9653
|
-
"build"
|
|
9903
|
+
"build",
|
|
9904
|
+
"dashboard"
|
|
9654
9905
|
]);
|
|
9655
9906
|
async function main(argv = process.argv.slice(2), deps = {}) {
|
|
9656
9907
|
const parsed = parseArgs(argv);
|
package/sdk/cli/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { queryCommand } from './commands/query.ts';
|
|
|
22
22
|
import { auditCommand } from './commands/audit.ts';
|
|
23
23
|
import { initCommand } from './commands/init.ts';
|
|
24
24
|
import { buildCommand } from './commands/build.ts';
|
|
25
|
+
import { dashboardCommand } from './commands/dashboard.ts';
|
|
25
26
|
|
|
26
27
|
// ---------------------------------------------------------------------------
|
|
27
28
|
// Top-level USAGE.
|
|
@@ -36,6 +37,7 @@ Commands:
|
|
|
36
37
|
audit Probe connections + dry-run verify.
|
|
37
38
|
init Bootstrap a new project.
|
|
38
39
|
build skills Compile per-harness skill bundles from source/skills/.
|
|
40
|
+
dashboard Open the GDD dashboard (TUI; --web for the browser graph).
|
|
39
41
|
|
|
40
42
|
Use 'gdd-sdk <command> -h' for command-specific flags.
|
|
41
43
|
|
|
@@ -60,6 +62,7 @@ export interface DispatcherDeps {
|
|
|
60
62
|
readonly audit?: typeof auditCommand;
|
|
61
63
|
readonly init?: typeof initCommand;
|
|
62
64
|
readonly build?: typeof buildCommand;
|
|
65
|
+
readonly dashboard?: typeof dashboardCommand;
|
|
63
66
|
};
|
|
64
67
|
}
|
|
65
68
|
|
|
@@ -85,6 +88,7 @@ export async function dispatch(
|
|
|
85
88
|
audit: deps.commands?.audit ?? auditCommand,
|
|
86
89
|
init: deps.commands?.init ?? initCommand,
|
|
87
90
|
build: deps.commands?.build ?? buildCommand,
|
|
91
|
+
dashboard: deps.commands?.dashboard ?? dashboardCommand,
|
|
88
92
|
};
|
|
89
93
|
|
|
90
94
|
// Bare invocation or top-level help → USAGE.
|
|
@@ -117,6 +121,8 @@ export async function dispatch(
|
|
|
117
121
|
return await commands.init(parsed, { stdout, stderr });
|
|
118
122
|
case 'build':
|
|
119
123
|
return await commands.build(parsed, { stdout, stderr });
|
|
124
|
+
case 'dashboard':
|
|
125
|
+
return await commands.dashboard(parsed, { stdout, stderr });
|
|
120
126
|
default:
|
|
121
127
|
stderr.write(
|
|
122
128
|
`gdd-sdk: unknown subcommand "${parsed.subcommand}"\n${USAGE}`,
|
|
@@ -132,6 +138,7 @@ const KNOWN_SUBCOMMANDS: ReadonlySet<string> = new Set([
|
|
|
132
138
|
'audit',
|
|
133
139
|
'init',
|
|
134
140
|
'build',
|
|
141
|
+
'dashboard',
|
|
135
142
|
]);
|
|
136
143
|
|
|
137
144
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* sdk/dashboard/data/_pkg-root.cjs — Phase 55 (GDD Dashboard, dep-free).
|
|
4
|
+
*
|
|
5
|
+
* Package-root walk-up for sibling resolution (the Phase 53/54 lesson): never
|
|
6
|
+
* resolve a cross-tree sibling via a fixed `__dirname`-relative `../../..`
|
|
7
|
+
* jump, because that breaks the moment a file is copied/moved or the layout
|
|
8
|
+
* shifts. Instead, walk UP from this file's directory until we find the GDD
|
|
9
|
+
* package.json (identified by `name === 'get-design-done'`), and resolve all
|
|
10
|
+
* in-repo siblings relative to that root.
|
|
11
|
+
*
|
|
12
|
+
* Even though these dashboard `.cjs` files are NOT esbuild-bundled (R8 — the
|
|
13
|
+
* bin trampoline runs them directly so the Phase 53 __dirname-rewrite trap
|
|
14
|
+
* does not apply), keeping the walk-up makes the data plane robust to future
|
|
15
|
+
* bundling or relocation. Pure + dependency-free; memoized per process.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('node:fs');
|
|
19
|
+
const path = require('node:path');
|
|
20
|
+
|
|
21
|
+
/** Memoized resolved package root (computed once per process). */
|
|
22
|
+
let _cachedRoot = null;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Walk up from `startDir` looking for the GDD package root. The GDD root is
|
|
26
|
+
* the first ancestor whose package.json declares `name: "get-design-done"`;
|
|
27
|
+
* if no such marker is found (e.g. running from an unusual layout), fall back
|
|
28
|
+
* to the FIRST ancestor that has any package.json, then to `startDir`.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} startDir
|
|
31
|
+
* @returns {string} absolute package-root directory
|
|
32
|
+
*/
|
|
33
|
+
function findPackageRoot(startDir) {
|
|
34
|
+
let dir = path.resolve(startDir);
|
|
35
|
+
let firstWithPkg = null;
|
|
36
|
+
// Bound the climb defensively (deep trees / odd mounts).
|
|
37
|
+
for (let i = 0; i < 12; i++) {
|
|
38
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
39
|
+
let pkg = null;
|
|
40
|
+
try {
|
|
41
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
42
|
+
} catch {
|
|
43
|
+
pkg = null;
|
|
44
|
+
}
|
|
45
|
+
if (pkg) {
|
|
46
|
+
if (firstWithPkg === null) firstWithPkg = dir;
|
|
47
|
+
if (pkg.name === 'get-design-done') return dir;
|
|
48
|
+
}
|
|
49
|
+
const parent = path.dirname(dir);
|
|
50
|
+
if (parent === dir) break;
|
|
51
|
+
dir = parent;
|
|
52
|
+
}
|
|
53
|
+
return firstWithPkg || path.resolve(startDir);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resolved GDD package root, memoized. Computed by walking up from THIS file's
|
|
58
|
+
* directory (`__dirname`) — which is correct regardless of the caller's cwd.
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
function packageRoot() {
|
|
62
|
+
if (_cachedRoot === null) _cachedRoot = findPackageRoot(__dirname);
|
|
63
|
+
return _cachedRoot;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Absolute path to an in-repo file given its repo-relative path.
|
|
68
|
+
* @param {string} relPath e.g. 'scripts/lib/install/runtime-homes.cjs'
|
|
69
|
+
* @returns {string}
|
|
70
|
+
*/
|
|
71
|
+
function resolveFromPackageRoot(relPath) {
|
|
72
|
+
return path.join(packageRoot(), relPath);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* require() an in-repo sibling .cjs module by its repo-relative path, resolved
|
|
77
|
+
* via the package-root walk-up. Use ONLY for .cjs siblings — .ts libs must be
|
|
78
|
+
* loaded via dynamic import(pathToFileURL) (a .cjs cannot static-require a .ts).
|
|
79
|
+
*
|
|
80
|
+
* @param {string} relPath e.g. 'scripts/lib/design-context-query.cjs'
|
|
81
|
+
* @returns {*} the required module
|
|
82
|
+
*/
|
|
83
|
+
function requireFromPackageRoot(relPath) {
|
|
84
|
+
return require(resolveFromPackageRoot(relPath));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = {
|
|
88
|
+
findPackageRoot,
|
|
89
|
+
packageRoot,
|
|
90
|
+
resolveFromPackageRoot,
|
|
91
|
+
requireFromPackageRoot,
|
|
92
|
+
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* sdk/dashboard/data/cost-aggregator.cjs — Phase 55 (GDD Dashboard, dep-free).
|
|
4
|
+
*
|
|
5
|
+
* Pure roll-up of cost events into per-runtime / cumulative / per-cycle
|
|
6
|
+
* buckets, plus a tolerant JSONL reader for `.design/telemetry/costs.jsonl`.
|
|
7
|
+
*
|
|
8
|
+
* Cost rows on disk have evolved across phases (Phase 10.1 -> 26 -> 27 -> 33.6),
|
|
9
|
+
* so this aggregator is deliberately field-shape tolerant:
|
|
10
|
+
*
|
|
11
|
+
* - cost field: `est_cost_usd` (the on-disk tier-resolver/budget-enforcer
|
|
12
|
+
* shape) OR `cost_usd` (the newer event-payload shape).
|
|
13
|
+
* - runtime key: `runtime` (Phase 27+) -> else `tier` -> else `agent`
|
|
14
|
+
* -> else "unknown". Grouping is best-effort: the dashboard
|
|
15
|
+
* just needs a stable label per row.
|
|
16
|
+
* - cycle key: `cycle` -> else "unknown".
|
|
17
|
+
* - tokens: `tokens_in` / `tokens_out`, coerced via Number(... || 0).
|
|
18
|
+
*
|
|
19
|
+
* NEVER throws. Pure (no FS) except `readCosts()`, which reads one file and
|
|
20
|
+
* tolerates malformed lines (skips them silently, like the event-stream reader).
|
|
21
|
+
*
|
|
22
|
+
* Public API:
|
|
23
|
+
* aggregateCosts(costEvents) -> { byRuntime, cumulative, byCycle }
|
|
24
|
+
* readCosts({ root?, path? }) -> cost row array (tolerant; [] when absent)
|
|
25
|
+
*
|
|
26
|
+
* Determinism: no Date.now()/Math.random(); output ordering follows input
|
|
27
|
+
* ordering of first-seen keys (object insertion order).
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const fs = require('node:fs');
|
|
31
|
+
const path = require('node:path');
|
|
32
|
+
|
|
33
|
+
const DEFAULT_COSTS_PATH = '.design/telemetry/costs.jsonl';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Coerce a possibly-missing numeric field to a finite number (0 on garbage).
|
|
37
|
+
* @param {unknown} v
|
|
38
|
+
* @returns {number}
|
|
39
|
+
*/
|
|
40
|
+
function num(v) {
|
|
41
|
+
const n = Number(v);
|
|
42
|
+
return Number.isFinite(n) ? n : 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Read the per-runtime label for a cost row. Prefers the explicit `runtime`
|
|
47
|
+
* tag (Phase 27+), then `tier`, then `agent`, then a literal "unknown".
|
|
48
|
+
* @param {Record<string, unknown>} row
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
function runtimeKeyOf(row) {
|
|
52
|
+
if (row && typeof row.runtime === 'string' && row.runtime.length) return row.runtime;
|
|
53
|
+
if (row && typeof row.tier === 'string' && row.tier.length) return row.tier;
|
|
54
|
+
if (row && typeof row.agent === 'string' && row.agent.length) return row.agent;
|
|
55
|
+
return 'unknown';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Read the USD cost for a row, tolerant of both on-disk shapes.
|
|
60
|
+
* @param {Record<string, unknown>} row
|
|
61
|
+
* @returns {number}
|
|
62
|
+
*/
|
|
63
|
+
function costUsdOf(row) {
|
|
64
|
+
if (!row) return 0;
|
|
65
|
+
if (typeof row.est_cost_usd !== 'undefined') return num(row.est_cost_usd);
|
|
66
|
+
if (typeof row.cost_usd !== 'undefined') return num(row.cost_usd);
|
|
67
|
+
if (typeof row.usd !== 'undefined') return num(row.usd);
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Read the cycle label for a row.
|
|
73
|
+
* @param {Record<string, unknown>} row
|
|
74
|
+
* @returns {string}
|
|
75
|
+
*/
|
|
76
|
+
function cycleKeyOf(row) {
|
|
77
|
+
if (row && typeof row.cycle === 'string' && row.cycle.length) return row.cycle;
|
|
78
|
+
if (row && typeof row.cycle === 'number') return String(row.cycle);
|
|
79
|
+
return 'unknown';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Fresh zeroed accumulator bucket. */
|
|
83
|
+
function emptyBucket() {
|
|
84
|
+
return { tokens_in: 0, tokens_out: 0, est_cost_usd: 0 };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Add one row's measures into an accumulator bucket (mutates `bucket`).
|
|
89
|
+
* @param {{tokens_in:number,tokens_out:number,est_cost_usd:number}} bucket
|
|
90
|
+
* @param {Record<string, unknown>} row
|
|
91
|
+
*/
|
|
92
|
+
function addInto(bucket, row) {
|
|
93
|
+
bucket.tokens_in += num(row.tokens_in);
|
|
94
|
+
bucket.tokens_out += num(row.tokens_out);
|
|
95
|
+
bucket.est_cost_usd += costUsdOf(row);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Aggregate an array (or any iterable) of cost rows into per-runtime,
|
|
100
|
+
* cumulative, and per-cycle roll-ups. Pure — never throws, never reads FS.
|
|
101
|
+
*
|
|
102
|
+
* Non-array / nullish input degrades to empty buckets.
|
|
103
|
+
*
|
|
104
|
+
* @param {Iterable<Record<string, unknown>> | null | undefined} costEvents
|
|
105
|
+
* @returns {{
|
|
106
|
+
* byRuntime: Record<string, {tokens_in:number,tokens_out:number,est_cost_usd:number}>,
|
|
107
|
+
* cumulative: {tokens_in:number,tokens_out:number,est_cost_usd:number},
|
|
108
|
+
* byCycle: Record<string, {tokens_in:number,tokens_out:number,est_cost_usd:number}>,
|
|
109
|
+
* }}
|
|
110
|
+
*/
|
|
111
|
+
function aggregateCosts(costEvents) {
|
|
112
|
+
/** @type {Record<string, ReturnType<typeof emptyBucket>>} */
|
|
113
|
+
const byRuntime = {};
|
|
114
|
+
/** @type {Record<string, ReturnType<typeof emptyBucket>>} */
|
|
115
|
+
const byCycle = {};
|
|
116
|
+
const cumulative = emptyBucket();
|
|
117
|
+
|
|
118
|
+
if (!costEvents || typeof costEvents[Symbol.iterator] !== 'function') {
|
|
119
|
+
return { byRuntime, cumulative, byCycle };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const row of costEvents) {
|
|
123
|
+
if (!row || typeof row !== 'object') continue;
|
|
124
|
+
const rt = runtimeKeyOf(row);
|
|
125
|
+
const cy = cycleKeyOf(row);
|
|
126
|
+
if (!byRuntime[rt]) byRuntime[rt] = emptyBucket();
|
|
127
|
+
if (!byCycle[cy]) byCycle[cy] = emptyBucket();
|
|
128
|
+
addInto(byRuntime[rt], row);
|
|
129
|
+
addInto(byCycle[cy], row);
|
|
130
|
+
addInto(cumulative, row);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { byRuntime, cumulative, byCycle };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Resolve the costs.jsonl path: explicit `path` wins (absolute or relative to
|
|
138
|
+
* cwd); else `<root>/.design/telemetry/costs.jsonl`; else cwd-relative default.
|
|
139
|
+
* @param {{root?: string, path?: string}} [opts]
|
|
140
|
+
* @returns {string}
|
|
141
|
+
*/
|
|
142
|
+
function costsPathFor(opts = {}) {
|
|
143
|
+
if (opts.path) {
|
|
144
|
+
return path.isAbsolute(opts.path) ? opts.path : path.resolve(process.cwd(), opts.path);
|
|
145
|
+
}
|
|
146
|
+
const root = opts.root || process.cwd();
|
|
147
|
+
return path.join(root, DEFAULT_COSTS_PATH);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Read + parse `.design/telemetry/costs.jsonl` into a cost-row array.
|
|
152
|
+
*
|
|
153
|
+
* Tolerant: a missing file returns []; malformed JSON lines are skipped
|
|
154
|
+
* silently (the writer guarantees well-formed output, so a bad line is a
|
|
155
|
+
* corruption signal that must not crash a read-only dashboard). NEVER throws.
|
|
156
|
+
*
|
|
157
|
+
* @param {{root?: string, path?: string}} [opts]
|
|
158
|
+
* @returns {Array<Record<string, unknown>>}
|
|
159
|
+
*/
|
|
160
|
+
function readCosts(opts = {}) {
|
|
161
|
+
const file = costsPathFor(opts);
|
|
162
|
+
let raw;
|
|
163
|
+
try {
|
|
164
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
165
|
+
} catch {
|
|
166
|
+
return []; // absent / unreadable -> graceful empty
|
|
167
|
+
}
|
|
168
|
+
const out = [];
|
|
169
|
+
for (const line of raw.split('\n')) {
|
|
170
|
+
const trimmed = line.trim();
|
|
171
|
+
if (trimmed === '') continue;
|
|
172
|
+
try {
|
|
173
|
+
const parsed = JSON.parse(trimmed);
|
|
174
|
+
if (parsed && typeof parsed === 'object') out.push(parsed);
|
|
175
|
+
} catch {
|
|
176
|
+
// tolerate a malformed line — skip it, keep reading
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = {
|
|
183
|
+
aggregateCosts,
|
|
184
|
+
readCosts,
|
|
185
|
+
costsPathFor,
|
|
186
|
+
DEFAULT_COSTS_PATH,
|
|
187
|
+
};
|