@tekyzinc/gsd-t 2.73.25 → 2.74.10
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/CHANGELOG.md +28 -0
- package/bin/archive-progress.js +335 -0
- package/bin/context-budget-audit.js +432 -0
- package/bin/gsd-t.js +79 -1
- package/bin/log-tail.js +81 -0
- package/bin/orchestrator.js +233 -47
- package/commands/gsd-t-design-decompose.md +26 -2
- package/docs/context-budget-recovery-plan.md +170 -0
- package/package.json +1 -1
- package/scripts/gsd-t-design-review-server.js +157 -3
- package/scripts/gsd-t-design-review.html +676 -14
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* node gsd-t-design-review-server.js [--port 3456] [--target http://localhost:5173] [--project /path/to/project]
|
|
11
11
|
*/
|
|
12
12
|
const http = require("http");
|
|
13
|
+
const { spawn } = require("child_process");
|
|
13
14
|
const fs = require("fs");
|
|
14
15
|
const path = require("path");
|
|
15
16
|
const url = require("url");
|
|
@@ -90,6 +91,15 @@ function extractFixtureFromContract(componentPath) {
|
|
|
90
91
|
} catch { return null; }
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
function readContractForComponent(componentPath) {
|
|
95
|
+
const match = componentPath.match(/src\/components\/(\w+)\/(\w+)\.\w+$/);
|
|
96
|
+
if (!match) return null;
|
|
97
|
+
const [, tier, name] = match;
|
|
98
|
+
const kebab = name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
99
|
+
const contractPath = path.join(PROJECT_DIR, ".gsd-t", "contracts", "design", tier, `${kebab}.contract.md`);
|
|
100
|
+
try { return fs.readFileSync(contractPath, "utf8"); } catch { return null; }
|
|
101
|
+
}
|
|
102
|
+
|
|
93
103
|
function generatePreviewHtml(componentPath) {
|
|
94
104
|
const linkTags = GLOBAL_STYLES.map(s => ` <link rel="stylesheet" href="/${s}">`).join("\n");
|
|
95
105
|
const fixture = extractFixtureFromContract(componentPath);
|
|
@@ -372,10 +382,39 @@ function readFeedback() {
|
|
|
372
382
|
} catch { return []; }
|
|
373
383
|
}
|
|
374
384
|
|
|
385
|
+
function persistAttachments(item) {
|
|
386
|
+
if (!Array.isArray(item.attachments) || item.attachments.length === 0) return item;
|
|
387
|
+
const attDir = path.join(REVIEW_DIR, "feedback", "attachments");
|
|
388
|
+
ensureDir(attDir);
|
|
389
|
+
const persisted = [];
|
|
390
|
+
item.attachments.forEach((att, idx) => {
|
|
391
|
+
if (!att || typeof att.dataUrl !== "string") return;
|
|
392
|
+
const m = att.dataUrl.match(/^data:(image\/[a-zA-Z0-9+.-]+);base64,(.+)$/);
|
|
393
|
+
if (!m) return;
|
|
394
|
+
const mime = m[1];
|
|
395
|
+
const ext = mime.split("/")[1].replace("+xml", "").replace("jpeg", "jpg");
|
|
396
|
+
const ts = Date.now();
|
|
397
|
+
const safeId = String(item.id).replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
398
|
+
const filename = `${safeId}-${ts}-${idx}.${ext}`;
|
|
399
|
+
const absPath = path.join(attDir, filename);
|
|
400
|
+
try {
|
|
401
|
+
fs.writeFileSync(absPath, Buffer.from(m[2], "base64"));
|
|
402
|
+
persisted.push({
|
|
403
|
+
name: att.name || filename,
|
|
404
|
+
path: path.relative(REVIEW_DIR, absPath),
|
|
405
|
+
mime,
|
|
406
|
+
size: Buffer.byteLength(m[2], "base64"),
|
|
407
|
+
});
|
|
408
|
+
} catch {}
|
|
409
|
+
});
|
|
410
|
+
return { ...item, attachments: persisted };
|
|
411
|
+
}
|
|
412
|
+
|
|
375
413
|
function writeFeedback(items) {
|
|
376
414
|
const fbDir = path.join(REVIEW_DIR, "feedback");
|
|
377
415
|
ensureDir(fbDir);
|
|
378
|
-
for (const
|
|
416
|
+
for (const rawItem of items) {
|
|
417
|
+
const item = persistAttachments(rawItem);
|
|
379
418
|
const fname = `${item.id}.json`;
|
|
380
419
|
fs.writeFileSync(path.join(fbDir, fname), JSON.stringify(item, null, 2));
|
|
381
420
|
}
|
|
@@ -596,6 +635,116 @@ const server = http.createServer((req, res) => {
|
|
|
596
635
|
return;
|
|
597
636
|
}
|
|
598
637
|
|
|
638
|
+
if (pathname === "/review/api/contract") {
|
|
639
|
+
const component = parsed.query.component;
|
|
640
|
+
const content = component ? readContractForComponent(component) : null;
|
|
641
|
+
res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
|
|
642
|
+
res.end(JSON.stringify({ content: content || "" }));
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (pathname === "/review/api/ai-assist" && req.method === "POST") {
|
|
647
|
+
let body = "";
|
|
648
|
+
req.on("data", (chunk) => { body += chunk; });
|
|
649
|
+
req.on("end", () => {
|
|
650
|
+
try {
|
|
651
|
+
const { messages, componentContext } = JSON.parse(body);
|
|
652
|
+
|
|
653
|
+
// Build a single prompt from system context + conversation history
|
|
654
|
+
const systemLines = [
|
|
655
|
+
"You are a design review assistant. A human is reviewing UI components against design contracts in the GSD-T Design Review panel.",
|
|
656
|
+
"Help them:",
|
|
657
|
+
"1. Answer questions about the component (properties, styles, measurements, data)",
|
|
658
|
+
"2. Translate vague corrections into precise, actionable contract language",
|
|
659
|
+
"3. Suggest specific property changes in the format: property: current → target",
|
|
660
|
+
"",
|
|
661
|
+
"Be concise. When suggesting corrections, format them so they can be pasted directly as a review comment.",
|
|
662
|
+
"",
|
|
663
|
+
"=== Component Context ===",
|
|
664
|
+
componentContext || "(no component selected)",
|
|
665
|
+
"=== End Context ===",
|
|
666
|
+
];
|
|
667
|
+
|
|
668
|
+
// Include conversation history for multi-turn
|
|
669
|
+
if (messages && messages.length > 1) {
|
|
670
|
+
systemLines.push("", "=== Conversation History ===");
|
|
671
|
+
for (const msg of messages.slice(0, -1)) {
|
|
672
|
+
systemLines.push(`${msg.role === "user" ? "Human" : "Assistant"}: ${msg.content}`);
|
|
673
|
+
}
|
|
674
|
+
systemLines.push("=== End History ===");
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const lastMessage = messages && messages.length > 0 ? messages[messages.length - 1].content : "";
|
|
678
|
+
const fullPrompt = systemLines.join("\n") + "\n\nHuman: " + lastMessage;
|
|
679
|
+
const model = process.env.GSD_AI_ASSIST_MODEL || "opus";
|
|
680
|
+
|
|
681
|
+
res.writeHead(200, {
|
|
682
|
+
"Content-Type": "text/event-stream",
|
|
683
|
+
"Cache-Control": "no-cache",
|
|
684
|
+
"Connection": "keep-alive",
|
|
685
|
+
"Access-Control-Allow-Origin": "*",
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
const claude = spawn("claude", [
|
|
689
|
+
"-p", fullPrompt,
|
|
690
|
+
"--model", model,
|
|
691
|
+
"--output-format", "stream-json",
|
|
692
|
+
"--verbose",
|
|
693
|
+
], { env: { ...process.env, NO_COLOR: "1" } });
|
|
694
|
+
|
|
695
|
+
let buf = "";
|
|
696
|
+
let textSent = 0;
|
|
697
|
+
|
|
698
|
+
claude.stdout.on("data", (chunk) => {
|
|
699
|
+
buf += chunk.toString();
|
|
700
|
+
const lines = buf.split("\n");
|
|
701
|
+
buf = lines.pop();
|
|
702
|
+
for (const line of lines) {
|
|
703
|
+
if (!line.trim()) continue;
|
|
704
|
+
try {
|
|
705
|
+
const evt = JSON.parse(line);
|
|
706
|
+
if (evt.type === "assistant" && evt.message?.content) {
|
|
707
|
+
for (const block of evt.message.content) {
|
|
708
|
+
if (block.type === "text" && block.text) {
|
|
709
|
+
const newText = block.text.slice(textSent);
|
|
710
|
+
if (newText) {
|
|
711
|
+
res.write(`data: ${JSON.stringify({ text: newText })}\n\n`);
|
|
712
|
+
textSent = block.text.length;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
} catch { /* skip non-JSON lines */ }
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
claude.stderr.on("data", () => { /* suppress stderr */ });
|
|
722
|
+
|
|
723
|
+
claude.on("close", (code) => {
|
|
724
|
+
if (!res.writableEnded) {
|
|
725
|
+
if (code !== 0 && textSent === 0) {
|
|
726
|
+
res.write(`event: error\ndata: ${JSON.stringify({ error: "Claude CLI exited with code " + code + ". Is claude installed and authenticated?" })}\n\n`);
|
|
727
|
+
}
|
|
728
|
+
res.write("event: done\ndata: {}\n\n");
|
|
729
|
+
res.end();
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
claude.on("error", (err) => {
|
|
734
|
+
if (!res.writableEnded) {
|
|
735
|
+
res.write(`event: error\ndata: ${JSON.stringify({ error: "Failed to spawn claude: " + err.message + ". Install Claude Code CLI to enable AI assist." })}\n\n`);
|
|
736
|
+
res.write("event: done\ndata: {}\n\n");
|
|
737
|
+
res.end();
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
} catch (err) {
|
|
741
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
742
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
|
|
599
748
|
if (pathname === "/review/api/feedback" && req.method === "GET") {
|
|
600
749
|
res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
|
|
601
750
|
res.end(JSON.stringify(readFeedback()));
|
|
@@ -647,9 +796,14 @@ const server = http.createServer((req, res) => {
|
|
|
647
796
|
removed.push({ id, source: srcPath });
|
|
648
797
|
}
|
|
649
798
|
}
|
|
650
|
-
// Remove from queue
|
|
799
|
+
// Remove from queue (memory + disk)
|
|
651
800
|
for (let i = reviewQueue.length - 1; i >= 0; i--) {
|
|
652
|
-
if (excludedIds.includes(reviewQueue[i].id))
|
|
801
|
+
if (excludedIds.includes(reviewQueue[i].id)) {
|
|
802
|
+
// Delete queue JSON file from disk
|
|
803
|
+
const queueFile = path.join(REVIEW_DIR, "queue", `${reviewQueue[i].id}.json`);
|
|
804
|
+
try { if (fs.existsSync(queueFile)) fs.unlinkSync(queueFile); } catch {}
|
|
805
|
+
reviewQueue.splice(i, 1);
|
|
806
|
+
}
|
|
653
807
|
}
|
|
654
808
|
broadcast("queue-update", reviewQueue);
|
|
655
809
|
res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
|