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.
@@ -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 };
@@ -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;