Skip to content

Commit 2e38496

Browse files
committed
update tools for certification
1 parent 0e67796 commit 2e38496

11 files changed

Lines changed: 269 additions & 5 deletions

docs/idd/certification-guide.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,8 @@ certify:
187187
188188
- name: Verify traceability
189189
run: |
190-
# Verify every story has features, every feature has tests, etc.
191-
python tools/verify-traceability.py \
190+
# Validate certification thresholds
191+
node tools/validate-evidence.js \
192192
--evidence certification/${{ env.CAPABILITY }}/evidence.yaml
193193
194194
- name: Commit evidence
@@ -197,7 +197,7 @@ certify:
197197
git commit -m "cert: ${{ env.CAPABILITY }} evidence"
198198
```
199199

200-
The current `generate-evidence.js` tool is a scaffold generator, not full certification automation. It can derive capability-scoped links and seed the manifest from available reports, but some fields still require human review or a later validator pass.
200+
The current `generate-evidence.js` tool is a scaffold generator, not full certification automation. It can derive capability-scoped links and seed the manifest from available reports, but some fields still require human review or a later validator pass. Pair it with `validate-evidence.js` to enforce the current certification thresholds.
201201

202202
## Manual certification (for early adoption)
203203

@@ -206,8 +206,8 @@ If CI automation isn't set up yet, create the evidence manifest by hand:
206206
1. Run tests and collect results.
207207
2. Generate a starting manifest with `node tools/generate-evidence.js --capability specs/capabilities/{capability}.capability.yaml --write` or create `certification/{capability}/evidence.yaml` by hand using the template above.
208208
3. Fill in test counts from actual results.
209-
4. Verify traceability manually: check that every story has features, every feature has tests.
210-
5. Document gaps honestly.
209+
4. Validate it with `node tools/validate-evidence.js --evidence certification/{capability}/evidence.yaml`.
210+
5. Document gaps honestly. If human approval exists for explicit gaps, validate with `--allow-gaps`.
211211
6. Commit alongside the implementation.
212212

213213
The manifest format is the same whether generated by CI or written by hand. Automation removes human effort; it doesn't change the standard.

tools/check-capability-scope.js

100644100755
File mode changed.

tools/check-front-matter.js

100644100755
File mode changed.

tools/check-traceability.js

100644100755
File mode changed.

tools/generate-evidence.js

100644100755
File mode changed.

tools/package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tools/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"private": true,
55
"bin": {
66
"idd-generate-evidence": "./generate-evidence.js",
7+
"idd-validate-evidence": "./validate-evidence.js",
78
"idd-validate-front-matter": "./validate-front-matter.js",
89
"idd-validate-capability-scope": "./validate-capability-scope.js",
910
"idd-validate-traceability": "./validate-traceability.js",

tools/validate-capability-scope.js

100644100755
File mode changed.

tools/validate-evidence.js

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Validate a certification evidence manifest against IDD certification thresholds.
5+
*
6+
* Usage:
7+
* node tools/validate-evidence.js --evidence certification/foo/evidence.yaml
8+
*
9+
* Options:
10+
* --evidence <path> Required evidence manifest path
11+
* --allow-gaps Allow non-empty gaps list without failing
12+
* --json Output machine-readable results
13+
*
14+
* Exit: 0 = pass, 1 = errors found
15+
*/
16+
17+
const fs = require('fs');
18+
const path = require('path');
19+
const yaml = require('js-yaml');
20+
const { fileExists, formatResults } = require('./lib/parse-front-matter');
21+
22+
const args = process.argv.slice(2);
23+
let evidencePath = null;
24+
let allowGaps = false;
25+
let jsonOutput = false;
26+
27+
for (let i = 0; i < args.length; i += 1) {
28+
const arg = args[i];
29+
if (arg === '--evidence') {
30+
evidencePath = path.resolve(args[i + 1]);
31+
i += 1;
32+
} else if (arg === '--allow-gaps') {
33+
allowGaps = true;
34+
} else if (arg === '--json') {
35+
jsonOutput = true;
36+
}
37+
}
38+
39+
const results = {
40+
errors: [],
41+
warnings: [],
42+
info: [],
43+
};
44+
45+
function toProjectRelative(absolutePath) {
46+
return path.relative(process.cwd(), absolutePath).replace(/\\/g, '/');
47+
}
48+
49+
function parseYaml(filePath) {
50+
return yaml.load(fs.readFileSync(filePath, 'utf8'));
51+
}
52+
53+
function isObject(value) {
54+
return value && typeof value === 'object' && !Array.isArray(value);
55+
}
56+
57+
function parseRatio(value) {
58+
if (typeof value !== 'string') return null;
59+
const match = value.match(/^(\d+)\s*\/\s*(\d+)$/);
60+
if (!match) return null;
61+
return {
62+
covered: Number(match[1]),
63+
total: Number(match[2]),
64+
};
65+
}
66+
67+
function checkRequiredField(obj, fieldPath, label = fieldPath) {
68+
const value = fieldPath.split('.').reduce((current, key) => {
69+
if (!isObject(current)) return undefined;
70+
return current[key];
71+
}, obj);
72+
73+
if (value === undefined || value === null || value === '') {
74+
results.errors.push(`Missing required field: ${label}`);
75+
return undefined;
76+
}
77+
78+
return value;
79+
}
80+
81+
function checkReportPath(evidenceDir, sectionName, reportPath) {
82+
if (reportPath == null || reportPath === '') {
83+
results.warnings.push(`${sectionName}: No report path recorded`);
84+
return;
85+
}
86+
87+
const resolved = path.resolve(evidenceDir, reportPath);
88+
if (!fs.existsSync(resolved)) {
89+
results.errors.push(`${sectionName}: Report file not found: ${reportPath}`);
90+
}
91+
}
92+
93+
function checkFailedCount(sectionName, section) {
94+
if (!isObject(section)) {
95+
results.errors.push(`${sectionName}: Evidence section is missing or invalid`);
96+
return;
97+
}
98+
99+
if (section.failed == null) {
100+
results.warnings.push(`${sectionName}: Failed count is not recorded`);
101+
return;
102+
}
103+
104+
if (typeof section.failed !== 'number') {
105+
results.errors.push(`${sectionName}: Failed count must be numeric`);
106+
return;
107+
}
108+
109+
if (section.failed > 0) {
110+
results.errors.push(`${sectionName}: Failed count must be 0 (found ${section.failed})`);
111+
}
112+
}
113+
114+
function validateTraceabilitySection(traceability) {
115+
if (!isObject(traceability)) {
116+
results.errors.push('Missing required field: traceability');
117+
return;
118+
}
119+
120+
const ratioFields = [
121+
'stories_with_features',
122+
'features_with_contracts',
123+
'endpoints_with_tests',
124+
'journeys_with_e2e',
125+
];
126+
127+
for (const field of ratioFields) {
128+
const value = traceability[field];
129+
if (value == null || value === '') {
130+
results.errors.push(`traceability.${field}: Missing required value`);
131+
continue;
132+
}
133+
134+
const parsed = parseRatio(value);
135+
if (!parsed) {
136+
results.errors.push(`traceability.${field}: Expected X/Y ratio, found '${value}'`);
137+
continue;
138+
}
139+
140+
if (parsed.covered !== parsed.total) {
141+
results.errors.push(`traceability.${field}: Expected 100% coverage, found ${value}`);
142+
}
143+
}
144+
145+
const orphanFields = [
146+
'orphan_tests',
147+
'orphan_features',
148+
'orphan_endpoints',
149+
];
150+
151+
for (const field of orphanFields) {
152+
const value = traceability[field];
153+
if (value == null) {
154+
results.errors.push(`traceability.${field}: Missing required value`);
155+
continue;
156+
}
157+
if (typeof value !== 'number') {
158+
results.errors.push(`traceability.${field}: Expected numeric value`);
159+
continue;
160+
}
161+
if (value !== 0) {
162+
results.errors.push(`traceability.${field}: Expected 0, found ${value}`);
163+
}
164+
}
165+
}
166+
167+
function validateGaps(gaps) {
168+
if (!Array.isArray(gaps)) {
169+
results.errors.push('Missing required field: gaps');
170+
return;
171+
}
172+
173+
if (gaps.length === 0) {
174+
results.info.push('No declared certification gaps');
175+
return;
176+
}
177+
178+
if (allowGaps) {
179+
results.warnings.push(`Certification includes ${gaps.length} declared gap(s); allowed via --allow-gaps`);
180+
} else {
181+
results.errors.push(`Certification includes ${gaps.length} declared gap(s); use --allow-gaps only when human approval exists`);
182+
}
183+
}
184+
185+
function main() {
186+
if (!evidencePath) {
187+
results.errors.push('Missing required option: --evidence <path>');
188+
return finish();
189+
}
190+
191+
if (!fs.existsSync(evidencePath)) {
192+
results.errors.push(`Evidence file not found: ${toProjectRelative(evidencePath)}`);
193+
return finish();
194+
}
195+
196+
let evidence;
197+
try {
198+
evidence = parseYaml(evidencePath);
199+
} catch (error) {
200+
results.errors.push(`Failed to parse evidence file: ${error.message}`);
201+
return finish();
202+
}
203+
204+
if (!isObject(evidence)) {
205+
results.errors.push('Evidence manifest is empty or invalid');
206+
return finish();
207+
}
208+
209+
const capabilityRef = checkRequiredField(evidence, 'capability');
210+
checkRequiredField(evidence, 'certified_at');
211+
checkRequiredField(evidence, 'certified_by');
212+
const evidenceSection = checkRequiredField(evidence, 'evidence');
213+
const traceabilitySection = checkRequiredField(evidence, 'traceability');
214+
const gapsSection = checkRequiredField(evidence, 'gaps');
215+
216+
if (typeof capabilityRef === 'string') {
217+
if (!fileExists(capabilityRef)) {
218+
results.errors.push(`Referenced capability file not found: ${capabilityRef}`);
219+
} else {
220+
results.info.push(`Referenced capability found: ${capabilityRef}`);
221+
}
222+
}
223+
224+
const evidenceDir = path.dirname(evidencePath);
225+
if (isObject(evidenceSection)) {
226+
const unitTests = evidenceSection.unit_tests;
227+
const contractTests = evidenceSection.contract_tests;
228+
const e2eTests = evidenceSection.e2e_tests;
229+
const coverage = evidenceSection.coverage;
230+
231+
checkFailedCount('evidence.unit_tests', unitTests);
232+
checkFailedCount('evidence.contract_tests', contractTests);
233+
checkFailedCount('evidence.e2e_tests', e2eTests);
234+
235+
checkReportPath(evidenceDir, 'evidence.unit_tests', unitTests?.report);
236+
checkReportPath(evidenceDir, 'evidence.contract_tests', contractTests?.report);
237+
checkReportPath(evidenceDir, 'evidence.e2e_tests', e2eTests?.report);
238+
239+
if (coverage?.report) {
240+
checkReportPath(evidenceDir, 'evidence.coverage', coverage.report);
241+
} else {
242+
results.info.push('Coverage report not recorded (informational only)');
243+
}
244+
}
245+
246+
validateTraceabilitySection(traceabilitySection);
247+
validateGaps(gapsSection);
248+
249+
finish();
250+
}
251+
252+
function finish() {
253+
if (jsonOutput) {
254+
console.log(JSON.stringify(results, null, 2));
255+
} else {
256+
console.log(formatResults(results));
257+
}
258+
259+
process.exit(results.errors.length > 0 ? 1 : 0);
260+
}
261+
262+
main();

tools/validate-front-matter.js

100644100755
File mode changed.

0 commit comments

Comments
 (0)