iikit-dashboard 1.1.0 → 1.3.1
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/package.json +2 -2
- package/src/analyze.js +244 -0
- package/src/parser.js +325 -1
- package/src/planview.js +116 -3
- package/src/public/index.html +1271 -5
- package/src/server.js +46 -0
- package/src/testify.js +178 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iikit-dashboard",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.3.1",
|
|
4
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": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
],
|
|
27
27
|
"repository": {
|
|
28
28
|
"type": "git",
|
|
29
|
-
"url": "https://github.com/intent-integrity-chain/iikit-dashboard"
|
|
29
|
+
"url": "git+https://github.com/intent-integrity-chain/iikit-dashboard.git"
|
|
30
30
|
},
|
|
31
31
|
"author": "Baruch Sadogursky",
|
|
32
32
|
"license": "MIT",
|
package/src/analyze.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const {
|
|
6
|
+
parseAnalysisFindings,
|
|
7
|
+
parseAnalysisCoverage,
|
|
8
|
+
parseAnalysisMetrics,
|
|
9
|
+
parseConstitutionAlignment,
|
|
10
|
+
parsePhaseSeparation,
|
|
11
|
+
parseRequirements,
|
|
12
|
+
parseSuccessCriteria
|
|
13
|
+
} = require('./parser');
|
|
14
|
+
|
|
15
|
+
const SEVERITY_PENALTIES = { CRITICAL: 25, HIGH: 15, MEDIUM: 5, LOW: 2 };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Compute phase separation score from violation entries.
|
|
19
|
+
* Score = max(0, 100 - sum(penalties)).
|
|
20
|
+
*
|
|
21
|
+
* @param {Array<{severity: string}>} violations
|
|
22
|
+
* @returns {number}
|
|
23
|
+
*/
|
|
24
|
+
function computePhaseSeparationScore(violations) {
|
|
25
|
+
if (!violations || violations.length === 0) return 100;
|
|
26
|
+
|
|
27
|
+
const penalty = violations.reduce((sum, v) => {
|
|
28
|
+
return sum + (SEVERITY_PENALTIES[v.severity] || 0);
|
|
29
|
+
}, 0);
|
|
30
|
+
|
|
31
|
+
return Math.max(0, 100 - penalty);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Compute constitution compliance percentage from alignment entries.
|
|
36
|
+
* (ALIGNED count / total) * 100, rounded.
|
|
37
|
+
*
|
|
38
|
+
* @param {Array<{status: string}>} entries
|
|
39
|
+
* @returns {number}
|
|
40
|
+
*/
|
|
41
|
+
function computeConstitutionCompliance(entries) {
|
|
42
|
+
if (!entries || entries.length === 0) return 100;
|
|
43
|
+
|
|
44
|
+
const aligned = entries.filter(e => e.status === 'ALIGNED').length;
|
|
45
|
+
return Math.round((aligned / entries.length) * 100);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Compute health score from four factors (25% each).
|
|
50
|
+
*
|
|
51
|
+
* @param {{requirementsCoverage: number, constitutionCompliance: number, phaseSeparation: number, testCoverage: number}} factors
|
|
52
|
+
* @returns {{score: number, zone: string, factors: Object}}
|
|
53
|
+
*/
|
|
54
|
+
function computeHealthScore(factors) {
|
|
55
|
+
const { requirementsCoverage, constitutionCompliance, phaseSeparation, testCoverage } = factors;
|
|
56
|
+
|
|
57
|
+
const score = Math.round(
|
|
58
|
+
(requirementsCoverage + constitutionCompliance + phaseSeparation + testCoverage) / 4
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
let zone;
|
|
62
|
+
if (score <= 40) zone = 'red';
|
|
63
|
+
else if (score <= 70) zone = 'yellow';
|
|
64
|
+
else zone = 'green';
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
score,
|
|
68
|
+
zone,
|
|
69
|
+
factors: {
|
|
70
|
+
requirementsCoverage: { value: requirementsCoverage, label: 'Requirements Coverage' },
|
|
71
|
+
constitutionCompliance: { value: constitutionCompliance, label: 'Constitution Compliance' },
|
|
72
|
+
phaseSeparation: { value: phaseSeparation, label: 'Phase Separation' },
|
|
73
|
+
testCoverage: { value: testCoverage, label: 'Test Coverage' }
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Map a coverage entry to a cell status object.
|
|
80
|
+
*
|
|
81
|
+
* @param {boolean} hasArtifact
|
|
82
|
+
* @param {string[]} ids
|
|
83
|
+
* @param {string|null} statusStr - e.g., "Partial", "Full"
|
|
84
|
+
* @returns {{status: string, refs: string[]}}
|
|
85
|
+
*/
|
|
86
|
+
function mapCellStatus(hasArtifact, ids, statusStr) {
|
|
87
|
+
if (statusStr && /partial/i.test(statusStr)) {
|
|
88
|
+
return { status: 'partial', refs: ids || [] };
|
|
89
|
+
}
|
|
90
|
+
if (hasArtifact && ids && ids.length > 0) {
|
|
91
|
+
return { status: 'covered', refs: ids };
|
|
92
|
+
}
|
|
93
|
+
if (!hasArtifact || !ids || ids.length === 0) {
|
|
94
|
+
return { status: 'missing', refs: [] };
|
|
95
|
+
}
|
|
96
|
+
return { status: 'covered', refs: ids };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Build heatmap rows from requirements and coverage data.
|
|
101
|
+
*
|
|
102
|
+
* @param {Array<{id: string, text: string}>} requirements
|
|
103
|
+
* @param {Array} coverageEntries
|
|
104
|
+
* @returns {Array<{id: string, text: string, cells: Object}>}
|
|
105
|
+
*/
|
|
106
|
+
function buildHeatmapRows(requirements, coverageEntries) {
|
|
107
|
+
if (!requirements || requirements.length === 0) return [];
|
|
108
|
+
|
|
109
|
+
const coverageMap = {};
|
|
110
|
+
for (const entry of (coverageEntries || [])) {
|
|
111
|
+
coverageMap[entry.id] = entry;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return requirements.map(req => {
|
|
115
|
+
const coverage = coverageMap[req.id];
|
|
116
|
+
if (!coverage) {
|
|
117
|
+
return {
|
|
118
|
+
id: req.id,
|
|
119
|
+
text: req.text,
|
|
120
|
+
cells: {
|
|
121
|
+
tasks: { status: 'missing', refs: [] },
|
|
122
|
+
tests: { status: 'missing', refs: [] },
|
|
123
|
+
plan: { status: 'na', refs: [] }
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
id: req.id,
|
|
130
|
+
text: req.text,
|
|
131
|
+
cells: {
|
|
132
|
+
tasks: mapCellStatus(coverage.hasTask, coverage.taskIds, coverage.status === 'Partial' && !coverage.hasTask ? 'Partial' : null),
|
|
133
|
+
tests: mapCellStatus(coverage.hasTest, coverage.testIds, null),
|
|
134
|
+
plan: coverage.hasPlan !== undefined
|
|
135
|
+
? mapCellStatus(coverage.hasPlan, coverage.planRefs, null)
|
|
136
|
+
: { status: 'na', refs: [] }
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Compute the full analyze view state for a feature.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} projectPath
|
|
146
|
+
* @param {string} featureId
|
|
147
|
+
* @returns {Object}
|
|
148
|
+
*/
|
|
149
|
+
function computeAnalyzeState(projectPath, featureId) {
|
|
150
|
+
const featureDir = path.join(projectPath, 'specs', featureId);
|
|
151
|
+
const analysisPath = path.join(featureDir, 'analysis.md');
|
|
152
|
+
const specPath = path.join(featureDir, 'spec.md');
|
|
153
|
+
|
|
154
|
+
if (!fs.existsSync(analysisPath)) {
|
|
155
|
+
return {
|
|
156
|
+
healthScore: null,
|
|
157
|
+
heatmap: { columns: [], rows: [] },
|
|
158
|
+
issues: [],
|
|
159
|
+
metrics: null,
|
|
160
|
+
constitutionAlignment: [],
|
|
161
|
+
exists: false
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const analysisContent = fs.readFileSync(analysisPath, 'utf-8');
|
|
166
|
+
const specContent = fs.existsSync(specPath) ? fs.readFileSync(specPath, 'utf-8') : '';
|
|
167
|
+
|
|
168
|
+
// Parse all sections
|
|
169
|
+
const findings = parseAnalysisFindings(analysisContent);
|
|
170
|
+
const coverage = parseAnalysisCoverage(analysisContent);
|
|
171
|
+
const metrics = parseAnalysisMetrics(analysisContent);
|
|
172
|
+
const constitutionAlignment = parseConstitutionAlignment(analysisContent);
|
|
173
|
+
const phaseSeparationViolations = parsePhaseSeparation(analysisContent);
|
|
174
|
+
|
|
175
|
+
// Build heatmap from spec requirements + coverage data
|
|
176
|
+
const requirements = [
|
|
177
|
+
...parseRequirements(specContent),
|
|
178
|
+
...parseSuccessCriteria(specContent)
|
|
179
|
+
];
|
|
180
|
+
const heatmapRows = buildHeatmapRows(requirements, coverage);
|
|
181
|
+
|
|
182
|
+
// Compute health score factors
|
|
183
|
+
const reqCovPct = metrics.requirementCoveragePct || 0;
|
|
184
|
+
const testCovPct = metrics.testCoveragePct || 100;
|
|
185
|
+
const constitutionCompliancePct = computeConstitutionCompliance(constitutionAlignment);
|
|
186
|
+
const phaseSepScore = computePhaseSeparationScore(
|
|
187
|
+
phaseSeparationViolations.filter(v => v.severity)
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const healthScore = computeHealthScore({
|
|
191
|
+
requirementsCoverage: reqCovPct,
|
|
192
|
+
constitutionCompliance: constitutionCompliancePct,
|
|
193
|
+
phaseSeparation: phaseSepScore,
|
|
194
|
+
testCoverage: testCovPct
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Map findings to issues (API terminology)
|
|
198
|
+
const issues = findings.map(f => ({
|
|
199
|
+
id: f.id,
|
|
200
|
+
category: f.category,
|
|
201
|
+
severity: f.severity.toLowerCase(),
|
|
202
|
+
location: f.location,
|
|
203
|
+
summary: f.summary,
|
|
204
|
+
recommendation: f.recommendation,
|
|
205
|
+
resolved: f.resolved
|
|
206
|
+
}));
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
healthScore: {
|
|
210
|
+
...healthScore,
|
|
211
|
+
trend: null
|
|
212
|
+
},
|
|
213
|
+
heatmap: {
|
|
214
|
+
columns: ['tasks', 'tests', 'plan'],
|
|
215
|
+
rows: heatmapRows
|
|
216
|
+
},
|
|
217
|
+
issues,
|
|
218
|
+
metrics: {
|
|
219
|
+
totalRequirements: metrics.totalRequirements,
|
|
220
|
+
totalTasks: metrics.totalTasks,
|
|
221
|
+
totalTestSpecs: metrics.totalTestSpecs,
|
|
222
|
+
requirementCoverage: metrics.requirementCoverage,
|
|
223
|
+
criticalIssues: metrics.criticalIssues,
|
|
224
|
+
highIssues: metrics.highIssues,
|
|
225
|
+
mediumIssues: metrics.mediumIssues,
|
|
226
|
+
lowIssues: metrics.lowIssues
|
|
227
|
+
},
|
|
228
|
+
constitutionAlignment: constitutionAlignment.map(a => ({
|
|
229
|
+
principle: a.principle,
|
|
230
|
+
status: a.status,
|
|
231
|
+
evidence: a.evidence
|
|
232
|
+
})),
|
|
233
|
+
exists: true
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = {
|
|
238
|
+
computeHealthScore,
|
|
239
|
+
computeAnalyzeState,
|
|
240
|
+
buildHeatmapRows,
|
|
241
|
+
mapCellStatus,
|
|
242
|
+
computePhaseSeparationScore,
|
|
243
|
+
computeConstitutionCompliance
|
|
244
|
+
};
|
package/src/parser.js
CHANGED
|
@@ -855,4 +855,328 @@ function parseResearchDecisions(content) {
|
|
|
855
855
|
return decisions;
|
|
856
856
|
}
|
|
857
857
|
|
|
858
|
-
|
|
858
|
+
/**
|
|
859
|
+
* Parse tests/test-specs.md to extract test specification entries.
|
|
860
|
+
* Pattern: ### TS-XXX: Title, then **Type**: value, **Priority**: value, **Traceability**: refs
|
|
861
|
+
*
|
|
862
|
+
* @param {string} content - Raw markdown content of test-specs.md
|
|
863
|
+
* @returns {Array<{id: string, title: string, type: string, priority: string, traceability: string[]}>}
|
|
864
|
+
*/
|
|
865
|
+
function parseTestSpecs(content) {
|
|
866
|
+
if (!content || typeof content !== 'string') return [];
|
|
867
|
+
|
|
868
|
+
const specs = [];
|
|
869
|
+
const headingRegex = /### TS-(\d+): (.+)/g;
|
|
870
|
+
const headingStarts = [];
|
|
871
|
+
let match;
|
|
872
|
+
|
|
873
|
+
while ((match = headingRegex.exec(content)) !== null) {
|
|
874
|
+
headingStarts.push({
|
|
875
|
+
id: `TS-${match[1]}`,
|
|
876
|
+
title: match[2].trim(),
|
|
877
|
+
index: match.index
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
for (let i = 0; i < headingStarts.length; i++) {
|
|
882
|
+
const start = headingStarts[i].index;
|
|
883
|
+
const end = i + 1 < headingStarts.length ? headingStarts[i + 1].index : content.length;
|
|
884
|
+
const section = content.substring(start, end);
|
|
885
|
+
|
|
886
|
+
// Extract type
|
|
887
|
+
const typeMatch = section.match(/\*\*Type\*\*:\s*(acceptance|contract|validation)/);
|
|
888
|
+
const type = typeMatch ? typeMatch[1] : 'validation';
|
|
889
|
+
|
|
890
|
+
// Extract priority
|
|
891
|
+
const priorityMatch = section.match(/\*\*Priority\*\*:\s*(P\d+)/);
|
|
892
|
+
const priority = priorityMatch ? priorityMatch[1] : 'P3';
|
|
893
|
+
|
|
894
|
+
// Extract traceability — comma-separated IDs, filter to FR-/SC- only
|
|
895
|
+
let traceability = [];
|
|
896
|
+
const traceMatch = section.match(/\*\*Traceability\*\*:\s*(.+)/);
|
|
897
|
+
if (traceMatch) {
|
|
898
|
+
traceability = traceMatch[1]
|
|
899
|
+
.split(/,\s*/)
|
|
900
|
+
.map(s => s.trim())
|
|
901
|
+
.filter(s => /^(FR|SC)-\d+$/.test(s));
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
specs.push({
|
|
905
|
+
id: headingStarts[i].id,
|
|
906
|
+
title: headingStarts[i].title,
|
|
907
|
+
type,
|
|
908
|
+
priority,
|
|
909
|
+
traceability
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
return specs;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Extract "must pass TS-xxx" references from already-parsed task descriptions.
|
|
918
|
+
*
|
|
919
|
+
* @param {Array<{id: string, description: string}>} tasks - Parsed tasks array
|
|
920
|
+
* @returns {Object<string, string[]>} Map of taskId to testSpecIds array
|
|
921
|
+
*/
|
|
922
|
+
function parseTaskTestRefs(tasks) {
|
|
923
|
+
if (!tasks || !Array.isArray(tasks)) return {};
|
|
924
|
+
|
|
925
|
+
const refs = {};
|
|
926
|
+
for (const task of tasks) {
|
|
927
|
+
const matches = task.description ? task.description.match(/TS-\d+/g) : null;
|
|
928
|
+
refs[task.id] = matches ? [...new Set(matches)] : [];
|
|
929
|
+
}
|
|
930
|
+
return refs;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Extract a markdown section by heading (## Title), returning content until next ## heading.
|
|
935
|
+
*/
|
|
936
|
+
function extractSection(content, heading) {
|
|
937
|
+
const regex = new RegExp(`^## ${heading}\\s*$`, 'm');
|
|
938
|
+
const match = content.match(regex);
|
|
939
|
+
if (!match) return null;
|
|
940
|
+
|
|
941
|
+
const start = match.index + match[0].length;
|
|
942
|
+
const nextSection = content.indexOf('\n## ', start);
|
|
943
|
+
return content.substring(start, nextSection >= 0 ? nextSection : content.length).trim();
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Parse rows from a pipe-delimited markdown table.
|
|
948
|
+
* Returns array of arrays (one per row, cells trimmed). Skips header separator row (|---|).
|
|
949
|
+
*/
|
|
950
|
+
function parseMarkdownTable(text) {
|
|
951
|
+
const lines = text.split('\n').filter(l => l.trim().startsWith('|'));
|
|
952
|
+
if (lines.length < 2) return [];
|
|
953
|
+
|
|
954
|
+
// Skip header row (index 0) and separator row (index 1)
|
|
955
|
+
return lines.slice(2).map(line =>
|
|
956
|
+
line.split('|').slice(1, -1).map(cell => cell.trim())
|
|
957
|
+
).filter(cells => cells.length > 0 && cells.some(c => c !== ''));
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Parse analysis.md Findings section.
|
|
962
|
+
* Extracts issues with id, category, severity, resolved, location, summary, recommendation.
|
|
963
|
+
*
|
|
964
|
+
* @param {string} content - Raw analysis.md content
|
|
965
|
+
* @returns {Array<{id: string, category: string, severity: string, resolved: boolean, location: string, summary: string, recommendation: string}>}
|
|
966
|
+
*/
|
|
967
|
+
function parseAnalysisFindings(content) {
|
|
968
|
+
if (!content || typeof content !== 'string') return [];
|
|
969
|
+
|
|
970
|
+
const section = extractSection(content, 'Findings');
|
|
971
|
+
if (!section) return [];
|
|
972
|
+
|
|
973
|
+
const rows = parseMarkdownTable(section);
|
|
974
|
+
if (rows.length === 0) return [];
|
|
975
|
+
|
|
976
|
+
return rows.map(cells => {
|
|
977
|
+
if (cells.length < 6) return null;
|
|
978
|
+
|
|
979
|
+
const rawSeverity = cells[2];
|
|
980
|
+
// Detect ~~SEVERITY~~ RESOLVED pattern
|
|
981
|
+
const resolvedMatch = rawSeverity.match(/~~(\w+)~~\s*RESOLVED/);
|
|
982
|
+
const resolved = !!resolvedMatch;
|
|
983
|
+
const severity = resolved ? resolvedMatch[1] : rawSeverity;
|
|
984
|
+
|
|
985
|
+
return {
|
|
986
|
+
id: cells[0],
|
|
987
|
+
category: cells[1],
|
|
988
|
+
severity,
|
|
989
|
+
resolved,
|
|
990
|
+
location: cells[3],
|
|
991
|
+
summary: cells[4],
|
|
992
|
+
recommendation: cells[5]
|
|
993
|
+
};
|
|
994
|
+
}).filter(Boolean);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Parse analysis.md Coverage Summary section.
|
|
999
|
+
* Handles both simple (Requirement, Has Task?, Notes) and
|
|
1000
|
+
* detailed (Requirement, Has Task?, Task IDs, Has Test?, Test IDs, Status) formats.
|
|
1001
|
+
*
|
|
1002
|
+
* @param {string} content - Raw analysis.md content
|
|
1003
|
+
* @returns {Array<{id: string, hasTask: boolean, taskIds: string[], hasTest: boolean, testIds: string[], status: string|null, notes: string}>}
|
|
1004
|
+
*/
|
|
1005
|
+
function parseAnalysisCoverage(content) {
|
|
1006
|
+
if (!content || typeof content !== 'string') return [];
|
|
1007
|
+
|
|
1008
|
+
const section = extractSection(content, 'Coverage Summary');
|
|
1009
|
+
if (!section) return [];
|
|
1010
|
+
|
|
1011
|
+
const rows = parseMarkdownTable(section);
|
|
1012
|
+
if (rows.length === 0) return [];
|
|
1013
|
+
|
|
1014
|
+
// Detect format by number of columns in first data row
|
|
1015
|
+
const hasPlanCols = rows[0].length >= 8;
|
|
1016
|
+
const isDetailed = rows[0].length >= 6;
|
|
1017
|
+
|
|
1018
|
+
return rows.map(cells => {
|
|
1019
|
+
const id = cells[0];
|
|
1020
|
+
const hasTask = /^yes$/i.test(cells[1]);
|
|
1021
|
+
|
|
1022
|
+
if (isDetailed) {
|
|
1023
|
+
// Detailed: Requirement | Has Task? | Task IDs | Has Test? | Test IDs | [Has Plan? | Plan Refs |] Status
|
|
1024
|
+
const taskIds = parseIdList(cells[2]);
|
|
1025
|
+
const hasTest = /^yes$/i.test(cells[3]);
|
|
1026
|
+
const testIds = parseIdList(cells[4]);
|
|
1027
|
+
|
|
1028
|
+
if (hasPlanCols) {
|
|
1029
|
+
const hasPlan = /^yes$/i.test(cells[5]);
|
|
1030
|
+
const planRefs = parseIdList(cells[6]);
|
|
1031
|
+
const status = cells[7] && cells[7] !== '—' && cells[7] !== '-' ? cells[7] : null;
|
|
1032
|
+
return { id, hasTask, taskIds, hasTest, testIds, hasPlan, planRefs, status, notes: '' };
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const status = cells[5] && cells[5] !== '—' && cells[5] !== '-' ? cells[5] : null;
|
|
1036
|
+
return { id, hasTask, taskIds, hasTest, testIds, status, notes: '' };
|
|
1037
|
+
} else {
|
|
1038
|
+
// Simple: Requirement | Has Task? | Notes
|
|
1039
|
+
const notes = cells[2] || '';
|
|
1040
|
+
return { id, hasTask, taskIds: [], hasTest: false, testIds: [], status: null, notes };
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Parse a comma-separated list of IDs (e.g., "T001, T002" or "TS-001, TS-037").
|
|
1047
|
+
* Filters out dashes and empty entries.
|
|
1048
|
+
*/
|
|
1049
|
+
function parseIdList(cell) {
|
|
1050
|
+
if (!cell || cell === '—' || cell === '-' || cell === '–') return [];
|
|
1051
|
+
return cell.split(',').map(s => s.trim()).filter(s => s && s !== '—' && s !== '-' && s !== '–');
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* Parse analysis.md Metrics section.
|
|
1056
|
+
* Handles both table format (| Metric | Value |) and bullet format (- Metric: Value).
|
|
1057
|
+
*
|
|
1058
|
+
* @param {string} content - Raw analysis.md content
|
|
1059
|
+
* @returns {{totalRequirements: number, totalTasks: number, totalTestSpecs: number, requirementCoverage: string, requirementCoveragePct: number, testCoverage: string|null, testCoveragePct: number, criticalIssues: number, highIssues: number, mediumIssues: number, lowIssues: number}}
|
|
1060
|
+
*/
|
|
1061
|
+
function parseAnalysisMetrics(content) {
|
|
1062
|
+
const defaults = {
|
|
1063
|
+
totalRequirements: 0, totalTasks: 0, totalTestSpecs: 0,
|
|
1064
|
+
requirementCoverage: '', requirementCoveragePct: 0,
|
|
1065
|
+
testCoverage: null, testCoveragePct: 100,
|
|
1066
|
+
criticalIssues: 0, highIssues: 0, mediumIssues: 0, lowIssues: 0
|
|
1067
|
+
};
|
|
1068
|
+
if (!content || typeof content !== 'string') return defaults;
|
|
1069
|
+
|
|
1070
|
+
const section = extractSection(content, 'Metrics');
|
|
1071
|
+
if (!section) return defaults;
|
|
1072
|
+
|
|
1073
|
+
// Build key-value map from either table or bullet format
|
|
1074
|
+
const kvMap = {};
|
|
1075
|
+
|
|
1076
|
+
// Try table format first
|
|
1077
|
+
const tableRows = parseMarkdownTable(section);
|
|
1078
|
+
if (tableRows.length > 0) {
|
|
1079
|
+
for (const cells of tableRows) {
|
|
1080
|
+
if (cells.length >= 2) kvMap[cells[0].toLowerCase()] = cells[1];
|
|
1081
|
+
}
|
|
1082
|
+
} else {
|
|
1083
|
+
// Try bullet format: - Key: Value
|
|
1084
|
+
const bulletRegex = /^-\s+(.+?):\s+(.+)$/gm;
|
|
1085
|
+
let match;
|
|
1086
|
+
while ((match = bulletRegex.exec(section)) !== null) {
|
|
1087
|
+
kvMap[match[1].trim().toLowerCase()] = match[2].trim();
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function findValue(keys) {
|
|
1092
|
+
for (const key of keys) {
|
|
1093
|
+
for (const [k, v] of Object.entries(kvMap)) {
|
|
1094
|
+
if (k.includes(key)) return v;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
return null;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function extractPct(raw) {
|
|
1101
|
+
if (!raw) return null;
|
|
1102
|
+
const pctMatch = raw.match(/(\d+)%/);
|
|
1103
|
+
if (pctMatch) return parseInt(pctMatch[1], 10);
|
|
1104
|
+
const fracMatch = raw.match(/\((\d+)%\)/);
|
|
1105
|
+
if (fracMatch) return parseInt(fracMatch[1], 10);
|
|
1106
|
+
return null;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
const reqCovRaw = findValue(['requirement coverage']);
|
|
1110
|
+
const testCovRaw = findValue(['test coverage']);
|
|
1111
|
+
|
|
1112
|
+
return {
|
|
1113
|
+
totalRequirements: parseInt(findValue(['total requirements']) || '0', 10),
|
|
1114
|
+
totalTasks: parseInt(findValue(['total tasks']) || '0', 10),
|
|
1115
|
+
totalTestSpecs: parseInt(findValue(['total test spec']) || '0', 10),
|
|
1116
|
+
requirementCoverage: reqCovRaw || '',
|
|
1117
|
+
requirementCoveragePct: extractPct(reqCovRaw) || 0,
|
|
1118
|
+
testCoverage: testCovRaw || null,
|
|
1119
|
+
testCoveragePct: testCovRaw ? (extractPct(testCovRaw) || 0) : 100,
|
|
1120
|
+
criticalIssues: parseInt(findValue(['critical']) || '0', 10),
|
|
1121
|
+
highIssues: parseInt(findValue(['high']) || '0', 10),
|
|
1122
|
+
mediumIssues: parseInt(findValue(['medium']) || '0', 10),
|
|
1123
|
+
lowIssues: parseInt(findValue(['low']) || '0', 10)
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/**
|
|
1128
|
+
* Parse analysis.md Constitution Alignment section.
|
|
1129
|
+
*
|
|
1130
|
+
* @param {string} content - Raw analysis.md content
|
|
1131
|
+
* @returns {Array<{principle: string, status: string, evidence: string}>}
|
|
1132
|
+
*/
|
|
1133
|
+
function parseConstitutionAlignment(content) {
|
|
1134
|
+
if (!content || typeof content !== 'string') return [];
|
|
1135
|
+
|
|
1136
|
+
const section = extractSection(content, 'Constitution Alignment');
|
|
1137
|
+
if (!section) return [];
|
|
1138
|
+
|
|
1139
|
+
// Check for "None detected" text
|
|
1140
|
+
if (/none detected/i.test(section) && !section.includes('|')) return [];
|
|
1141
|
+
|
|
1142
|
+
const rows = parseMarkdownTable(section);
|
|
1143
|
+
return rows.map(cells => {
|
|
1144
|
+
if (cells.length < 3) return null;
|
|
1145
|
+
return {
|
|
1146
|
+
principle: cells[0],
|
|
1147
|
+
status: cells[1],
|
|
1148
|
+
evidence: cells[2]
|
|
1149
|
+
};
|
|
1150
|
+
}).filter(Boolean);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/**
|
|
1154
|
+
* Parse analysis.md Phase Separation Violations section.
|
|
1155
|
+
*
|
|
1156
|
+
* @param {string} content - Raw analysis.md content
|
|
1157
|
+
* @returns {Array<{artifact: string, status: string, severity: string|null}>}
|
|
1158
|
+
*/
|
|
1159
|
+
function parsePhaseSeparation(content) {
|
|
1160
|
+
if (!content || typeof content !== 'string') return [];
|
|
1161
|
+
|
|
1162
|
+
const section = extractSection(content, 'Phase Separation Violations');
|
|
1163
|
+
if (!section) return [];
|
|
1164
|
+
|
|
1165
|
+
// Check for "None detected" before the table
|
|
1166
|
+
const noneIdx = section.search(/none detected/i);
|
|
1167
|
+
const tableIdx = section.indexOf('|');
|
|
1168
|
+
if (noneIdx >= 0 && (tableIdx < 0 || noneIdx < tableIdx)) return [];
|
|
1169
|
+
|
|
1170
|
+
const rows = parseMarkdownTable(section);
|
|
1171
|
+
return rows.map(cells => {
|
|
1172
|
+
if (cells.length < 2) return null;
|
|
1173
|
+
const severity = cells.length >= 3 && cells[2] && cells[2] !== '—' && cells[2] !== '-' && cells[2] !== '–' ? cells[2] : null;
|
|
1174
|
+
return {
|
|
1175
|
+
artifact: cells[0],
|
|
1176
|
+
status: cells[1],
|
|
1177
|
+
severity
|
|
1178
|
+
};
|
|
1179
|
+
}).filter(Boolean);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
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 };
|