airgen-cli 0.15.0 → 0.16.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/dist/commands/diagrams.js +11 -3
- package/dist/commands/requirements.js +26 -0
- package/dist/commands/verify.js +47 -17
- package/package.json +1 -1
|
@@ -225,9 +225,13 @@ export function registerDiagramCommands(program, client) {
|
|
|
225
225
|
.description("List all diagrams in a project")
|
|
226
226
|
.argument("<tenant>", "Tenant slug")
|
|
227
227
|
.argument("<project>", "Project slug")
|
|
228
|
-
.
|
|
228
|
+
.option("--name <name>", "Filter by exact name match")
|
|
229
|
+
.action(async (tenant, project, opts) => {
|
|
229
230
|
const data = await client.get(`/architecture/diagrams/${tenant}/${project}`);
|
|
230
|
-
|
|
231
|
+
let diagrams = data.diagrams ?? [];
|
|
232
|
+
if (opts.name) {
|
|
233
|
+
diagrams = diagrams.filter(d => d.name === opts.name);
|
|
234
|
+
}
|
|
231
235
|
if (isJsonMode()) {
|
|
232
236
|
output(diagrams);
|
|
233
237
|
}
|
|
@@ -283,7 +287,11 @@ export function registerDiagramCommands(program, client) {
|
|
|
283
287
|
if (opts.format === "mermaid") {
|
|
284
288
|
rendered = renderMermaid(blocks, connectors, opts.direction);
|
|
285
289
|
if (opts.clean) {
|
|
286
|
-
rendered = rendered
|
|
290
|
+
rendered = rendered
|
|
291
|
+
.split("\n")
|
|
292
|
+
.filter(l => !l.trim().startsWith("classDef ") && !l.trim().startsWith("class ") && !l.trim().startsWith("style "))
|
|
293
|
+
.map(l => l.replace(/<<[^>]+>><br\/>/g, "")) // Strip <<stereotype>><br/> from node labels
|
|
294
|
+
.join("\n");
|
|
287
295
|
}
|
|
288
296
|
if (isJsonMode()) {
|
|
289
297
|
output({ mermaid: rendered, blocks: blocks.length, connectors: connectors.length });
|
|
@@ -14,7 +14,29 @@ export function registerRequirementCommands(program, client) {
|
|
|
14
14
|
.option("--order <dir>", "Sort order: asc, desc")
|
|
15
15
|
.option("--tags <tags>", "Comma-separated tags to filter by (server-side)")
|
|
16
16
|
.option("--document <slug>", "Filter by document slug (server-side)")
|
|
17
|
+
.option("--homeless", "Show only requirements not assigned to any document")
|
|
17
18
|
.action(async (tenant, project, opts) => {
|
|
19
|
+
// Handle --homeless: fetch all and filter to unassigned
|
|
20
|
+
if (opts.homeless) {
|
|
21
|
+
const all = [];
|
|
22
|
+
for (let page = 1; page <= 50; page++) {
|
|
23
|
+
const data = await client.get(`/requirements/${tenant}/${project}`, { page: String(page), limit: "500" });
|
|
24
|
+
all.push(...(data.data ?? []));
|
|
25
|
+
if (page >= (data.meta?.totalPages ?? 1))
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
const homeless = all.filter(r => !r.documentSlug);
|
|
29
|
+
if (isJsonMode()) {
|
|
30
|
+
output({ data: homeless, meta: { totalItems: homeless.length } });
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
console.log(`Homeless requirements (no document): ${homeless.length}/${all.length}\n`);
|
|
34
|
+
if (homeless.length > 0) {
|
|
35
|
+
printTable(["Ref", "Text", "QA"], homeless.map(r => [r.ref ?? "?", truncate(r.text ?? "", 65), r.qaScore != null ? String(r.qaScore) : "-"]));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
18
40
|
// Handle --limit all: fetch all pages
|
|
19
41
|
if (opts.limit.toLowerCase() === "all") {
|
|
20
42
|
const all = [];
|
|
@@ -116,6 +138,10 @@ export function registerRequirementCommands(program, client) {
|
|
|
116
138
|
.option("--tags <tags>", "Comma-separated tags")
|
|
117
139
|
.option("--idempotency-key <key>", "Prevent duplicates on retry — returns existing if key was already used")
|
|
118
140
|
.action(async (tenant, projectKey, opts) => {
|
|
141
|
+
if (!opts.section && !opts.document) {
|
|
142
|
+
console.error("Warning: No --section or --document specified. Requirement will be project-level with a generic ref (e.g., REQ-PROJECTNAME-001).");
|
|
143
|
+
console.error(" Use --section <id> to assign to a document section for proper ref prefixing.");
|
|
144
|
+
}
|
|
119
145
|
const data = await client.post("/requirements", {
|
|
120
146
|
tenant,
|
|
121
147
|
projectKey,
|
package/dist/commands/verify.js
CHANGED
|
@@ -268,31 +268,61 @@ export function registerVerifyCommands(program, client) {
|
|
|
268
268
|
// ── Engine ─────────────────────────────────────────────
|
|
269
269
|
cmd
|
|
270
270
|
.command("run")
|
|
271
|
-
.description("Run the verification engine —
|
|
271
|
+
.description("Run the verification engine — checks both verification activities and 'verifies' trace links")
|
|
272
272
|
.argument("<tenant>", "Tenant slug")
|
|
273
273
|
.argument("<project>", "Project slug")
|
|
274
274
|
.action(async (tenant, project) => {
|
|
275
|
-
|
|
276
|
-
const
|
|
275
|
+
// Fetch engine report (activities-based) and trace links in parallel
|
|
276
|
+
const [engineData, linkData, reqData] = await Promise.all([
|
|
277
|
+
client.get(`/verification/engine/${tenant}/${project}`).catch(() => ({ report: null })),
|
|
278
|
+
client.get(`/trace-links/${tenant}/${project}`).catch(() => ({ traceLinks: [] })),
|
|
279
|
+
client.get(`/requirements/${tenant}/${project}`, { page: "1", limit: "1" }).catch(() => ({ meta: { totalItems: 0 } })),
|
|
280
|
+
]);
|
|
281
|
+
const report = engineData.report;
|
|
282
|
+
const totalReqs = reqData.meta?.totalItems ?? 0;
|
|
283
|
+
// Compute trace-link-based coverage (requirements targeted by 'verifies' links)
|
|
284
|
+
const verifiedByLinks = new Set();
|
|
285
|
+
for (const link of linkData.traceLinks ?? []) {
|
|
286
|
+
if (link.linkType === "verifies" && link.targetRequirementId) {
|
|
287
|
+
verifiedByLinks.add(link.targetRequirementId);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const traceCoverage = totalReqs > 0 ? Math.round((verifiedByLinks.size / totalReqs) * 100) : 0;
|
|
277
291
|
if (isJsonMode()) {
|
|
278
|
-
output(
|
|
292
|
+
output({
|
|
293
|
+
activities: report,
|
|
294
|
+
traceLinkCoverage: {
|
|
295
|
+
totalRequirements: totalReqs,
|
|
296
|
+
verifiedByTraceLinks: verifiedByLinks.size,
|
|
297
|
+
coveragePercent: traceCoverage,
|
|
298
|
+
},
|
|
299
|
+
});
|
|
279
300
|
return;
|
|
280
301
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
console.log(`Coverage: ${
|
|
284
|
-
|
|
285
|
-
if (report
|
|
286
|
-
|
|
302
|
+
console.log("Verification Report\n");
|
|
303
|
+
// Trace link coverage (most common approach)
|
|
304
|
+
console.log(`Trace Link Coverage: ${traceCoverage}% (${verifiedByLinks.size}/${totalReqs} have 'verifies' links)`);
|
|
305
|
+
// Activities coverage (formal V&V workflow)
|
|
306
|
+
if (report) {
|
|
307
|
+
const s = report.summary;
|
|
308
|
+
console.log(`Activity Coverage: ${s.coveragePercent}% (${s.verified}/${s.totalRequirements} have verification activities)`);
|
|
309
|
+
console.log(` Unverified: ${s.unverified} | Incomplete: ${s.incomplete} | Drifted: ${s.driftedEvidence}`);
|
|
310
|
+
if (report.findings.length > 0) {
|
|
311
|
+
console.log(`\nFindings (${report.findings.length}):`);
|
|
312
|
+
printTable(["Severity", "Type", "Req", "Message"], report.findings.map(f => [
|
|
313
|
+
severityIcon(f.severity) + " " + f.severity,
|
|
314
|
+
f.type,
|
|
315
|
+
f.requirementRef ?? "",
|
|
316
|
+
truncate(f.message, 60),
|
|
317
|
+
]));
|
|
318
|
+
}
|
|
287
319
|
}
|
|
288
320
|
else {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
]));
|
|
295
|
-
console.log(`\n${report.findings.length} finding(s) total.`);
|
|
321
|
+
console.log("Activity Coverage: N/A (no verification activities configured)");
|
|
322
|
+
}
|
|
323
|
+
if (traceCoverage === 0 && (!report || report.summary.coveragePercent === 0)) {
|
|
324
|
+
console.log("\nTip: Create 'verifies' trace links from VER requirements to SYS/SUB requirements,");
|
|
325
|
+
console.log("or set up verification activities via 'airgen verify activities create'.");
|
|
296
326
|
}
|
|
297
327
|
});
|
|
298
328
|
// ── Matrix ─────────────────────────────────────────────
|