airgen-cli 0.21.0 → 0.22.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.
@@ -17,7 +17,7 @@ export function registerBaselineCommands(program, client) {
17
17
  b.ref ?? b.id,
18
18
  b.label ?? "",
19
19
  b.createdAt ?? "",
20
- String(b.requirementCount ?? 0),
20
+ String(b.requirementVersionCount ?? b.requirementRefs?.length ?? b.requirementCount ?? 0),
21
21
  ]));
22
22
  }
23
23
  });
@@ -159,10 +159,21 @@ export function registerDiffCommand(program, client) {
159
159
  .argument("<tenant>", "Tenant slug")
160
160
  .argument("<project>", "Project slug")
161
161
  .requiredOption("--from <ref>", "Source baseline ref (earlier)")
162
- .requiredOption("--to <ref>", "Target baseline ref (later)")
162
+ .option("--to <ref>", "Target baseline ref (later). If omitted, lists available baselines.")
163
163
  .option("--format <fmt>", "Output format: text, markdown", "text")
164
164
  .option("-o, --output <file>", "Write report to file")
165
165
  .action(async (tenant, project, opts) => {
166
+ if (!opts.to) {
167
+ // Show available baselines when --to is omitted
168
+ const blData = await client.get(`/baselines/${tenant}/${project}`);
169
+ const baselines = blData.items ?? [];
170
+ console.error(`Available baselines (use --to <ref>):\n`);
171
+ for (const b of baselines) {
172
+ const marker = b.ref === opts.from ? " ← --from" : "";
173
+ console.error(` ${b.ref} ${b.label ?? ""} ${b.createdAt ?? ""}${marker}`);
174
+ }
175
+ process.exit(1);
176
+ }
166
177
  const data = await client.get(`/baselines/${tenant}/${project}/compare`, { from: opts.from, to: opts.to });
167
178
  let result;
168
179
  if (isJsonMode()) {
@@ -1,6 +1,12 @@
1
1
  import { readFileSync, writeFileSync, existsSync } from "node:fs";
2
2
  import { UhtClient, decodeHexTraits } from "../uht-client.js";
3
3
  import { isJsonMode } from "../output.js";
4
+ function sourceRef(l) {
5
+ return l.sourceRequirement?.ref ?? l.sourceRequirementId ?? "";
6
+ }
7
+ function targetRef(l) {
8
+ return l.targetRequirement?.ref ?? l.targetRequirementId ?? "";
9
+ }
4
10
  const TRAIT_CHECKS = [
5
11
  // Physical Layer (1-8)
6
12
  { trait: "Physical Object", bit: 1, severity: "high", inverted: true,
@@ -225,12 +231,16 @@ async function parallelMap(items, fn, concurrency) {
225
231
  function extractConcepts(requirements) {
226
232
  const conceptRefs = new Map();
227
233
  const skip = new Set(["system", "the system", "it", "this", "all", "each", "any", "user", "operator"]);
234
+ // Mode names are system-level operating states, not decomposable concepts
235
+ const modePattern = /^(?:normal|degraded|emergency|standby|idle|startup|shutdown|maintenance|manual|automatic|autonomous|safe|fault|failsafe|backup|override|test|diagnostic|calibration)\s+(?:mode|operation|state|condition|running|navigation)/i;
228
236
  function addConcept(concept, ref) {
229
237
  const normalized = concept.toLowerCase().trim();
230
238
  if (normalized.length < 3 || normalized.length > 60)
231
239
  return;
232
240
  if (skip.has(normalized))
233
241
  return;
242
+ if (modePattern.test(normalized))
243
+ return;
234
244
  const refs = conceptRefs.get(normalized) ?? [];
235
245
  if (!refs.includes(ref))
236
246
  refs.push(ref);
@@ -450,7 +460,7 @@ function analyzeFindings(concepts, comparisons, requirements, traceLinks, reqTie
450
460
  if (traceLinks.length > 0) {
451
461
  const outgoing = new Map();
452
462
  for (const link of traceLinks) {
453
- const src = link.sourceRef ?? link.sourceRequirementId ?? "";
463
+ const src = sourceRef(link);
454
464
  if (!outgoing.has(src))
455
465
  outgoing.set(src, []);
456
466
  outgoing.get(src).push(link);
@@ -571,6 +581,80 @@ function analyzeFindings(concepts, comparisons, requirements, traceLinks, reqTie
571
581
  });
572
582
  }
573
583
  }
584
+ // ── 11. Quantitative value consistency across subsystems ────
585
+ // Extract numeric values + units, group by value across document tiers, flag duplicates without derivation links
586
+ const valuePattern = /(\d+(?:\.\d+)?)\s*(ms|s|sec|seconds?|minutes?|min|hours?|hr|Hz|kHz|MHz|GHz|V|kV|mV|A|mA|W|kW|MW|m|km|cm|mm|m\/s|km\/h|mph|dB|dBm|°C|°F|K|Pa|kPa|MPa|bar|psi|kg|g|lb|N|kN|%)/gi;
587
+ const valueOccurrences = new Map();
588
+ for (const req of requirements) {
589
+ if (!req.text || !req.ref)
590
+ continue;
591
+ const tier = reqTierMap.get(req.ref) ?? "?";
592
+ let vm;
593
+ while ((vm = valuePattern.exec(req.text)) !== null) {
594
+ const key = `${vm[1]}${vm[2].toLowerCase()}`;
595
+ const list = valueOccurrences.get(key) ?? [];
596
+ list.push({ ref: req.ref, tier });
597
+ valueOccurrences.set(key, list);
598
+ }
599
+ }
600
+ // Flag values appearing across different subsystems without trace links
601
+ for (const [value, occurrences] of valueOccurrences) {
602
+ const tiers = new Set(occurrences.map(o => o.tier));
603
+ if (tiers.size < 2)
604
+ continue; // Only flag cross-tier
605
+ // Check if any pair has a derives trace link
606
+ const refs = occurrences.map(o => o.ref);
607
+ const hasDerivation = traceLinks.some(l => l.linkType === "derives" &&
608
+ refs.includes(sourceRef(l)) && refs.includes(targetRef(l)));
609
+ if (!hasDerivation && occurrences.length >= 2) {
610
+ findings.push({
611
+ severity: "medium",
612
+ category: "Value Consistency",
613
+ title: `Threshold "${value}" appears across ${tiers.size} document tiers without derivation`,
614
+ description: `"${value}" appears in ${occurrences.length} requirements across tiers ${[...tiers].join(", ")} with no derives trace link connecting them.`,
615
+ affectedReqs: refs.slice(0, 10),
616
+ recommendation: `Add derivation rationale or trace links explaining why "${value}" is used across subsystems.`,
617
+ });
618
+ }
619
+ }
620
+ // ── 12. Round-number threshold detector ───────────────────
621
+ const roundPattern = /\b(\d+)\s*(ms|s|Hz|V|W|kW|MW|m|km|km\/h|mph|dB|%)\b/gi;
622
+ const roundCounts = new Map();
623
+ for (const req of requirements) {
624
+ if (!req.text || !req.ref)
625
+ continue;
626
+ let rm;
627
+ while ((rm = roundPattern.exec(req.text)) !== null) {
628
+ const num = parseInt(rm[1], 10);
629
+ // Only flag round numbers (multiples of 10, 50, 100, 500, 1000)
630
+ if (num >= 10 && (num % 10 === 0 || num % 50 === 0 || num % 100 === 0)) {
631
+ const key = `${num}${rm[2].toLowerCase()}`;
632
+ const refs = roundCounts.get(key) ?? [];
633
+ if (!refs.includes(req.ref))
634
+ refs.push(req.ref);
635
+ roundCounts.set(key, refs);
636
+ }
637
+ }
638
+ }
639
+ for (const [value, refs] of roundCounts) {
640
+ if (refs.length <= 3)
641
+ continue; // Only flag if >3 occurrences
642
+ // Check if any has derivation rationale
643
+ const hasRationale = refs.some(ref => {
644
+ const req = requirements.find(r => r.ref === ref);
645
+ return req?.rationale && /derived from|allocated from|calculated|analysis|budget/i.test(req.rationale);
646
+ });
647
+ if (!hasRationale) {
648
+ findings.push({
649
+ severity: "low",
650
+ category: "Design Assumption",
651
+ title: `Round threshold "${value}" appears ${refs.length} times without derivation rationale`,
652
+ description: `"${value}" is used in ${refs.length} requirements — likely an undocumented design assumption.`,
653
+ affectedReqs: refs.slice(0, 10),
654
+ recommendation: `Add rationale explaining why "${value}" was chosen, or trace to the source requirement that allocates this value.`,
655
+ });
656
+ }
657
+ }
574
658
  return findings.sort((a, b) => {
575
659
  const sev = { high: 0, medium: 1, low: 2 };
576
660
  return sev[a.severity] - sev[b.severity];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "airgen-cli",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "AIRGen CLI — requirements engineering from the command line",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",