airgen-cli 0.19.1 → 0.20.1

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.
@@ -693,7 +693,7 @@ export function registerLintCommands(program, client) {
693
693
  .option("--threshold <n>", "Jaccard similarity threshold (0.0-1.0)", "0.6")
694
694
  .option("--spray-threshold <n>", "Outgoing trace link count for spray pattern detection", "8")
695
695
  .option("--min-severity <level>", "Only show findings at this severity or above: low, medium, high", "low")
696
- .option("--substrate-namespace <ns>", "Use existing Substrate entity classifications instead of re-classifying (e.g. SE, SE:naval)")
696
+ .option("--substrate-namespace <ns>", "Substrate namespace for entity lookups (default: SE)", "SE")
697
697
  .action(async (tenant, project, opts) => {
698
698
  const uht = new UhtClient();
699
699
  if (!uht.isConfigured) {
@@ -722,14 +722,13 @@ export function registerLintCommands(program, client) {
722
722
  const conceptRefs = extractConcepts(requirements);
723
723
  const top = topConcepts(conceptRefs, maxConcepts);
724
724
  console.error(` ${conceptRefs.size} unique concepts found, classifying top ${top.length}.`);
725
- // Step 3: Classify concepts — use Substrate namespace if provided, else fresh classify
726
- const ns = opts.substrateNamespace;
727
- console.error(ns
728
- ? `Looking up concepts in Substrate namespace "${ns}"...`
729
- : "Classifying concepts via UHT...");
725
+ // Step 3: Classify concepts — use Substrate namespace (default: SE) for pre-classified entities
726
+ // Only falls back to fresh POST /classify for concepts not found in the namespace
727
+ const ns = opts.substrateNamespace ?? "SE";
728
+ console.error(`Looking up concepts in Substrate namespace "${ns}"...`);
730
729
  const concepts = [];
731
- if (ns) {
732
- // Namespace mode: look up pre-classified entities
730
+ {
731
+ // Namespace mode: look up pre-classified entities, fall back to fresh classify
733
732
  const lookupResults = await parallelMap(top, async ([name]) => uht.lookupEntity(name, ns), 5);
734
733
  for (let i = 0; i < lookupResults.length; i++) {
735
734
  const [name, refs] = top[i];
@@ -765,28 +764,6 @@ export function registerLintCommands(program, client) {
765
764
  }
766
765
  }
767
766
  }
768
- else {
769
- // Fresh classification mode (original behavior)
770
- const classifyResults = await parallelMap(top, async ([name]) => uht.classify(name), 5);
771
- for (let i = 0; i < classifyResults.length; i++) {
772
- const entry = classifyResults[i];
773
- const [name, refs] = top[i];
774
- if (entry.result) {
775
- const traitNames = entry.result.traits.map(t => t.name).filter(Boolean);
776
- concepts.push({
777
- name,
778
- hexCode: entry.result.hex_code,
779
- traits: traitNames,
780
- traitSet: new Set(traitNames),
781
- reqs: refs,
782
- });
783
- console.error(` ✓ ${name} → ${entry.result.hex_code} (${traitNames.length} traits)`);
784
- }
785
- else if (entry.error) {
786
- console.error(` ✗ ${name}: ${entry.error.message}`);
787
- }
788
- }
789
- }
790
767
  // Step 4: Cross-compare concepts in batches
791
768
  console.error("Cross-comparing concepts...");
792
769
  const comparisons = [];
@@ -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.1",
3
+ "version": "0.20.1",
4
4
  "description": "AIRGen CLI — requirements engineering from the command line",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",