iikit-dashboard 1.0.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 +77 -0
- package/bin/iikit-dashboard.js +68 -0
- package/package.json +45 -0
- package/src/board.js +93 -0
- package/src/integrity.js +63 -0
- package/src/parser.js +768 -0
- package/src/pipeline.js +130 -0
- package/src/planview.js +195 -0
- package/src/public/index.html +3322 -0
- package/src/server.js +302 -0
- package/src/storymap.js +40 -0
package/src/pipeline.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { parseTasks, parseChecklists, parseConstitutionTDD, hasClarifications } = require('./parser');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Compute pipeline phase states for a feature by examining artifacts on disk.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} projectPath - Path to the project root
|
|
11
|
+
* @param {string} featureId - Feature directory name (e.g., "001-kanban-board")
|
|
12
|
+
* @returns {{phases: Array<{id: string, name: string, status: string, progress: string|null, optional: boolean}>}}
|
|
13
|
+
*/
|
|
14
|
+
function computePipelineState(projectPath, featureId) {
|
|
15
|
+
const featureDir = path.join(projectPath, 'specs', featureId);
|
|
16
|
+
const constitutionPath = path.join(projectPath, 'CONSTITUTION.md');
|
|
17
|
+
const specPath = path.join(featureDir, 'spec.md');
|
|
18
|
+
const planPath = path.join(featureDir, 'plan.md');
|
|
19
|
+
const checklistDir = path.join(featureDir, 'checklists');
|
|
20
|
+
const testSpecsPath = path.join(featureDir, 'tests', 'test-specs.md');
|
|
21
|
+
const tasksPath = path.join(featureDir, 'tasks.md');
|
|
22
|
+
|
|
23
|
+
const analysisPath = path.join(featureDir, 'analysis.md');
|
|
24
|
+
|
|
25
|
+
const specExists = fs.existsSync(specPath);
|
|
26
|
+
const planExists = fs.existsSync(planPath);
|
|
27
|
+
const tasksExists = fs.existsSync(tasksPath);
|
|
28
|
+
const testSpecsExists = fs.existsSync(testSpecsPath);
|
|
29
|
+
const constitutionExists = fs.existsSync(constitutionPath);
|
|
30
|
+
const analysisExists = fs.existsSync(analysisPath);
|
|
31
|
+
|
|
32
|
+
// Read spec content for clarifications check
|
|
33
|
+
const specContent = specExists ? fs.readFileSync(specPath, 'utf-8') : '';
|
|
34
|
+
|
|
35
|
+
// Parse tasks for implement progress
|
|
36
|
+
const tasksContent = tasksExists ? fs.readFileSync(tasksPath, 'utf-8') : '';
|
|
37
|
+
const tasks = parseTasks(tasksContent);
|
|
38
|
+
const checkedCount = tasks.filter(t => t.checked).length;
|
|
39
|
+
const totalCount = tasks.length;
|
|
40
|
+
|
|
41
|
+
// Parse checklists
|
|
42
|
+
const checklistStatus = parseChecklists(checklistDir);
|
|
43
|
+
|
|
44
|
+
// TDD requirement check
|
|
45
|
+
const tddRequired = constitutionExists ? parseConstitutionTDD(constitutionPath) : false;
|
|
46
|
+
|
|
47
|
+
const phases = [
|
|
48
|
+
{
|
|
49
|
+
id: 'constitution',
|
|
50
|
+
name: 'Constitution',
|
|
51
|
+
status: constitutionExists ? 'complete' : 'not_started',
|
|
52
|
+
progress: null,
|
|
53
|
+
optional: false
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'spec',
|
|
57
|
+
name: 'Spec',
|
|
58
|
+
status: specExists ? 'complete' : 'not_started',
|
|
59
|
+
progress: null,
|
|
60
|
+
optional: false
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'clarify',
|
|
64
|
+
name: 'Clarify',
|
|
65
|
+
status: hasClarifications(specContent) ? 'complete' : (planExists && !hasClarifications(specContent) ? 'skipped' : 'not_started'),
|
|
66
|
+
progress: null,
|
|
67
|
+
optional: true
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 'plan',
|
|
71
|
+
name: 'Plan',
|
|
72
|
+
status: planExists ? 'complete' : 'not_started',
|
|
73
|
+
progress: null,
|
|
74
|
+
optional: false
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'checklist',
|
|
78
|
+
name: 'Checklist',
|
|
79
|
+
status: checklistStatus.total === 0
|
|
80
|
+
? 'not_started'
|
|
81
|
+
: checklistStatus.checked === checklistStatus.total
|
|
82
|
+
? 'complete'
|
|
83
|
+
: 'in_progress',
|
|
84
|
+
progress: checklistStatus.total > 0
|
|
85
|
+
? `${Math.round((checklistStatus.checked / checklistStatus.total) * 100)}%`
|
|
86
|
+
: null,
|
|
87
|
+
optional: false
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'testify',
|
|
91
|
+
name: 'Testify',
|
|
92
|
+
status: testSpecsExists
|
|
93
|
+
? 'complete'
|
|
94
|
+
: (!tddRequired && planExists ? 'skipped' : 'not_started'),
|
|
95
|
+
progress: null,
|
|
96
|
+
optional: !tddRequired
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'tasks',
|
|
100
|
+
name: 'Tasks',
|
|
101
|
+
status: tasksExists ? 'complete' : 'not_started',
|
|
102
|
+
progress: null,
|
|
103
|
+
optional: false
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'analyze',
|
|
107
|
+
name: 'Analyze',
|
|
108
|
+
status: analysisExists ? 'complete' : 'not_started',
|
|
109
|
+
progress: null,
|
|
110
|
+
optional: false
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: 'implement',
|
|
114
|
+
name: 'Implement',
|
|
115
|
+
status: totalCount === 0 || checkedCount === 0
|
|
116
|
+
? 'not_started'
|
|
117
|
+
: checkedCount === totalCount
|
|
118
|
+
? 'complete'
|
|
119
|
+
: 'in_progress',
|
|
120
|
+
progress: totalCount > 0 && checkedCount > 0
|
|
121
|
+
? `${Math.round((checkedCount / totalCount) * 100)}%`
|
|
122
|
+
: null,
|
|
123
|
+
optional: false
|
|
124
|
+
}
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
return { phases };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = { computePipelineState };
|
package/src/planview.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { parseTechContext, parseFileStructure, parseAsciiDiagram, parseTesslJson, parseResearchDecisions } = require('./parser');
|
|
6
|
+
|
|
7
|
+
module.exports = { computePlanViewState, classifyNodeTypes };
|
|
8
|
+
|
|
9
|
+
// In-memory cache for LLM classification results per feature
|
|
10
|
+
const classificationCache = new Map();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Compute plan view state from parsed plan.md data.
|
|
14
|
+
* Follows the same pattern as board.js and storymap.js.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} projectPath - Path to project root
|
|
17
|
+
* @param {string} featureId - Feature directory name
|
|
18
|
+
* @returns {Promise<Object>} Plan view state
|
|
19
|
+
*/
|
|
20
|
+
async function computePlanViewState(projectPath, featureId) {
|
|
21
|
+
const featureDir = path.join(projectPath, 'specs', featureId);
|
|
22
|
+
const planPath = path.join(featureDir, 'plan.md');
|
|
23
|
+
|
|
24
|
+
if (!fs.existsSync(planPath)) {
|
|
25
|
+
return {
|
|
26
|
+
techContext: [],
|
|
27
|
+
researchDecisions: [],
|
|
28
|
+
fileStructure: null,
|
|
29
|
+
diagram: null,
|
|
30
|
+
tesslTiles: [],
|
|
31
|
+
exists: false,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const planContent = fs.readFileSync(planPath, 'utf-8');
|
|
36
|
+
|
|
37
|
+
// Parse tech context
|
|
38
|
+
const techContext = parseTechContext(planContent);
|
|
39
|
+
|
|
40
|
+
// Parse research decisions for tooltips
|
|
41
|
+
const researchPath = path.join(featureDir, 'research.md');
|
|
42
|
+
let researchDecisions = [];
|
|
43
|
+
if (fs.existsSync(researchPath)) {
|
|
44
|
+
const researchContent = fs.readFileSync(researchPath, 'utf-8');
|
|
45
|
+
researchDecisions = parseResearchDecisions(researchContent);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Parse file structure and check existence
|
|
49
|
+
let fileStructure = parseFileStructure(planContent);
|
|
50
|
+
if (fileStructure) {
|
|
51
|
+
fileStructure.entries = fileStructure.entries.map(entry => {
|
|
52
|
+
const filePath = buildFilePath(fileStructure.entries, entry, fileStructure.rootName);
|
|
53
|
+
const fullPath = path.join(projectPath, filePath);
|
|
54
|
+
return { ...entry, exists: fs.existsSync(fullPath) };
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Parse ASCII diagram
|
|
59
|
+
let diagram = parseAsciiDiagram(planContent);
|
|
60
|
+
if (diagram && diagram.nodes.length > 0) {
|
|
61
|
+
// Classify node types via LLM (with cache and fallback)
|
|
62
|
+
const cacheKey = `${featureId}:${planContent.length}`;
|
|
63
|
+
if (classificationCache.has(cacheKey)) {
|
|
64
|
+
const cached = classificationCache.get(cacheKey);
|
|
65
|
+
diagram.nodes = diagram.nodes.map(n => ({
|
|
66
|
+
...n,
|
|
67
|
+
type: cached[n.label] || 'default'
|
|
68
|
+
}));
|
|
69
|
+
} else {
|
|
70
|
+
const labels = diagram.nodes.map(n => n.label);
|
|
71
|
+
const types = await classifyNodeTypes(labels);
|
|
72
|
+
classificationCache.set(cacheKey, types);
|
|
73
|
+
diagram.nodes = diagram.nodes.map(n => ({
|
|
74
|
+
...n,
|
|
75
|
+
type: types[n.label] || 'default'
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Parse tessl.json
|
|
81
|
+
const tesslTiles = parseTesslJson(projectPath);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
techContext,
|
|
85
|
+
researchDecisions,
|
|
86
|
+
fileStructure,
|
|
87
|
+
diagram,
|
|
88
|
+
tesslTiles,
|
|
89
|
+
exists: true,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Build file path from tree entries by walking up the depth chain.
|
|
95
|
+
*/
|
|
96
|
+
function buildFilePath(entries, targetEntry, rootName) {
|
|
97
|
+
const idx = entries.indexOf(targetEntry);
|
|
98
|
+
const parts = [targetEntry.name];
|
|
99
|
+
let currentDepth = targetEntry.depth;
|
|
100
|
+
|
|
101
|
+
// Walk backwards to find all parent directories
|
|
102
|
+
for (let i = idx - 1; i >= 0; i--) {
|
|
103
|
+
if (entries[i].depth < currentDepth && entries[i].type === 'directory') {
|
|
104
|
+
parts.unshift(entries[i].name);
|
|
105
|
+
currentDepth = entries[i].depth;
|
|
106
|
+
if (currentDepth === 0) break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// If entry is at depth 0 and has no directory parent, prepend rootName
|
|
111
|
+
if (currentDepth === 0 && rootName && parts[0] !== rootName) {
|
|
112
|
+
// Check if the entry is NOT under a different depth-0 directory
|
|
113
|
+
// (i.e., it belongs to the rootName section)
|
|
114
|
+
let hasDepth0DirParent = false;
|
|
115
|
+
for (let i = idx - 1; i >= 0; i--) {
|
|
116
|
+
if (entries[i].depth === 0 && entries[i].type === 'directory') {
|
|
117
|
+
hasDepth0DirParent = true;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!hasDepth0DirParent) {
|
|
122
|
+
parts.unshift(rootName);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return parts.join('/');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Classify diagram node labels into component types using LLM.
|
|
131
|
+
* Falls back to all "default" if API key is missing or call fails.
|
|
132
|
+
*
|
|
133
|
+
* @param {string[]} labels - Node labels to classify
|
|
134
|
+
* @returns {Promise<Object>} Mapping of label -> type
|
|
135
|
+
*/
|
|
136
|
+
async function classifyNodeTypes(labels) {
|
|
137
|
+
const result = {};
|
|
138
|
+
for (const label of labels) result[label] = 'default';
|
|
139
|
+
|
|
140
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
141
|
+
if (!apiKey || labels.length === 0) return result;
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const Anthropic = require('@anthropic-ai/sdk');
|
|
145
|
+
const client = new Anthropic({ apiKey });
|
|
146
|
+
|
|
147
|
+
const controller = new AbortController();
|
|
148
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
149
|
+
|
|
150
|
+
const response = await client.messages.create({
|
|
151
|
+
model: 'claude-haiku-4-5-20251001',
|
|
152
|
+
max_tokens: 256,
|
|
153
|
+
messages: [{
|
|
154
|
+
role: 'user',
|
|
155
|
+
content: `Classify each of these software architecture diagram component labels into exactly one category: "client", "server", "storage", or "external".
|
|
156
|
+
|
|
157
|
+
Labels: ${JSON.stringify(labels)}
|
|
158
|
+
|
|
159
|
+
Respond with ONLY a JSON object mapping each label to its category. Example: {"Browser": "client", "API Server": "server"}
|
|
160
|
+
No explanation, just the JSON.`
|
|
161
|
+
}]
|
|
162
|
+
}, { signal: controller.signal });
|
|
163
|
+
|
|
164
|
+
clearTimeout(timeout);
|
|
165
|
+
|
|
166
|
+
const text = response.content[0]?.text || '';
|
|
167
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
168
|
+
if (jsonMatch) {
|
|
169
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
170
|
+
const validTypes = new Set(['client', 'server', 'storage', 'external']);
|
|
171
|
+
for (const [label, type] of Object.entries(parsed)) {
|
|
172
|
+
if (validTypes.has(type)) {
|
|
173
|
+
result[label] = type;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch {
|
|
178
|
+
// Fallback: all nodes get "default" type
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Invalidate classification cache for a feature.
|
|
186
|
+
*/
|
|
187
|
+
function invalidateCache(featureId) {
|
|
188
|
+
for (const key of classificationCache.keys()) {
|
|
189
|
+
if (key.startsWith(`${featureId}:`)) {
|
|
190
|
+
classificationCache.delete(key);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports.invalidateCache = invalidateCache;
|