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
- // Styles
209
- const styles = blocks.map(mermaidStyle).filter(Boolean);
210
- if (styles.length > 0) {
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 s of styles)
213
- lines.push(` ${s}`);
216
+ for (const cl of classLines)
217
+ lines.push(cl);
214
218
  }
215
219
  return lines.join("\n");
216
220
  }
@@ -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 = /\d+%|\d+\s*(?:second|ms|metre|meter|m\b)/i.test(req.text ?? "");
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
- console.log("Requirement created.");
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>", "Description")
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "airgen-cli",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "AIRGen CLI — requirements engineering from the command line",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",