airgen-cli 0.1.7 → 0.2.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/README.md ADDED
@@ -0,0 +1,261 @@
1
+ # airgen-cli
2
+
3
+ Requirements engineering from the command line. Manage requirements, architecture diagrams, traceability, baselines, and more — all from your terminal.
4
+
5
+ Pairs with [AIRGen Studio](https://airgen.studio) and the AIRGen MCP server.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g airgen-cli
11
+ ```
12
+
13
+ ## Configuration
14
+
15
+ Set credentials via environment variables or `~/.airgenrc`:
16
+
17
+ ```bash
18
+ # Environment variables
19
+ export AIRGEN_API_URL=https://api.airgen.studio/api
20
+ export AIRGEN_EMAIL=you@example.com
21
+ export AIRGEN_PASSWORD=your-password
22
+ ```
23
+
24
+ Or create `~/.airgenrc`:
25
+
26
+ ```json
27
+ {
28
+ "apiUrl": "https://api.airgen.studio/api",
29
+ "email": "you@example.com",
30
+ "password": "your-password"
31
+ }
32
+ ```
33
+
34
+ For semantic linting, also set a UHT token:
35
+
36
+ ```bash
37
+ export UHT_API_KEY=your-token # or UHT_TOKEN
38
+ ```
39
+
40
+ ## Quick start
41
+
42
+ ```bash
43
+ # List your tenants and projects
44
+ airgen tenants list
45
+ airgen projects list my-tenant
46
+
47
+ # List requirements
48
+ airgen reqs list my-tenant my-project
49
+
50
+ # Render a diagram in the terminal
51
+ airgen diag list my-tenant my-project
52
+ airgen diag render my-tenant my-project diagram-123
53
+
54
+ # Run semantic lint
55
+ airgen lint my-tenant my-project
56
+
57
+ # Get a compliance report
58
+ airgen report compliance my-tenant my-project
59
+ ```
60
+
61
+ ## Global options
62
+
63
+ | Flag | Description |
64
+ |---|---|
65
+ | `--json` | Output as JSON (works with any command) |
66
+ | `-V, --version` | Print version |
67
+ | `-h, --help` | Show help |
68
+
69
+ ## Commands
70
+
71
+ ### Tenants & Projects
72
+
73
+ ```bash
74
+ airgen tenants list # List all tenants
75
+ airgen projects list <tenant> # List projects in a tenant
76
+ airgen projects create <tenant> --name "X" # Create a project
77
+ airgen projects delete <tenant> <project> # Delete a project
78
+ ```
79
+
80
+ ### Requirements
81
+
82
+ ```bash
83
+ airgen reqs list <tenant> <project> # List (paginated)
84
+ airgen reqs list <tenant> <project> --page 2 --limit 50
85
+ airgen reqs get <tenant> <project> <ref> # Full detail
86
+ airgen reqs create <tenant> <project> --text "The system shall..."
87
+ airgen reqs update <tenant> <project> <id> --text "..." --tags safety,critical
88
+ airgen reqs delete <tenant> <project> <id> # Soft-delete
89
+ airgen reqs history <tenant> <project> <id> # Version history
90
+ airgen reqs search <tenant> <project> --query "thermal" --mode semantic
91
+ airgen reqs filter <tenant> <project> --pattern functional --tag safety
92
+ ```
93
+
94
+ ### Architecture Diagrams
95
+
96
+ ```bash
97
+ airgen diag list <tenant> <project> # List diagrams
98
+ airgen diag get <tenant> <project> <id> # Blocks + connectors JSON
99
+ airgen diag render <tenant> <project> <id> # Terminal display (default)
100
+ airgen diag render <tenant> <project> <id> --format mermaid # Mermaid syntax
101
+ airgen diag render <tenant> <project> <id> --format mermaid --wrap -o diagram.md
102
+ airgen diag create <tenant> <project> --name "X" --view block
103
+ airgen diag update <tenant> <project> <id> --name "Y"
104
+ airgen diag delete <tenant> <project> <id>
105
+ ```
106
+
107
+ **Blocks:**
108
+
109
+ ```bash
110
+ airgen diag blocks library <tenant> <project>
111
+ airgen diag blocks create <tenant> <project> --diagram <id> --name "X" --kind subsystem
112
+ airgen diag blocks delete <tenant> <project> <block-id>
113
+ ```
114
+
115
+ **Connectors:**
116
+
117
+ ```bash
118
+ airgen diag conn create <tenant> <project> --diagram <id> --source <id> --target <id> --kind flow --label "data"
119
+ airgen diag conn delete <tenant> <project> <conn-id> --diagram <id>
120
+ ```
121
+
122
+ ### Traceability
123
+
124
+ ```bash
125
+ airgen trace list <tenant> <project> # List trace links
126
+ airgen trace create <tenant> <project> --source <id> --target <id> --type derives
127
+ airgen trace delete <tenant> <project> <link-id>
128
+ airgen trace linksets list <tenant> <project> # Document linksets
129
+ ```
130
+
131
+ ### Baselines
132
+
133
+ ```bash
134
+ airgen bl list <tenant> <project>
135
+ airgen bl create <tenant> <project> --name "v1.0"
136
+ airgen bl compare <tenant> <project> --from <id1> --to <id2>
137
+ ```
138
+
139
+ ### Quality & AI
140
+
141
+ ```bash
142
+ airgen qa analyze "The system shall..." # Analyze single requirement
143
+ airgen qa score start <tenant> <project> # Background QA scoring
144
+ airgen qa draft "user needs thermal imaging" # Draft requirements from NL
145
+
146
+ airgen ai generate <tenant> <project> --prompt "..." # Generate candidates
147
+ airgen ai candidates <tenant> <project> # List pending candidates
148
+ airgen ai accept <candidate-id> # Promote to requirement
149
+ airgen ai reject <candidate-id> # Reject candidate
150
+ ```
151
+
152
+ ### Reports
153
+
154
+ ```bash
155
+ airgen report stats <tenant> <project> # Overview statistics
156
+ airgen report quality <tenant> <project> # QA score summary
157
+ airgen report compliance <tenant> <project> # Compliance + impl status
158
+ airgen report orphans <tenant> <project> # Untraced requirements
159
+ ```
160
+
161
+ All report commands auto-paginate through the full requirement set (up to 5000).
162
+
163
+ ### Implementation Tracking
164
+
165
+ ```bash
166
+ airgen impl status <tenant> <project> <req> --status implemented --notes "done in v2"
167
+ airgen impl summary <tenant> <project> # Coverage breakdown
168
+ airgen impl list <tenant> <project> --status blocked # Filter by status
169
+ airgen impl bulk-update <tenant> <project> --file updates.json
170
+
171
+ # Artifact linking
172
+ airgen impl link <tenant> <project> <req> --type file --path src/engine.ts
173
+ airgen impl unlink <tenant> <project> <req> --artifact <id>
174
+ ```
175
+
176
+ **Statuses:** `not_started`, `in_progress`, `implemented`, `verified`, `blocked`
177
+
178
+ **Bulk update file format:**
179
+
180
+ ```json
181
+ [
182
+ { "ref": "REQ-001", "status": "implemented", "notes": "shipped" },
183
+ { "ref": "REQ-002", "status": "in_progress" }
184
+ ]
185
+ ```
186
+
187
+ ### Import / Export
188
+
189
+ ```bash
190
+ airgen import requirements <tenant> <project> --file reqs.csv
191
+ airgen export requirements <tenant> <project> # Markdown
192
+ airgen export requirements <tenant> <project> --json # JSON
193
+ ```
194
+
195
+ ### Activity
196
+
197
+ ```bash
198
+ airgen activity list <tenant> <project> # Recent activity
199
+ airgen activity list <tenant> <project> --limit 50
200
+ ```
201
+
202
+ ### Documents
203
+
204
+ ```bash
205
+ airgen docs list <tenant> <project>
206
+ airgen docs get <tenant> <project> <slug>
207
+ airgen docs create <tenant> <project> --title "X" --kind structured
208
+ airgen docs delete <tenant> <project> <slug>
209
+ airgen docs export <tenant> <project> <slug> # Markdown export
210
+ airgen docs sec list <tenant> <project> <slug> # List sections
211
+ ```
212
+
213
+ ### Semantic Lint
214
+
215
+ Classifies domain concepts from your requirements using the [Universal Hex Taxonomy](https://universalhex.org) and flags ontological issues, structural problems, and coverage gaps.
216
+
217
+ ```bash
218
+ airgen lint <tenant> <project> # Full lint (top 15 concepts)
219
+ airgen lint <tenant> <project> --concepts 20 # Classify more concepts
220
+ airgen lint <tenant> <project> --format markdown -o lint-report.md
221
+ airgen lint <tenant> <project> --format json # Machine-readable
222
+ ```
223
+
224
+ **What it detects:**
225
+
226
+ - Ontological mismatches (e.g., non-physical entity with physical constraints)
227
+ - Abstract metrics missing statistical parameters
228
+ - Verification requirements mixed with functional requirements
229
+ - Degraded modes without performance criteria
230
+ - Ontological ambiguity between similar concepts
231
+ - Requirements lacking "shall" keyword
232
+
233
+ **Requires:** `UHT_TOKEN` or `UHT_API_KEY` environment variable. Get a token at [universalhex.org](https://universalhex.org).
234
+
235
+ ## JSON mode
236
+
237
+ Append `--json` to any command for machine-readable output:
238
+
239
+ ```bash
240
+ airgen reqs list my-tenant my-project --json | jq '.[].ref'
241
+ airgen report compliance my-tenant my-project --json | jq '.summary'
242
+ ```
243
+
244
+ ## Aliases
245
+
246
+ | Full command | Alias |
247
+ |---|---|
248
+ | `requirements` | `reqs` |
249
+ | `diagrams` | `diag` |
250
+ | `documents` | `docs` |
251
+ | `connectors` | `conn` |
252
+ | `baselines` | `bl` |
253
+ | `traces` | `trace` |
254
+ | `quality` | `qa` |
255
+ | `reports` | `report` |
256
+ | `projects` | `proj` |
257
+ | `sections` | `sec` |
258
+
259
+ ## License
260
+
261
+ MIT
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ import type { AirgenClient } from "../client.js";
3
+ export declare function registerLintCommands(program: Command, client: AirgenClient): void;
@@ -0,0 +1,415 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { UhtClient } from "../uht-client.js";
3
+ import { isJsonMode } from "../output.js";
4
+ // ── Constants ────────────────────────────────────────────────
5
+ const PAGE_SIZE = 100;
6
+ const MAX_PAGES = 50;
7
+ // ── Helpers ──────────────────────────────────────────────────
8
+ async function fetchAllRequirements(client, tenant, project) {
9
+ const all = [];
10
+ for (let page = 1; page <= MAX_PAGES; page++) {
11
+ const data = await client.get(`/requirements/${tenant}/${project}`, {
12
+ page: String(page),
13
+ limit: String(PAGE_SIZE),
14
+ });
15
+ all.push(...(data.data ?? []));
16
+ if (page >= (data.meta?.totalPages ?? 1))
17
+ break;
18
+ }
19
+ return all.filter(r => !r.deleted && !r.deletedAt);
20
+ }
21
+ /**
22
+ * Extract domain concepts from requirement text.
23
+ * Looks for:
24
+ * - Subjects: "The <concept> shall..."
25
+ * - References: "using the <concept>", "via the <concept>", "from the <concept>"
26
+ * - Named systems: multi-word capitalized terms, known patterns
27
+ */
28
+ function extractConcepts(requirements) {
29
+ const conceptRefs = new Map();
30
+ function addConcept(concept, ref) {
31
+ const normalized = concept.toLowerCase().trim();
32
+ if (normalized.length < 3 || normalized.length > 60)
33
+ return;
34
+ // Skip generic words
35
+ const skip = new Set(["system", "the system", "it", "this", "all", "each", "any"]);
36
+ if (skip.has(normalized))
37
+ return;
38
+ const refs = conceptRefs.get(normalized) ?? [];
39
+ if (!refs.includes(ref))
40
+ refs.push(ref);
41
+ conceptRefs.set(normalized, refs);
42
+ }
43
+ for (const req of requirements) {
44
+ if (!req.text || !req.ref)
45
+ continue;
46
+ const text = req.text;
47
+ // "The <concept> shall"
48
+ const subjectMatch = text.match(/^(?:the|a|an)\s+(.+?)\s+shall\b/i);
49
+ if (subjectMatch)
50
+ addConcept(subjectMatch[1], req.ref);
51
+ // "If the <concept> detects/is/has..."
52
+ const ifMatch = text.match(/^if\s+the\s+(.+?)\s+(?:detects?|is|has|does|fails?|receives?)\b/i);
53
+ if (ifMatch)
54
+ addConcept(ifMatch[1], req.ref);
55
+ // "While the <concept> is..."
56
+ const whileMatch = text.match(/^while\s+(?:the\s+)?(.+?)\s+is\b/i);
57
+ if (whileMatch)
58
+ addConcept(whileMatch[1], req.ref);
59
+ // "When the <concept> designates/detects..."
60
+ const whenMatch = text.match(/^when\s+the\s+(.+?)\s+(?:designates?|detects?|receives?|completes?)\b/i);
61
+ if (whenMatch)
62
+ addConcept(whenMatch[1], req.ref);
63
+ // References: "using the X", "via X", "from the X", "to the X"
64
+ const refPatterns = [
65
+ /using\s+(?:the\s+)?(.+?)(?:\s+(?:for|to|at|in|with)\b|[.,;]|$)/gi,
66
+ /via\s+(?:the\s+)?(.+?)(?:\s+(?:for|to|at|in|with)\b|[.,;]|$)/gi,
67
+ /from\s+the\s+(.+?)(?:\s+(?:for|to|at|in|with)\b|[.,;]|$)/gi,
68
+ /(?:to|into)\s+the\s+(.+?)(?:\s+(?:for|to|at|in|with)\b|[.,;]|$)/gi,
69
+ /(?:against|per|in accordance with)\s+(.+?)(?:\s+(?:for|to|at|in|with)\b|[.,;]|$)/gi,
70
+ ];
71
+ for (const pat of refPatterns) {
72
+ let m;
73
+ while ((m = pat.exec(text)) !== null) {
74
+ addConcept(m[1], req.ref);
75
+ }
76
+ }
77
+ }
78
+ return conceptRefs;
79
+ }
80
+ /**
81
+ * Rank concepts by frequency and pick top N.
82
+ */
83
+ function topConcepts(conceptRefs, maxCount) {
84
+ return [...conceptRefs.entries()]
85
+ .sort((a, b) => b[1].length - a[1].length)
86
+ .slice(0, maxCount);
87
+ }
88
+ // ── Analysis ─────────────────────────────────────────────────
89
+ function analyzeFindings(concepts, comparisons, requirements) {
90
+ const findings = [];
91
+ const conceptMap = new Map(concepts.map(c => [c.name, c]));
92
+ // 1. Physical mismatch: non-physical concepts with environmental/physical requirements
93
+ const envKeywords = /temperature|shock|vibrat|humidity|nbc|contamina|electromagnetic|emc|climatic/i;
94
+ for (const c of concepts) {
95
+ if (c.isPhysical)
96
+ continue;
97
+ const envReqs = c.reqs.filter(ref => {
98
+ const req = requirements.find(r => r.ref === ref);
99
+ return req?.text && envKeywords.test(req.text);
100
+ });
101
+ if (envReqs.length > 0) {
102
+ findings.push({
103
+ severity: "high",
104
+ category: "Ontological Mismatch",
105
+ title: `"${c.name}" lacks Physical Object trait but has physical constraints`,
106
+ description: `UHT classifies "${c.name}" (${c.hexCode}) without the Physical Object trait, but ${envReqs.length} requirement(s) impose physical/environmental constraints on it.`,
107
+ affectedReqs: envReqs,
108
+ recommendation: `Add a requirement defining the physical embodiment of "${c.name}" (e.g., housing, LRU, equipment rack).`,
109
+ });
110
+ }
111
+ }
112
+ // 2. Abstract metrics without statistical parameters
113
+ const metricKeywords = /probability|rate|percentage|ratio|mtbf|availability/i;
114
+ const statKeywords = /confidence|sample size|number of|minimum of \d+ |statistical/i;
115
+ for (const c of concepts) {
116
+ if (c.traits.length > 3)
117
+ continue; // very abstract = few traits
118
+ const metricReqs = c.reqs.filter(ref => {
119
+ const req = requirements.find(r => r.ref === ref);
120
+ return req?.text && metricKeywords.test(req.text);
121
+ });
122
+ if (metricReqs.length === 0)
123
+ continue;
124
+ const hasStats = metricReqs.some(ref => {
125
+ const req = requirements.find(r => r.ref === ref);
126
+ return req?.text && statKeywords.test(req.text);
127
+ });
128
+ if (!hasStats) {
129
+ findings.push({
130
+ severity: "medium",
131
+ category: "Missing Statistical Context",
132
+ title: `"${c.name}" is an abstract metric without statistical parameters`,
133
+ description: `"${c.name}" (${c.hexCode}) has only ${c.traits.length} UHT traits (very abstract). Requirements set thresholds but don't specify confidence level, sample size, or test conditions.`,
134
+ affectedReqs: metricReqs,
135
+ recommendation: `Add statistical parameters (confidence level, sample size, conditions) to requirements referencing "${c.name}".`,
136
+ });
137
+ }
138
+ }
139
+ // 3. Verification requirements mixed with functional requirements
140
+ const verificationReqs = requirements.filter(r => r.text && /shall be verified|verification|shall be demonstrated|shall be tested/i.test(r.text));
141
+ const functionalReqs = requirements.filter(r => r.text && /shall\b/i.test(r.text) && !/shall be verified|verification/i.test(r.text));
142
+ if (verificationReqs.length > 0 && functionalReqs.length > 0) {
143
+ const ratio = verificationReqs.length / requirements.length;
144
+ if (ratio > 0.05 && ratio < 0.95) {
145
+ findings.push({
146
+ severity: "medium",
147
+ category: "Structural Issue",
148
+ title: "Verification requirements mixed with functional requirements",
149
+ description: `${verificationReqs.length} verification requirement(s) (${(ratio * 100).toFixed(0)}%) are co-mingled with ${functionalReqs.length} functional requirements. This makes traceability harder.`,
150
+ affectedReqs: verificationReqs.map(r => r.ref).filter(Boolean),
151
+ recommendation: "Move verification requirements to a separate document or tag them with a distinct pattern. Create trace links to parent functional requirements.",
152
+ });
153
+ }
154
+ }
155
+ // 4. Degraded mode gaps: requirements mentioning "manual", "reversion", "fallback" without performance criteria
156
+ const degradedReqs = requirements.filter(r => r.text && /manual\s+(?:reversion|mode|override|backup)|fallback|degraded/i.test(r.text));
157
+ for (const req of degradedReqs) {
158
+ const hasPerf = /\d+%|\d+\s*(?:second|ms|metre|meter|m\b)/i.test(req.text ?? "");
159
+ if (!hasPerf) {
160
+ findings.push({
161
+ severity: "medium",
162
+ category: "Coverage Gap",
163
+ title: `Degraded mode without performance criteria: ${req.ref}`,
164
+ description: `${req.ref} specifies a degraded/manual mode but provides no acceptance criteria for performance in that mode.`,
165
+ affectedReqs: [req.ref],
166
+ recommendation: "Add measurable performance criteria for degraded operation (e.g., acceptable accuracy, response time, available subsystems).",
167
+ });
168
+ }
169
+ }
170
+ // 5. Cross-comparison: high similarity between concepts in different categories
171
+ for (const batch of comparisons) {
172
+ for (const comp of batch.comparisons) {
173
+ const a = conceptMap.get(batch.entity);
174
+ const b = conceptMap.get(comp.candidate);
175
+ if (!a || !b)
176
+ continue;
177
+ // Different physical classification but high similarity = potential confusion
178
+ if (comp.jaccard_similarity > 0.6 && a.isPhysical !== b.isPhysical) {
179
+ findings.push({
180
+ severity: "low",
181
+ category: "Ontological Ambiguity",
182
+ title: `"${a.name}" and "${b.name}" are similar (${(comp.jaccard_similarity * 100).toFixed(0)}%) but differ in physical classification`,
183
+ description: `"${a.name}" is ${a.isPhysical ? "" : "not "}a Physical Object; "${b.name}" is ${b.isPhysical ? "" : "not "}. High Jaccard similarity (${comp.jaccard_similarity.toFixed(3)}) suggests they should be treated consistently.`,
184
+ affectedReqs: [...a.reqs, ...b.reqs],
185
+ recommendation: `Review whether both concepts should have consistent physical classification. Consider adding clarifying requirements.`,
186
+ });
187
+ }
188
+ }
189
+ }
190
+ // 6. Requirements without "shall" (weak language)
191
+ const weakReqs = requirements.filter(r => r.text && !/\bshall\b/i.test(r.text) && !/shall be verified/i.test(r.text));
192
+ if (weakReqs.length > 0) {
193
+ findings.push({
194
+ severity: "low",
195
+ category: "Language Quality",
196
+ title: `${weakReqs.length} requirement(s) lack "shall" keyword`,
197
+ description: `Requirements without "shall" may be informational text rather than testable requirements.`,
198
+ affectedReqs: weakReqs.map(r => r.ref).filter(Boolean),
199
+ recommendation: 'Rephrase using "shall" for testable requirements, or move informational text to notes/rationale.',
200
+ });
201
+ }
202
+ return findings.sort((a, b) => {
203
+ const sev = { high: 0, medium: 1, low: 2 };
204
+ return sev[a.severity] - sev[b.severity];
205
+ });
206
+ }
207
+ // ── Report formatting ────────────────────────────────────────
208
+ function formatReport(tenant, project, requirements, concepts, comparisons, findings) {
209
+ const lines = [];
210
+ const high = findings.filter(f => f.severity === "high").length;
211
+ const med = findings.filter(f => f.severity === "medium").length;
212
+ const low = findings.filter(f => f.severity === "low").length;
213
+ lines.push(" Semantic Lint Report");
214
+ lines.push(" ════════════════════");
215
+ lines.push(` Project: ${project} (${tenant})`);
216
+ lines.push(` Requirements: ${requirements.length} | Concepts classified: ${concepts.length}`);
217
+ lines.push(` Findings: ${findings.length} (${high} high, ${med} medium, ${low} low)`);
218
+ lines.push("");
219
+ // Concept classifications table
220
+ lines.push(" ┄┄ Concept Classifications ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄");
221
+ lines.push("");
222
+ const nameW = Math.max(...concepts.map(c => c.name.length), 10);
223
+ for (const c of concepts) {
224
+ const phys = c.isPhysical ? "Physical" : "Abstract";
225
+ const pad = " ".repeat(Math.max(0, nameW - c.name.length));
226
+ lines.push(` ${c.name}${pad} ${c.hexCode} ${phys.padEnd(8)} ${c.traits.slice(0, 4).join(", ")}${c.traits.length > 4 ? "..." : ""}`);
227
+ }
228
+ lines.push("");
229
+ // Cross-comparison highlights
230
+ if (comparisons.length > 0) {
231
+ lines.push(" ┄┄ Key Similarities ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄");
232
+ lines.push("");
233
+ for (const batch of comparisons) {
234
+ for (const comp of batch.comparisons) {
235
+ if (comp.jaccard_similarity >= 0.4) {
236
+ const pct = (comp.jaccard_similarity * 100).toFixed(0);
237
+ lines.push(` ${batch.entity} ↔ ${comp.candidate}: ${pct}% Jaccard`);
238
+ }
239
+ }
240
+ }
241
+ lines.push("");
242
+ }
243
+ // Findings
244
+ lines.push(" ┄┄ Findings ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄");
245
+ lines.push("");
246
+ for (let i = 0; i < findings.length; i++) {
247
+ const f = findings[i];
248
+ const sevIcon = f.severity === "high" ? "!!!" : f.severity === "medium" ? " ! " : " . ";
249
+ lines.push(` ${i + 1}. [${sevIcon}] ${f.title}`);
250
+ lines.push(` Category: ${f.category}`);
251
+ lines.push(` ${f.description}`);
252
+ lines.push(` Affects: ${f.affectedReqs.join(", ")}`);
253
+ lines.push(` Fix: ${f.recommendation}`);
254
+ lines.push("");
255
+ }
256
+ if (findings.length === 0) {
257
+ lines.push(" No findings — requirements look clean.");
258
+ lines.push("");
259
+ }
260
+ return lines.join("\n");
261
+ }
262
+ function formatMarkdown(tenant, project, requirements, concepts, comparisons, findings) {
263
+ const lines = [];
264
+ const high = findings.filter(f => f.severity === "high").length;
265
+ const med = findings.filter(f => f.severity === "medium").length;
266
+ const low = findings.filter(f => f.severity === "low").length;
267
+ lines.push("## Semantic Lint Report");
268
+ lines.push(`**Project:** ${project} (\`${tenant}\`) `);
269
+ lines.push(`**Requirements:** ${requirements.length} | **Concepts classified:** ${concepts.length} `);
270
+ lines.push(`**Findings:** ${findings.length} (${high} high, ${med} medium, ${low} low)`);
271
+ lines.push("");
272
+ // Concept table
273
+ lines.push("### Concept Classifications");
274
+ lines.push("| Concept | UHT Code | Physical? | Key Traits |");
275
+ lines.push("|---|---|---|---|");
276
+ for (const c of concepts) {
277
+ lines.push(`| ${c.name} | \`${c.hexCode}\` | ${c.isPhysical ? "Yes" : "No"} | ${c.traits.slice(0, 4).join(", ")} |`);
278
+ }
279
+ lines.push("");
280
+ // Similarities
281
+ if (comparisons.length > 0) {
282
+ lines.push("### Key Similarities");
283
+ lines.push("| Pair | Jaccard |");
284
+ lines.push("|---|---|");
285
+ for (const batch of comparisons) {
286
+ for (const comp of batch.comparisons) {
287
+ if (comp.jaccard_similarity >= 0.4) {
288
+ lines.push(`| ${batch.entity} / ${comp.candidate} | **${(comp.jaccard_similarity * 100).toFixed(0)}%** |`);
289
+ }
290
+ }
291
+ }
292
+ lines.push("");
293
+ }
294
+ // Findings
295
+ lines.push("### Findings");
296
+ lines.push("| # | Severity | Title | Affected |");
297
+ lines.push("|---|---|---|---|");
298
+ for (let i = 0; i < findings.length; i++) {
299
+ const f = findings[i];
300
+ lines.push(`| ${i + 1} | **${f.severity}** | ${f.title} | ${f.affectedReqs.join(", ")} |`);
301
+ }
302
+ lines.push("");
303
+ for (const f of findings) {
304
+ lines.push(`#### ${f.title}`);
305
+ lines.push(`- **Severity:** ${f.severity} | **Category:** ${f.category}`);
306
+ lines.push(`- ${f.description}`);
307
+ lines.push(`- **Affects:** ${f.affectedReqs.join(", ")}`);
308
+ lines.push(`- **Recommendation:** ${f.recommendation}`);
309
+ lines.push("");
310
+ }
311
+ return lines.join("\n");
312
+ }
313
+ // ── Command registration ─────────────────────────────────────
314
+ export function registerLintCommands(program, client) {
315
+ program
316
+ .command("lint")
317
+ .description("Semantic requirements lint — classifies domain concepts via UHT and flags ontological issues")
318
+ .argument("<tenant>", "Tenant slug")
319
+ .argument("<project>", "Project slug")
320
+ .option("--concepts <n>", "Max concepts to classify", "15")
321
+ .option("--format <fmt>", "Output format: text, markdown, json", "text")
322
+ .option("-o, --output <file>", "Write report to file")
323
+ .action(async (tenant, project, opts) => {
324
+ const uht = new UhtClient();
325
+ if (!uht.isConfigured) {
326
+ console.error("UHT not configured. Set UHT_TOKEN environment variable.");
327
+ console.error("Get a token at https://universalhex.org");
328
+ process.exit(1);
329
+ }
330
+ const maxConcepts = parseInt(opts.concepts, 10) || 15;
331
+ // Step 1: Fetch all requirements
332
+ console.error("Fetching requirements...");
333
+ const requirements = await fetchAllRequirements(client, tenant, project);
334
+ if (requirements.length === 0) {
335
+ console.error("No requirements found.");
336
+ process.exit(1);
337
+ }
338
+ console.error(` ${requirements.length} requirements loaded.`);
339
+ // Step 2: Extract domain concepts
340
+ console.error("Extracting domain concepts...");
341
+ const conceptRefs = extractConcepts(requirements);
342
+ const top = topConcepts(conceptRefs, maxConcepts);
343
+ console.error(` ${conceptRefs.size} unique concepts found, classifying top ${top.length}.`);
344
+ // Step 3: Classify each concept via UHT
345
+ console.error("Classifying concepts via UHT...");
346
+ const concepts = [];
347
+ for (const [name, refs] of top) {
348
+ try {
349
+ const result = await uht.classify(name);
350
+ const traitNames = result.traits.map(t => t.name).filter(Boolean);
351
+ concepts.push({
352
+ name,
353
+ hexCode: result.hex_code,
354
+ isPhysical: traitNames.includes("Physical Object"),
355
+ traits: traitNames,
356
+ reqs: refs,
357
+ });
358
+ console.error(` ✓ ${name} → ${result.hex_code} (${traitNames.length} traits)`);
359
+ }
360
+ catch (err) {
361
+ console.error(` ✗ ${name}: ${err.message}`);
362
+ }
363
+ }
364
+ // Step 4: Cross-compare concepts in batches
365
+ console.error("Cross-comparing concepts...");
366
+ const comparisons = [];
367
+ if (concepts.length >= 2) {
368
+ // Compare top concept against others, then second against rest
369
+ const names = concepts.map(c => c.name);
370
+ const batchSize = Math.min(names.length - 1, 15);
371
+ try {
372
+ const result = await uht.batchCompare(names[0], names.slice(1, batchSize + 1));
373
+ comparisons.push(result);
374
+ console.error(` ✓ ${names[0]} vs ${batchSize} others`);
375
+ }
376
+ catch (err) {
377
+ console.error(` ✗ batch compare: ${err.message}`);
378
+ }
379
+ if (names.length > 3) {
380
+ try {
381
+ const mid = Math.floor(names.length / 2);
382
+ const candidates = [...names.slice(0, mid), ...names.slice(mid + 1)].slice(0, 10);
383
+ const result = await uht.batchCompare(names[mid], candidates);
384
+ comparisons.push(result);
385
+ console.error(` ✓ ${names[mid]} vs ${candidates.length} others`);
386
+ }
387
+ catch (err) {
388
+ console.error(` ✗ batch compare: ${err.message}`);
389
+ }
390
+ }
391
+ }
392
+ // Step 5: Analyze findings
393
+ console.error("Analyzing...");
394
+ const findings = analyzeFindings(concepts, comparisons, requirements);
395
+ // Step 6: Output report
396
+ let report;
397
+ if (opts.format === "json" || isJsonMode()) {
398
+ const data = { tenant, project, requirements: requirements.length, concepts, comparisons, findings };
399
+ report = JSON.stringify(data, null, 2);
400
+ }
401
+ else if (opts.format === "markdown") {
402
+ report = formatMarkdown(tenant, project, requirements, concepts, comparisons, findings);
403
+ }
404
+ else {
405
+ report = formatReport(tenant, project, requirements, concepts, comparisons, findings);
406
+ }
407
+ if (opts.output) {
408
+ writeFileSync(opts.output, report + "\n", "utf-8");
409
+ console.error(`Report written to ${opts.output}`);
410
+ }
411
+ else {
412
+ console.log(report);
413
+ }
414
+ });
415
+ }
package/dist/index.js CHANGED
@@ -19,6 +19,7 @@ import { registerReportCommands } from "./commands/reports.js";
19
19
  import { registerImportExportCommands } from "./commands/import-export.js";
