iikit-dashboard 1.0.0 → 1.1.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 +10 -19
- package/package.json +2 -2
- package/src/checklist.js +68 -0
- package/src/parser.js +91 -1
- package/src/public/index.html +385 -0
- package/src/server.js +23 -0
package/README.md
CHANGED
|
@@ -2,27 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
**Watch your AI agent develop features in real time.**
|
|
4
4
|
|
|
5
|
-
A browser-based dashboard for [Intent Integrity Kit](https://github.com/intent-integrity-chain/kit) projects.
|
|
5
|
+
A browser-based dashboard for [Intent Integrity Kit (IIKit)](https://github.com/intent-integrity-chain/kit) projects. IIKit is a specification-driven development framework that guides AI agents through a structured workflow — from governance constitution through specification, clarification, planning, testing, and implementation. The dashboard visualizes every phase of that workflow with live updates as artifacts change on disk.
|
|
6
6
|
|
|
7
7
|
## Usage
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
# Run in your IIKit project directory
|
|
11
|
-
npx iikit-dashboard
|
|
12
|
-
|
|
13
|
-
# Or specify a project path
|
|
14
|
-
npx iikit-dashboard /path/to/your/project
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
The dashboard opens at `http://localhost:3000`.
|
|
9
|
+
The dashboard launches automatically early in the IIKit workflow — no manual setup needed.
|
|
18
10
|
|
|
19
|
-
|
|
11
|
+
You can also start it standalone to browse historical data for any project that has feature specs:
|
|
20
12
|
|
|
21
13
|
```bash
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
npx iikit-dashboard # current directory
|
|
15
|
+
npx iikit-dashboard /path/to/project # specific project
|
|
24
16
|
```
|
|
25
17
|
|
|
18
|
+
The dashboard opens at `http://localhost:3000`.
|
|
19
|
+
|
|
26
20
|
## Views
|
|
27
21
|
|
|
28
22
|
The pipeline bar at the top shows all nine IIKit workflow phases. Click any phase to see its visualization:
|
|
@@ -33,7 +27,7 @@ The pipeline bar at the top shows all nine IIKit workflow phases. Click any phas
|
|
|
33
27
|
| **Spec** | Story map with swim lanes by priority + interactive requirements graph (US / FR / SC nodes and edges) |
|
|
34
28
|
| **Clarify** | Q&A trail from clarification sessions, with clickable spec-item references that navigate back to the Spec view |
|
|
35
29
|
| **Plan** | Tech stack badge wall, interactive file-structure tree (existing vs. planned files), rendered architecture diagram, and Tessl tile cards |
|
|
36
|
-
| **Checklist** |
|
|
30
|
+
| **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 |
|
|
37
31
|
| **Testify** | *Coming soon* |
|
|
38
32
|
| **Tasks** | *Coming soon* |
|
|
39
33
|
| **Analyze** | *Coming soon* |
|
|
@@ -58,19 +52,16 @@ The server reads directly from your project's `specs/` directory:
|
|
|
58
52
|
| `spec.md` | User stories, requirements, success criteria, and clarification Q&A |
|
|
59
53
|
| `plan.md` | Tech stack, file structure, and architecture diagram |
|
|
60
54
|
| `tasks.md` | Task checkboxes grouped by `[US1]`, `[US2]` tags |
|
|
55
|
+
| `checklists/*.md` | Checklist items with completion status, CHK IDs, and category groupings |
|
|
61
56
|
| `CONSTITUTION.md` | Governance principles and obligation levels |
|
|
62
57
|
| `tessl.json` | Installed Tessl tiles for the dependency panel |
|
|
63
58
|
|
|
64
59
|
A file watcher (chokidar) detects changes and pushes updates to the browser via WebSocket with 300 ms debounce.
|
|
65
60
|
|
|
66
|
-
## Integration with IIKit
|
|
67
|
-
|
|
68
|
-
When you run `/iikit-08-implement`, the implement skill automatically launches the dashboard in the background. No manual setup needed.
|
|
69
|
-
|
|
70
61
|
## Requirements
|
|
71
62
|
|
|
72
63
|
- Node.js 18+
|
|
73
|
-
-
|
|
64
|
+
- A project with a `specs/` directory containing IIKit feature artifacts
|
|
74
65
|
|
|
75
66
|
## License
|
|
76
67
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iikit-dashboard",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.0",
|
|
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": {
|
|
7
7
|
"iikit-dashboard": "bin/iikit-dashboard.js"
|
package/src/checklist.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { parseChecklistsDetailed } = require('./parser');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Map a percentage (0-100) to a color bracket.
|
|
8
|
+
* @param {number} percentage
|
|
9
|
+
* @returns {string} "red" | "yellow" | "green"
|
|
10
|
+
*/
|
|
11
|
+
function percentageToColor(percentage) {
|
|
12
|
+
if (percentage <= 33) return 'red';
|
|
13
|
+
if (percentage <= 66) return 'yellow';
|
|
14
|
+
return 'green';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Compute gate status from an array of file objects with percentage fields.
|
|
19
|
+
* Uses worst-case precedence: red if any at 0%, yellow if all 1-99%, green if all 100%.
|
|
20
|
+
*
|
|
21
|
+
* @param {Array<{percentage: number}>} files
|
|
22
|
+
* @returns {{status: string, level: string, label: string}}
|
|
23
|
+
*/
|
|
24
|
+
function computeGateStatus(files) {
|
|
25
|
+
if (files.length === 0) {
|
|
26
|
+
return { status: 'blocked', level: 'red', label: 'GATE: BLOCKED' };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const anyAtZero = files.some(f => f.percentage === 0);
|
|
30
|
+
if (anyAtZero) {
|
|
31
|
+
return { status: 'blocked', level: 'red', label: 'GATE: BLOCKED' };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const allComplete = files.every(f => f.percentage === 100);
|
|
35
|
+
if (allComplete) {
|
|
36
|
+
return { status: 'open', level: 'green', label: 'GATE: OPEN' };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { status: 'blocked', level: 'yellow', label: 'GATE: BLOCKED' };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Compute checklist view state for a feature.
|
|
44
|
+
* Returns per-file detail with items, percentage, color, and aggregate gate status.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} projectPath - Path to the project root
|
|
47
|
+
* @param {string} featureId - Feature directory name (e.g., "001-kanban-board")
|
|
48
|
+
* @returns {{files: Array, gate: {status: string, level: string, label: string}}}
|
|
49
|
+
*/
|
|
50
|
+
function computeChecklistViewState(projectPath, featureId) {
|
|
51
|
+
const checklistDir = path.join(projectPath, 'specs', featureId, 'checklists');
|
|
52
|
+
const parsed = parseChecklistsDetailed(checklistDir);
|
|
53
|
+
|
|
54
|
+
const files = parsed.map(file => {
|
|
55
|
+
const percentage = file.total > 0 ? Math.round((file.checked / file.total) * 100) : 0;
|
|
56
|
+
return {
|
|
57
|
+
...file,
|
|
58
|
+
percentage,
|
|
59
|
+
color: percentageToColor(percentage)
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const gate = computeGateStatus(files);
|
|
64
|
+
|
|
65
|
+
return { files, gate };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = { computeChecklistViewState };
|
package/src/parser.js
CHANGED
|
@@ -115,6 +115,96 @@ function parseChecklists(checklistDir) {
|
|
|
115
115
|
return result;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Parse all checklist files in a directory and return detailed per-file data
|
|
120
|
+
* with individual items, categories, CHK IDs, and tags.
|
|
121
|
+
*
|
|
122
|
+
* Applies same requirements.md-only filter as parseChecklists:
|
|
123
|
+
* if requirements.md is the only file, returns empty array.
|
|
124
|
+
*
|
|
125
|
+
* @param {string} checklistDir - Path to checklists/ directory
|
|
126
|
+
* @returns {Array<{name: string, filename: string, total: number, checked: number, items: Array}>}
|
|
127
|
+
*/
|
|
128
|
+
function parseChecklistsDetailed(checklistDir) {
|
|
129
|
+
if (!fs.existsSync(checklistDir)) return [];
|
|
130
|
+
|
|
131
|
+
const files = fs.readdirSync(checklistDir).filter(f => f.endsWith('.md'));
|
|
132
|
+
|
|
133
|
+
// Same filter as parseChecklists: skip if requirements.md is the only file
|
|
134
|
+
const hasDomainChecklists = files.some(f => f !== 'requirements.md');
|
|
135
|
+
if (!hasDomainChecklists) return [];
|
|
136
|
+
|
|
137
|
+
const result = [];
|
|
138
|
+
|
|
139
|
+
for (const file of files) {
|
|
140
|
+
const content = fs.readFileSync(path.join(checklistDir, file), 'utf-8');
|
|
141
|
+
const lines = content.split('\n');
|
|
142
|
+
|
|
143
|
+
// Derive human-readable name from filename
|
|
144
|
+
const baseName = file.replace(/\.md$/, '');
|
|
145
|
+
const name = baseName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
146
|
+
|
|
147
|
+
const items = [];
|
|
148
|
+
let currentCategory = null;
|
|
149
|
+
let totalCount = 0;
|
|
150
|
+
let checkedCount = 0;
|
|
151
|
+
|
|
152
|
+
for (const line of lines) {
|
|
153
|
+
// Track category headings (## or ###)
|
|
154
|
+
const headingMatch = line.match(/^#{2,3}\s+(.+)/);
|
|
155
|
+
if (headingMatch) {
|
|
156
|
+
currentCategory = headingMatch[1].trim();
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Parse checkbox items
|
|
161
|
+
const checkboxMatch = line.match(/^- \[([ x])\]\s+(.*)/i);
|
|
162
|
+
if (!checkboxMatch) continue;
|
|
163
|
+
|
|
164
|
+
const isChecked = checkboxMatch[1].toLowerCase() === 'x';
|
|
165
|
+
let itemText = checkboxMatch[2].trim();
|
|
166
|
+
totalCount++;
|
|
167
|
+
if (isChecked) checkedCount++;
|
|
168
|
+
|
|
169
|
+
// Extract CHK-xxx ID
|
|
170
|
+
let chkId = null;
|
|
171
|
+
const chkMatch = itemText.match(/^(CHK-\d{3})\s+/);
|
|
172
|
+
if (chkMatch) {
|
|
173
|
+
chkId = chkMatch[1];
|
|
174
|
+
itemText = itemText.substring(chkMatch[0].length);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Extract trailing tags [tag1] [tag2] — but not the checkbox itself
|
|
178
|
+
const tags = [];
|
|
179
|
+
const tagRegex = /\[([^\]]+)\]\s*$/;
|
|
180
|
+
let tagMatch;
|
|
181
|
+
while ((tagMatch = itemText.match(tagRegex))) {
|
|
182
|
+
// Don't treat spec references like [Completeness, FR-004] as simple tags
|
|
183
|
+
tags.unshift(tagMatch[1]);
|
|
184
|
+
itemText = itemText.substring(0, tagMatch.index).trim();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
items.push({
|
|
188
|
+
text: itemText,
|
|
189
|
+
checked: isChecked,
|
|
190
|
+
chkId,
|
|
191
|
+
category: currentCategory,
|
|
192
|
+
tags
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
result.push({
|
|
197
|
+
name,
|
|
198
|
+
filename: file,
|
|
199
|
+
total: totalCount,
|
|
200
|
+
checked: checkedCount,
|
|
201
|
+
items
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
|
|
118
208
|
/**
|
|
119
209
|
* Parse CONSTITUTION.md to determine if TDD is required.
|
|
120
210
|
* Looks for strong TDD indicators combined with MUST/NON-NEGOTIABLE.
|
|
@@ -765,4 +855,4 @@ function parseResearchDecisions(content) {
|
|
|
765
855
|
return decisions;
|
|
766
856
|
}
|
|
767
857
|
|
|
768
|
-
module.exports = { parseSpecStories, parseTasks, parseChecklists, parseConstitutionTDD, hasClarifications, parseConstitutionPrinciples, parseRequirements, parseSuccessCriteria, parseClarifications, parseStoryRequirementRefs, parseTechContext, parseFileStructure, parseAsciiDiagram, parseTesslJson, parseResearchDecisions };
|
|
858
|
+
module.exports = { parseSpecStories, parseTasks, parseChecklists, parseChecklistsDetailed, parseConstitutionTDD, hasClarifications, parseConstitutionPrinciples, parseRequirements, parseSuccessCriteria, parseClarifications, parseStoryRequirementRefs, parseTechContext, parseFileStructure, parseAsciiDiagram, parseTesslJson, parseResearchDecisions };
|
package/src/public/index.html
CHANGED
|
@@ -854,6 +854,234 @@
|
|
|
854
854
|
max-width: 360px;
|
|
855
855
|
}
|
|
856
856
|
|
|
857
|
+
/* ====== Checklist Quality Gates Tab ====== */
|
|
858
|
+
.checklist-view {
|
|
859
|
+
padding: 24px 28px;
|
|
860
|
+
max-width: 900px;
|
|
861
|
+
margin: 0 auto;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
.gate-indicator {
|
|
865
|
+
display: flex;
|
|
866
|
+
align-items: center;
|
|
867
|
+
gap: 12px;
|
|
868
|
+
padding: 16px 20px;
|
|
869
|
+
border-radius: var(--radius-lg);
|
|
870
|
+
background: var(--color-surface-elevated);
|
|
871
|
+
border: 1px solid var(--color-border);
|
|
872
|
+
margin-bottom: 28px;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
.gate-dot {
|
|
876
|
+
width: 18px;
|
|
877
|
+
height: 18px;
|
|
878
|
+
border-radius: 50%;
|
|
879
|
+
flex-shrink: 0;
|
|
880
|
+
transition: background-color 0.4s ease;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
.gate-dot.green { background-color: var(--color-green, #22c55e); }
|
|
884
|
+
.gate-dot.yellow { background-color: var(--color-yellow, #eab308); }
|
|
885
|
+
.gate-dot.red { background-color: var(--color-red, #ef4444); }
|
|
886
|
+
|
|
887
|
+
.gate-label {
|
|
888
|
+
font-size: 15px;
|
|
889
|
+
font-weight: 700;
|
|
890
|
+
letter-spacing: 0.5px;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
.gate-label.open { color: var(--color-green, #22c55e); }
|
|
894
|
+
.gate-label.blocked-yellow { color: var(--color-yellow, #eab308); }
|
|
895
|
+
.gate-label.blocked-red { color: var(--color-red, #ef4444); }
|
|
896
|
+
|
|
897
|
+
.checklist-rings {
|
|
898
|
+
display: flex;
|
|
899
|
+
flex-wrap: wrap;
|
|
900
|
+
gap: 24px;
|
|
901
|
+
justify-content: center;
|
|
902
|
+
margin-bottom: 24px;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
.checklist-ring-wrapper {
|
|
906
|
+
display: flex;
|
|
907
|
+
flex-direction: column;
|
|
908
|
+
align-items: center;
|
|
909
|
+
cursor: pointer;
|
|
910
|
+
padding: 12px;
|
|
911
|
+
border-radius: var(--radius-lg);
|
|
912
|
+
transition: background-color 0.15s ease;
|
|
913
|
+
border: 2px solid transparent;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
.checklist-ring-wrapper:hover {
|
|
917
|
+
background: var(--color-surface-elevated);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
.checklist-ring-wrapper:focus-visible {
|
|
921
|
+
outline: 2px solid var(--color-primary);
|
|
922
|
+
outline-offset: 2px;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
.checklist-ring-wrapper.expanded {
|
|
926
|
+
border-color: var(--color-primary);
|
|
927
|
+
background: var(--color-surface-elevated);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
.checklist-ring-svg {
|
|
931
|
+
width: 100px;
|
|
932
|
+
height: 100px;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
.checklist-ring-track {
|
|
936
|
+
fill: none;
|
|
937
|
+
stroke: var(--color-border);
|
|
938
|
+
stroke-width: 8;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
.checklist-ring-fill {
|
|
942
|
+
fill: none;
|
|
943
|
+
stroke-width: 8;
|
|
944
|
+
stroke-linecap: round;
|
|
945
|
+
transition: stroke-dashoffset 0.8s cubic-bezier(0.4, 0, 0.2, 1), stroke 0.4s ease;
|
|
946
|
+
transform: rotate(-90deg);
|
|
947
|
+
transform-origin: 50% 50%;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
.checklist-ring-fill.red { stroke: var(--color-red, #ef4444); }
|
|
951
|
+
.checklist-ring-fill.yellow { stroke: var(--color-yellow, #eab308); }
|
|
952
|
+
.checklist-ring-fill.green { stroke: var(--color-green, #22c55e); }
|
|
953
|
+
|
|
954
|
+
.checklist-ring-pct {
|
|
955
|
+
font-size: 18px;
|
|
956
|
+
font-weight: 700;
|
|
957
|
+
fill: var(--color-text);
|
|
958
|
+
text-anchor: middle;
|
|
959
|
+
dominant-baseline: central;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
.checklist-ring-name {
|
|
963
|
+
margin-top: 8px;
|
|
964
|
+
font-size: 13px;
|
|
965
|
+
font-weight: 600;
|
|
966
|
+
color: var(--color-text);
|
|
967
|
+
text-align: center;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
.checklist-ring-fraction {
|
|
971
|
+
font-size: 12px;
|
|
972
|
+
color: var(--color-text-muted);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
.checklist-detail {
|
|
976
|
+
max-height: 0;
|
|
977
|
+
overflow: hidden;
|
|
978
|
+
transition: max-height 0.35s cubic-bezier(0.4, 0, 0.2, 1), border-color 0.15s ease, opacity 0.15s ease;
|
|
979
|
+
border-radius: var(--radius-lg);
|
|
980
|
+
background: var(--color-surface-elevated);
|
|
981
|
+
border: 1px solid transparent;
|
|
982
|
+
margin-bottom: 0;
|
|
983
|
+
opacity: 0;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
.checklist-detail.open {
|
|
987
|
+
max-height: 2000px;
|
|
988
|
+
border-color: var(--color-border);
|
|
989
|
+
margin-bottom: 16px;
|
|
990
|
+
opacity: 1;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
.checklist-detail-inner {
|
|
994
|
+
padding: 16px 20px;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
.checklist-category {
|
|
998
|
+
font-size: 12px;
|
|
999
|
+
font-weight: 700;
|
|
1000
|
+
text-transform: uppercase;
|
|
1001
|
+
letter-spacing: 0.5px;
|
|
1002
|
+
color: var(--color-text-muted);
|
|
1003
|
+
margin: 16px 0 8px;
|
|
1004
|
+
padding-bottom: 4px;
|
|
1005
|
+
border-bottom: 1px solid var(--color-border);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
.checklist-category:first-child {
|
|
1009
|
+
margin-top: 0;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
.checklist-item {
|
|
1013
|
+
display: flex;
|
|
1014
|
+
align-items: flex-start;
|
|
1015
|
+
gap: 8px;
|
|
1016
|
+
padding: 6px 0;
|
|
1017
|
+
font-size: 13px;
|
|
1018
|
+
color: var(--color-text);
|
|
1019
|
+
line-height: 1.4;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
.checklist-item-icon {
|
|
1023
|
+
flex-shrink: 0;
|
|
1024
|
+
width: 18px;
|
|
1025
|
+
font-size: 14px;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
.checklist-item-icon.checked { color: var(--color-green, #22c55e); }
|
|
1029
|
+
.checklist-item-icon.unchecked { color: var(--color-text-muted); }
|
|
1030
|
+
|
|
1031
|
+
.checklist-item-id {
|
|
1032
|
+
font-family: var(--font-mono, monospace);
|
|
1033
|
+
font-size: 11px;
|
|
1034
|
+
color: var(--color-text-muted);
|
|
1035
|
+
background: var(--color-surface);
|
|
1036
|
+
padding: 1px 5px;
|
|
1037
|
+
border-radius: var(--radius-sm, 4px);
|
|
1038
|
+
flex-shrink: 0;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
.checklist-item-tag {
|
|
1042
|
+
font-size: 10px;
|
|
1043
|
+
font-weight: 600;
|
|
1044
|
+
padding: 1px 6px;
|
|
1045
|
+
border-radius: 9999px;
|
|
1046
|
+
background: var(--color-primary-muted, rgba(99, 102, 241, 0.15));
|
|
1047
|
+
color: var(--color-primary, #6366f1);
|
|
1048
|
+
flex-shrink: 0;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
.checklist-empty {
|
|
1052
|
+
display: flex;
|
|
1053
|
+
flex-direction: column;
|
|
1054
|
+
align-items: center;
|
|
1055
|
+
justify-content: center;
|
|
1056
|
+
padding: 80px 20px;
|
|
1057
|
+
text-align: center;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
.checklist-empty-icon {
|
|
1061
|
+
width: 56px;
|
|
1062
|
+
height: 56px;
|
|
1063
|
+
background: var(--color-surface-elevated);
|
|
1064
|
+
border-radius: var(--radius-lg);
|
|
1065
|
+
display: flex;
|
|
1066
|
+
align-items: center;
|
|
1067
|
+
justify-content: center;
|
|
1068
|
+
font-size: 24px;
|
|
1069
|
+
margin-bottom: 16px;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
.checklist-empty-title {
|
|
1073
|
+
font-size: 16px;
|
|
1074
|
+
font-weight: 600;
|
|
1075
|
+
color: var(--color-text);
|
|
1076
|
+
margin-bottom: 6px;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
.checklist-empty-text {
|
|
1080
|
+
font-size: 13px;
|
|
1081
|
+
color: var(--color-text-muted);
|
|
1082
|
+
max-width: 360px;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
857
1085
|
/* ====== Spec Story Map Tab ====== */
|
|
858
1086
|
.storymap-view {
|
|
859
1087
|
padding: 24px 28px;
|
|
@@ -1707,6 +1935,8 @@
|
|
|
1707
1935
|
let currentFeature = null;
|
|
1708
1936
|
let currentBoard = null;
|
|
1709
1937
|
let currentPipeline = null;
|
|
1938
|
+
let currentChecklist = null;
|
|
1939
|
+
let expandedChecklist = null;
|
|
1710
1940
|
let activeTab = null;
|
|
1711
1941
|
let ws = null;
|
|
1712
1942
|
let reconnectTimer = null;
|
|
@@ -1783,6 +2013,8 @@
|
|
|
1783
2013
|
renderPlanView();
|
|
1784
2014
|
} else if (phaseId === 'clarify') {
|
|
1785
2015
|
renderClarifyView();
|
|
2016
|
+
} else if (phaseId === 'checklist') {
|
|
2017
|
+
renderChecklistView();
|
|
1786
2018
|
} else {
|
|
1787
2019
|
renderPlaceholderView(phaseId);
|
|
1788
2020
|
}
|
|
@@ -1814,6 +2046,150 @@
|
|
|
1814
2046
|
</div>`;
|
|
1815
2047
|
}
|
|
1816
2048
|
|
|
2049
|
+
// ====== Checklist Quality Gates View ======
|
|
2050
|
+
|
|
2051
|
+
async function renderChecklistView() {
|
|
2052
|
+
if (!currentFeature) return;
|
|
2053
|
+
|
|
2054
|
+
try {
|
|
2055
|
+
const res = await fetch(`/api/checklist/${currentFeature}`);
|
|
2056
|
+
if (!res.ok) throw new Error('Failed to load');
|
|
2057
|
+
currentChecklist = await res.json();
|
|
2058
|
+
expandedChecklist = null;
|
|
2059
|
+
renderChecklistContent(currentChecklist);
|
|
2060
|
+
} catch {
|
|
2061
|
+
contentArea.innerHTML = '<div class="checklist-empty"><div class="checklist-empty-title">Failed to load checklist data.</div></div>';
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
function renderChecklistContent(data) {
|
|
2066
|
+
if (!data) return;
|
|
2067
|
+
|
|
2068
|
+
// Empty state
|
|
2069
|
+
if (!data.files || data.files.length === 0) {
|
|
2070
|
+
contentArea.innerHTML = `
|
|
2071
|
+
<div class="checklist-empty">
|
|
2072
|
+
<div class="checklist-empty-icon">☐</div>
|
|
2073
|
+
<div class="checklist-empty-title">No checklists generated for this feature</div>
|
|
2074
|
+
<div class="checklist-empty-text">Run <code>/iikit-04-checklist</code> to generate domain-specific quality checklists for requirements validation.</div>
|
|
2075
|
+
</div>`;
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
const CIRCUMFERENCE = 2 * Math.PI * 42; // radius=42 for viewBox 100x100
|
|
2080
|
+
|
|
2081
|
+
let html = '<div class="checklist-view">';
|
|
2082
|
+
|
|
2083
|
+
// Gate indicator
|
|
2084
|
+
html += renderGateIndicator(data.gate);
|
|
2085
|
+
|
|
2086
|
+
// Progress rings
|
|
2087
|
+
html += '<div class="checklist-rings">';
|
|
2088
|
+
data.files.forEach((file) => {
|
|
2089
|
+
const isExpanded = expandedChecklist === file.filename;
|
|
2090
|
+
const offset = CIRCUMFERENCE - (file.percentage / 100) * CIRCUMFERENCE;
|
|
2091
|
+
|
|
2092
|
+
html += `<div class="checklist-ring-wrapper${isExpanded ? ' expanded' : ''}"
|
|
2093
|
+
tabindex="0" role="button"
|
|
2094
|
+
aria-expanded="${isExpanded}"
|
|
2095
|
+
aria-controls="checklist-detail-${escapeHtml(file.filename)}"
|
|
2096
|
+
aria-label="${escapeHtml(file.name)}: ${file.checked} of ${file.total} items complete, ${file.percentage}%"
|
|
2097
|
+
data-filename="${escapeHtml(file.filename)}">
|
|
2098
|
+
<svg class="checklist-ring-svg" viewBox="0 0 100 100" role="img"
|
|
2099
|
+
aria-label="${escapeHtml(file.name)}: ${file.checked} of ${file.total} items complete, ${file.percentage}%">
|
|
2100
|
+
<circle class="checklist-ring-track" cx="50" cy="50" r="42" />
|
|
2101
|
+
<circle class="checklist-ring-fill ${file.color}" cx="50" cy="50" r="42"
|
|
2102
|
+
stroke-dasharray="${CIRCUMFERENCE}"
|
|
2103
|
+
stroke-dashoffset="${offset}" />
|
|
2104
|
+
<text class="checklist-ring-pct" x="50" y="50">${file.percentage}%</text>
|
|
2105
|
+
</svg>
|
|
2106
|
+
<div class="checklist-ring-name">${escapeHtml(file.name)}</div>
|
|
2107
|
+
<div class="checklist-ring-fraction">${file.checked}/${file.total}</div>
|
|
2108
|
+
</div>`;
|
|
2109
|
+
});
|
|
2110
|
+
html += '</div>';
|
|
2111
|
+
|
|
2112
|
+
// Accordion detail panels
|
|
2113
|
+
data.files.forEach((file) => {
|
|
2114
|
+
const isExpanded = expandedChecklist === file.filename;
|
|
2115
|
+
html += `<div id="checklist-detail-${escapeHtml(file.filename)}" class="checklist-detail${isExpanded ? ' open' : ''}">`;
|
|
2116
|
+
html += '<div class="checklist-detail-inner">';
|
|
2117
|
+
html += renderChecklistDetail(file);
|
|
2118
|
+
html += '</div></div>';
|
|
2119
|
+
});
|
|
2120
|
+
|
|
2121
|
+
html += '</div>';
|
|
2122
|
+
contentArea.innerHTML = html;
|
|
2123
|
+
|
|
2124
|
+
// Attach click and keyboard handlers to rings
|
|
2125
|
+
document.querySelectorAll('.checklist-ring-wrapper').forEach(wrapper => {
|
|
2126
|
+
const filename = wrapper.getAttribute('data-filename');
|
|
2127
|
+
wrapper.addEventListener('click', () => toggleChecklistExpand(filename, data));
|
|
2128
|
+
wrapper.addEventListener('keydown', (e) => {
|
|
2129
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
2130
|
+
e.preventDefault();
|
|
2131
|
+
toggleChecklistExpand(filename, data);
|
|
2132
|
+
}
|
|
2133
|
+
});
|
|
2134
|
+
});
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
function renderGateIndicator(gate) {
|
|
2138
|
+
if (!gate) return '';
|
|
2139
|
+
const levelClass = gate.level === 'yellow' ? 'blocked-yellow' : (gate.level === 'red' ? 'blocked-red' : 'open');
|
|
2140
|
+
return `<div class="gate-indicator" role="status" aria-live="polite">
|
|
2141
|
+
<div class="gate-dot ${gate.level}"></div>
|
|
2142
|
+
<span class="gate-label ${levelClass}">${escapeHtml(gate.label)}</span>
|
|
2143
|
+
</div>`;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
function renderChecklistDetail(file) {
|
|
2147
|
+
if (!file || !file.items || file.items.length === 0) {
|
|
2148
|
+
return '<div style="color:var(--color-text-muted);font-size:13px;">No items detected in this checklist.</div>';
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
let html = '';
|
|
2152
|
+
let currentCategory = null;
|
|
2153
|
+
|
|
2154
|
+
file.items.forEach(item => {
|
|
2155
|
+
if (item.category !== currentCategory) {
|
|
2156
|
+
currentCategory = item.category;
|
|
2157
|
+
if (currentCategory) {
|
|
2158
|
+
html += `<h4 class="checklist-category">${escapeHtml(currentCategory)}</h4>`;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
const icon = item.checked
|
|
2163
|
+
? '<span class="checklist-item-icon checked">✓</span>'
|
|
2164
|
+
: '<span class="checklist-item-icon unchecked">☐</span>';
|
|
2165
|
+
|
|
2166
|
+
const chkId = item.chkId
|
|
2167
|
+
? `<span class="checklist-item-id">${escapeHtml(item.chkId)}</span>`
|
|
2168
|
+
: '';
|
|
2169
|
+
|
|
2170
|
+
const tags = (item.tags || []).map(t =>
|
|
2171
|
+
`<span class="checklist-item-tag">${escapeHtml(t)}</span>`
|
|
2172
|
+
).join('');
|
|
2173
|
+
|
|
2174
|
+
html += `<div class="checklist-item">
|
|
2175
|
+
${icon}${chkId}
|
|
2176
|
+
<span style="flex:1">${escapeHtml(item.text)}</span>
|
|
2177
|
+
${tags}
|
|
2178
|
+
</div>`;
|
|
2179
|
+
});
|
|
2180
|
+
|
|
2181
|
+
return html;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
function toggleChecklistExpand(filename, data) {
|
|
2185
|
+
if (expandedChecklist === filename) {
|
|
2186
|
+
expandedChecklist = null;
|
|
2187
|
+
} else {
|
|
2188
|
+
expandedChecklist = filename;
|
|
2189
|
+
}
|
|
2190
|
+
renderChecklistContent(data);
|
|
2191
|
+
}
|
|
2192
|
+
|
|
1817
2193
|
// ====== Spec Story Map View ======
|
|
1818
2194
|
let currentStoryMap = null;
|
|
1819
2195
|
|
|
@@ -3212,6 +3588,15 @@
|
|
|
3212
3588
|
}
|
|
3213
3589
|
break;
|
|
3214
3590
|
|
|
3591
|
+
case 'checklist_update':
|
|
3592
|
+
if (msg.feature === currentFeature && msg.checklist) {
|
|
3593
|
+
currentChecklist = msg.checklist;
|
|
3594
|
+
if (activeTab === 'checklist') {
|
|
3595
|
+
renderChecklistContent(msg.checklist);
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
break;
|
|
3599
|
+
|
|
3215
3600
|
case 'constitution_update':
|
|
3216
3601
|
if (msg.constitution) {
|
|
3217
3602
|
currentConstitution = msg.constitution;
|
package/src/server.js
CHANGED
|
@@ -12,6 +12,7 @@ const { computeAssertionHash, checkIntegrity } = require('./integrity');
|
|
|
12
12
|
const { computePipelineState } = require('./pipeline');
|
|
13
13
|
const { computeStoryMapState } = require('./storymap');
|
|
14
14
|
const { computePlanViewState } = require('./planview');
|
|
15
|
+
const { computeChecklistViewState } = require('./checklist');
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* List features from specs/ directory.
|
|
@@ -167,6 +168,20 @@ function createServer({ projectPath, port = 3000 }) {
|
|
|
167
168
|
}
|
|
168
169
|
});
|
|
169
170
|
|
|
171
|
+
// API: checklist view state for a feature
|
|
172
|
+
app.get('/api/checklist/:feature', (req, res) => {
|
|
173
|
+
try {
|
|
174
|
+
const featureDir = path.join(projectPath, 'specs', req.params.feature);
|
|
175
|
+
if (!fs.existsSync(featureDir)) {
|
|
176
|
+
return res.status(404).json({ error: 'Feature not found' });
|
|
177
|
+
}
|
|
178
|
+
const checklist = computeChecklistViewState(projectPath, req.params.feature);
|
|
179
|
+
res.json(checklist);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
res.status(500).json({ error: err.message });
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
170
185
|
// API: board state for a feature
|
|
171
186
|
app.get('/api/board/:feature', (req, res) => {
|
|
172
187
|
try {
|
|
@@ -256,6 +271,14 @@ function createServer({ projectPath, port = 3000 }) {
|
|
|
256
271
|
planview
|
|
257
272
|
}));
|
|
258
273
|
}
|
|
274
|
+
const checklist = computeChecklistViewState(projectPath, ws.currentFeature);
|
|
275
|
+
if (checklist) {
|
|
276
|
+
ws.send(JSON.stringify({
|
|
277
|
+
type: 'checklist_update',
|
|
278
|
+
feature: ws.currentFeature,
|
|
279
|
+
checklist
|
|
280
|
+
}));
|
|
281
|
+
}
|
|
259
282
|
} catch {
|
|
260
283
|
// ignore errors during push
|
|
261
284
|
}
|