Closing the intent-to-code chasm - specification-driven development with BDD verification chain
Overall
score
96%
Does it follow best practices?
Validation for skill structure
#!/usr/bin/env node
'use strict';
const path = require('path');
const fs = require('fs');
const { parseSpecStories, parseTasks, parseConstitutionPrinciples, parsePremise } = require('./parser');
const { computeBoardState } = require('./board');
const { computeAssertionHash, checkIntegrity } = require('./integrity');
const { computePipelineState } = require('./pipeline');
const { computeStoryMapState } = require('./storymap');
const { computePlanViewState } = require('./planview');
const { computeChecklistViewState } = require('./checklist');
const { computeTestifyState, getFeatureFiles } = require('./testify');
const { computeAnalyzeState } = require('./analyze');
const { computeBugsState } = require('./bugs');
/**
* List features from specs/ directory.
* A feature is a directory under specs/ that contains spec.md (FR-004).
*/
function listFeatures(projectPath) {
const specsDir = path.join(projectPath, 'specs');
if (!fs.existsSync(specsDir)) return [];
const entries = fs.readdirSync(specsDir, { withFileTypes: true });
const features = [];
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const featureDir = path.join(specsDir, entry.name);
const specPath = path.join(featureDir, 'spec.md');
if (!fs.existsSync(specPath)) continue;
const tasksPath = path.join(featureDir, 'tasks.md');
const specContent = fs.readFileSync(specPath, 'utf-8');
const tasksContent = fs.existsSync(tasksPath) ? fs.readFileSync(tasksPath, 'utf-8') : '';
const stories = parseSpecStories(specContent);
const tasks = parseTasks(tasksContent);
const checkedCount = tasks.filter(t => t.checked).length;
const totalCount = tasks.length;
const namePart = entry.name.replace(/^\d+-/, '');
const name = namePart.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
features.push({
id: entry.name,
name,
stories: stories.length,
progress: `${checkedCount}/${totalCount}`
});
}
return features.reverse();
}
/**
* Compute board state with integrity for a feature.
*/
function getBoardState(projectPath, featureId) {
const featureDir = path.join(projectPath, 'specs', featureId);
const specPath = path.join(featureDir, 'spec.md');
const tasksPath = path.join(featureDir, 'tasks.md');
const contextPath = path.join(featureDir, 'context.json');
const specContent = fs.existsSync(specPath) ? fs.readFileSync(specPath, 'utf-8') : '';
const tasksContent = fs.existsSync(tasksPath) ? fs.readFileSync(tasksPath, 'utf-8') : '';
const stories = parseSpecStories(specContent);
const tasks = parseTasks(tasksContent);
const board = computeBoardState(stories, tasks);
let integrity = { status: 'missing', currentHash: null, storedHash: null };
const featureFiles = getFeatureFiles(featureDir);
if (featureFiles.length > 0) {
const allFeatureContent = featureFiles.map(f => fs.readFileSync(f, 'utf-8')).join('\n');
const currentHash = computeAssertionHash(allFeatureContent);
let storedHash = null;
if (fs.existsSync(contextPath)) {
try {
const context = JSON.parse(fs.readFileSync(contextPath, 'utf-8'));
storedHash = context?.testify?.assertion_hash || null;
} catch {
// malformed context.json
}
}
integrity = checkIntegrity(currentHash, storedHash);
}
return { ...board, integrity };
}
/**
* Assemble DASHBOARD_DATA for all features.
*/
async function assembleDashboardData(projectPath) {
const resolvedPath = path.resolve(projectPath);
const features = listFeatures(resolvedPath);
const constitution = parseConstitutionPrinciples(resolvedPath);
const premise = parsePremise(resolvedPath);
const featureData = {};
for (const feature of features) {
const fid = feature.id;
try {
featureData[fid] = {
board: getBoardState(resolvedPath, fid),
pipeline: computePipelineState(resolvedPath, fid),
storyMap: computeStoryMapState(resolvedPath, fid),
planView: await computePlanViewState(resolvedPath, fid),
checklist: computeChecklistViewState(resolvedPath, fid),
testify: computeTestifyState(resolvedPath, fid),
analyze: computeAnalyzeState(resolvedPath, fid),
bugs: computeBugsState(resolvedPath, fid)
};
} catch (err) {
process.stderr.write(`Error: Parser failed on specs/${fid}/spec.md: ${err.message}. Check artifact syntax.\n`);
process.exit(5);
}
}
return {
meta: {
projectPath: resolvedPath,
generatedAt: new Date().toISOString()
},
features,
constitution,
premise,
featureData
};
}
/**
* Inject data into HTML template and return the complete HTML string.
*/
function buildHtml(templateHtml, dashboardData) {
let html = templateHtml;
// Inject DASHBOARD_DATA into <head> (FR-004, FR-005)
// DASHBOARD_DATA must be in <head> so it's available before the IIFE in <body> runs
// Escape </script> to prevent script-tag injection from data file content (SC-008)
const safeJson = JSON.stringify(dashboardData).replace(/<\/script>/gi, '<\\/script>');
const headInject = ` <script>window.DASHBOARD_DATA = ${safeJson};</script>\n`;
html = html.replace('</head>', headInject + '</head>');
// No auto-reload — it destroys user interaction (expanded cards, scroll position).
// Dashboard is regenerated by generate-dashboard-safe.sh after each skill invocation.
// User refreshes manually (F5) or clicks the refresh button in the header.
return html;
}
/**
* Write HTML atomically: write to .tmp then rename (FR-011).
*/
function writeAtomic(outputPath, content) {
const dir = path.dirname(outputPath);
fs.mkdirSync(dir, { recursive: true });
const tmpPath = outputPath + '.tmp';
fs.writeFileSync(tmpPath, content, 'utf-8');
fs.renameSync(tmpPath, outputPath);
}
// Template HTML — loaded from template.js (published) or public/index.html (dev)
let _cachedTemplate = null;
function loadTemplate() {
if (_cachedTemplate) return _cachedTemplate;
// Try template.js first (published tiles)
const templateJs = path.join(__dirname, '..', 'template.js');
if (fs.existsSync(templateJs)) {
_cachedTemplate = require(templateJs);
return _cachedTemplate;
}
// Fall back to public/index.html (dev layout)
const templatePath = path.join(__dirname, 'public', 'index.html');
if (fs.existsSync(templatePath)) {
_cachedTemplate = fs.readFileSync(templatePath, 'utf-8');
return _cachedTemplate;
}
throw new Error('Dashboard template not found. Checked: ' + templateJs + ', ' + templatePath);
}
/**
* Run one generation cycle.
*/
async function generate(projectPath) {
const resolvedPath = path.resolve(projectPath);
const templateHtml = loadTemplate();
const dashboardData = await assembleDashboardData(resolvedPath);
// Size warning (SC-007)
for (const feature of dashboardData.features) {
const featureJson = JSON.stringify(dashboardData.featureData[feature.id] || {});
if (featureJson.length > 500 * 1024) {
const sizeMB = (featureJson.length / (1024 * 1024)).toFixed(1);
process.stderr.write(`Warning: Feature ${feature.id}: large artifacts detected (${sizeMB} MB). Dashboard may load slowly.\n`);
}
}
const html = buildHtml(templateHtml, dashboardData);
const outputPath = path.join(resolvedPath, '.specify', 'dashboard.html');
writeAtomic(outputPath, html);
const now = new Date().toISOString().slice(0, 19).replace('T', ' ');
process.stdout.write(`[${now}] Generated dashboard.html (${(html.length / 1024).toFixed(0)} KB)\n`);
}
/**
* Main CLI entry point.
*/
async function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
process.stderr.write('Error: Project path is required. Usage: generate-dashboard.js <projectPath>\n');
process.exit(1);
}
const projectPath = path.resolve(args[0]);
// Validate project directory exists (exit 1)
if (!fs.existsSync(projectPath) || !fs.statSync(projectPath).isDirectory()) {
process.stderr.write(`Error: Project directory not found: ${projectPath}. Verify the path is correct.\n`);
process.exit(1);
}
// Validate CONSTITUTION.md exists (exit 3)
const constitutionPath = path.join(projectPath, 'CONSTITUTION.md');
if (!fs.existsSync(constitutionPath)) {
process.stderr.write('Error: CONSTITUTION.md not found in project root. Create one using /iikit-00-constitution.\n');
process.exit(3);
}
// Check write permissions (exit 4)
const specifyDir = path.join(projectPath, '.specify');
try {
fs.mkdirSync(specifyDir, { recursive: true });
// Test write by creating and removing a temp file
const testFile = path.join(specifyDir, '.write-test-' + process.pid);
fs.writeFileSync(testFile, '');
fs.unlinkSync(testFile);
} catch (err) {
process.stderr.write(`Error: Permission denied writing to .specify/dashboard.html. Check directory permissions.\n`);
process.exit(4);
}
// Run generation
try {
await generate(projectPath);
} catch (err) {
process.stderr.write(`Error: ${err.message}\n`);
process.exit(5);
}
}
if (require.main === module) {
main();
}
module.exports = { generate, assembleDashboardData, buildHtml, listFeatures, getBoardState };Install with Tessl CLI
npx tessl i tessl-labs/intent-integrity-kit@2.3.5rules
skills
iikit-00-constitution
scripts
iikit-01-specify
iikit-02-clarify
iikit-03-plan
iikit-04-checklist
scripts
dashboard
iikit-05-testify
iikit-06-tasks
iikit-07-analyze
iikit-08-implement
iikit-09-taskstoissues
iikit-bugfix
scripts
iikit-core
scripts
bash
dashboard
powershell