airgen-cli 0.19.0 → 0.20.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.
@@ -199,8 +199,8 @@ export function registerRequirementCommands(program, client) {
199
199
  if (opts.section)
200
200
  body.sectionId = opts.section;
201
201
  if (opts.addTags || opts.removeTags) {
202
- // Read-modify-write for add/remove tags
203
- const existing = await client.get(`/requirements/${tenant}/${project}/${resolvedId}`);
202
+ // Read-modify-write for add/remove tags — use original ref for GET (resolvedId may be composite)
203
+ const existing = await client.get(`/requirements/${tenant}/${project}/${encodeURIComponent(id)}`);
204
204
  let currentTags = existing.record?.tags ?? [];
205
205
  if (opts.addTags) {
206
206
  const toAdd = opts.addTags.split(",").map(t => t.trim());
@@ -1,5 +1,11 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { output, printTable, isJsonMode } from "../output.js";
3
+ function sourceRef(l) {
4
+ return l.sourceRequirement?.ref ?? l.sourceRequirementId ?? "";
5
+ }
6
+ function targetRef(l) {
7
+ return l.targetRequirement?.ref ?? l.targetRequirementId ?? "";
8
+ }
3
9
  export function registerTraceabilityCommands(program, client) {
4
10
  const cmd = program.command("traces").alias("trace").description("Traceability links");
5
11
  cmd
@@ -28,8 +34,8 @@ export function registerTraceabilityCommands(program, client) {
28
34
  console.log(`Trace links: ${links.length}${total > links.length ? ` of ${total}` : ""}\n`);
29
35
  printTable(["ID", "Source", "Target", "Type", "Description"], links.map(l => [
30
36
  l.id,
31
- l.sourceRef ?? l.sourceRequirementId ?? "",
32
- l.targetRef ?? l.targetRequirementId ?? "",
37
+ sourceRef(l),
38
+ targetRef(l),
33
39
  l.linkType ?? "",
34
40
  l.description ?? "",
35
41
  ]));
@@ -164,6 +170,66 @@ export function registerTraceabilityCommands(program, client) {
164
170
  }
165
171
  console.log(`${opts.dryRun ? "Would create" : "Created"} ${created} trace links. Errors: ${errors}.`);
166
172
  });
173
+ // ── trace validate: check link direction conventions ──
174
+ cmd
175
+ .command("validate")
176
+ .description("Validate trace link directions against SE conventions (derives: parent→child, verifies: VER→target)")
177
+ .argument("<tenant>", "Tenant slug")
178
+ .argument("<project>", "Project slug")
179
+ .option("--fix", "Reverse incorrectly directed links")
180
+ .action(async (tenant, project, opts) => {
181
+ const data = await client.get(`/trace-links/${tenant}/${project}`);
182
+ const links = data.traceLinks ?? [];
183
+ // Infer document tier from ref prefix
184
+ const tierOrder = { STK: 0, SYS: 1, IFC: 2, SUB: 3, ARC: 2, VER: 4, HAZ: 1 };
185
+ function refTier(ref) {
186
+ const prefix = ref.split("-")[0];
187
+ return tierOrder[prefix] ?? -1;
188
+ }
189
+ const issues = [];
190
+ for (const l of links) {
191
+ const src = sourceRef(l);
192
+ const tgt = targetRef(l);
193
+ const srcTier = refTier(src);
194
+ const tgtTier = refTier(tgt);
195
+ if (l.linkType === "derives" && srcTier > tgtTier && srcTier >= 0 && tgtTier >= 0) {
196
+ issues.push({ link: l, reason: `derives should flow parent→child (${src} tier ${srcTier} > ${tgt} tier ${tgtTier})` });
197
+ }
198
+ if (l.linkType === "verifies" && !src.startsWith("VER-") && tgt.startsWith("VER-")) {
199
+ issues.push({ link: l, reason: `verifies should flow VER→target, not ${src}→${tgt}` });
200
+ }
201
+ }
202
+ if (issues.length === 0) {
203
+ console.log(`All ${links.length} trace links pass direction validation.`);
204
+ return;
205
+ }
206
+ console.log(`Found ${issues.length} direction issue(s):\n`);
207
+ for (const { link, reason } of issues) {
208
+ console.log(` ${sourceRef(link)} --${link.linkType}--> ${targetRef(link)}`);
209
+ console.log(` ${reason}`);
210
+ if (opts.fix) {
211
+ try {
212
+ // Delete old link, create reversed
213
+ await client.delete(`/trace-links/${tenant}/${project}/${link.id}`);
214
+ await client.post("/trace-links", {
215
+ tenant, projectKey: project,
216
+ sourceRequirementId: link.targetRequirementId,
217
+ targetRequirementId: link.sourceRequirementId,
218
+ linkType: link.linkType,
219
+ description: link.description,
220
+ rationale: link.rationale,
221
+ });
222
+ console.log(` Fixed: reversed to ${targetRef(link)} --${link.linkType}--> ${sourceRef(link)}`);
223
+ }
224
+ catch (err) {
225
+ console.error(` Failed to fix: ${err.message}`);
226
+ }
227
+ }
228
+ }
229
+ if (!opts.fix) {
230
+ console.log(`\nUse --fix to reverse these links.`);
231
+ }
232
+ });
167
233
  // Linksets
168
234
  const linksets = cmd.command("linksets").description("Document linksets");
169
235
  linksets
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "airgen-cli",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "AIRGen CLI — requirements engineering from the command line",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",