@tekyzinc/gsd-t 2.73.23 → 2.73.25

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 CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable changes to GSD-T are documented here. Updated with each release.
4
4
 
5
+ ## [2.73.25] - 2026-04-09
6
+
7
+ ### Added
8
+ - **Undo remove** — excluded elements show as struck-through with a green ↩ restore button instead of disappearing. Click to undo before submitting.
9
+ - **Contract deletion on submit** — when excluded elements are submitted, their contract files and source files are deleted from the project. The `/review/api/exclude` endpoint handles cleanup.
10
+
11
+ ## [2.73.24] - 2026-04-09
12
+
13
+ ### Added
14
+ - **Remove element button** — hover over any component in the list to reveal a red × button. Clicking it excludes the element from review and auto-comments "EXCLUDED — not in Figma design". Excluded count shown in submit stats.
15
+
16
+ ### Fixed
17
+ - **Comment validation removed** — all comments are now accepted (questions, exclusions, feedback). The "don't suggest specific changes" popup no longer blocks submission.
18
+
19
+ ### Changed
20
+ - **Submit stats** show removed count alongside changed/commented.
21
+
5
22
  ## [2.73.23] - 2026-04-09
6
23
 
7
24
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekyzinc/gsd-t",
3
- "version": "2.73.23",
3
+ "version": "2.73.25",
4
4
  "description": "GSD-T: Contract-Driven Development for Claude Code — 56 slash commands with headless CI/CD mode, graph-powered code analysis, real-time agent dashboard, execution intelligence, task telemetry, doc-ripple enforcement, backlog management, impact analysis, test sync, milestone archival, and PRD generation",
5
5
  "author": "Tekyz, Inc.",
6
6
  "license": "MIT",
