airgen-cli 0.6.0 → 0.8.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.
|
@@ -32,23 +32,12 @@ function mermaidNodeShape(block) {
|
|
|
32
32
|
}
|
|
33
33
|
function mermaidArrow(kind) {
|
|
34
34
|
switch (kind) {
|
|
35
|
-
case "flow": return "
|
|
35
|
+
case "flow": return "-->";
|
|
36
36
|
case "dependency": return "-.->";
|
|
37
37
|
case "composition": return "-->";
|
|
38
38
|
default: return "-->";
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
function mermaidStyle(block) {
|
|
42
|
-
const id = sanitizeId(block.id);
|
|
43
|
-
switch (block.kind) {
|
|
44
|
-
case "system": return `style ${id} fill:#ebf8ff,stroke:#1a365d,color:#1a365d`;
|
|
45
|
-
case "subsystem": return `style ${id} fill:#f0f5ff,stroke:#2c5282,color:#2c5282`;
|
|
46
|
-
case "actor": return `style ${id} fill:#f0fff4,stroke:#276749,color:#276749`;
|
|
47
|
-
case "external": return `style ${id} fill:#fffbeb,stroke:#92400e,color:#92400e`;
|
|
48
|
-
case "interface": return `style ${id} fill:#faf5ff,stroke:#6b21a8,color:#6b21a8`;
|
|
49
|
-
default: return null;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
41
|
// ── Terminal (layered) rendering ─────────────────────────────
|
|
53
42
|
const KIND_ICONS = {
|
|
54
43
|
system: "■", subsystem: "□", component: "◦",
|
|
@@ -205,12 +194,27 @@ function renderMermaid(blocks, connectors, direction) {
|
|
|
205
194
|
lines.push(` ${src} ${arrow} ${tgt}`);
|
|
206
195
|
}
|
|
207
196
|
}
|
|
208
|
-
//
|
|
209
|
-
const
|
|
210
|
-
|
|
197
|
+
// Class definitions for block kinds (more robust than per-node style lines)
|
|
198
|
+
const kindsUsed = new Set(blocks.map(b => b.kind).filter(Boolean));
|
|
199
|
+
const classDefs = {
|
|
200
|
+
system: "fill:#ebf8ff,stroke:#1a365d,color:#1a365d",
|
|
201
|
+
subsystem: "fill:#f0f5ff,stroke:#2c5282,color:#2c5282",
|
|
202
|
+
actor: "fill:#f0fff4,stroke:#276749,color:#276749",
|
|
203
|
+
external: "fill:#fffbeb,stroke:#92400e,color:#92400e",
|
|
204
|
+
interface: "fill:#faf5ff,stroke:#6b21a8,color:#6b21a8",
|
|
205
|
+
};
|
|
206
|
+
const classLines = [];
|
|
207
|
+
for (const kind of kindsUsed) {
|
|
208
|
+
if (kind && classDefs[kind]) {
|
|
209
|
+
classLines.push(` classDef ${kind} ${classDefs[kind]}`);
|
|
210
|
+
const members = blocks.filter(b => b.kind === kind).map(b => sanitizeId(b.id));
|
|
211
|
+
classLines.push(` class ${members.join(",")} ${kind}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (classLines.length > 0) {
|
|
211
215
|
lines.push("");
|
|
212
|
-
for (const
|
|
213
|
-
lines.push(
|
|
216
|
+
for (const cl of classLines)
|
|
217
|
+
lines.push(cl);
|
|
214
218
|
}
|
|
215
219
|
return lines.join("\n");
|
|
216
220
|
}
|
package/dist/commands/lint.js
CHANGED
|
@@ -154,14 +154,17 @@ function analyzeFindings(concepts, comparisons, requirements) {
|
|
|
154
154
|
}
|
|
155
155
|
// 4. Degraded mode gaps: requirements mentioning "manual", "reversion", "fallback" without performance criteria
|
|
156
156
|
const degradedReqs = requirements.filter(r => r.text && /manual\s+(?:reversion|mode|override|backup)|fallback|degraded/i.test(r.text));
|
|
157
|
+
// Numeric performance patterns: "10 Hz", "3.6m", "250 ms", "95%", "< 5 seconds", ">= 99.9%",
|
|
158
|
+
// "within 100ms", "at least 10", units with numbers, ranges like "5-10"
|
|
159
|
+
const perfPattern = /\d+\.?\d*\s*(?:%|Hz|kHz|MHz|s\b|ms\b|sec|second|minute|min\b|hour|hr|m\b|km|cm|mm|m\/s|km\/h|mph|kph|dB|dBm|°C|°F|K\b|V\b|A\b|W\b|kW|MW|N\b|kN|Pa|kPa|MPa|bar|psi|kg|g\b|lb|fps|bps|Mbps|Gbps)|(?:within|under|below|above|at least|no (?:more|less) than|maximum|minimum)\s+\d|\d+\s*(?:to|-)\s*\d/i;
|
|
157
160
|
for (const req of degradedReqs) {
|
|
158
|
-
const hasPerf =
|
|
161
|
+
const hasPerf = perfPattern.test(req.text ?? "");
|
|
159
162
|
if (!hasPerf) {
|
|
160
163
|
findings.push({
|
|
161
164
|
severity: "medium",
|
|
162
165
|
category: "Coverage Gap",
|
|
163
166
|
title: `Degraded mode without performance criteria: ${req.ref}`,
|
|
164
|
-
description: `${req.ref} specifies a degraded/manual mode but provides no acceptance criteria for performance in that mode.`,
|
|
167
|
+
description: `${req.ref} specifies a degraded/manual mode but provides no measurable acceptance criteria for performance in that mode.`,
|
|
165
168
|
affectedReqs: [req.ref],
|
|
166
169
|
recommendation: "Add measurable performance criteria for degraded operation (e.g., acceptable accuracy, response time, available subsystems).",
|
|
167
170
|
});
|
|
@@ -87,6 +87,7 @@ export function registerRequirementCommands(program, client) {
|
|
|
87
87
|
.option("--document <slug>", "Document slug")
|
|
88
88
|
.option("--section <id>", "Section ID")
|
|
89
89
|
.option("--tags <tags>", "Comma-separated tags")
|
|
90
|
+
.option("--idempotency-key <key>", "Prevent duplicates on retry — returns existing if key was already used")
|
|
90
91
|
.action(async (tenant, projectKey, opts) => {
|
|
91
92
|
const data = await client.post("/requirements", {
|
|
92
93
|
tenant,
|
|
@@ -99,12 +100,18 @@ export function registerRequirementCommands(program, client) {
|
|
|
99
100
|
documentSlug: opts.document,
|
|
100
101
|
sectionId: opts.section,
|
|
101
102
|
tags: opts.tags?.split(",").map(t => t.trim()),
|
|
103
|
+
idempotencyKey: opts.idempotencyKey,
|
|
102
104
|
});
|
|
103
105
|
if (isJsonMode()) {
|
|
104
106
|
output(data);
|
|
105
107
|
}
|
|
106
108
|
else {
|
|
107
|
-
|
|
109
|
+
if (data.deduplicated) {
|
|
110
|
+
console.log("Requirement already exists (idempotency key matched).");
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
console.log("Requirement created.");
|
|
114
|
+
}
|
|
108
115
|
output(data);
|
|
109
116
|
}
|
|
110
117
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
1
2
|
import { output, printTable, isJsonMode } from "../output.js";
|
|
2
3
|
export function registerTraceabilityCommands(program, client) {
|
|
3
4
|
const cmd = program.command("traces").alias("trace").description("Traceability links");
|
|
@@ -34,7 +35,8 @@ export function registerTraceabilityCommands(program, client) {
|
|
|
34
35
|
.requiredOption("--source <id>", "Source requirement ID")
|
|
35
36
|
.requiredOption("--target <id>", "Target requirement ID")
|
|
36
37
|
.requiredOption("--type <type>", "Link type: satisfies, derives, verifies, implements, refines, conflicts")
|
|
37
|
-
.option("--description <desc>", "
|
|
38
|
+
.option("--description <desc>", "Short description for traceability matrices")
|
|
39
|
+
.option("--rationale <text>", "Engineering justification for why this link exists")
|
|
38
40
|
.action(async (tenant, projectKey, opts) => {
|
|
39
41
|
const data = await client.post("/trace-links", {
|
|
40
42
|
tenant,
|
|
@@ -43,6 +45,7 @@ export function registerTraceabilityCommands(program, client) {
|
|
|
43
45
|
targetRequirementId: opts.target,
|
|
44
46
|
linkType: opts.type,
|
|
45
47
|
description: opts.description,
|
|
48
|
+
rationale: opts.rationale,
|
|
46
49
|
});
|
|
47
50
|
if (isJsonMode()) {
|
|
48
51
|
output(data);
|
|
@@ -61,6 +64,98 @@ export function registerTraceabilityCommands(program, client) {
|
|
|
61
64
|
await client.delete(`/trace-links/${tenant}/${project}/${linkId}`);
|
|
62
65
|
console.log("Trace link deleted.");
|
|
63
66
|
});
|
|
67
|
+
cmd
|
|
68
|
+
.command("update")
|
|
69
|
+
.description("Update a trace link (description, rationale, or type)")
|
|
70
|
+
.argument("<tenant>", "Tenant slug")
|
|
71
|
+
.argument("<project>", "Project slug")
|
|
72
|
+
.argument("<link-id>", "Link ID")
|
|
73
|
+
.option("--description <desc>", "Short description for traceability matrices")
|
|
74
|
+
.option("--rationale <text>", "Engineering justification for why this link exists")
|
|
75
|
+
.option("--type <type>", "Link type: satisfies, derives, verifies, implements, refines, conflicts")
|
|
76
|
+
.action(async (tenant, project, linkId, opts) => {
|
|
77
|
+
const body = {};
|
|
78
|
+
if (opts.description)
|
|
79
|
+
body.description = opts.description;
|
|
80
|
+
if (opts.rationale)
|
|
81
|
+
body.rationale = opts.rationale;
|
|
82
|
+
if (opts.type)
|
|
83
|
+
body.linkType = opts.type;
|
|
84
|
+
if (Object.keys(body).length === 0) {
|
|
85
|
+
console.error("Nothing to update. Provide --description, --rationale, or --type.");
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
const data = await client.patch(`/trace-links/${tenant}/${project}/${linkId}`, body);
|
|
89
|
+
if (isJsonMode()) {
|
|
90
|
+
output(data);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
console.log("Trace link updated.");
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
cmd
|
|
97
|
+
.command("import")
|
|
98
|
+
.description("Bulk import trace links from CSV (columns: source, target, type, description, rationale)")
|
|
99
|
+
.argument("<tenant>", "Tenant slug")
|
|
100
|
+
.argument("<project-key>", "Project key")
|
|
101
|
+
.requiredOption("--file <path>", "Path to CSV file")
|
|
102
|
+
.option("--dry-run", "Validate without creating")
|
|
103
|
+
.action(async (tenant, projectKey, opts) => {
|
|
104
|
+
const content = readFileSync(opts.file, "utf-8");
|
|
105
|
+
const lines = content.split("\n").map(l => l.trim()).filter(l => l.length > 0);
|
|
106
|
+
if (lines.length < 2) {
|
|
107
|
+
console.error("CSV must have a header row and at least one data row.");
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
const headers = lines[0].split(",").map(h => h.trim().toLowerCase());
|
|
111
|
+
const srcIdx = headers.findIndex(h => h === "source" || h === "sourcerequirementid");
|
|
112
|
+
const tgtIdx = headers.findIndex(h => h === "target" || h === "targetrequirementid");
|
|
113
|
+
const typeIdx = headers.findIndex(h => h === "type" || h === "linktype");
|
|
114
|
+
const descIdx = headers.findIndex(h => h === "description");
|
|
115
|
+
const ratIdx = headers.findIndex(h => h === "rationale");
|
|
116
|
+
if (srcIdx === -1 || tgtIdx === -1 || typeIdx === -1) {
|
|
117
|
+
console.error("CSV must have source, target, and type columns.");
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
const rows = lines.slice(1);
|
|
121
|
+
let created = 0;
|
|
122
|
+
let errors = 0;
|
|
123
|
+
for (let i = 0; i < rows.length; i++) {
|
|
124
|
+
const cols = rows[i].split(",").map(c => c.trim());
|
|
125
|
+
const source = cols[srcIdx];
|
|
126
|
+
const target = cols[tgtIdx];
|
|
127
|
+
const linkType = cols[typeIdx];
|
|
128
|
+
if (!source || !target || !linkType) {
|
|
129
|
+
errors++;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (opts.dryRun) {
|
|
133
|
+
console.log(` [dry-run] ${source} --${linkType}--> ${target}`);
|
|
134
|
+
created++;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const body = {
|
|
139
|
+
tenant,
|
|
140
|
+
projectKey,
|
|
141
|
+
sourceRequirementId: source,
|
|
142
|
+
targetRequirementId: target,
|
|
143
|
+
linkType,
|
|
144
|
+
};
|
|
145
|
+
if (descIdx >= 0 && cols[descIdx])
|
|
146
|
+
body.description = cols[descIdx];
|
|
147
|
+
if (ratIdx >= 0 && cols[ratIdx])
|
|
148
|
+
body.rationale = cols[ratIdx];
|
|
149
|
+
await client.post("/trace-links", body);
|
|
150
|
+
created++;
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
console.error(` Row ${i + 2}: ${err instanceof Error ? err.message : String(err)}`);
|
|
154
|
+
errors++;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
console.log(`${opts.dryRun ? "Would create" : "Created"} ${created} trace links. Errors: ${errors}.`);
|
|
158
|
+
});
|
|
64
159
|
// Linksets
|
|
65
160
|
const linksets = cmd.command("linksets").description("Document linksets");
|
|
66
161
|
linksets
|