iikit-dashboard 1.5.0 → 1.6.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/README.md +30 -21
- package/package.json +5 -2
- package/src/board.js +34 -1
- package/src/bugs.js +168 -0
- package/src/parser.js +85 -6
- package/src/public/index.html +584 -15
- package/src/server.js +23 -0
package/README.md
CHANGED
|
@@ -11,11 +11,12 @@ The dashboard launches automatically early in the IIKit workflow — no manual s
|
|
|
11
11
|
You can also start it standalone to browse historical data for any project that has feature specs:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npx iikit-dashboard
|
|
15
|
-
npx iikit-dashboard /path/to/project
|
|
14
|
+
npx iikit-dashboard # current directory
|
|
15
|
+
npx iikit-dashboard /path/to/project # specific project
|
|
16
|
+
npx iikit-dashboard --port 3001 # custom port
|
|
16
17
|
```
|
|
17
18
|
|
|
18
|
-
The dashboard opens at `http://localhost:3000`.
|
|
19
|
+
The dashboard opens at `http://localhost:3000` by default. A pidfile (`.specify/dashboard.pid.json`) is written on startup so external tools can discover which project a running dashboard serves and at which port.
|
|
19
20
|
|
|
20
21
|
## Views
|
|
21
22
|
|
|
@@ -23,40 +24,48 @@ The pipeline bar at the top shows all nine IIKit workflow phases. Click any phas
|
|
|
23
24
|
|
|
24
25
|
| Phase | View |
|
|
25
26
|
|-------|------|
|
|
26
|
-
| **Constitution** | Radar chart of governance principles with obligation levels (MUST / SHOULD / MAY) |
|
|
27
|
-
| **Spec** | Story map with swim lanes by priority + interactive requirements graph (US / FR / SC nodes and edges) |
|
|
28
|
-
| **Clarify** | Q&A trail from clarification sessions, with clickable spec-item references that navigate
|
|
29
|
-
| **Plan** | Tech
|
|
27
|
+
| **Constitution** | Radar chart of governance principles with obligation levels (MUST / SHOULD / MAY) and version timeline |
|
|
28
|
+
| **Spec** | Story map with swim lanes by priority + interactive force-directed requirements graph (US / FR / SC nodes and edges) with detail side-panel |
|
|
29
|
+
| **Clarify** | Q&A trail from clarification sessions grouped by date, with clickable spec-item references that navigate to the Spec view |
|
|
30
|
+
| **Plan** | Tech context key-value pairs, interactive file-structure tree (existing vs. planned files), rendered architecture diagram, and Tessl tile cards with live eval scores |
|
|
30
31
|
| **Checklist** | Progress rings per checklist file with color coding (red/yellow/green), gate traffic light (OPEN/BLOCKED), and accordion detail view with CHK IDs and tag badges |
|
|
31
|
-
| **Testify** |
|
|
32
|
-
| **Tasks** |
|
|
33
|
-
| **Analyze** |
|
|
34
|
-
| **Implement** | Kanban board with cards sliding Todo → In Progress → Done as the agent checks off tasks |
|
|
32
|
+
| **Testify** | Assertion integrity seal (Verified/Tampered/Missing), Sankey traceability diagram (Requirements → Test Specs → Tasks), test pyramid, and gap highlighting for untested requirements |
|
|
33
|
+
| **Tasks** | Redirects to the Implement board (tasks are managed there) |
|
|
34
|
+
| **Analyze** | Health gauge (0–100) with four weighted factors, coverage heatmap (Tasks/Tests/Plan per requirement), and sortable/filterable severity table of analysis findings |
|
|
35
|
+
| **Implement** | Kanban board with cards sliding Todo → In Progress → Done as the agent checks off tasks, with collapsible per-story task lists |
|
|
35
36
|
|
|
36
37
|
## Features
|
|
37
38
|
|
|
38
39
|
- **Live updates** — all views refresh in real time via WebSocket as project files change
|
|
39
40
|
- **Pipeline navigation** — phase nodes show status (complete / in-progress / skipped / not started) with progress percentages
|
|
41
|
+
- **Cross-panel navigation** — Cmd/Ctrl+click any FR, US, SC, or task identifier to jump to its linked panel (Spec, Testify, Implement, Checklist, or Clarify)
|
|
40
42
|
- **Feature selector** — dropdown to switch between features in `specs/`, sorted newest-first
|
|
41
|
-
- **
|
|
42
|
-
- **Integrity badges** — shows whether test assertions have been tampered with
|
|
43
|
+
- **Project label** — header shows the project directory name with full path on hover, so you know which project a dashboard tab belongs to
|
|
44
|
+
- **Integrity badges** — shows whether test assertions have been tampered with (verified / tampered / missing)
|
|
45
|
+
- **Tessl eval scores** — Plan view tile cards display live eval data (score, pass/fail chart) when available
|
|
46
|
+
- **Activity indicator** — green dot pulses in the header when files are actively changing
|
|
47
|
+
- **Multi-project support** — pidfile at `.specify/dashboard.pid.json` lets external scripts identify running instances per project
|
|
43
48
|
- **Three-state theme** — cycles System (OS preference) → Light → Dark
|
|
44
49
|
- **Zero build step** — single HTML file with inline CSS and JS
|
|
45
50
|
|
|
46
51
|
## How It Works
|
|
47
52
|
|
|
48
|
-
The server reads directly from your project's
|
|
53
|
+
The server reads directly from your project's directory:
|
|
49
54
|
|
|
50
55
|
| File | Purpose |
|
|
51
56
|
|------|---------|
|
|
52
|
-
| `spec.md` | User stories, requirements, success criteria, and clarification Q&A |
|
|
53
|
-
| `plan.md` | Tech stack, file structure, and architecture diagram |
|
|
54
|
-
| `tasks.md` | Task checkboxes grouped by `[US1]`, `[US2]` tags |
|
|
55
|
-
| `checklists/*.md` | Checklist items with completion status, CHK IDs, and category groupings |
|
|
56
57
|
| `CONSTITUTION.md` | Governance principles and obligation levels |
|
|
57
|
-
| `
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
| `specs/<feature>/spec.md` | User stories, requirements, success criteria, and clarification Q&A |
|
|
59
|
+
| `specs/<feature>/plan.md` | Tech stack, file structure, and architecture diagram |
|
|
60
|
+
| `specs/<feature>/research.md` | Research decisions (displayed as tooltips in Plan view) |
|
|
61
|
+
| `specs/<feature>/tasks.md` | Task checkboxes grouped by `[US1]`, `[US2]` tags |
|
|
62
|
+
| `specs/<feature>/checklists/*.md` | Checklist items with completion status, CHK IDs, and category groupings |
|
|
63
|
+
| `specs/<feature>/tests/test-specs.md` | Test specifications for the Testify traceability view |
|
|
64
|
+
| `specs/<feature>/context.json` | Assertion hash for integrity verification |
|
|
65
|
+
| `specs/<feature>/analysis.md` | Consistency analysis findings, coverage, and metrics |
|
|
66
|
+
| `tessl.json` | Installed Tessl tiles for the Plan dependency panel |
|
|
67
|
+
|
|
68
|
+
A file watcher (chokidar) monitors the project tree (excluding `node_modules` and `.git`) and pushes updates to the browser via WebSocket with 300 ms debounce.
|
|
60
69
|
|
|
61
70
|
## Requirements
|
|
62
71
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iikit-dashboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Real-time dashboard for Intent Integrity Kit (IIKit) — visualizes every phase of specification-driven AI development",
|
|
5
5
|
"main": "src/server.js",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
],
|
|
13
13
|
"scripts": {
|
|
14
14
|
"start": "node bin/iikit-dashboard.js",
|
|
15
|
-
"test": "jest --forceExit"
|
|
15
|
+
"test": "jest --forceExit --testPathIgnorePatterns=test/visual",
|
|
16
|
+
"test:visual": "npx playwright test",
|
|
17
|
+
"test:visual:update": "npx playwright test --update-snapshots"
|
|
16
18
|
},
|
|
17
19
|
"keywords": [
|
|
18
20
|
"iikit",
|
|
@@ -40,6 +42,7 @@
|
|
|
40
42
|
"ws": "^8.19.0"
|
|
41
43
|
},
|
|
42
44
|
"devDependencies": {
|
|
45
|
+
"@playwright/test": "^1.58.2",
|
|
43
46
|
"jest": "^30.2.0"
|
|
44
47
|
}
|
|
45
48
|
}
|
package/src/board.js
CHANGED
|
@@ -19,8 +19,9 @@ function computeBoardState(stories, tasks) {
|
|
|
19
19
|
if (!stories || !Array.isArray(stories)) return board;
|
|
20
20
|
if (!tasks) tasks = [];
|
|
21
21
|
|
|
22
|
-
// Group tasks by storyTag
|
|
22
|
+
// Group tasks by storyTag or bugTag
|
|
23
23
|
const tasksByStory = {};
|
|
24
|
+
const tasksByBug = {};
|
|
24
25
|
const untaggedTasks = [];
|
|
25
26
|
|
|
26
27
|
for (const task of tasks) {
|
|
@@ -29,6 +30,11 @@ function computeBoardState(stories, tasks) {
|
|
|
29
30
|
tasksByStory[task.storyTag] = [];
|
|
30
31
|
}
|
|
31
32
|
tasksByStory[task.storyTag].push(task);
|
|
33
|
+
} else if (task.bugTag) {
|
|
34
|
+
if (!tasksByBug[task.bugTag]) {
|
|
35
|
+
tasksByBug[task.bugTag] = [];
|
|
36
|
+
}
|
|
37
|
+
tasksByBug[task.bugTag].push(task);
|
|
32
38
|
} else {
|
|
33
39
|
untaggedTasks.push(task);
|
|
34
40
|
}
|
|
@@ -87,6 +93,33 @@ function computeBoardState(stories, tasks) {
|
|
|
87
93
|
board[column].push(card);
|
|
88
94
|
}
|
|
89
95
|
|
|
96
|
+
// Handle bug fix tasks — group into per-bug cards
|
|
97
|
+
for (const [bugId, bugTasks] of Object.entries(tasksByBug)) {
|
|
98
|
+
const checkedCount = bugTasks.filter(t => t.checked).length;
|
|
99
|
+
const totalCount = bugTasks.length;
|
|
100
|
+
|
|
101
|
+
let column;
|
|
102
|
+
if (checkedCount === 0) {
|
|
103
|
+
column = 'todo';
|
|
104
|
+
} else if (checkedCount === totalCount) {
|
|
105
|
+
column = 'done';
|
|
106
|
+
} else {
|
|
107
|
+
column = 'in_progress';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const card = {
|
|
111
|
+
id: bugId,
|
|
112
|
+
title: `Bug Fix: ${bugId}`,
|
|
113
|
+
priority: 'P2',
|
|
114
|
+
tasks: bugTasks,
|
|
115
|
+
progress: `${checkedCount}/${totalCount}`,
|
|
116
|
+
column,
|
|
117
|
+
isBugCard: true
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
board[column].push(card);
|
|
121
|
+
}
|
|
122
|
+
|
|
90
123
|
return board;
|
|
91
124
|
}
|
|
92
125
|
|
package/src/bugs.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { parseBugs, parseTasks } = require('./parser');
|
|
6
|
+
|
|
7
|
+
const SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve a GitHub issue reference like "#13" to a full URL.
|
|
11
|
+
*
|
|
12
|
+
* @param {string|null} issueRef - Issue reference (e.g., "#13") or null
|
|
13
|
+
* @param {string|null} repoUrl - Repository URL from git remote or null
|
|
14
|
+
* @returns {string|null} Full URL or null
|
|
15
|
+
*/
|
|
16
|
+
function resolveGitHubIssueUrl(issueRef, repoUrl) {
|
|
17
|
+
if (!issueRef || !repoUrl) return null;
|
|
18
|
+
|
|
19
|
+
// Skip sentinel values
|
|
20
|
+
if (/^_\(/.test(issueRef)) return null;
|
|
21
|
+
|
|
22
|
+
// Extract issue number
|
|
23
|
+
const numMatch = issueRef.match(/#(\d+)/);
|
|
24
|
+
if (!numMatch) return null;
|
|
25
|
+
|
|
26
|
+
const issueNumber = numMatch[1];
|
|
27
|
+
|
|
28
|
+
// Normalize repo URL
|
|
29
|
+
let baseUrl = repoUrl;
|
|
30
|
+
|
|
31
|
+
// Handle SSH format: git@github.com:user/repo.git
|
|
32
|
+
const sshMatch = baseUrl.match(/^git@([^:]+):(.+?)(?:\.git)?$/);
|
|
33
|
+
if (sshMatch) {
|
|
34
|
+
baseUrl = `https://${sshMatch[1]}/${sshMatch[2]}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Strip trailing .git
|
|
38
|
+
baseUrl = baseUrl.replace(/\.git$/, '');
|
|
39
|
+
|
|
40
|
+
return `${baseUrl}/issues/${issueNumber}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Compute the bugs state for a feature.
|
|
45
|
+
* Reads bugs.md and tasks.md, cross-references fix tasks by [BUG-NNN] tag.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} projectPath - Path to the project root
|
|
48
|
+
* @param {string} featureId - Feature directory name (e.g., "009-bugs-tab")
|
|
49
|
+
* @returns {{exists: boolean, bugs: Array, orphanedTasks: Array, summary: Object, repoUrl: string|null}}
|
|
50
|
+
*/
|
|
51
|
+
function computeBugsState(projectPath, featureId) {
|
|
52
|
+
const featureDir = path.join(projectPath, 'specs', featureId);
|
|
53
|
+
const bugsPath = path.join(featureDir, 'bugs.md');
|
|
54
|
+
const tasksPath = path.join(featureDir, 'tasks.md');
|
|
55
|
+
|
|
56
|
+
const emptySummary = {
|
|
57
|
+
total: 0,
|
|
58
|
+
open: 0,
|
|
59
|
+
fixed: 0,
|
|
60
|
+
highestOpenSeverity: null,
|
|
61
|
+
bySeverity: { critical: 0, high: 0, medium: 0, low: 0 }
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
if (!fs.existsSync(bugsPath)) {
|
|
65
|
+
return { exists: false, bugs: [], orphanedTasks: [], summary: emptySummary, repoUrl: null };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const bugsContent = fs.readFileSync(bugsPath, 'utf-8');
|
|
69
|
+
const bugs = parseBugs(bugsContent);
|
|
70
|
+
|
|
71
|
+
// Parse tasks for fix task cross-referencing
|
|
72
|
+
const tasksContent = fs.existsSync(tasksPath) ? fs.readFileSync(tasksPath, 'utf-8') : '';
|
|
73
|
+
const allTasks = parseTasks(tasksContent);
|
|
74
|
+
const bugFixTasks = allTasks.filter(t => t.isBugFix && t.bugTag);
|
|
75
|
+
|
|
76
|
+
// Build lookup: BUG-NNN -> [task, ...]
|
|
77
|
+
const tasksByBug = {};
|
|
78
|
+
for (const task of bugFixTasks) {
|
|
79
|
+
if (!tasksByBug[task.bugTag]) tasksByBug[task.bugTag] = [];
|
|
80
|
+
tasksByBug[task.bugTag].push(task);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Track which bug IDs exist
|
|
84
|
+
const bugIds = new Set(bugs.map(b => b.id));
|
|
85
|
+
|
|
86
|
+
// Enrich bugs with fix tasks
|
|
87
|
+
for (const bug of bugs) {
|
|
88
|
+
const tasks = tasksByBug[bug.id] || [];
|
|
89
|
+
bug.fixTasks = {
|
|
90
|
+
total: tasks.length,
|
|
91
|
+
checked: tasks.filter(t => t.checked).length,
|
|
92
|
+
tasks: tasks.map(t => ({
|
|
93
|
+
id: t.id,
|
|
94
|
+
description: t.description,
|
|
95
|
+
checked: t.checked
|
|
96
|
+
}))
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Sort bugs: severity descending (critical first), then ID ascending
|
|
101
|
+
bugs.sort((a, b) => {
|
|
102
|
+
const sevA = SEVERITY_ORDER[a.severity] !== undefined ? SEVERITY_ORDER[a.severity] : 3;
|
|
103
|
+
const sevB = SEVERITY_ORDER[b.severity] !== undefined ? SEVERITY_ORDER[b.severity] : 3;
|
|
104
|
+
const sevDiff = sevA - sevB;
|
|
105
|
+
if (sevDiff !== 0) return sevDiff;
|
|
106
|
+
return a.id.localeCompare(b.id);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Detect orphaned tasks (T-B tasks referencing non-existent BUG-NNN)
|
|
110
|
+
const orphanedTasks = bugFixTasks
|
|
111
|
+
.filter(t => !bugIds.has(t.bugTag))
|
|
112
|
+
.map(t => ({ id: t.id, bugTag: t.bugTag, description: t.description, checked: t.checked }));
|
|
113
|
+
|
|
114
|
+
// Compute summary
|
|
115
|
+
const openBugs = bugs.filter(b => b.status !== 'fixed');
|
|
116
|
+
const fixedBugs = bugs.filter(b => b.status === 'fixed');
|
|
117
|
+
|
|
118
|
+
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
119
|
+
for (const bug of openBugs) {
|
|
120
|
+
if (bySeverity.hasOwnProperty(bug.severity)) {
|
|
121
|
+
bySeverity[bug.severity]++;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let highestOpenSeverity = null;
|
|
126
|
+
for (const sev of ['critical', 'high', 'medium', 'low']) {
|
|
127
|
+
if (bySeverity[sev] > 0) {
|
|
128
|
+
highestOpenSeverity = sev;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Try to get repo URL from git remote
|
|
134
|
+
let repoUrl = null;
|
|
135
|
+
try {
|
|
136
|
+
const { execSync } = require('child_process');
|
|
137
|
+
repoUrl = execSync('git remote get-url origin', {
|
|
138
|
+
cwd: projectPath,
|
|
139
|
+
encoding: 'utf-8',
|
|
140
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
141
|
+
}).trim();
|
|
142
|
+
// Strip .git suffix
|
|
143
|
+
repoUrl = repoUrl.replace(/\.git$/, '');
|
|
144
|
+
// Handle SSH format
|
|
145
|
+
const sshMatch = repoUrl.match(/^git@([^:]+):(.+)$/);
|
|
146
|
+
if (sshMatch) {
|
|
147
|
+
repoUrl = `https://${sshMatch[1]}/${sshMatch[2]}`;
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
// No git remote — repoUrl stays null
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
exists: true,
|
|
155
|
+
bugs,
|
|
156
|
+
orphanedTasks,
|
|
157
|
+
summary: {
|
|
158
|
+
total: bugs.length,
|
|
159
|
+
open: openBugs.length,
|
|
160
|
+
fixed: fixedBugs.length,
|
|
161
|
+
highestOpenSeverity,
|
|
162
|
+
bySeverity
|
|
163
|
+
},
|
|
164
|
+
repoUrl
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
module.exports = { computeBugsState, resolveGitHubIssueUrl };
|
package/src/parser.js
CHANGED
|
@@ -57,23 +57,31 @@ function parseSpecStories(content) {
|
|
|
57
57
|
/**
|
|
58
58
|
* Parse tasks.md to extract tasks with checkbox status and story tags.
|
|
59
59
|
* Pattern: - [x] TXXX [P]? [USy]? Description
|
|
60
|
+
* Extended: also matches T-B\d+ IDs and [BUG-\d+] tags for bug fix tasks.
|
|
60
61
|
*
|
|
61
62
|
* @param {string} content - Raw markdown content of tasks.md
|
|
62
|
-
* @returns {Array<{id: string, storyTag: string|null, description: string, checked: boolean}>}
|
|
63
|
+
* @returns {Array<{id: string, storyTag: string|null, bugTag: string|null, description: string, checked: boolean, isBugFix: boolean}>}
|
|
63
64
|
*/
|
|
64
65
|
function parseTasks(content) {
|
|
65
66
|
if (!content || typeof content !== 'string') return [];
|
|
66
67
|
|
|
67
|
-
const regex = /- \[([ x])\] (T
|
|
68
|
+
const regex = /- \[([ x])\] (T(?:-B)?\d+)\s+(?:\[P\]\s*)?(?:\[(US\d+|BUG-\d+)\]\s*)?(.*)/g;
|
|
68
69
|
const tasks = [];
|
|
69
70
|
let match;
|
|
70
71
|
|
|
71
72
|
while ((match = regex.exec(content)) !== null) {
|
|
73
|
+
const id = match[2];
|
|
74
|
+
const tag = match[3] || null;
|
|
75
|
+
const isBugFix = id.startsWith('T-B');
|
|
76
|
+
const isBugTag = tag && /^BUG-\d+$/.test(tag);
|
|
77
|
+
|
|
72
78
|
tasks.push({
|
|
73
|
-
id
|
|
74
|
-
storyTag:
|
|
79
|
+
id,
|
|
80
|
+
storyTag: (tag && !isBugTag) ? tag : null,
|
|
81
|
+
bugTag: isBugTag ? tag : null,
|
|
75
82
|
description: match[4].trim(),
|
|
76
|
-
checked: match[1] === 'x'
|
|
83
|
+
checked: match[1] === 'x',
|
|
84
|
+
isBugFix
|
|
77
85
|
});
|
|
78
86
|
}
|
|
79
87
|
|
|
@@ -1179,4 +1187,75 @@ function parsePhaseSeparation(content) {
|
|
|
1179
1187
|
}).filter(Boolean);
|
|
1180
1188
|
}
|
|
1181
1189
|
|
|
1182
|
-
|
|
1190
|
+
/**
|
|
1191
|
+
* Parse bugs.md to extract bug entries.
|
|
1192
|
+
* Pattern: ## BUG-NNN headings with field lines.
|
|
1193
|
+
* Permissive parsing — returns [] on missing/empty/malformed input.
|
|
1194
|
+
*
|
|
1195
|
+
* @param {string} content - Raw markdown content of bugs.md
|
|
1196
|
+
* @returns {Array<{id: string, reported: string|null, severity: string, status: string, githubIssue: string|null, description: string|null, rootCause: string|null, fixReference: string|null}>}
|
|
1197
|
+
*/
|
|
1198
|
+
function parseBugs(content) {
|
|
1199
|
+
if (!content || typeof content !== 'string') return [];
|
|
1200
|
+
|
|
1201
|
+
const validSeverities = new Set(['critical', 'high', 'medium', 'low']);
|
|
1202
|
+
const validStatuses = new Set(['reported', 'fixed']);
|
|
1203
|
+
|
|
1204
|
+
const headingRegex = /^## (BUG-\d+)\s*$/gm;
|
|
1205
|
+
const bugStarts = [];
|
|
1206
|
+
let match;
|
|
1207
|
+
|
|
1208
|
+
while ((match = headingRegex.exec(content)) !== null) {
|
|
1209
|
+
bugStarts.push({ id: match[1], index: match.index });
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
const bugs = [];
|
|
1213
|
+
|
|
1214
|
+
for (let i = 0; i < bugStarts.length; i++) {
|
|
1215
|
+
const start = bugStarts[i].index;
|
|
1216
|
+
const end = i + 1 < bugStarts.length ? bugStarts[i + 1].index : content.length;
|
|
1217
|
+
const section = content.substring(start, end);
|
|
1218
|
+
|
|
1219
|
+
const bug = {
|
|
1220
|
+
id: bugStarts[i].id,
|
|
1221
|
+
reported: extractField(section, 'Reported'),
|
|
1222
|
+
severity: extractField(section, 'Severity') || 'medium',
|
|
1223
|
+
status: extractField(section, 'Status') || 'reported',
|
|
1224
|
+
githubIssue: extractField(section, 'GitHub Issue'),
|
|
1225
|
+
description: extractField(section, 'Description'),
|
|
1226
|
+
rootCause: extractField(section, 'Root Cause'),
|
|
1227
|
+
fixReference: extractField(section, 'Fix Reference')
|
|
1228
|
+
};
|
|
1229
|
+
|
|
1230
|
+
// Validate severity
|
|
1231
|
+
if (!validSeverities.has(bug.severity)) {
|
|
1232
|
+
bug.severity = 'medium';
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// Validate status
|
|
1236
|
+
if (!validStatuses.has(bug.status)) {
|
|
1237
|
+
bug.status = 'reported';
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
bugs.push(bug);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
return bugs;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
/**
|
|
1247
|
+
* Extract a **Field**: Value from a bug section.
|
|
1248
|
+
* Returns null for _(none)_, _(empty...)_ patterns, and missing fields.
|
|
1249
|
+
*/
|
|
1250
|
+
function extractField(section, fieldName) {
|
|
1251
|
+
const regex = new RegExp(`\\*\\*${fieldName}\\*\\*:\\s*(.+)`, 'm');
|
|
1252
|
+
const match = section.match(regex);
|
|
1253
|
+
if (!match) return null;
|
|
1254
|
+
|
|
1255
|
+
const value = match[1].trim();
|
|
1256
|
+
if (!value || /^_\(/.test(value)) return null;
|
|
1257
|
+
|
|
1258
|
+
return value;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
module.exports = { parseSpecStories, parseTasks, parseChecklists, parseChecklistsDetailed, parseConstitutionTDD, hasClarifications, parseConstitutionPrinciples, parseRequirements, parseSuccessCriteria, parseClarifications, parseStoryRequirementRefs, parseTechContext, parseFileStructure, parseAsciiDiagram, parseTesslJson, parseResearchDecisions, parseTestSpecs, parseTaskTestRefs, parseAnalysisFindings, parseAnalysisCoverage, parseAnalysisMetrics, parseConstitutionAlignment, parsePhaseSeparation, parseBugs };
|
package/src/public/index.html
CHANGED
|
@@ -724,9 +724,9 @@
|
|
|
724
724
|
border-radius: var(--radius-md);
|
|
725
725
|
cursor: pointer;
|
|
726
726
|
transition: background var(--transition-fast), transform var(--transition-fast);
|
|
727
|
-
flex:
|
|
728
|
-
width:
|
|
729
|
-
|
|
727
|
+
flex: 1 1 0;
|
|
728
|
+
min-width: 72px;
|
|
729
|
+
max-width: 110px;
|
|
730
730
|
position: relative;
|
|
731
731
|
border: 1px solid var(--color-border-subtle);
|
|
732
732
|
background: transparent;
|
|
@@ -830,6 +830,340 @@
|
|
|
830
830
|
opacity: 0.8;
|
|
831
831
|
}
|
|
832
832
|
|
|
833
|
+
/* ====== Bugs Tab (standalone, no connector) ====== */
|
|
834
|
+
.bugs-tab-gap {
|
|
835
|
+
width: 16px;
|
|
836
|
+
flex-shrink: 0;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
.bugs-tab {
|
|
840
|
+
display: flex;
|
|
841
|
+
flex-direction: column;
|
|
842
|
+
align-items: center;
|
|
843
|
+
gap: 4px;
|
|
844
|
+
padding: 6px 12px;
|
|
845
|
+
cursor: pointer;
|
|
846
|
+
border-radius: 8px;
|
|
847
|
+
border: 1px solid var(--color-border);
|
|
848
|
+
background: var(--color-surface);
|
|
849
|
+
transition: all 0.15s ease;
|
|
850
|
+
position: relative;
|
|
851
|
+
font-family: inherit;
|
|
852
|
+
color: inherit;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
.bugs-tab:hover {
|
|
856
|
+
border-color: var(--color-text-secondary);
|
|
857
|
+
background: rgba(255, 255, 255, 0.06);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
.bugs-tab.active {
|
|
861
|
+
border-color: var(--color-accent);
|
|
862
|
+
background: rgba(99, 102, 241, 0.08);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
.bugs-tab.muted {
|
|
866
|
+
opacity: 0.45;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
.bugs-tab-icon {
|
|
870
|
+
font-size: 14px;
|
|
871
|
+
line-height: 1;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
.bugs-tab-label {
|
|
875
|
+
font-size: 10px;
|
|
876
|
+
text-transform: uppercase;
|
|
877
|
+
letter-spacing: 0.5px;
|
|
878
|
+
color: var(--color-text-secondary);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
.bugs-tab-badge {
|
|
882
|
+
position: absolute;
|
|
883
|
+
top: -6px;
|
|
884
|
+
right: -6px;
|
|
885
|
+
min-width: 18px;
|
|
886
|
+
height: 18px;
|
|
887
|
+
padding: 0 5px;
|
|
888
|
+
border-radius: 9px;
|
|
889
|
+
font-size: 11px;
|
|
890
|
+
font-weight: 600;
|
|
891
|
+
line-height: 18px;
|
|
892
|
+
text-align: center;
|
|
893
|
+
color: #fff;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
.bugs-tab-badge.muted {
|
|
897
|
+
opacity: 0.4;
|
|
898
|
+
background: var(--color-text-secondary);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
.bugs-tab-badge.severity-critical { background: #ff4757; }
|
|
902
|
+
.bugs-tab-badge.severity-high { background: #ffa502; }
|
|
903
|
+
.bugs-tab-badge.severity-medium { background: #f1c40f; color: #1a1a2e; }
|
|
904
|
+
.bugs-tab-badge.severity-low { background: #6b7189; }
|
|
905
|
+
|
|
906
|
+
/* ====== Bug Severity Colors ====== */
|
|
907
|
+
.severity-critical { color: #ff4757; }
|
|
908
|
+
.severity-high { color: #ffa502; }
|
|
909
|
+
.severity-medium { color: #f1c40f; }
|
|
910
|
+
.severity-low { color: #6b7189; }
|
|
911
|
+
|
|
912
|
+
.severity-badge {
|
|
913
|
+
display: inline-block;
|
|
914
|
+
padding: 2px 8px;
|
|
915
|
+
border-radius: 4px;
|
|
916
|
+
font-size: 11px;
|
|
917
|
+
font-weight: 600;
|
|
918
|
+
text-transform: uppercase;
|
|
919
|
+
letter-spacing: 0.3px;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
.severity-badge.severity-critical { background: rgba(255, 71, 87, 0.15); color: #ff4757; }
|
|
923
|
+
.severity-badge.severity-high { background: rgba(255, 165, 2, 0.15); color: #ffa502; }
|
|
924
|
+
.severity-badge.severity-medium { background: rgba(241, 196, 15, 0.15); color: #f1c40f; }
|
|
925
|
+
.severity-badge.severity-low { background: rgba(107, 113, 137, 0.15); color: #6b7189; }
|
|
926
|
+
|
|
927
|
+
/* ====== Bugs View ====== */
|
|
928
|
+
.bugs-view {
|
|
929
|
+
padding: 24px;
|
|
930
|
+
max-width: 1200px;
|
|
931
|
+
margin: 0 auto;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
.bugs-view-header {
|
|
935
|
+
margin-bottom: 20px;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
.bugs-view-title {
|
|
939
|
+
font-size: 20px;
|
|
940
|
+
font-weight: 600;
|
|
941
|
+
color: var(--color-text-primary);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
.bugs-view-subtitle {
|
|
945
|
+
font-size: 13px;
|
|
946
|
+
color: var(--color-text-secondary);
|
|
947
|
+
margin-top: 4px;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
.bugs-table {
|
|
951
|
+
width: 100%;
|
|
952
|
+
border-collapse: collapse;
|
|
953
|
+
font-size: 13px;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
.bugs-table th {
|
|
957
|
+
text-align: left;
|
|
958
|
+
padding: 10px 12px;
|
|
959
|
+
font-weight: 600;
|
|
960
|
+
color: var(--color-text-secondary);
|
|
961
|
+
border-bottom: 1px solid var(--color-border);
|
|
962
|
+
font-size: 11px;
|
|
963
|
+
text-transform: uppercase;
|
|
964
|
+
letter-spacing: 0.5px;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
.bugs-table td {
|
|
968
|
+
padding: 10px 12px;
|
|
969
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
|
970
|
+
vertical-align: middle;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
.bugs-table tr:hover td {
|
|
974
|
+
background: rgba(255, 255, 255, 0.02);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
.bugs-table tr.highlighted td {
|
|
978
|
+
background: rgba(99, 102, 241, 0.12);
|
|
979
|
+
transition: background 0.3s ease;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
.bugs-table .bug-id {
|
|
983
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
984
|
+
font-weight: 600;
|
|
985
|
+
color: var(--color-text-primary);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
.bugs-table .bug-status {
|
|
989
|
+
text-transform: capitalize;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
.bugs-table .bug-status.fixed {
|
|
993
|
+
color: var(--color-done);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
.bugs-table .bug-progress {
|
|
997
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
998
|
+
font-size: 12px;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
.bugs-table .bug-description {
|
|
1002
|
+
max-width: 300px;
|
|
1003
|
+
overflow: hidden;
|
|
1004
|
+
text-overflow: ellipsis;
|
|
1005
|
+
white-space: nowrap;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
.bugs-table .github-link {
|
|
1009
|
+
color: var(--color-accent);
|
|
1010
|
+
text-decoration: none;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
.bugs-table .github-link:hover {
|
|
1014
|
+
text-decoration: underline;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
.bugs-empty {
|
|
1018
|
+
display: flex;
|
|
1019
|
+
flex-direction: column;
|
|
1020
|
+
align-items: center;
|
|
1021
|
+
justify-content: center;
|
|
1022
|
+
padding: 80px 20px;
|
|
1023
|
+
text-align: center;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
.bugs-empty-icon {
|
|
1027
|
+
font-size: 48px;
|
|
1028
|
+
margin-bottom: 16px;
|
|
1029
|
+
opacity: 0.4;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
.bugs-empty-title {
|
|
1033
|
+
font-size: 18px;
|
|
1034
|
+
font-weight: 600;
|
|
1035
|
+
color: var(--color-text-primary);
|
|
1036
|
+
margin-bottom: 8px;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
.bugs-empty-text {
|
|
1040
|
+
font-size: 14px;
|
|
1041
|
+
color: var(--color-text-secondary);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
.orphaned-section {
|
|
1045
|
+
margin-top: 24px;
|
|
1046
|
+
padding: 16px;
|
|
1047
|
+
border: 1px dashed var(--color-border);
|
|
1048
|
+
border-radius: 8px;
|
|
1049
|
+
opacity: 0.7;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
.orphaned-title {
|
|
1053
|
+
font-size: 13px;
|
|
1054
|
+
font-weight: 600;
|
|
1055
|
+
color: var(--color-text-secondary);
|
|
1056
|
+
margin-bottom: 8px;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
.orphaned-task {
|
|
1060
|
+
font-size: 12px;
|
|
1061
|
+
color: var(--color-text-secondary);
|
|
1062
|
+
padding: 4px 0;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
.bug-icon-inline {
|
|
1066
|
+
display: inline-block;
|
|
1067
|
+
width: 14px;
|
|
1068
|
+
height: 14px;
|
|
1069
|
+
margin-right: 4px;
|
|
1070
|
+
vertical-align: middle;
|
|
1071
|
+
opacity: 0.7;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/* Pipeline bugs gap & node (spec aliases) */
|
|
1075
|
+
.pipeline-bugs-gap {
|
|
1076
|
+
width: 20px;
|
|
1077
|
+
flex-shrink: 0;
|
|
1078
|
+
}
|
|
1079
|
+
.pipeline-node-bugs.muted {
|
|
1080
|
+
opacity: 0.5;
|
|
1081
|
+
}
|
|
1082
|
+
.bugs-dot {
|
|
1083
|
+
background: var(--color-surface-elevated) !important;
|
|
1084
|
+
border: 2px solid var(--color-border) !important;
|
|
1085
|
+
color: var(--color-text-muted) !important;
|
|
1086
|
+
}
|
|
1087
|
+
.bug-count-badge {
|
|
1088
|
+
display: inline-flex;
|
|
1089
|
+
align-items: center;
|
|
1090
|
+
justify-content: center;
|
|
1091
|
+
min-width: 18px;
|
|
1092
|
+
height: 18px;
|
|
1093
|
+
padding: 0 5px;
|
|
1094
|
+
border-radius: 9px;
|
|
1095
|
+
font-size: 11px;
|
|
1096
|
+
font-weight: 700;
|
|
1097
|
+
margin-left: 4px;
|
|
1098
|
+
vertical-align: middle;
|
|
1099
|
+
}
|
|
1100
|
+
.bug-count-badge.muted {
|
|
1101
|
+
background: var(--color-surface-elevated);
|
|
1102
|
+
color: var(--color-text-muted);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/* Bug view additional styles */
|
|
1106
|
+
.bugs-view-container {
|
|
1107
|
+
padding: 24px;
|
|
1108
|
+
max-width: 1200px;
|
|
1109
|
+
}
|
|
1110
|
+
.bugs-view-container h2 {
|
|
1111
|
+
font-size: 18px;
|
|
1112
|
+
font-weight: 600;
|
|
1113
|
+
margin-bottom: 16px;
|
|
1114
|
+
color: var(--color-text);
|
|
1115
|
+
}
|
|
1116
|
+
.bug-row:hover {
|
|
1117
|
+
background: var(--color-surface-hover);
|
|
1118
|
+
}
|
|
1119
|
+
.fix-progress {
|
|
1120
|
+
font-variant-numeric: tabular-nums;
|
|
1121
|
+
font-weight: 500;
|
|
1122
|
+
}
|
|
1123
|
+
.bug-status {
|
|
1124
|
+
text-transform: capitalize;
|
|
1125
|
+
}
|
|
1126
|
+
.bug-status.fixed {
|
|
1127
|
+
color: var(--color-success, #2ecc71);
|
|
1128
|
+
}
|
|
1129
|
+
.bug-github-link {
|
|
1130
|
+
color: var(--color-accent);
|
|
1131
|
+
text-decoration: none;
|
|
1132
|
+
}
|
|
1133
|
+
.bug-github-link:hover {
|
|
1134
|
+
text-decoration: underline;
|
|
1135
|
+
}
|
|
1136
|
+
.bug-empty-state {
|
|
1137
|
+
text-align: center;
|
|
1138
|
+
padding: 48px 24px;
|
|
1139
|
+
color: var(--color-text-muted);
|
|
1140
|
+
font-size: 14px;
|
|
1141
|
+
}
|
|
1142
|
+
.bug-empty-state svg {
|
|
1143
|
+
display: block;
|
|
1144
|
+
margin: 0 auto 12px;
|
|
1145
|
+
opacity: 0.4;
|
|
1146
|
+
}
|
|
1147
|
+
.orphaned-tasks-section {
|
|
1148
|
+
margin-top: 32px;
|
|
1149
|
+
padding: 16px;
|
|
1150
|
+
background: var(--color-surface-elevated);
|
|
1151
|
+
border-radius: var(--radius-md);
|
|
1152
|
+
border: 1px solid var(--color-warning, #f5a623);
|
|
1153
|
+
opacity: 0.7;
|
|
1154
|
+
}
|
|
1155
|
+
.orphaned-tasks-section h3 {
|
|
1156
|
+
font-size: 13px;
|
|
1157
|
+
font-weight: 600;
|
|
1158
|
+
color: var(--color-warning, #f5a623);
|
|
1159
|
+
margin-bottom: 8px;
|
|
1160
|
+
}
|
|
1161
|
+
.orphaned-tasks-section .task-item {
|
|
1162
|
+
font-size: 12px;
|
|
1163
|
+
color: var(--color-text-muted);
|
|
1164
|
+
padding: 4px 0;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
833
1167
|
/* ====== Content Area ====== */
|
|
834
1168
|
.content-area {
|
|
835
1169
|
flex: 1;
|
|
@@ -868,6 +1202,8 @@
|
|
|
868
1202
|
font-size: 13px;
|
|
869
1203
|
color: var(--color-text-muted);
|
|
870
1204
|
max-width: 360px;
|
|
1205
|
+
margin: 0 auto;
|
|
1206
|
+
text-align: center;
|
|
871
1207
|
}
|
|
872
1208
|
|
|
873
1209
|
/* ====== Checklist Quality Gates Tab ====== */
|
|
@@ -1559,6 +1895,10 @@
|
|
|
1559
1895
|
text-align: center;
|
|
1560
1896
|
padding: 60px 20px;
|
|
1561
1897
|
color: var(--color-text-muted);
|
|
1898
|
+
max-width: 480px;
|
|
1899
|
+
margin: 0 auto;
|
|
1900
|
+
word-break: normal;
|
|
1901
|
+
overflow-wrap: break-word;
|
|
1562
1902
|
}
|
|
1563
1903
|
|
|
1564
1904
|
.clarify-header {
|
|
@@ -2427,6 +2767,7 @@
|
|
|
2427
2767
|
let expandedChecklist = null;
|
|
2428
2768
|
let currentTestify = null;
|
|
2429
2769
|
let currentAnalyze = null;
|
|
2770
|
+
let currentBugs = null;
|
|
2430
2771
|
let activeTab = null;
|
|
2431
2772
|
let externalSankeyHighlight = null;
|
|
2432
2773
|
let ws = null;
|
|
@@ -2453,6 +2794,25 @@
|
|
|
2453
2794
|
available: '●'
|
|
2454
2795
|
};
|
|
2455
2796
|
|
|
2797
|
+
const BUG_ICON_SMALL = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 2l1.88 1.88M14.12 3.88L16 2M9 7.13v-1a3 3 0 0 1 6 0v1M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6zM3.5 11H2M5.06 6.5l-1.5-1M22 11h-1.5M18.94 6.5l1.5-1M12 20v2"/></svg>';
|
|
2798
|
+
|
|
2799
|
+
function getBugsBadgeHtml(bugsData) {
|
|
2800
|
+
if (!bugsData || !bugsData.exists) return '';
|
|
2801
|
+
const count = bugsData.summary.open;
|
|
2802
|
+
const sev = bugsData.summary.highestOpenSeverity;
|
|
2803
|
+
|
|
2804
|
+
const colors = { critical: '#ff4757', high: '#ffa502', medium: '#f1c40f', low: '#6b7189' };
|
|
2805
|
+
const textColors = { critical: '#fff', high: '#fff', medium: '#1a1a2e', low: '#fff' };
|
|
2806
|
+
|
|
2807
|
+
if (count === 0) {
|
|
2808
|
+
return ' <span class="bug-count-badge muted">0</span>';
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
const bg = colors[sev] || '#6b7189';
|
|
2812
|
+
const fg = textColors[sev] || '#fff';
|
|
2813
|
+
return ` <span class="bug-count-badge" style="background:${bg};color:${fg}">${count}</span>`;
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2456
2816
|
function renderPipeline(pipeline) {
|
|
2457
2817
|
if (!pipeline || !pipeline.phases) return;
|
|
2458
2818
|
currentPipeline = pipeline;
|
|
@@ -2487,6 +2847,28 @@
|
|
|
2487
2847
|
node.addEventListener('click', () => switchTab(phase.id));
|
|
2488
2848
|
pipelineBar.appendChild(node);
|
|
2489
2849
|
});
|
|
2850
|
+
|
|
2851
|
+
// Bugs tab — standalone, no connector
|
|
2852
|
+
const bugsGap = document.createElement('div');
|
|
2853
|
+
bugsGap.className = 'pipeline-bugs-gap';
|
|
2854
|
+
pipelineBar.appendChild(bugsGap);
|
|
2855
|
+
|
|
2856
|
+
const bugsTab = document.createElement('button');
|
|
2857
|
+
bugsTab.className = 'bugs-tab' + (activeTab === 'bugs' ? ' active' : '');
|
|
2858
|
+
bugsTab.setAttribute('data-view', 'bugs');
|
|
2859
|
+
bugsTab.setAttribute('tabindex', '0');
|
|
2860
|
+
if (activeTab === 'bugs') bugsTab.setAttribute('aria-current', 'true');
|
|
2861
|
+
|
|
2862
|
+
const badgeHtml = currentBugs ? getBugsBadgeHtml(currentBugs) : '';
|
|
2863
|
+
bugsTab.innerHTML = `
|
|
2864
|
+
<div class="pipeline-dot bugs-dot">${BUG_ICON_SMALL}</div>
|
|
2865
|
+
<span class="pipeline-label">Bugs${badgeHtml}</span>
|
|
2866
|
+
`;
|
|
2867
|
+
if (currentBugs && !currentBugs.exists) {
|
|
2868
|
+
bugsTab.classList.add('muted');
|
|
2869
|
+
}
|
|
2870
|
+
bugsTab.addEventListener('click', () => switchTab('bugs'));
|
|
2871
|
+
pipelineBar.appendChild(bugsTab);
|
|
2490
2872
|
}
|
|
2491
2873
|
|
|
2492
2874
|
function switchTab(phaseId) {
|
|
@@ -2519,6 +2901,8 @@
|
|
|
2519
2901
|
renderTestifyView();
|
|
2520
2902
|
} else if (phaseId === 'analyze') {
|
|
2521
2903
|
renderAnalyzeView();
|
|
2904
|
+
} else if (phaseId === 'bugs') {
|
|
2905
|
+
renderBugsView();
|
|
2522
2906
|
} else {
|
|
2523
2907
|
renderPlaceholderView(phaseId);
|
|
2524
2908
|
}
|
|
@@ -2533,7 +2917,8 @@
|
|
|
2533
2917
|
// Sentinel elements per panel — poll until the panel has rendered
|
|
2534
2918
|
const sentinels = {
|
|
2535
2919
|
spec: '.graph-node', testify: '.sankey-node', implement: '.card',
|
|
2536
|
-
checklist: '.checklist-item', clarify: '.clarify-entry', analyze: '.heatmap-row'
|
|
2920
|
+
checklist: '.checklist-item', clarify: '.clarify-entry', analyze: '.heatmap-row',
|
|
2921
|
+
bugs: '.bug-row'
|
|
2537
2922
|
};
|
|
2538
2923
|
const sel = sentinels[panelId];
|
|
2539
2924
|
if (sel) {
|
|
@@ -2549,7 +2934,8 @@
|
|
|
2549
2934
|
testify: highlightTestifyEntity,
|
|
2550
2935
|
implement: highlightBoardEntity,
|
|
2551
2936
|
checklist: highlightChecklistEntity,
|
|
2552
|
-
clarify: highlightClarifyEntity
|
|
2937
|
+
clarify: highlightClarifyEntity,
|
|
2938
|
+
bugs: highlightBugsEntity
|
|
2553
2939
|
};
|
|
2554
2940
|
const fn = highlighters[panelId];
|
|
2555
2941
|
if (fn) fn(entityId);
|
|
@@ -2687,6 +3073,138 @@
|
|
|
2687
3073
|
}
|
|
2688
3074
|
}
|
|
2689
3075
|
|
|
3076
|
+
// ====== Bugs View (T014, T015, T016) ======
|
|
3077
|
+
|
|
3078
|
+
async function renderBugsView() {
|
|
3079
|
+
if (!currentFeature) return;
|
|
3080
|
+
|
|
3081
|
+
contentArea.innerHTML = '<div class="loading">Loading bug data...</div>';
|
|
3082
|
+
try {
|
|
3083
|
+
const res = await fetch(`/api/bugs/${currentFeature}`);
|
|
3084
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
3085
|
+
const data = await res.json();
|
|
3086
|
+
currentBugs = data;
|
|
3087
|
+
if (currentPipeline) renderPipeline(currentPipeline);
|
|
3088
|
+
renderBugsContent(data);
|
|
3089
|
+
} catch (err) {
|
|
3090
|
+
contentArea.innerHTML = `<div class="error-message">Error loading bugs: ${escapeHtml(err.message)}</div>`;
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
function renderBugsContent(data) {
|
|
3095
|
+
if (!data || !data.exists || data.bugs.length === 0) {
|
|
3096
|
+
// T015: Empty state
|
|
3097
|
+
const msg = !data || !data.exists
|
|
3098
|
+
? 'No bugs have been reported for this feature.'
|
|
3099
|
+
: 'All clear — no bug entries found.';
|
|
3100
|
+
contentArea.innerHTML = `
|
|
3101
|
+
<div class="bugs-empty">
|
|
3102
|
+
<div class="bugs-empty-icon">✓</div>
|
|
3103
|
+
<div class="bugs-empty-title">No Bugs</div>
|
|
3104
|
+
<div class="bugs-empty-text">${msg}</div>
|
|
3105
|
+
</div>`;
|
|
3106
|
+
return;
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
const { bugs, orphanedTasks, summary, repoUrl } = data;
|
|
3110
|
+
|
|
3111
|
+
let tableRows = '';
|
|
3112
|
+
for (const bug of bugs) {
|
|
3113
|
+
// Fix task progress
|
|
3114
|
+
const progressText = bug.fixTasks.total > 0
|
|
3115
|
+
? `${bug.fixTasks.checked}/${bug.fixTasks.total}`
|
|
3116
|
+
: '\u2014';
|
|
3117
|
+
|
|
3118
|
+
// GitHub link (T016)
|
|
3119
|
+
let githubHtml = '\u2014';
|
|
3120
|
+
if (bug.githubIssue) {
|
|
3121
|
+
const resolvedUrl = resolveGitHubUrl(bug.githubIssue, repoUrl);
|
|
3122
|
+
if (resolvedUrl) {
|
|
3123
|
+
githubHtml = `<a href="${escapeHtml(resolvedUrl)}" class="github-link bug-github-link" target="_blank" rel="noopener">${escapeHtml(bug.githubIssue)} <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align:middle"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3"/></svg></a>`;
|
|
3124
|
+
} else {
|
|
3125
|
+
githubHtml = escapeHtml(bug.githubIssue);
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3129
|
+
// Fix task list for cross-links
|
|
3130
|
+
let taskLinks = '';
|
|
3131
|
+
if (bug.fixTasks.total > 0) {
|
|
3132
|
+
taskLinks = bug.fixTasks.tasks.map(t =>
|
|
3133
|
+
`<span class="task-id cross-link" data-cross-target="implement" data-cross-id="${t.id}" style="cursor:pointer;color:var(--color-accent);font-family:monospace;font-size:11px;">${t.id}</span>`
|
|
3134
|
+
).join(', ');
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
tableRows += `
|
|
3138
|
+
<tr class="bug-row" data-bug-id="${bug.id}">
|
|
3139
|
+
<td class="bug-id">${bug.id}</td>
|
|
3140
|
+
<td><span class="severity-badge severity-${bug.severity}">${bug.severity}</span></td>
|
|
3141
|
+
<td class="bug-status ${bug.status === 'fixed' ? 'fixed' : ''}">${bug.status}</td>
|
|
3142
|
+
<td class="bug-progress">${progressText}${taskLinks ? '<br>' + taskLinks : ''}</td>
|
|
3143
|
+
<td class="bug-description" title="${escapeHtml(bug.description || '')}">${escapeHtml(bug.description || '\u2014')}</td>
|
|
3144
|
+
<td>${githubHtml}</td>
|
|
3145
|
+
</tr>`;
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
// Orphaned tasks section (TS-020)
|
|
3149
|
+
let orphanedHtml = '';
|
|
3150
|
+
if (orphanedTasks && orphanedTasks.length > 0) {
|
|
3151
|
+
orphanedHtml = `
|
|
3152
|
+
<div class="orphaned-section">
|
|
3153
|
+
<div class="orphaned-title">⚠ Orphaned Fix Tasks (no matching bug in bugs.md)</div>
|
|
3154
|
+
${orphanedTasks.map(t => `
|
|
3155
|
+
<div class="orphaned-task">
|
|
3156
|
+
<span class="task-id cross-link" data-cross-target="implement" data-cross-id="${t.id}" style="cursor:pointer;color:var(--color-accent);">${t.id}</span>
|
|
3157
|
+
[${t.bugTag}] ${escapeHtml(t.description)}
|
|
3158
|
+
</div>
|
|
3159
|
+
`).join('')}
|
|
3160
|
+
</div>`;
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
contentArea.innerHTML = `
|
|
3164
|
+
<div class="bugs-view">
|
|
3165
|
+
<div class="bugs-view-header">
|
|
3166
|
+
<div class="bugs-view-title">Bug Tracking</div>
|
|
3167
|
+
<div class="bugs-view-subtitle">${summary.total} total · ${summary.open} open · ${summary.fixed} fixed</div>
|
|
3168
|
+
</div>
|
|
3169
|
+
<div class="cross-link-tip">Tip: ${CROSS_LINK_MOD_KEY}+click task IDs to navigate to the Implement board</div>
|
|
3170
|
+
<table class="bugs-table" role="table" aria-label="Bug status table">
|
|
3171
|
+
<thead>
|
|
3172
|
+
<tr>
|
|
3173
|
+
<th>Bug ID</th>
|
|
3174
|
+
<th aria-sort="descending">Severity</th>
|
|
3175
|
+
<th>Status</th>
|
|
3176
|
+
<th>Fix Tasks</th>
|
|
3177
|
+
<th>Description</th>
|
|
3178
|
+
<th>GitHub</th>
|
|
3179
|
+
</tr>
|
|
3180
|
+
</thead>
|
|
3181
|
+
<tbody>${tableRows}</tbody>
|
|
3182
|
+
</table>
|
|
3183
|
+
${orphanedHtml}
|
|
3184
|
+
</div>`;
|
|
3185
|
+
}
|
|
3186
|
+
|
|
3187
|
+
// T016: Client-side GitHub URL resolution
|
|
3188
|
+
function resolveGitHubUrl(issueRef, repoUrl) {
|
|
3189
|
+
if (!issueRef || !repoUrl) return null;
|
|
3190
|
+
const match = issueRef.match(/#(\d+)/);
|
|
3191
|
+
if (!match) return null;
|
|
3192
|
+
let base = repoUrl.replace(/\.git$/, '');
|
|
3193
|
+
const ssh = base.match(/^git@([^:]+):(.+)$/);
|
|
3194
|
+
if (ssh) base = 'https://' + ssh[1] + '/' + ssh[2];
|
|
3195
|
+
return base + '/issues/' + match[1];
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
// T021: Highlight a bug row in the Bugs tab
|
|
3199
|
+
function highlightBugsEntity(entityId) {
|
|
3200
|
+
const row = contentArea.querySelector(`tr[data-bug-id="${entityId}"]`);
|
|
3201
|
+
if (row) {
|
|
3202
|
+
row.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
3203
|
+
row.classList.add('highlighted');
|
|
3204
|
+
setTimeout(() => row.classList.remove('highlighted'), 2000);
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
|
|
2690
3208
|
function renderPlaceholderView(phaseId) {
|
|
2691
3209
|
const phaseNames = {
|
|
2692
3210
|
constitution: 'Constitution', spec: 'Specification', clarify: 'Clarification',
|
|
@@ -3720,11 +4238,22 @@
|
|
|
3720
4238
|
|
|
3721
4239
|
function renderClarifyContent(clarifications) {
|
|
3722
4240
|
if (!clarifications.length) {
|
|
4241
|
+
// TS-019/TS-020: Distinguish "clarify was run, no issues" from "never run"
|
|
4242
|
+
const clarifyPhase = currentPipeline?.phases?.find(p => p.id === 'clarify');
|
|
4243
|
+
const clarifyWasRun = clarifyPhase && (clarifyPhase.status === 'complete' || clarifyPhase.status === 'skipped');
|
|
4244
|
+
|
|
4245
|
+
const title = clarifyWasRun
|
|
4246
|
+
? 'Clarification Complete'
|
|
4247
|
+
: 'Clarify Not Yet Run';
|
|
4248
|
+
const text = clarifyWasRun
|
|
4249
|
+
? 'The clarification phase was completed with no ambiguities found. The specification is clear as written.'
|
|
4250
|
+
: 'This is an optional phase. Run <code>/iikit-02-clarify</code> to identify and resolve ambiguities in the spec.';
|
|
4251
|
+
|
|
3723
4252
|
contentArea.innerHTML = `
|
|
3724
4253
|
<div class="clarify-view" role="region" aria-label="Clarifications">
|
|
3725
4254
|
<div class="clarify-empty">
|
|
3726
|
-
<div class="placeholder-view-title"
|
|
3727
|
-
<div class="placeholder-view-text"
|
|
4255
|
+
<div class="placeholder-view-title">${title}</div>
|
|
4256
|
+
<div class="placeholder-view-text">${text}</div>
|
|
3728
4257
|
</div>
|
|
3729
4258
|
</div>`;
|
|
3730
4259
|
return;
|
|
@@ -4419,6 +4948,7 @@
|
|
|
4419
4948
|
featureSelect.value = currentFeature;
|
|
4420
4949
|
loadPipeline(currentFeature);
|
|
4421
4950
|
loadBoard(currentFeature);
|
|
4951
|
+
loadBugs(currentFeature);
|
|
4422
4952
|
} else if (features.length === 0) {
|
|
4423
4953
|
showEmptyState();
|
|
4424
4954
|
}
|
|
@@ -4451,8 +4981,10 @@
|
|
|
4451
4981
|
currentPlanView = null; // Reset plan view cache
|
|
4452
4982
|
currentTestify = null; // Reset testify cache
|
|
4453
4983
|
currentAnalyze = null; // Reset analyze cache
|
|
4984
|
+
currentBugs = null; // Reset bugs cache
|
|
4454
4985
|
loadPipeline(val);
|
|
4455
4986
|
loadBoard(val);
|
|
4987
|
+
loadBugs(val);
|
|
4456
4988
|
// Resubscribe WebSocket
|
|
4457
4989
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
4458
4990
|
ws.send(JSON.stringify({ type: 'subscribe', feature: val }));
|
|
@@ -4483,6 +5015,21 @@
|
|
|
4483
5015
|
}
|
|
4484
5016
|
}
|
|
4485
5017
|
|
|
5018
|
+
async function loadBugs(featureId) {
|
|
5019
|
+
try {
|
|
5020
|
+
const res = await fetch(`/api/bugs/${featureId}`);
|
|
5021
|
+
if (res.ok) {
|
|
5022
|
+
currentBugs = await res.json();
|
|
5023
|
+
// Update badge on pipeline bar
|
|
5024
|
+
if (currentPipeline) renderPipeline(currentPipeline);
|
|
5025
|
+
// If bugs view is active, re-render it
|
|
5026
|
+
if (activeTab === 'bugs') renderBugsContent(currentBugs);
|
|
5027
|
+
}
|
|
5028
|
+
} catch {
|
|
5029
|
+
// Silently fail — bugs are optional
|
|
5030
|
+
}
|
|
5031
|
+
}
|
|
5032
|
+
|
|
4486
5033
|
function showLoading() {
|
|
4487
5034
|
boardEl.innerHTML = '<div class="loading"><div class="loading-spinner"></div></div>';
|
|
4488
5035
|
}
|
|
@@ -4576,9 +5123,11 @@
|
|
|
4576
5123
|
if (targetEl) renderBoardInto(targetEl, board);
|
|
4577
5124
|
}
|
|
4578
5125
|
|
|
5126
|
+
const BUG_SVG_ICON = '<svg class="bug-icon-inline" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 2l1.88 1.88M14.12 3.88L16 2M9 7.13v-1a3.003 3.003 0 116 0v1M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 014-4h4a4 4 0 014 4v3c0 3.3-2.7 6-6 6zM2 11h3M19 11h3M2 16h3M19 16h3"/></svg>';
|
|
5127
|
+
|
|
4579
5128
|
function createCardElement(card, columnKey) {
|
|
4580
5129
|
const el = document.createElement('div');
|
|
4581
|
-
el.className = 'card';
|
|
5130
|
+
el.className = 'card' + (card.isBugCard ? ' bug-card' : '');
|
|
4582
5131
|
el.setAttribute('data-card-id', card.id);
|
|
4583
5132
|
el.setAttribute('role', 'article');
|
|
4584
5133
|
el.setAttribute('aria-label', `${card.title} - ${card.priority} - ${card.progress} tasks complete`);
|
|
@@ -4590,10 +5139,14 @@
|
|
|
4590
5139
|
|
|
4591
5140
|
const priorityClass = card.priority ? card.priority.toLowerCase() : 'p3';
|
|
4592
5141
|
|
|
5142
|
+
const cardIdHtml = card.isBugCard
|
|
5143
|
+
? `<div class="card-id cross-link" data-cross-target="bugs" data-cross-id="${card.id}">${BUG_SVG_ICON}${card.id}</div>`
|
|
5144
|
+
: `<div class="card-id cross-link" data-cross-target="spec" data-cross-id="${card.id}">${card.id}</div>`;
|
|
5145
|
+
|
|
4593
5146
|
el.innerHTML = `
|
|
4594
|
-
|
|
5147
|
+
${cardIdHtml}
|
|
4595
5148
|
<div class="card-header">
|
|
4596
|
-
<div class="card-title" title="${escapeHtml(card.title)}">${escapeHtml(card.title)}</div>
|
|
5149
|
+
<div class="card-title" title="${escapeHtml(card.title)}">${card.isBugCard ? BUG_SVG_ICON : ''}${escapeHtml(card.title)}</div>
|
|
4597
5150
|
<span class="priority-badge ${priorityClass}" aria-label="Priority ${card.priority}">${card.priority}</span>
|
|
4598
5151
|
</div>
|
|
4599
5152
|
<div class="progress-container">
|
|
@@ -4610,13 +5163,19 @@
|
|
|
4610
5163
|
${(card.tasks || []).length} tasks
|
|
4611
5164
|
</button>
|
|
4612
5165
|
<ul class="task-list collapsed" id="tasks-${card.id}" aria-label="Tasks for ${card.id}">
|
|
4613
|
-
${(card.tasks || []).map(t =>
|
|
4614
|
-
|
|
5166
|
+
${(card.tasks || []).map(t => {
|
|
5167
|
+
const isBugTask = t.isBugFix || (t.id && t.id.startsWith('T-B'));
|
|
5168
|
+
const taskIcon = isBugTask ? BUG_SVG_ICON : '';
|
|
5169
|
+
const bugTagLink = t.bugTag
|
|
5170
|
+
? ` <span class="cross-link" data-cross-target="bugs" data-cross-id="${t.bugTag}" style="cursor:pointer;color:var(--color-accent);font-size:11px;">[${t.bugTag}]</span>`
|
|
5171
|
+
: '';
|
|
5172
|
+
return `
|
|
5173
|
+
<li class="task-item ${t.checked ? 'checked' : ''}" data-task-id="${t.id}">
|
|
4615
5174
|
<span class="task-checkbox ${t.checked ? 'checked' : ''}" aria-hidden="true"></span>
|
|
4616
|
-
<span class="task-id cross-link" data-cross-target="testify" data-cross-id="${t.id}">${t.id}</span
|
|
5175
|
+
${taskIcon}<span class="task-id cross-link" data-cross-target="testify" data-cross-id="${t.id}">${t.id}</span>${bugTagLink}
|
|
4617
5176
|
<span class="task-description">${escapeHtml(t.description)}</span>
|
|
4618
|
-
</li
|
|
4619
|
-
|
|
5177
|
+
</li>`;
|
|
5178
|
+
}).join('')}
|
|
4620
5179
|
</ul>`;
|
|
4621
5180
|
|
|
4622
5181
|
return el;
|
|
@@ -5126,6 +5685,16 @@
|
|
|
5126
5685
|
}
|
|
5127
5686
|
break;
|
|
5128
5687
|
|
|
5688
|
+
case 'bugs_update':
|
|
5689
|
+
if (msg.feature === currentFeature && msg.bugs) {
|
|
5690
|
+
currentBugs = msg.bugs;
|
|
5691
|
+
if (currentPipeline) renderPipeline(currentPipeline);
|
|
5692
|
+
if (activeTab === 'bugs') {
|
|
5693
|
+
renderBugsContent(msg.bugs);
|
|
5694
|
+
}
|
|
5695
|
+
}
|
|
5696
|
+
break;
|
|
5697
|
+
|
|
5129
5698
|
case 'features_update':
|
|
5130
5699
|
if (msg.features) {
|
|
5131
5700
|
updateFeatureSelector(msg.features);
|
package/src/server.js
CHANGED
|
@@ -15,6 +15,7 @@ const { computePlanViewState } = require('./planview');
|
|
|
15
15
|
const { computeChecklistViewState } = require('./checklist');
|
|
16
16
|
const { computeTestifyState } = require('./testify');
|
|
17
17
|
const { computeAnalyzeState } = require('./analyze');
|
|
18
|
+
const { computeBugsState } = require('./bugs');
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* List features from specs/ directory.
|
|
@@ -245,6 +246,20 @@ function createServer({ projectPath, port = 3000 }) {
|
|
|
245
246
|
}
|
|
246
247
|
});
|
|
247
248
|
|
|
249
|
+
// API: bugs state for a feature
|
|
250
|
+
app.get('/api/bugs/:feature', (req, res) => {
|
|
251
|
+
try {
|
|
252
|
+
const featureDir = path.join(projectPath, 'specs', req.params.feature);
|
|
253
|
+
if (!fs.existsSync(featureDir)) {
|
|
254
|
+
return res.status(404).json({ error: 'Feature not found' });
|
|
255
|
+
}
|
|
256
|
+
const bugs = computeBugsState(projectPath, req.params.feature);
|
|
257
|
+
res.json(bugs);
|
|
258
|
+
} catch (err) {
|
|
259
|
+
res.status(500).json({ error: err.message });
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
248
263
|
// API: board state for a feature
|
|
249
264
|
app.get('/api/board/:feature', (req, res) => {
|
|
250
265
|
try {
|
|
@@ -358,6 +373,14 @@ function createServer({ projectPath, port = 3000 }) {
|
|
|
358
373
|
analyze
|
|
359
374
|
}));
|
|
360
375
|
}
|
|
376
|
+
const bugs = computeBugsState(projectPath, ws.currentFeature);
|
|
377
|
+
if (bugs) {
|
|
378
|
+
ws.send(JSON.stringify({
|
|
379
|
+
type: 'bugs_update',
|
|
380
|
+
feature: ws.currentFeature,
|
|
381
|
+
bugs
|
|
382
|
+
}));
|
|
383
|
+
}
|
|
361
384
|
} catch {
|
|
362
385
|
// ignore errors during push
|
|
363
386
|
}
|