@@ -620,6 +620,48 @@ const server = http.createServer((req, res) => {
620
620
  return;
621
621
  }
622
622
 
623
+ if (pathname === "/review/api/exclude" && req.method === "POST") {
624
+ // Remove excluded elements: delete contract files and remove from queue
625
+ let body = "";
626
+ req.on("data", (chunk) => { body += chunk; });
627
+ req.on("end", () => {
628
+ try {
629
+ const { excludedIds } = JSON.parse(body);
630
+ const removed = [];
631
+ for (const id of excludedIds) {
632
+ const item = reviewQueue.find(q => q.id === id);
633
+ if (!item || !item.sourcePath) continue;
634
+ const match = item.sourcePath.match(/src\/components\/(\w+)\/(\w+)\.vue$/);
635
+ if (!match) continue;
636
+ const [, tier, name] = match;
637
+ const kebab = name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
638
+ const contractPath = path.join(PROJECT_DIR, ".gsd-t", "contracts", "design", tier, `${kebab}.contract.md`);
639
+ if (fs.existsSync(contractPath)) {
640
+ fs.unlinkSync(contractPath);
641
+ removed.push({ id, contract: contractPath });
642
+ }
643
+ // Remove source file too
644
+ const srcPath = path.join(PROJECT_DIR, item.sourcePath);
645
+ if (fs.existsSync(srcPath)) {
646
+ fs.unlinkSync(srcPath);
647
+ removed.push({ id, source: srcPath });
648
+ }
649
+ }
650
+ // Remove from queue
651
+ for (let i = reviewQueue.length - 1; i >= 0; i--) {
652
+ if (excludedIds.includes(reviewQueue[i].id)) reviewQueue.splice(i, 1);
653
+ }
654
+ broadcast("queue-update", reviewQueue);
655
+ res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
656
+ res.end(JSON.stringify({ ok: true, removed }));
657
+ } catch (err) {
658
+ res.writeHead(400, { "Content-Type": "application/json" });
659
+ res.end(JSON.stringify({ error: err.message }));
660
+ }
661
+ });
662
+ return;
663
+ }
664
+
623
665
  if (pathname === "/review/api/write-source" && req.method === "POST") {
624
666
  // Apply CSS property changes back to source files
625
667
  let body = "";
@@ -195,6 +195,14 @@
195
195
 
196
196
  .component-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
197
197
  .component-type { font-size: 10px; color: #94a3b8; }
198
+ .remove-btn, .undo-remove-btn {
199
+ display: none; background: none; border: none; font-size: 14px;
200
+ cursor: pointer; padding: 0 4px; line-height: 1; opacity: 0.6;
201
+ }
202
+ .remove-btn { color: #ef4444; }
203
+ .undo-remove-btn { color: #22c55e; display: block; }
204
+ .remove-btn:hover, .undo-remove-btn:hover { opacity: 1; }
205
+ .component-item:hover .remove-btn { display: block; }
198
206
 
199
207
  /* ── Submit bar ─────────────────────────────────── */
200
208
  .submit-bar {
@@ -801,6 +809,7 @@
801
809
  let inspectActive = false;
802
810
  const changes = new Map(); // componentId → [{path, property, oldValue, newValue}]
803
811
  const comments = new Map(); // componentId → string
812
+ const excluded = new Set(); // componentIds removed from review
804
813
  let currentElementPath = null;
805
814
  let currentStyles = null;
806
815
 
@@ -952,14 +961,45 @@
952
961
  statusClass = "rejected";
953
962
  }
954
963
 
964
+ const isExcluded = excluded.has(item.id);
965
+
955
966
  const div = document.createElement("div");
956
- div.className = `component-item${idx === selectedIdx ? " selected" : ""}`;
957
- div.innerHTML = `
958
- <div class="component-status ${statusClass}"></div>
959
- <div class="component-name">${item.name || item.id}</div>
960
- <div class="component-type">${item.type || ""}</div>
961
- `;
962
- div.addEventListener("click", () => selectComponent(idx));
967
+ div.className = `component-item${idx === selectedIdx ? " selected" : ""}${isExcluded ? " excluded" : ""}`;
968
+
969
+ if (isExcluded) {
970
+ div.innerHTML = `
971
+ <div class="component-status" style="background:#ef4444"></div>
972
+ <div class="component-name" style="text-decoration:line-through;opacity:0.5">${item.name || item.id}</div>
973
+ <button class="undo-remove-btn" title="Restore element">↩</button>
974
+ `;
975
+ div.querySelector(".undo-remove-btn").addEventListener("click", (e) => {
976
+ e.stopPropagation();
977
+ excluded.delete(item.id);
978
+ comments.delete(item.id);
979
+ renderComponentList();
980
+ updateSubmitStats();
981
+ });
982
+ } else {
983
+ div.innerHTML = `
984
+ <div class="component-status ${statusClass}"></div>
985
+ <div class="component-name">${item.name || item.id}</div>
986
+ <div class="component-type">${item.type || ""}</div>
987
+ <button class="remove-btn" title="Remove from review">×</button>
988
+ `;
989
+ div.querySelector(".component-name").addEventListener("click", () => selectComponent(idx));
990
+ div.querySelector(".component-status").addEventListener("click", () => selectComponent(idx));
991
+ div.querySelector(".remove-btn").addEventListener("click", (e) => {
992
+ e.stopPropagation();
993
+ excluded.add(item.id);
994
+ comments.set(item.id, "EXCLUDED — not in Figma design");
995
+ if (selectedIdx === idx) {
996
+ const remaining = filteredQueue.filter(q => !excluded.has(q.id));
997
+ selectedIdx = remaining.length > 0 ? filteredQueue.indexOf(remaining[0]) : -1;
998
+ }
999
+ renderComponentList();
1000
+ updateSubmitStats();
1001
+ });
1002
+ }
963
1003
  componentList.appendChild(div);
964
1004
  });
965
1005
  }
@@ -2110,21 +2150,20 @@
2110
2150
  // ── Submit ────────────────────────────────────────
2111
2151
  function updateSubmitStats() {
2112
2152
  const total = queue.length;
2153
+ const excludedCount = excluded.size;
2113
2154
  const changed = Array.from(changes.keys()).filter(id => (changes.get(id) || []).length > 0).length;
2114
- const commented = comments.size;
2115
-
2116
- if (changed > 0 || commented > 0) {
2117
- submitStats.innerHTML = `
2118
- <span><span class="component-status changed" style="display:inline-block"></span> ${changed} changed</span>
2119
- <span><span class="component-status rejected" style="display:inline-block"></span> ${commented} commented</span>
2120
- <span style="color:var(--text-dim)">${total} total</span>
2121
- `;
2122
- } else {
2123
- submitStats.innerHTML = `<span style="color:var(--text-dim)">${total} elements no changes</span>`;
2124
- }
2125
-
2126
- submitAll.textContent = changed > 0 || commented > 0
2127
- ? `Submit (${changed} changes, ${commented} comments)`
2155
+ const commented = Array.from(comments.keys()).filter(id => !excluded.has(id)).length;
2156
+
2157
+ const parts = [];
2158
+ if (changed > 0) parts.push(`<span><span class="component-status changed" style="display:inline-block"></span> ${changed} changed</span>`);
2159
+ if (commented > 0) parts.push(`<span><span class="component-status rejected" style="display:inline-block"></span> ${commented} commented</span>`);
2160
+ if (excludedCount > 0) parts.push(`<span style="color:#ef4444">${excludedCount} removed</span>`);
2161
+ parts.push(`<span style="color:var(--text-dim)">${total - excludedCount} of ${total}</span>`);
2162
+ submitStats.innerHTML = parts.join(" ");
2163
+
2164
+ const actionCount = changed + commented + excludedCount;
2165
+ submitAll.textContent = actionCount > 0
2166
+ ? `Submit (${changed} changes, ${commented} comments, ${excludedCount} removed)`
2128
2167
  : "Submit — Approve All";
2129
2168
  }
2130
2169
 
@@ -2136,28 +2175,7 @@
2136
2175
  else comments.delete(filteredQueue[selectedIdx].id);
2137
2176
  }
2138
2177
 
2139
- // Check for non-actionable comments (documentation, not change requests)
2140
- const actionWords = /change|make|set|move|add|remove|reduce|increase|fix|use|switch|replace|adjust|align|center|should be|needs to|too |bigger|smaller|wider|narrower|thicker|thinner|lighter|darker|bolder|px|rem|%|#[0-9a-f]/i;
2141
- const docComments = [];
2142
- for (const [compId, comment] of comments) {
2143
- if (!actionWords.test(comment)) {
2144
- const item = queue.find(q => q.id === compId);
2145
- docComments.push(item ? item.name : compId);
2146
- }
2147
- }
2148
- if (docComments.length > 0) {
2149
- const proceed = confirm(
2150
- "These comments don't suggest specific changes:\n\n" +
2151
- docComments.map(n => " \u2022 " + n).join("\n") +
2152
- "\n\nComments should describe what to change, e.g.:\n" +
2153
- ' "make padding 8px"\n "use darker blue"\n "reduce gap between title and chart"\n\n' +
2154
- "Non-actionable comments will be discarded.\n\nSubmit anyway?"
2155
- );
2156
- if (!proceed) return;
2157
- for (const [compId, comment] of comments) {
2158
- if (!actionWords.test(comment)) comments.delete(compId);
2159
- }
2160
- }
2178
+ // All comments are accepted — questions, exclusions, and change requests alike
2161
2179
 
2162
2180
  // Build feedback: each element gets its changes and comments
2163
2181
  const feedback = queue.map(item => ({
@@ -2193,6 +2211,15 @@
2193
2211
  body: JSON.stringify({ changes: allChanges }),
2194
2212
  });
2195
2213
  }
2214
+
2215
+ // Delete contracts and source files for excluded elements
2216
+ if (excluded.size > 0) {
2217
+ await fetch("/review/api/exclude", {
2218
+ method: "POST",
2219
+ headers: { "Content-Type": "application/json" },
2220
+ body: JSON.stringify({ excludedIds: Array.from(excluded) }),
2221
+ });
2222
+ }
2196
2223
  }
2197
2224
  } catch (err) {
2198
2225
  submitAll.textContent = "Error — retry";