airgen-cli 0.13.0 → 0.15.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.
- package/dist/commands/diagrams.js +4 -0
- package/dist/commands/lint.js +190 -132
- package/dist/commands/requirements.js +137 -1
- package/package.json +1 -1
|
@@ -264,6 +264,7 @@ export function registerDiagramCommands(program, client) {
|
|
|
264
264
|
.option("--direction <dir>", "Layout direction for mermaid: TB, LR, BT, RL", "TB")
|
|
265
265
|
.option("-o, --output <file>", "Write to file instead of stdout")
|
|
266
266
|
.option("--wrap", "Wrap mermaid in markdown fenced code block")
|
|
267
|
+
.option("--clean", "Strip classDef/class/style lines from mermaid output (maximum compatibility)")
|
|
267
268
|
.action(async (tenant, project, id, opts) => {
|
|
268
269
|
// Fetch diagram metadata + blocks + connectors in parallel
|
|
269
270
|
const [diagramData, blocksData, connectorsData] = await Promise.all([
|
|
@@ -281,6 +282,9 @@ export function registerDiagramCommands(program, client) {
|
|
|
281
282
|
let rendered;
|
|
282
283
|
if (opts.format === "mermaid") {
|
|
283
284
|
rendered = renderMermaid(blocks, connectors, opts.direction);
|
|
285
|
+
if (opts.clean) {
|
|
286
|
+
rendered = rendered.split("\n").filter(l => !l.trim().startsWith("classDef ") && !l.trim().startsWith("class ") && !l.trim().startsWith("style ")).join("\n");
|
|
287
|
+
}
|
|
284
288
|
if (isJsonMode()) {
|
|
285
289
|
output({ mermaid: rendered, blocks: blocks.length, connectors: connectors.length });
|
|
286
290
|
return;
|
package/dist/commands/lint.js
CHANGED
|
@@ -1,6 +1,140 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
2
|
import { UhtClient } from "../uht-client.js";
|
|
3
3
|
import { isJsonMode } from "../output.js";
|
|
4
|
+
const TRAIT_CHECKS = [
|
|
5
|
+
// Physical Layer (1-8)
|
|
6
|
+
{ trait: "Physical Object", bit: 1, severity: "high", inverted: true,
|
|
7
|
+
missingWhat: "physical embodiment",
|
|
8
|
+
keywords: /temperature|shock|vibrat|humidity|nbc|contamina|electromagnetic|emc|climatic/i,
|
|
9
|
+
recommendation: "Add a requirement defining the physical embodiment (housing, LRU, equipment rack)." },
|
|
10
|
+
{ trait: "Synthetic", bit: 2, severity: "medium",
|
|
11
|
+
missingWhat: "manufacturing/material requirements",
|
|
12
|
+
keywords: /manufactur|assembl|tolerance|material|fabricat|finish|coating|process|mach[iu]n/i,
|
|
13
|
+
recommendation: "Add manufacturing, material, or assembly requirements." },
|
|
14
|
+
{ trait: "Biological/Biomimetic", bit: 3, severity: "high",
|
|
15
|
+
missingWhat: "biocompatibility/sterilization requirements",
|
|
16
|
+
keywords: /biocompat|steril|cytotox|implant|tissue|ISO.?10993|clean.?room|contaminat|biolog/i,
|
|
17
|
+
recommendation: "Add biocompatibility, sterilization, or biological safety requirements." },
|
|
18
|
+
{ trait: "Powered", bit: 4, severity: "high",
|
|
19
|
+
missingWhat: "power source/budget requirements",
|
|
20
|
+
keywords: /power|voltage|current|battery|watt|energy|consumption|supply|mains|generator/i,
|
|
21
|
+
recommendation: "Add power source, voltage range, and consumption requirements." },
|
|
22
|
+
{ trait: "Structural", bit: 5, severity: "medium",
|
|
23
|
+
missingWhat: "structural integrity requirements",
|
|
24
|
+
keywords: /load|stress|fatigue|deform|yield|strength|FEA|resonan|torque|buckling|stiffness/i,
|
|
25
|
+
recommendation: "Add load-bearing, stress, or fatigue requirements." },
|
|
26
|
+
{ trait: "Observable", bit: 6, severity: "low",
|
|
27
|
+
missingWhat: "monitoring/diagnostics requirements",
|
|
28
|
+
keywords: /monitor|diagnos|instrument|telemetry|log|alert|BIT|health|status|indicator/i,
|
|
29
|
+
recommendation: "Add monitoring, diagnostics, or built-in-test requirements." },
|
|
30
|
+
{ trait: "Physical Medium", bit: 7, severity: "medium",
|
|
31
|
+
missingWhat: "material property requirements",
|
|
32
|
+
keywords: /material|density|viscosity|thermal.?conduct|corrosion|wear|hardness|porosity/i,
|
|
33
|
+
recommendation: "Add material property, corrosion, or wear requirements." },
|
|
34
|
+
{ trait: "Active", bit: 8, severity: "medium",
|
|
35
|
+
missingWhat: "stimulus-response/timing requirements",
|
|
36
|
+
keywords: /response.?time|latency|react|trigger|within.*(?:ms|second)|timeout|watchdog|stimul/i,
|
|
37
|
+
recommendation: "Add response time, latency, or stimulus-response requirements." },
|
|
38
|
+
// Functional Layer (9-16)
|
|
39
|
+
{ trait: "Intentionally Designed", bit: 9, severity: "low",
|
|
40
|
+
missingWhat: "verification/validation method",
|
|
41
|
+
keywords: /verif|validat|test|inspect|review|acceptance|qualif|demonstrat/i,
|
|
42
|
+
recommendation: "Specify a verification or validation method." },
|
|
43
|
+
{ trait: "Outputs Effect", bit: 10, severity: "medium",
|
|
44
|
+
missingWhat: "output specification",
|
|
45
|
+
keywords: /output|emit|produce|generate|signal|transmit|radiat|illuminate|actuate/i,
|
|
46
|
+
recommendation: "Add output specification requirements (signal type, range, accuracy)." },
|
|
47
|
+
{ trait: "Processes Signals/Logic", bit: 11, severity: "medium",
|
|
48
|
+
missingWhat: "I/O format/protocol requirements",
|
|
49
|
+
keywords: /protocol|format|interface|data|message|bus|bandwidth|encoding|API|input.*output/i,
|
|
50
|
+
recommendation: "Add data format, protocol, or interface requirements." },
|
|
51
|
+
{ trait: "State-Transforming", bit: 12, severity: "medium",
|
|
52
|
+
missingWhat: "state machine/mode transition requirements",
|
|
53
|
+
keywords: /state|mode|transition|startup|shutdown|initializ|reset|idle|standby|operat.*mode/i,
|
|
54
|
+
recommendation: "Add state machine, mode transition, or initialization requirements." },
|
|
55
|
+
{ trait: "Human-Interactive", bit: 13, severity: "medium",
|
|
56
|
+
missingWhat: "usability/HMI requirements",
|
|
57
|
+
keywords: /HMI|display|ergonomic|response.?time|feedback|indicator|touch|screen|readab|legib/i,
|
|
58
|
+
recommendation: "Add usability, HMI, or human factors requirements." },
|
|
59
|
+
{ trait: "System-integrated", bit: 14, severity: "medium",
|
|
60
|
+
missingWhat: "interface/integration requirements",
|
|
61
|
+
keywords: /interface|integrat|compatible|interoperab|plug|connector|harness|mount|install/i,
|
|
62
|
+
recommendation: "Add interface, integration, or interoperability requirements." },
|
|
63
|
+
{ trait: "Functionally Autonomous", bit: 15, severity: "high",
|
|
64
|
+
missingWhat: "safety/override constraints",
|
|
65
|
+
keywords: /safety|watchdog|override|kill.?switch|emergency.?stop|fail.?safe|interlock|manual.?override|human.?in.?the.?loop/i,
|
|
66
|
+
recommendation: "Add safety constraints: override mechanism, watchdog, fail-safe state, or human-in-the-loop." },
|
|
67
|
+
{ trait: "System-Essential", bit: 16, severity: "medium",
|
|
68
|
+
missingWhat: "redundancy/failover requirements",
|
|
69
|
+
keywords: /redundan|failover|backup|graceful.?degrad|fault.?toleran|availability|single.?point.?of.?failure|hot.?standby/i,
|
|
70
|
+
recommendation: "Add redundancy, failover, or fault tolerance requirements." },
|
|
71
|
+
// Abstract Layer (17-24)
|
|
72
|
+
{ trait: "Symbolic", bit: 17, severity: "low",
|
|
73
|
+
missingWhat: "representation fidelity requirements",
|
|
74
|
+
keywords: /accuracy|fidelity|resolution|precision|representation|model|symbol|semantic/i,
|
|
75
|
+
recommendation: "Add representation accuracy, fidelity, or resolution requirements." },
|
|
76
|
+
{ trait: "Signalling", bit: 18, severity: "medium",
|
|
77
|
+
missingWhat: "signal integrity/protocol requirements",
|
|
78
|
+
keywords: /protocol|bandwidth|latency|throughput|packet|frame|bit.?rate|SNR|jitter|signal/i,
|
|
79
|
+
recommendation: "Add signal protocol, bandwidth, or integrity requirements." },
|
|
80
|
+
{ trait: "Rule-governed", bit: 19, severity: "low",
|
|
81
|
+
missingWhat: "algorithm/logic validation requirements",
|
|
82
|
+
keywords: /algorithm|logic|rule|protocol|compliance|valid|verify|formal|deterministic/i,
|
|
83
|
+
recommendation: "Add algorithm validation or formal verification requirements." },
|
|
84
|
+
{ trait: "Compositional", bit: 20, severity: "low",
|
|
85
|
+
missingWhat: "modularity/interface requirements",
|
|
86
|
+
keywords: /module|layer|component|partition|encapsulat|abstraction|API|decompos|architect/i,
|
|
87
|
+
recommendation: "Add modularity, decomposition, or internal interface requirements." },
|
|
88
|
+
{ trait: "Normative", bit: 21, severity: "medium",
|
|
89
|
+
missingWhat: "constraint validation requirements",
|
|
90
|
+
keywords: /constraint|rule|limit|bound|threshold|policy|enforce|comply|govern/i,
|
|
91
|
+
recommendation: "Add constraint enforcement or policy compliance requirements." },
|
|
92
|
+
{ trait: "Meta", bit: 22, severity: "low",
|
|
93
|
+
missingWhat: "configuration/metadata requirements",
|
|
94
|
+
keywords: /config|metadata|schema|version|self.?test|self.?diagnos|introspect|calibrat/i,
|
|
95
|
+
recommendation: "Add configuration management, versioning, or self-test requirements." },
|
|
96
|
+
{ trait: "Temporal", bit: 23, severity: "medium",
|
|
97
|
+
missingWhat: "timing/scheduling requirements",
|
|
98
|
+
keywords: /timing|sequence|schedul|deadline|period|interval|cycle|synchron|clock|real.?time/i,
|
|
99
|
+
recommendation: "Add timing, scheduling, or real-time requirements." },
|
|
100
|
+
{ trait: "Digital/Virtual", bit: 24, severity: "medium",
|
|
101
|
+
missingWhat: "cybersecurity requirements",
|
|
102
|
+
keywords: /security|authenticat|encrypt|access.?control|authoriz|cyber|firewall|TLS|SSL|certificate/i,
|
|
103
|
+
recommendation: "Add cybersecurity, authentication, or encryption requirements." },
|
|
104
|
+
// Social Layer (25-32)
|
|
105
|
+
{ trait: "Social Construct", bit: 25, severity: "low",
|
|
106
|
+
missingWhat: "stakeholder/user requirements",
|
|
107
|
+
keywords: /stakeholder|user|role|persona|scenario|use.?case|workflow|actor/i,
|
|
108
|
+
recommendation: "Add stakeholder, user role, or use case requirements." },
|
|
109
|
+
{ trait: "Institutionally Defined", bit: 26, severity: "medium",
|
|
110
|
+
missingWhat: "standard/specification references",
|
|
111
|
+
keywords: /standard|specification|definition|classif|categor|nomenclature|taxonomy|MIL.?STD|DEF.?STAN/i,
|
|
112
|
+
recommendation: "Add references to applicable standards or specifications." },
|
|
113
|
+
{ trait: "Identity-Linked", bit: 27, severity: "low",
|
|
114
|
+
missingWhat: "access control/identity requirements",
|
|
115
|
+
keywords: /role|identity|credential|permission|access|privilege|authenticat|authoriz/i,
|
|
116
|
+
recommendation: "Add access control, role-based, or identity management requirements." },
|
|
117
|
+
{ trait: "Regulated", bit: 28, severity: "medium",
|
|
118
|
+
missingWhat: "compliance/certification requirements",
|
|
119
|
+
keywords: /standard|compliance|certif|approv|regulat|accredit|qualif|homologat|type.?approv/i,
|
|
120
|
+
recommendation: "Add compliance, certification, or regulatory approval requirements." },
|
|
121
|
+
{ trait: "Economically Significant", bit: 29, severity: "low",
|
|
122
|
+
missingWhat: "cost/lifecycle requirements",
|
|
123
|
+
keywords: /cost|budget|price|procurement|COTS|lifecycle|maintenance|obsolescence|sustainab/i,
|
|
124
|
+
recommendation: "Add cost, lifecycle, or obsolescence management requirements." },
|
|
125
|
+
{ trait: "Politicised", bit: 30, severity: "low",
|
|
126
|
+
missingWhat: "export control/classification requirements",
|
|
127
|
+
keywords: /export.?control|ITAR|EAR|classification|restricted|controlled|clearance|secret|classified/i,
|
|
128
|
+
recommendation: "Add export control, security classification, or handling requirements." },
|
|
129
|
+
{ trait: "Ritualised", bit: 31, severity: "low",
|
|
130
|
+
missingWhat: "procedure/protocol requirements",
|
|
131
|
+
keywords: /procedure|protocol|ceremony|ritual|formal|checklist|handover|commissioning/i,
|
|
132
|
+
recommendation: "Add procedural, commissioning, or handover requirements." },
|
|
133
|
+
{ trait: "Ethically Significant", bit: 32, severity: "medium",
|
|
134
|
+
missingWhat: "ethical/safety requirements",
|
|
135
|
+
keywords: /ethical|moral|harm|consent|privacy|bias|proportional|rules.?of.?engagement|collateral/i,
|
|
136
|
+
recommendation: "Add ethical, proportionality, or rules-of-engagement requirements." },
|
|
137
|
+
];
|
|
4
138
|
// ── Constants ────────────────────────────────────────────────
|
|
5
139
|
const PAGE_SIZE = 100;
|
|
6
140
|
const MAX_PAGES = 50;
|
|
@@ -179,128 +313,46 @@ function topConcepts(conceptRefs, maxCount) {
|
|
|
179
313
|
function analyzeFindings(concepts, comparisons, requirements, traceLinks, reqTierMap, conceptRefs, options) {
|
|
180
314
|
const findings = [];
|
|
181
315
|
const conceptMap = new Map(concepts.map(c => [c.name, c]));
|
|
182
|
-
// Helper: get requirement
|
|
316
|
+
// Helper: get all requirement text for a concept (joined)
|
|
183
317
|
const reqTextsFor = (c) => c.reqs.map(ref => requirements.find(r => r.ref === ref)?.text ?? "").join(" ");
|
|
184
|
-
// ──
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const safetyKeywords = /safety|watchdog|override|kill.?switch|emergency.?stop|fail.?safe|interlock|manual.?override|human.?in.?the.?loop/i;
|
|
223
|
-
for (const c of concepts) {
|
|
224
|
-
if (!c.isAutonomous)
|
|
225
|
-
continue;
|
|
226
|
-
const allText = reqTextsFor(c);
|
|
227
|
-
if (!safetyKeywords.test(allText)) {
|
|
228
|
-
findings.push({
|
|
229
|
-
severity: "high",
|
|
230
|
-
category: "Ontological Mismatch",
|
|
231
|
-
title: `"${c.name}" is Functionally Autonomous but has no safety constraints`,
|
|
232
|
-
description: `UHT classifies "${c.name}" (${c.hexCode}) as autonomous, but no requirements specify safety constraints, watchdog, override, or emergency stop.`,
|
|
233
|
-
affectedReqs: c.reqs,
|
|
234
|
-
recommendation: `Add safety requirements: override mechanism, watchdog timer, fail-safe state, or human-in-the-loop constraints for "${c.name}".`,
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
// ── 1d. Human-Interactive without usability ────────────────
|
|
239
|
-
const usabilityKeywords = /hmi|display|ergonomic|response.?time|feedback|indicator|user.?interface|touch|screen|readab|legib|contrast|font|icon/i;
|
|
240
|
-
for (const c of concepts) {
|
|
241
|
-
if (!c.isHumanInteractive)
|
|
242
|
-
continue;
|
|
243
|
-
const allText = reqTextsFor(c);
|
|
244
|
-
if (!usabilityKeywords.test(allText)) {
|
|
245
|
-
findings.push({
|
|
246
|
-
severity: "medium",
|
|
247
|
-
category: "Ontological Mismatch",
|
|
248
|
-
title: `"${c.name}" is Human-Interactive but has no usability requirements`,
|
|
249
|
-
description: `UHT classifies "${c.name}" (${c.hexCode}) as human-interactive, but no requirements address HMI, display, ergonomics, or user feedback.`,
|
|
250
|
-
affectedReqs: c.reqs,
|
|
251
|
-
recommendation: `Add usability requirements: display format, response time, ergonomic constraints, or feedback indicators for "${c.name}".`,
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
// ── 1e. Digital/Virtual without cybersecurity ──────────────
|
|
256
|
-
const cyberKeywords = /security|authenticat|encrypt|access.?control|authoriz|cyber|firewall|certificate|tls|ssl|password|credential|privilege/i;
|
|
257
|
-
for (const c of concepts) {
|
|
258
|
-
if (!c.isDigitalVirtual)
|
|
259
|
-
continue;
|
|
260
|
-
const allText = reqTextsFor(c);
|
|
261
|
-
if (!cyberKeywords.test(allText)) {
|
|
262
|
-
findings.push({
|
|
263
|
-
severity: "medium",
|
|
264
|
-
category: "Ontological Mismatch",
|
|
265
|
-
title: `"${c.name}" is Digital/Virtual but has no cybersecurity requirements`,
|
|
266
|
-
description: `UHT classifies "${c.name}" (${c.hexCode}) as digital/virtual, but no requirements address security, authentication, encryption, or access control.`,
|
|
267
|
-
affectedReqs: c.reqs,
|
|
268
|
-
recommendation: `Add cybersecurity requirements: authentication, encryption, access control, or data protection for "${c.name}".`,
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
// ── 1f. Regulated without compliance ──────────────────────
|
|
273
|
-
const complianceKeywords = /standard|compliance|certif|approv|regulat|accredit|qualif|homologat|type.?approv|conformance|audit/i;
|
|
274
|
-
for (const c of concepts) {
|
|
275
|
-
if (!c.isRegulated)
|
|
276
|
-
continue;
|
|
277
|
-
const allText = reqTextsFor(c);
|
|
278
|
-
if (!complianceKeywords.test(allText)) {
|
|
279
|
-
findings.push({
|
|
280
|
-
severity: "medium",
|
|
281
|
-
category: "Ontological Mismatch",
|
|
282
|
-
title: `"${c.name}" is Regulated but has no compliance requirements`,
|
|
283
|
-
description: `UHT classifies "${c.name}" (${c.hexCode}) as regulated, but no requirements reference standards, certification, or compliance.`,
|
|
284
|
-
affectedReqs: c.reqs,
|
|
285
|
-
recommendation: `Add compliance requirements: applicable standards, certification authority, or qualification criteria for "${c.name}".`,
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
// ── 1g. System-Essential without redundancy ────────────────
|
|
290
|
-
const redundancyKeywords = /redundan|failover|backup|graceful.?degrad|hot.?standby|cold.?standby|replicate|fault.?toleran|availability|\d+\s*%.*uptime|single.?point.?of.?failure/i;
|
|
291
|
-
for (const c of concepts) {
|
|
292
|
-
if (!c.isSystemEssential)
|
|
293
|
-
continue;
|
|
294
|
-
const allText = reqTextsFor(c);
|
|
295
|
-
if (!redundancyKeywords.test(allText)) {
|
|
296
|
-
findings.push({
|
|
297
|
-
severity: "medium",
|
|
298
|
-
category: "Ontological Mismatch",
|
|
299
|
-
title: `"${c.name}" is System-Essential but has no redundancy requirements`,
|
|
300
|
-
description: `UHT classifies "${c.name}" (${c.hexCode}) as system-essential, but no requirements address redundancy, failover, or fault tolerance.`,
|
|
301
|
-
affectedReqs: c.reqs,
|
|
302
|
-
recommendation: `Add redundancy or availability requirements: failover mechanism, backup, or graceful degradation for "${c.name}".`,
|
|
303
|
-
});
|
|
318
|
+
// ── UHT Trait Checks (all 32 traits, data-driven) ─────────
|
|
319
|
+
for (const check of TRAIT_CHECKS) {
|
|
320
|
+
for (const c of concepts) {
|
|
321
|
+
if (check.inverted) {
|
|
322
|
+
// Inverted check (Physical Object): fires when trait ABSENT but keywords PRESENT
|
|
323
|
+
if (c.traitSet.has(check.trait))
|
|
324
|
+
continue;
|
|
325
|
+
const matchingReqs = c.reqs.filter(ref => {
|
|
326
|
+
const req = requirements.find(r => r.ref === ref);
|
|
327
|
+
return req?.text && check.keywords.test(req.text);
|
|
328
|
+
});
|
|
329
|
+
if (matchingReqs.length > 0) {
|
|
330
|
+
findings.push({
|
|
331
|
+
severity: check.severity,
|
|
332
|
+
category: "Ontological Mismatch",
|
|
333
|
+
title: `"${c.name}" lacks ${check.trait} trait but has ${check.missingWhat}`,
|
|
334
|
+
description: `UHT classifies "${c.name}" (${c.hexCode}) without ${check.trait}, but ${matchingReqs.length} requirement(s) impose related constraints.`,
|
|
335
|
+
affectedReqs: matchingReqs,
|
|
336
|
+
recommendation: check.recommendation,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
// Standard check: fires when trait PRESENT but keywords ABSENT
|
|
342
|
+
if (!c.traitSet.has(check.trait))
|
|
343
|
+
continue;
|
|
344
|
+
const allText = reqTextsFor(c);
|
|
345
|
+
if (!check.keywords.test(allText)) {
|
|
346
|
+
findings.push({
|
|
347
|
+
severity: check.severity,
|
|
348
|
+
category: "Ontological Mismatch",
|
|
349
|
+
title: `"${c.name}" is ${check.trait} but has no ${check.missingWhat}`,
|
|
350
|
+
description: `UHT classifies "${c.name}" (${c.hexCode}) with ${check.trait} (bit ${check.bit}), but no requirements address this.`,
|
|
351
|
+
affectedReqs: c.reqs,
|
|
352
|
+
recommendation: check.recommendation,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
304
356
|
}
|
|
305
357
|
}
|
|
306
358
|
// ── 2. Abstract metrics without statistical parameters ────
|
|
@@ -368,12 +420,14 @@ function analyzeFindings(concepts, comparisons, requirements, traceLinks, reqTie
|
|
|
368
420
|
const b = conceptMap.get(comp.candidate);
|
|
369
421
|
if (!a || !b)
|
|
370
422
|
continue;
|
|
371
|
-
|
|
423
|
+
const aPhys = a.traitSet.has("Physical Object");
|
|
424
|
+
const bPhys = b.traitSet.has("Physical Object");
|
|
425
|
+
if (comp.jaccard_similarity > options.jaccardThreshold && aPhys !== bPhys) {
|
|
372
426
|
findings.push({
|
|
373
427
|
severity: "low",
|
|
374
428
|
category: "Ontological Ambiguity",
|
|
375
|
-
title: `"${a.name}" and "${b.name}" similar (${(comp.jaccard_similarity * 100).toFixed(0)}%) but differ in
|
|
376
|
-
description: `"${a.name}" is ${
|
|
429
|
+
title: `"${a.name}" and "${b.name}" similar (${(comp.jaccard_similarity * 100).toFixed(0)}%) but differ in Physical Object trait`,
|
|
430
|
+
description: `"${a.name}" is ${aPhys ? "" : "not "}Physical Object; "${b.name}" is ${bPhys ? "" : "not "}. High Jaccard similarity suggests they should be treated consistently.`,
|
|
377
431
|
affectedReqs: [...a.reqs, ...b.reqs],
|
|
378
432
|
recommendation: "Review whether both concepts should have consistent physical classification.",
|
|
379
433
|
});
|
|
@@ -638,6 +692,7 @@ export function registerLintCommands(program, client) {
|
|
|
638
692
|
.option("--save-baseline <file>", "Write current finding titles to a baseline file for future suppression")
|
|
639
693
|
.option("--threshold <n>", "Jaccard similarity threshold (0.0-1.0)", "0.6")
|
|
640
694
|
.option("--spray-threshold <n>", "Outgoing trace link count for spray pattern detection", "8")
|
|
695
|
+
.option("--min-severity <level>", "Only show findings at this severity or above: low, medium, high", "low")
|
|
641
696
|
.action(async (tenant, project, opts) => {
|
|
642
697
|
const uht = new UhtClient();
|
|
643
698
|
if (!uht.isConfigured) {
|
|
@@ -675,18 +730,11 @@ export function registerLintCommands(program, client) {
|
|
|
675
730
|
const [name, refs] = top[i];
|
|
676
731
|
if (entry.result) {
|
|
677
732
|
const traitNames = entry.result.traits.map(t => t.name).filter(Boolean);
|
|
678
|
-
const has = (t) => traitNames.includes(t);
|
|
679
733
|
concepts.push({
|
|
680
734
|
name,
|
|
681
735
|
hexCode: entry.result.hex_code,
|
|
682
736
|
traits: traitNames,
|
|
683
|
-
|
|
684
|
-
isPowered: has("Powered"),
|
|
685
|
-
isHumanInteractive: has("Human-Interactive"),
|
|
686
|
-
isAutonomous: has("Functionally Autonomous"),
|
|
687
|
-
isSystemEssential: has("System-Essential"),
|
|
688
|
-
isDigitalVirtual: has("Digital/Virtual"),
|
|
689
|
-
isRegulated: has("Regulated"),
|
|
737
|
+
traitSet: new Set(traitNames),
|
|
690
738
|
reqs: refs,
|
|
691
739
|
});
|
|
692
740
|
console.error(` ✓ ${name} → ${entry.result.hex_code} (${traitNames.length} traits)`);
|
|
@@ -744,6 +792,16 @@ export function registerLintCommands(program, client) {
|
|
|
744
792
|
if (suppressed > 0)
|
|
745
793
|
console.error(`Suppressed ${suppressed} known finding(s).`);
|
|
746
794
|
}
|
|
795
|
+
// Step 5d: Apply severity filter
|
|
796
|
+
const sevOrder = { high: 0, medium: 1, low: 2 };
|
|
797
|
+
const minSev = sevOrder[opts.minSeverity] ?? 2;
|
|
798
|
+
if (minSev < 2) {
|
|
799
|
+
const before = findings.length;
|
|
800
|
+
findings = findings.filter(f => sevOrder[f.severity] <= minSev);
|
|
801
|
+
const filtered = before - findings.length;
|
|
802
|
+
if (filtered > 0)
|
|
803
|
+
console.error(`Filtered ${filtered} finding(s) below ${opts.minSeverity} severity.`);
|
|
804
|
+
}
|
|
747
805
|
// Step 6: Output report
|
|
748
806
|
let report;
|
|
749
807
|
if (opts.format === "json" || isJsonMode()) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
1
2
|
import { output, printTable, isJsonMode, truncate } from "../output.js";
|
|
2
3
|
import { resolveRequirementId } from "../resolve.js";
|
|
3
4
|
export function registerRequirementCommands(program, client) {
|
|
@@ -8,12 +9,38 @@ export function registerRequirementCommands(program, client) {
|
|
|
8
9
|
.argument("<tenant>", "Tenant slug")
|
|
9
10
|
.argument("<project>", "Project slug")
|
|
10
11
|
.option("-p, --page <n>", "Page number", "1")
|
|
11
|
-
.option("-l, --limit <n>", "Items per page", "
|
|
12
|
+
.option("-l, --limit <n>", "Items per page (use 'all' to fetch everything)", "50")
|
|
12
13
|
.option("--sort <field>", "Sort by: ref, createdAt, qaScore")
|
|
13
14
|
.option("--order <dir>", "Sort order: asc, desc")
|
|
14
15
|
.option("--tags <tags>", "Comma-separated tags to filter by (server-side)")
|
|
15
16
|
.option("--document <slug>", "Filter by document slug (server-side)")
|
|
16
17
|
.action(async (tenant, project, opts) => {
|
|
18
|
+
// Handle --limit all: fetch all pages
|
|
19
|
+
if (opts.limit.toLowerCase() === "all") {
|
|
20
|
+
const all = [];
|
|
21
|
+
for (let page = 1; page <= 50; page++) {
|
|
22
|
+
const params = {
|
|
23
|
+
page: String(page), limit: "500",
|
|
24
|
+
sortBy: opts.sort, sortOrder: opts.order,
|
|
25
|
+
};
|
|
26
|
+
if (opts.tags)
|
|
27
|
+
params.tags = opts.tags;
|
|
28
|
+
if (opts.document)
|
|
29
|
+
params.documentSlug = opts.document;
|
|
30
|
+
const data = await client.get(`/requirements/${tenant}/${project}`, params);
|
|
31
|
+
all.push(...(data.data ?? []));
|
|
32
|
+
if (page >= (data.meta?.totalPages ?? 1))
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
if (isJsonMode()) {
|
|
36
|
+
output({ data: all, meta: { totalItems: all.length } });
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
console.log(`All requirements: ${all.length}\n`);
|
|
40
|
+
printTable(["Ref", "Text", "Pattern", "QA", "Tags"], all.map(r => [r.ref ?? "?", truncate(r.text ?? "", 60), r.pattern ?? "", r.qaScore != null ? String(r.qaScore) : "-", (r.tags ?? []).join(", ")]));
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
17
44
|
const params = {
|
|
18
45
|
page: opts.page,
|
|
19
46
|
limit: opts.limit,
|
|
@@ -258,4 +285,113 @@ export function registerRequirementCommands(program, client) {
|
|
|
258
285
|
]));
|
|
259
286
|
}
|
|
260
287
|
});
|
|
288
|
+
// ── reassign: move requirement to a different document/section ──
|
|
289
|
+
cmd
|
|
290
|
+
.command("reassign")
|
|
291
|
+
.description("Move a requirement to a different document/section (preserves ID and trace links)")
|
|
292
|
+
.argument("<tenant>", "Tenant slug")
|
|
293
|
+
.argument("<project>", "Project slug")
|
|
294
|
+
.argument("<id>", "Requirement ref, ID, or hashId")
|
|
295
|
+
.requiredOption("--section <id>", "Target section ID (determines the target document)")
|
|
296
|
+
.action(async (tenant, project, id, opts) => {
|
|
297
|
+
const resolvedId = await resolveRequirementId(client, tenant, project, id);
|
|
298
|
+
await client.patch(`/requirements/${tenant}/${project}/${resolvedId}`, {
|
|
299
|
+
sectionId: opts.section,
|
|
300
|
+
});
|
|
301
|
+
if (isJsonMode()) {
|
|
302
|
+
output({ ok: true, movedTo: opts.section });
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
console.log(`Requirement reassigned to section ${opts.section}. Ref will update to match new document prefix.`);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
// ── bulk-create: create multiple requirements from JSON array ──
|
|
309
|
+
cmd
|
|
310
|
+
.command("bulk-create")
|
|
311
|
+
.description("Create multiple requirements from a JSON file (array of objects)")
|
|
312
|
+
.argument("<tenant>", "Tenant slug")
|
|
313
|
+
.argument("<project-key>", "Project key")
|
|
314
|
+
.requiredOption("--file <path>", "Path to JSON file (array of {text, document?, section?, verification?, rationale?, tags?, idempotencyKey?})")
|
|
315
|
+
.option("--dry-run", "Validate without creating")
|
|
316
|
+
.action(async (tenant, projectKey, opts) => {
|
|
317
|
+
const content = readFileSync(opts.file, "utf-8");
|
|
318
|
+
let items;
|
|
319
|
+
try {
|
|
320
|
+
items = JSON.parse(content);
|
|
321
|
+
if (!Array.isArray(items))
|
|
322
|
+
throw new Error("File must contain a JSON array");
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
console.error(`Invalid JSON: ${err.message}`);
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
console.error(`Processing ${items.length} requirement(s)...`);
|
|
329
|
+
let created = 0, skipped = 0, errors = 0;
|
|
330
|
+
for (let i = 0; i < items.length; i++) {
|
|
331
|
+
const item = items[i];
|
|
332
|
+
const text = String(item.text ?? "").trim();
|
|
333
|
+
if (!text || text.length < 10) {
|
|
334
|
+
console.error(` [${i}] Skipped: text too short`);
|
|
335
|
+
skipped++;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (opts.dryRun) {
|
|
339
|
+
console.log(` [dry-run] ${truncate(text, 80)}`);
|
|
340
|
+
created++;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
const body = { tenant, projectKey, text };
|
|
345
|
+
if (item.document || item.documentSlug)
|
|
346
|
+
body.documentSlug = item.document ?? item.documentSlug;
|
|
347
|
+
if (item.section || item.sectionId)
|
|
348
|
+
body.sectionId = item.section ?? item.sectionId;
|
|
349
|
+
if (item.verification)
|
|
350
|
+
body.verification = item.verification;
|
|
351
|
+
if (item.rationale)
|
|
352
|
+
body.rationale = item.rationale;
|
|
353
|
+
if (item.pattern)
|
|
354
|
+
body.pattern = item.pattern;
|
|
355
|
+
if (item.compliance)
|
|
356
|
+
body.complianceStatus = item.compliance;
|
|
357
|
+
if (item.idempotencyKey)
|
|
358
|
+
body.idempotencyKey = item.idempotencyKey;
|
|
359
|
+
if (Array.isArray(item.tags))
|
|
360
|
+
body.tags = item.tags;
|
|
361
|
+
else if (typeof item.tags === "string")
|
|
362
|
+
body.tags = item.tags.split(",").map(t => t.trim());
|
|
363
|
+
await client.post("/requirements", body);
|
|
364
|
+
created++;
|
|
365
|
+
}
|
|
366
|
+
catch (err) {
|
|
367
|
+
console.error(` [${i}] Error: ${err.message}`);
|
|
368
|
+
errors++;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
console.log(`${opts.dryRun ? "Would create" : "Created"} ${created}. Skipped: ${skipped}. Errors: ${errors}.`);
|
|
372
|
+
});
|
|
373
|
+
// ── text-search: search requirements by text content ──
|
|
374
|
+
cmd
|
|
375
|
+
.command("text-search")
|
|
376
|
+
.description("Search requirements by text content (case-insensitive substring match)")
|
|
377
|
+
.argument("<tenant>", "Tenant slug")
|
|
378
|
+
.argument("<project>", "Project slug")
|
|
379
|
+
.requiredOption("--text <query>", "Text to search for")
|
|
380
|
+
.option("-l, --limit <n>", "Max results", "50")
|
|
381
|
+
.action(async (tenant, project, opts) => {
|
|
382
|
+
const data = await client.get(`/requirements/${tenant}/${project}`, { textContains: opts.text, limit: opts.limit, sortBy: "ref", sortOrder: "asc" });
|
|
383
|
+
const reqs = data.data ?? [];
|
|
384
|
+
if (isJsonMode()) {
|
|
385
|
+
output(reqs);
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
console.log(`Found ${reqs.length} requirement(s) matching "${opts.text}":\n`);
|
|
389
|
+
printTable(["Ref", "Text", "Document", "QA"], reqs.map(r => [
|
|
390
|
+
r.ref ?? "?",
|
|
391
|
+
truncate(r.text ?? "", 55),
|
|
392
|
+
r.documentSlug ?? "",
|
|
393
|
+
r.qaScore != null ? String(r.qaScore) : "-",
|
|
394
|
+
]));
|
|
395
|
+
}
|
|
396
|
+
});
|
|
261
397
|
}
|