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.
- package/dist/commands/lint.js +7 -30
- package/dist/commands/traceability.js +68 -2
- package/package.json +1 -1
package/dist/commands/lint.js
CHANGED
|
@@ -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>", "
|
|
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
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|