20
20
  import { registerActivityCommands } from "./commands/activity.js";
21
21
  import { registerImplementationCommands } from "./commands/implementation.js";
22
+ import { registerLintCommands } from "./commands/lint.js";
22
23
  const program = new Command();
23
24
  // Lazy-init: only create client when a command actually runs
24
25
  let client = null;
@@ -67,6 +68,7 @@ registerReportCommands(program, clientProxy);
67
68
  registerImportExportCommands(program, clientProxy);
68
69
  registerActivityCommands(program, clientProxy);
69
70
  registerImplementationCommands(program, clientProxy);
71
+ registerLintCommands(program, clientProxy);
70
72
  // Handle async errors from Commander action handlers
71
73
  process.on("uncaughtException", (err) => {
72
74
  console.error(`Error: ${err.message}`);
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Minimal UHT (Universal Hex Taxonomy) API client.
3
+ *
4
+ * Talks to the UHT Substrate factory API for entity classification and comparison.
5
+ * Token resolution: UHT_TOKEN env → UHT_API_KEY env → ~/.config/uht-substrate/config.json
6
+ */
7
+ export interface UhtClassification {
8
+ entity: string;
9
+ hex_code: string;
10
+ traits: Array<{
11
+ name: string;
12
+ justification: string;
13
+ }>;
14
+ }
15
+ export interface UhtComparison {
16
+ candidate: string;
17
+ hex_code: string;
18
+ jaccard_similarity: number;
19
+ hamming_distance: number;
20
+ shared_traits: Array<{
21
+ name: string;
22
+ }>;
23
+ traits_entity_only: Array<{
24
+ name: string;
25
+ }>;
26
+ traits_candidate_only: Array<{
27
+ name: string;
28
+ }>;
29
+ }
30
+ export interface UhtBatchResult {
31
+ entity: string;
32
+ hex_code: string;
33
+ comparisons: UhtComparison[];
34
+ best_match: string;
35
+ best_jaccard: number;
36
+ }
37
+ export declare class UhtClient {
38
+ private baseUrl;
39
+ private token;
40
+ constructor();
41
+ get isConfigured(): boolean;
42
+ private request;
43
+ classify(entity: string): Promise<UhtClassification>;
44
+ batchCompare(entity: string, candidates: string[]): Promise<UhtBatchResult>;
45
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Minimal UHT (Universal Hex Taxonomy) API client.
3
+ *
4
+ * Talks to the UHT Substrate factory API for entity classification and comparison.
5
+ * Token resolution: UHT_TOKEN env → UHT_API_KEY env → ~/.config/uht-substrate/config.json
6
+ */
7
+ import { readFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { homedir } from "node:os";
10
+ const DEFAULT_UHT_URL = "https://substrate.universalhex.org/api";
11
+ function loadUhtConfigToken() {
12
+ try {
13
+ const configPath = join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "uht-substrate", "config.json");
14
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
15
+ return config.token ?? "";
16
+ }
17
+ catch {
18
+ return "";
19
+ }
20
+ }
21
+ export class UhtClient {
22
+ baseUrl;
23
+ token;
24
+ constructor() {
25
+ this.baseUrl = (process.env.UHT_API_URL ?? DEFAULT_UHT_URL).replace(/\/+$/, "");
26
+ this.token = process.env.UHT_TOKEN || process.env.UHT_API_KEY || loadUhtConfigToken();
27
+ }
28
+ get isConfigured() {
29
+ return this.token.length > 0;
30
+ }
31
+ async request(method, path, body) {
32
+ const url = `${this.baseUrl}${path}`;
33
+ const headers = {};
34
+ if (body)
35
+ headers["Content-Type"] = "application/json";
36
+ if (this.token)
37
+ headers["Authorization"] = `Bearer ${this.token}`;
38
+ const res = await globalThis.fetch(url, {
39
+ method,
40
+ headers,
41
+ body: body ? JSON.stringify(body) : undefined,
42
+ });
43
+ if (!res.ok) {
44
+ const text = await res.text();
45
+ throw new Error(`UHT API error (${res.status}): ${text}`);
46
+ }
47
+ return (await res.json());
48
+ }
49
+ async classify(entity) {
50
+ return this.request("POST", "/classify", { entity, context: "", use_semantic_priors: false });
51
+ }
52
+ async batchCompare(entity, candidates) {
53
+ return this.request("POST", "/batch-compare", { entity, candidates });
54
+ }
55
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "airgen-cli",
3
- "version": "0.1.7",
3
+ "version": "0.2.0",
4
4
  "description": "AIRGen CLI — requirements engineering from the command line",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -9,7 +9,8 @@
9
9
  },
10
10
  "files": [
11
11
  "dist",
12
- "package.json"
12
+ "package.json",
13
+ "README.md"
13
14
  ],
14
15
  "scripts": {
15
16
  "build": "tsc -p tsconfig.json",
@@ -21,7 +22,12 @@
21
22
  "requirements",
22
23
  "engineering",
23
24
  "cli",
24
- "requirements-management"
25
+ "requirements-management",
26
+ "systems-engineering",
27
+ "traceability",
28
+ "sysml",
29
+ "mbse",
30
+ "uht"
25
31
  ],
26
32
  "license": "MIT",
27
33
  "repository": {