iikit-dashboard 1.6.0 → 1.7.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/bin/iikit-dashboard.js +15 -55
- package/package.json +8 -9
- package/src/generate-dashboard.js +309 -0
- package/src/parser.js +18 -1
- package/src/pipeline.js +2 -1
- package/src/public/index.html +222 -246
- package/src/server.js +0 -428
package/bin/iikit-dashboard.js
CHANGED
|
@@ -2,69 +2,29 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const {
|
|
5
|
+
const { execFileSync } = require('child_process');
|
|
6
6
|
|
|
7
|
-
// Parse arguments
|
|
8
7
|
const args = process.argv.slice(2);
|
|
9
|
-
let projectPath =
|
|
10
|
-
let
|
|
8
|
+
let projectPath = '.';
|
|
9
|
+
let watch = false;
|
|
11
10
|
|
|
12
11
|
for (let i = 0; i < args.length; i++) {
|
|
13
|
-
if (args[i] === '--
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
port = parseInt(args[i + 1], 10);
|
|
12
|
+
if (args[i] === '--watch' || args[i] === '-w') {
|
|
13
|
+
watch = true;
|
|
14
|
+
} else if (args[i] === '--path' && args[i + 1]) {
|
|
15
|
+
projectPath = args[i + 1];
|
|
18
16
|
i++;
|
|
19
17
|
} else if (!args[i].startsWith('--')) {
|
|
20
|
-
|
|
21
|
-
projectPath = path.resolve(args[i]);
|
|
18
|
+
projectPath = args[i];
|
|
22
19
|
}
|
|
23
20
|
}
|
|
24
21
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
console.log(` Project: ${projectPath}`);
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
const result = await createServer({ projectPath, port });
|
|
32
|
-
const url = `http://localhost:${result.port}`;
|
|
33
|
-
console.log(` Server: ${url}`);
|
|
34
|
-
console.log(`\n Open your browser to view the dashboard.\n`);
|
|
35
|
-
|
|
36
|
-
// Try to open browser (best effort, don't fail if it doesn't work)
|
|
37
|
-
try {
|
|
38
|
-
const { exec } = require('child_process');
|
|
39
|
-
const cmd = process.platform === 'darwin' ? 'open' :
|
|
40
|
-
process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
41
|
-
exec(`${cmd} ${url}`);
|
|
42
|
-
} catch {
|
|
43
|
-
// Ignore browser open errors
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Handle graceful shutdown
|
|
47
|
-
process.on('SIGINT', () => {
|
|
48
|
-
console.log('\n Shutting down...');
|
|
49
|
-
removePidfile(projectPath);
|
|
50
|
-
result.server.close(() => {
|
|
51
|
-
if (result.watcher) result.watcher.close();
|
|
52
|
-
process.exit(0);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
22
|
+
const generatorPath = path.join(__dirname, '..', 'src', 'generate-dashboard.js');
|
|
23
|
+
const generatorArgs = [generatorPath, projectPath];
|
|
24
|
+
if (watch) generatorArgs.push('--watch');
|
|
55
25
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
process.exit(0);
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
} catch (err) {
|
|
65
|
-
console.error(` Error: ${err.message}`);
|
|
66
|
-
process.exit(1);
|
|
67
|
-
}
|
|
26
|
+
try {
|
|
27
|
+
execFileSync(process.execPath, generatorArgs, { stdio: 'inherit' });
|
|
28
|
+
} catch (err) {
|
|
29
|
+
process.exit(err.status || 1);
|
|
68
30
|
}
|
|
69
|
-
|
|
70
|
-
main();
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iikit-dashboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Real-time dashboard for Intent Integrity Kit (IIKit) — visualizes every phase of specification-driven AI development",
|
|
5
|
-
"main": "src/
|
|
5
|
+
"main": "src/generate-dashboard.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"iikit-dashboard": "bin/iikit-dashboard.js"
|
|
8
8
|
},
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
"src/"
|
|
12
12
|
],
|
|
13
13
|
"scripts": {
|
|
14
|
-
"start": "node
|
|
15
|
-
"test": "jest --
|
|
14
|
+
"start": "node src/generate-dashboard.js . --watch",
|
|
15
|
+
"test": "jest --testPathIgnorePatterns=test/visual",
|
|
16
16
|
"test:visual": "npx playwright test",
|
|
17
|
-
"test:visual:update": "npx playwright test --update-snapshots"
|
|
17
|
+
"test:visual:update": "npx playwright test --update-snapshots",
|
|
18
|
+
"build:generator": "node build/bundle-generator.js"
|
|
18
19
|
},
|
|
19
20
|
"keywords": [
|
|
20
21
|
"iikit",
|
|
@@ -36,13 +37,11 @@
|
|
|
36
37
|
"node": ">=18.0.0"
|
|
37
38
|
},
|
|
38
39
|
"dependencies": {
|
|
39
|
-
"
|
|
40
|
-
"chokidar": "^3.6.0",
|
|
41
|
-
"express": "^5.2.1",
|
|
42
|
-
"ws": "^8.19.0"
|
|
40
|
+
"chokidar": "^3.6.0"
|
|
43
41
|
},
|
|
44
42
|
"devDependencies": {
|
|
45
43
|
"@playwright/test": "^1.58.2",
|
|
44
|
+
"esbuild": "^0.27.3",
|
|
46
45
|
"jest": "^30.2.0"
|
|
47
46
|
}
|
|
48
47
|
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
|
|
7
|
+
const { parseSpecStories, parseTasks, parseConstitutionPrinciples, parsePremise } = require('./parser');
|
|
8
|
+
const { computeBoardState } = require('./board');
|
|
9
|
+
const { computeAssertionHash, checkIntegrity } = require('./integrity');
|
|
10
|
+
const { computePipelineState } = require('./pipeline');
|
|
11
|
+
const { computeStoryMapState } = require('./storymap');
|
|
12
|
+
const { computePlanViewState } = require('./planview');
|
|
13
|
+
const { computeChecklistViewState } = require('./checklist');
|
|
14
|
+
const { computeTestifyState } = require('./testify');
|
|
15
|
+
const { computeAnalyzeState } = require('./analyze');
|
|
16
|
+
const { computeBugsState } = require('./bugs');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* List features from specs/ directory.
|
|
20
|
+
* A feature is a directory under specs/ that contains spec.md (FR-004).
|
|
21
|
+
*/
|
|
22
|
+
function listFeatures(projectPath) {
|
|
23
|
+
const specsDir = path.join(projectPath, 'specs');
|
|
24
|
+
if (!fs.existsSync(specsDir)) return [];
|
|
25
|
+
|
|
26
|
+
const entries = fs.readdirSync(specsDir, { withFileTypes: true });
|
|
27
|
+
const features = [];
|
|
28
|
+
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
if (!entry.isDirectory()) continue;
|
|
31
|
+
const featureDir = path.join(specsDir, entry.name);
|
|
32
|
+
const specPath = path.join(featureDir, 'spec.md');
|
|
33
|
+
if (!fs.existsSync(specPath)) continue;
|
|
34
|
+
|
|
35
|
+
const tasksPath = path.join(featureDir, 'tasks.md');
|
|
36
|
+
const specContent = fs.readFileSync(specPath, 'utf-8');
|
|
37
|
+
const tasksContent = fs.existsSync(tasksPath) ? fs.readFileSync(tasksPath, 'utf-8') : '';
|
|
38
|
+
const stories = parseSpecStories(specContent);
|
|
39
|
+
const tasks = parseTasks(tasksContent);
|
|
40
|
+
|
|
41
|
+
const checkedCount = tasks.filter(t => t.checked).length;
|
|
42
|
+
const totalCount = tasks.length;
|
|
43
|
+
|
|
44
|
+
const namePart = entry.name.replace(/^\d+-/, '');
|
|
45
|
+
const name = namePart.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
46
|
+
|
|
47
|
+
features.push({
|
|
48
|
+
id: entry.name,
|
|
49
|
+
name,
|
|
50
|
+
stories: stories.length,
|
|
51
|
+
progress: `${checkedCount}/${totalCount}`
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return features.reverse();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Compute board state with integrity for a feature.
|
|
60
|
+
*/
|
|
61
|
+
function getBoardState(projectPath, featureId) {
|
|
62
|
+
const featureDir = path.join(projectPath, 'specs', featureId);
|
|
63
|
+
const specPath = path.join(featureDir, 'spec.md');
|
|
64
|
+
const tasksPath = path.join(featureDir, 'tasks.md');
|
|
65
|
+
const testSpecsPath = path.join(featureDir, 'tests', 'test-specs.md');
|
|
66
|
+
const contextPath = path.join(featureDir, 'context.json');
|
|
67
|
+
|
|
68
|
+
const specContent = fs.existsSync(specPath) ? fs.readFileSync(specPath, 'utf-8') : '';
|
|
69
|
+
const tasksContent = fs.existsSync(tasksPath) ? fs.readFileSync(tasksPath, 'utf-8') : '';
|
|
70
|
+
|
|
71
|
+
const stories = parseSpecStories(specContent);
|
|
72
|
+
const tasks = parseTasks(tasksContent);
|
|
73
|
+
const board = computeBoardState(stories, tasks);
|
|
74
|
+
|
|
75
|
+
let integrity = { status: 'missing', currentHash: null, storedHash: null };
|
|
76
|
+
if (fs.existsSync(testSpecsPath)) {
|
|
77
|
+
const testSpecsContent = fs.readFileSync(testSpecsPath, 'utf-8');
|
|
78
|
+
const currentHash = computeAssertionHash(testSpecsContent);
|
|
79
|
+
let storedHash = null;
|
|
80
|
+
if (fs.existsSync(contextPath)) {
|
|
81
|
+
try {
|
|
82
|
+
const context = JSON.parse(fs.readFileSync(contextPath, 'utf-8'));
|
|
83
|
+
storedHash = context?.testify?.assertion_hash || null;
|
|
84
|
+
} catch {
|
|
85
|
+
// malformed context.json
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
integrity = checkIntegrity(currentHash, storedHash);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { ...board, integrity };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Assemble DASHBOARD_DATA for all features.
|
|
96
|
+
*/
|
|
97
|
+
async function assembleDashboardData(projectPath) {
|
|
98
|
+
const resolvedPath = path.resolve(projectPath);
|
|
99
|
+
const features = listFeatures(resolvedPath);
|
|
100
|
+
const constitution = parseConstitutionPrinciples(resolvedPath);
|
|
101
|
+
const premise = parsePremise(resolvedPath);
|
|
102
|
+
|
|
103
|
+
const featureData = {};
|
|
104
|
+
for (const feature of features) {
|
|
105
|
+
const fid = feature.id;
|
|
106
|
+
try {
|
|
107
|
+
featureData[fid] = {
|
|
108
|
+
board: getBoardState(resolvedPath, fid),
|
|
109
|
+
pipeline: computePipelineState(resolvedPath, fid),
|
|
110
|
+
storyMap: computeStoryMapState(resolvedPath, fid),
|
|
111
|
+
planView: await computePlanViewState(resolvedPath, fid),
|
|
112
|
+
checklist: computeChecklistViewState(resolvedPath, fid),
|
|
113
|
+
testify: computeTestifyState(resolvedPath, fid),
|
|
114
|
+
analyze: computeAnalyzeState(resolvedPath, fid),
|
|
115
|
+
bugs: computeBugsState(resolvedPath, fid)
|
|
116
|
+
};
|
|
117
|
+
} catch (err) {
|
|
118
|
+
process.stderr.write(`Error: Parser failed on specs/${fid}/spec.md: ${err.message}. Check artifact syntax.\n`);
|
|
119
|
+
process.exit(5);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
meta: {
|
|
125
|
+
projectPath: resolvedPath,
|
|
126
|
+
generatedAt: new Date().toISOString()
|
|
127
|
+
},
|
|
128
|
+
features,
|
|
129
|
+
constitution,
|
|
130
|
+
premise,
|
|
131
|
+
featureData
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Inject data into HTML template and return the complete HTML string.
|
|
137
|
+
*/
|
|
138
|
+
function buildHtml(templateHtml, dashboardData) {
|
|
139
|
+
let html = templateHtml;
|
|
140
|
+
|
|
141
|
+
// Inject DASHBOARD_DATA and meta-refresh into <head> (FR-004, FR-005, FR-007)
|
|
142
|
+
// DASHBOARD_DATA must be in <head> so it's available before the IIFE in <body> runs
|
|
143
|
+
// Escape </script> to prevent script-tag injection from data file content (SC-008)
|
|
144
|
+
const safeJson = JSON.stringify(dashboardData).replace(/<\/script>/gi, '<\\/script>');
|
|
145
|
+
const headInject = ` <meta http-equiv="refresh" content="2">\n` +
|
|
146
|
+
` <script>window.DASHBOARD_DATA = ${safeJson};</script>\n`;
|
|
147
|
+
html = html.replace('</head>', headInject + '</head>');
|
|
148
|
+
|
|
149
|
+
// Inject JS fallback reload before </body> (FR-007)
|
|
150
|
+
html = html.replace('</body>', `<script>setInterval(() => location.reload(), 2000);</script>\n</body>`);
|
|
151
|
+
|
|
152
|
+
return html;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Write HTML atomically: write to .tmp then rename (FR-011).
|
|
157
|
+
*/
|
|
158
|
+
function writeAtomic(outputPath, content) {
|
|
159
|
+
const dir = path.dirname(outputPath);
|
|
160
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
161
|
+
|
|
162
|
+
const tmpPath = outputPath + '.tmp';
|
|
163
|
+
fs.writeFileSync(tmpPath, content, 'utf-8');
|
|
164
|
+
fs.renameSync(tmpPath, outputPath);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Template HTML — loaded from file (modular) or inlined by esbuild (bundled)
|
|
168
|
+
let _cachedTemplate = null;
|
|
169
|
+
function loadTemplate() {
|
|
170
|
+
if (_cachedTemplate) return _cachedTemplate;
|
|
171
|
+
const templatePath = path.join(__dirname, 'public', 'index.html');
|
|
172
|
+
_cachedTemplate = fs.readFileSync(templatePath, 'utf-8');
|
|
173
|
+
return _cachedTemplate;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Run one generation cycle.
|
|
178
|
+
*/
|
|
179
|
+
async function generate(projectPath) {
|
|
180
|
+
const resolvedPath = path.resolve(projectPath);
|
|
181
|
+
const templateHtml = loadTemplate();
|
|
182
|
+
|
|
183
|
+
const dashboardData = await assembleDashboardData(resolvedPath);
|
|
184
|
+
|
|
185
|
+
// Size warning (SC-007)
|
|
186
|
+
for (const feature of dashboardData.features) {
|
|
187
|
+
const featureJson = JSON.stringify(dashboardData.featureData[feature.id] || {});
|
|
188
|
+
if (featureJson.length > 500 * 1024) {
|
|
189
|
+
const sizeMB = (featureJson.length / (1024 * 1024)).toFixed(1);
|
|
190
|
+
process.stderr.write(`Warning: Feature ${feature.id}: large artifacts detected (${sizeMB} MB). Dashboard may load slowly.\n`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const html = buildHtml(templateHtml, dashboardData);
|
|
195
|
+
const outputPath = path.join(resolvedPath, '.specify', 'dashboard.html');
|
|
196
|
+
|
|
197
|
+
writeAtomic(outputPath, html);
|
|
198
|
+
const now = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
|
199
|
+
process.stdout.write(`[${now}] Generated dashboard.html (${(html.length / 1024).toFixed(0)} KB)\n`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Main CLI entry point.
|
|
204
|
+
*/
|
|
205
|
+
async function main() {
|
|
206
|
+
const args = process.argv.slice(2);
|
|
207
|
+
const watchFlag = args.includes('--watch');
|
|
208
|
+
const positionalArgs = args.filter(a => a !== '--watch');
|
|
209
|
+
|
|
210
|
+
if (positionalArgs.length === 0) {
|
|
211
|
+
process.stderr.write('Error: Project path is required. Usage: generate-dashboard.js <projectPath> [--watch]\n');
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const projectPath = path.resolve(positionalArgs[0]);
|
|
216
|
+
|
|
217
|
+
// Validate project directory exists (exit 1)
|
|
218
|
+
if (!fs.existsSync(projectPath) || !fs.statSync(projectPath).isDirectory()) {
|
|
219
|
+
process.stderr.write(`Error: Project directory not found: ${projectPath}. Verify the path is correct.\n`);
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Validate CONSTITUTION.md exists (exit 3)
|
|
224
|
+
const constitutionPath = path.join(projectPath, 'CONSTITUTION.md');
|
|
225
|
+
if (!fs.existsSync(constitutionPath)) {
|
|
226
|
+
process.stderr.write('Error: CONSTITUTION.md not found in project root. Create one using /iikit-00-constitution.\n');
|
|
227
|
+
process.exit(3);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Check write permissions (exit 4)
|
|
231
|
+
const specifyDir = path.join(projectPath, '.specify');
|
|
232
|
+
try {
|
|
233
|
+
fs.mkdirSync(specifyDir, { recursive: true });
|
|
234
|
+
// Test write by creating and removing a temp file
|
|
235
|
+
const testFile = path.join(specifyDir, '.write-test-' + process.pid);
|
|
236
|
+
fs.writeFileSync(testFile, '');
|
|
237
|
+
fs.unlinkSync(testFile);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
process.stderr.write(`Error: Permission denied writing to .specify/dashboard.html. Check directory permissions.\n`);
|
|
240
|
+
process.exit(4);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Run initial generation
|
|
244
|
+
try {
|
|
245
|
+
await generate(projectPath);
|
|
246
|
+
} catch (err) {
|
|
247
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
248
|
+
process.exit(5);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Watch mode (FR-008)
|
|
252
|
+
if (watchFlag) {
|
|
253
|
+
let chokidar;
|
|
254
|
+
try {
|
|
255
|
+
chokidar = require('chokidar');
|
|
256
|
+
} catch {
|
|
257
|
+
process.stderr.write('Error: chokidar is required for --watch mode. Install it: npm install chokidar\n');
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const watchGlobs = [
|
|
262
|
+
path.join(projectPath, 'specs', '**', '*.md'),
|
|
263
|
+
path.join(projectPath, 'CONSTITUTION.md'),
|
|
264
|
+
path.join(projectPath, 'PREMISE.md')
|
|
265
|
+
];
|
|
266
|
+
|
|
267
|
+
const watcher = chokidar.watch(watchGlobs, {
|
|
268
|
+
ignoreInitial: true,
|
|
269
|
+
ignored: [
|
|
270
|
+
'**/node_modules/**',
|
|
271
|
+
'**/.git/**',
|
|
272
|
+
'**/dist/**',
|
|
273
|
+
path.join(projectPath, '.specify', 'dashboard.html'),
|
|
274
|
+
/.*cache.*/,
|
|
275
|
+
/.*\.tmp$/
|
|
276
|
+
],
|
|
277
|
+
awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 }
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
let debounceTimer = null;
|
|
281
|
+
watcher.on('all', () => {
|
|
282
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
283
|
+
debounceTimer = setTimeout(async () => {
|
|
284
|
+
try {
|
|
285
|
+
await generate(projectPath);
|
|
286
|
+
} catch (err) {
|
|
287
|
+
process.stderr.write(`Error during re-generation: ${err.message}\n`);
|
|
288
|
+
}
|
|
289
|
+
}, 300);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
process.stdout.write(`Watching for changes in ${projectPath}...\n`);
|
|
293
|
+
|
|
294
|
+
process.on('SIGINT', () => {
|
|
295
|
+
watcher.close();
|
|
296
|
+
process.exit(0);
|
|
297
|
+
});
|
|
298
|
+
process.on('SIGTERM', () => {
|
|
299
|
+
watcher.close();
|
|
300
|
+
process.exit(0);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (require.main === module) {
|
|
306
|
+
main();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
module.exports = { generate, assembleDashboardData, buildHtml, listFeatures, getBoardState };
|
package/src/parser.js
CHANGED
|
@@ -241,6 +241,23 @@ function hasClarifications(specContent) {
|
|
|
241
241
|
return /^## Clarifications/m.test(specContent);
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Parse PREMISE.md to return its raw markdown content.
|
|
246
|
+
*
|
|
247
|
+
* @param {string} projectPath - Path to the project root
|
|
248
|
+
* @returns {{content: string|null, exists: boolean}}
|
|
249
|
+
*/
|
|
250
|
+
function parsePremise(projectPath) {
|
|
251
|
+
const premisePath = path.join(projectPath, 'PREMISE.md');
|
|
252
|
+
|
|
253
|
+
if (!fs.existsSync(premisePath)) {
|
|
254
|
+
return { content: null, exists: false };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const content = fs.readFileSync(premisePath, 'utf-8');
|
|
258
|
+
return { content, exists: true };
|
|
259
|
+
}
|
|
260
|
+
|
|
244
261
|
/**
|
|
245
262
|
* Parse CONSTITUTION.md to extract principles with full details and version metadata.
|
|
246
263
|
*
|
|
@@ -1258,4 +1275,4 @@ function extractField(section, fieldName) {
|
|
|
1258
1275
|
return value;
|
|
1259
1276
|
}
|
|
1260
1277
|
|
|
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 };
|
|
1278
|
+
module.exports = { parseSpecStories, parseTasks, parseChecklists, parseChecklistsDetailed, parseConstitutionTDD, hasClarifications, parseConstitutionPrinciples, parsePremise, parseRequirements, parseSuccessCriteria, parseClarifications, parseStoryRequirementRefs, parseTechContext, parseFileStructure, parseAsciiDiagram, parseTesslJson, parseResearchDecisions, parseTestSpecs, parseTaskTestRefs, parseAnalysisFindings, parseAnalysisCoverage, parseAnalysisMetrics, parseConstitutionAlignment, parsePhaseSeparation, parseBugs };
|
package/src/pipeline.js
CHANGED
|
@@ -27,6 +27,7 @@ function computePipelineState(projectPath, featureId) {
|
|
|
27
27
|
const tasksExists = fs.existsSync(tasksPath);
|
|
28
28
|
const testSpecsExists = fs.existsSync(testSpecsPath);
|
|
29
29
|
const constitutionExists = fs.existsSync(constitutionPath);
|
|
30
|
+
const premiseExists = fs.existsSync(path.join(projectPath, 'PREMISE.md'));
|
|
30
31
|
const analysisExists = fs.existsSync(analysisPath);
|
|
31
32
|
|
|
32
33
|
// Read spec content for clarifications check
|
|
@@ -47,7 +48,7 @@ function computePipelineState(projectPath, featureId) {
|
|
|
47
48
|
const phases = [
|
|
48
49
|
{
|
|
49
50
|
id: 'constitution',
|
|
50
|
-
name: 'Constitution',
|
|
51
|
+
name: premiseExists ? 'Premise &\nConstitution' : 'Constitution',
|
|
51
52
|
status: constitutionExists ? 'complete' : 'not_started',
|
|
52
53
|
progress: null,
|
|
53
54
|
optional: false
|