@rqml/cli 0.2.0 → 0.4.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 +10 -4
- package/dist/index.js +394 -33
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -18,10 +18,16 @@ rqml <command> [spec.rqml] [options]
|
|
|
18
18
|
validate [path] XML well-formedness, XSD, and referential integrity
|
|
19
19
|
status [path] Spec, coverage, and lint summary
|
|
20
20
|
check [path] Deterministic enforcement gate (validation + coverage + drift)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
show <id> One artifact: statement, acceptance criteria, trace neighborhood
|
|
22
|
+
impact <id> What is affected, transitively, if this artifact changes
|
|
23
|
+
matrix [path] Traceability matrix: status, goals, code, tests, coverage gaps
|
|
24
|
+
link <id> <uri> Record an implements/verifiedBy edge and its drift baseline
|
|
25
|
+
skeleton <kind> Print a schema-valid snippet (req|edge|testCase|stateMachine)
|
|
26
|
+
|
|
27
|
+
--json Machine-readable output (REQ-CLI-JSON)
|
|
28
|
+
--strictness <level> relaxed | standard | strict | certified
|
|
29
|
+
--base-dir <dir> Resolve the spec and implements code links against <dir>
|
|
30
|
+
--status/--type/--warning Filter matrix rows (comma-separated, e.g. --warning unverified)
|
|
25
31
|
```
|
|
26
32
|
|
|
27
33
|
When no spec path is given, the lone `*.rqml` in the working directory is used
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { __commonJS, __toESM } from './chunk-5WRI5ZAA.js';
|
|
3
|
+
import { createRequire } from 'module';
|
|
3
4
|
import { createHash } from 'crypto';
|
|
4
5
|
import { writeFileSync, existsSync, readFileSync, mkdirSync, statSync, readdirSync } from 'fs';
|
|
5
6
|
import { resolve, join, dirname, isAbsolute } from 'path';
|
|
@@ -13,7 +14,7 @@ var require_util = __commonJS({
|
|
|
13
14
|
var nameRegexp = "[" + nameStartChar + "][" + nameChar + "]*";
|
|
14
15
|
var regexName = new RegExp("^" + nameRegexp + "$");
|
|
15
16
|
var getAllMatches = function(string, regex) {
|
|
16
|
-
const
|
|
17
|
+
const matches2 = [];
|
|
17
18
|
let match = regex.exec(string);
|
|
18
19
|
while (match) {
|
|
19
20
|
const allmatches = [];
|
|
@@ -22,10 +23,10 @@ var require_util = __commonJS({
|
|
|
22
23
|
for (let index = 0; index < len; index++) {
|
|
23
24
|
allmatches.push(match[index]);
|
|
24
25
|
}
|
|
25
|
-
|
|
26
|
+
matches2.push(allmatches);
|
|
26
27
|
match = regex.exec(string);
|
|
27
28
|
}
|
|
28
|
-
return
|
|
29
|
+
return matches2;
|
|
29
30
|
};
|
|
30
31
|
var isName = function(string) {
|
|
31
32
|
const match = regexName.exec(string);
|
|
@@ -303,24 +304,24 @@ var require_validator = __commonJS({
|
|
|
303
304
|
}
|
|
304
305
|
var validAttrStrRegxp = new RegExp(`(\\s*)([^\\s=]+)(\\s*=)?(\\s*(['"])(([\\s\\S])*?)\\5)?`, "g");
|
|
305
306
|
function validateAttributeString(attrStr, options) {
|
|
306
|
-
const
|
|
307
|
+
const matches2 = util.getAllMatches(attrStr, validAttrStrRegxp);
|
|
307
308
|
const attrNames = {};
|
|
308
|
-
for (let i = 0; i <
|
|
309
|
-
if (
|
|
310
|
-
return getErrorObject("InvalidAttr", "Attribute '" +
|
|
311
|
-
} else if (
|
|
312
|
-
return getErrorObject("InvalidAttr", "Attribute '" +
|
|
313
|
-
} else if (
|
|
314
|
-
return getErrorObject("InvalidAttr", "boolean attribute '" +
|
|
309
|
+
for (let i = 0; i < matches2.length; i++) {
|
|
310
|
+
if (matches2[i][1].length === 0) {
|
|
311
|
+
return getErrorObject("InvalidAttr", "Attribute '" + matches2[i][2] + "' has no space in starting.", getPositionFromMatch(matches2[i]));
|
|
312
|
+
} else if (matches2[i][3] !== void 0 && matches2[i][4] === void 0) {
|
|
313
|
+
return getErrorObject("InvalidAttr", "Attribute '" + matches2[i][2] + "' is without value.", getPositionFromMatch(matches2[i]));
|
|
314
|
+
} else if (matches2[i][3] === void 0 && !options.allowBooleanAttributes) {
|
|
315
|
+
return getErrorObject("InvalidAttr", "boolean attribute '" + matches2[i][2] + "' is not allowed.", getPositionFromMatch(matches2[i]));
|
|
315
316
|
}
|
|
316
|
-
const attrName =
|
|
317
|
+
const attrName = matches2[i][2];
|
|
317
318
|
if (!validateAttrName(attrName)) {
|
|
318
|
-
return getErrorObject("InvalidAttr", "Attribute '" + attrName + "' is an invalid name.", getPositionFromMatch(
|
|
319
|
+
return getErrorObject("InvalidAttr", "Attribute '" + attrName + "' is an invalid name.", getPositionFromMatch(matches2[i]));
|
|
319
320
|
}
|
|
320
321
|
if (!attrNames.hasOwnProperty(attrName)) {
|
|
321
322
|
attrNames[attrName] = 1;
|
|
322
323
|
} else {
|
|
323
|
-
return getErrorObject("InvalidAttr", "Attribute '" + attrName + "' is repeated.", getPositionFromMatch(
|
|
324
|
+
return getErrorObject("InvalidAttr", "Attribute '" + attrName + "' is repeated.", getPositionFromMatch(matches2[i]));
|
|
324
325
|
}
|
|
325
326
|
}
|
|
326
327
|
return true;
|
|
@@ -1057,15 +1058,15 @@ var require_OrderedObjParser = __commonJS({
|
|
|
1057
1058
|
var attrsRegx = new RegExp(`([^\\s=]+)\\s*(=\\s*(['"])([\\s\\S]*?)\\3)?`, "gm");
|
|
1058
1059
|
function buildAttributesMap(attrStr, jPath, tagName) {
|
|
1059
1060
|
if (this.options.ignoreAttributes !== true && typeof attrStr === "string") {
|
|
1060
|
-
const
|
|
1061
|
-
const len =
|
|
1061
|
+
const matches2 = util.getAllMatches(attrStr, attrsRegx);
|
|
1062
|
+
const len = matches2.length;
|
|
1062
1063
|
const attrs = {};
|
|
1063
1064
|
for (let i = 0; i < len; i++) {
|
|
1064
|
-
const attrName = this.resolveNameSpace(
|
|
1065
|
+
const attrName = this.resolveNameSpace(matches2[i][1]);
|
|
1065
1066
|
if (this.ignoreAttributesFn(attrName, jPath)) {
|
|
1066
1067
|
continue;
|
|
1067
1068
|
}
|
|
1068
|
-
let oldVal =
|
|
1069
|
+
let oldVal = matches2[i][4];
|
|
1069
1070
|
let aName = this.options.attributeNamePrefix + attrName;
|
|
1070
1071
|
if (attrName.length) {
|
|
1071
1072
|
if (this.options.transformAttributeName) {
|
|
@@ -1327,9 +1328,9 @@ var require_OrderedObjParser = __commonJS({
|
|
|
1327
1328
|
}
|
|
1328
1329
|
for (let entityName in this.docTypeEntities) {
|
|
1329
1330
|
const entity = this.docTypeEntities[entityName];
|
|
1330
|
-
const
|
|
1331
|
-
if (
|
|
1332
|
-
this.entityExpansionCount +=
|
|
1331
|
+
const matches2 = val.match(entity.regx);
|
|
1332
|
+
if (matches2) {
|
|
1333
|
+
this.entityExpansionCount += matches2.length;
|
|
1333
1334
|
if (entityConfig.maxTotalExpansions && this.entityExpansionCount > entityConfig.maxTotalExpansions) {
|
|
1334
1335
|
throw new Error(
|
|
1335
1336
|
`Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}`
|
|
@@ -1350,9 +1351,9 @@ var require_OrderedObjParser = __commonJS({
|
|
|
1350
1351
|
if (val.indexOf("&") === -1) return val;
|
|
1351
1352
|
for (const entityName of Object.keys(this.lastEntities)) {
|
|
1352
1353
|
const entity = this.lastEntities[entityName];
|
|
1353
|
-
const
|
|
1354
|
-
if (
|
|
1355
|
-
this.entityExpansionCount +=
|
|
1354
|
+
const matches2 = val.match(entity.regex);
|
|
1355
|
+
if (matches2) {
|
|
1356
|
+
this.entityExpansionCount += matches2.length;
|
|
1356
1357
|
if (entityConfig.maxTotalExpansions && this.entityExpansionCount > entityConfig.maxTotalExpansions) {
|
|
1357
1358
|
throw new Error(
|
|
1358
1359
|
`Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}`
|
|
@@ -1365,9 +1366,9 @@ var require_OrderedObjParser = __commonJS({
|
|
|
1365
1366
|
if (this.options.htmlEntities) {
|
|
1366
1367
|
for (const entityName of Object.keys(this.htmlEntities)) {
|
|
1367
1368
|
const entity = this.htmlEntities[entityName];
|
|
1368
|
-
const
|
|
1369
|
-
if (
|
|
1370
|
-
this.entityExpansionCount +=
|
|
1369
|
+
const matches2 = val.match(entity.regex);
|
|
1370
|
+
if (matches2) {
|
|
1371
|
+
this.entityExpansionCount += matches2.length;
|
|
1371
1372
|
if (entityConfig.maxTotalExpansions && this.entityExpansionCount > entityConfig.maxTotalExpansions) {
|
|
1372
1373
|
throw new Error(
|
|
1373
1374
|
`Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}`
|
|
@@ -3372,6 +3373,157 @@ ${sectionIndent}</trace>
|
|
|
3372
3373
|
}
|
|
3373
3374
|
return { ok: true, xml: updated, edgeId, edgeXml: edgeXml.trim() };
|
|
3374
3375
|
}
|
|
3376
|
+
function escapeRegExp2(value) {
|
|
3377
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3378
|
+
}
|
|
3379
|
+
var EXTERNAL_ELEMENT = /<external\b[^>]*(?:\/>|>[\s\S]*?<\/external>)/g;
|
|
3380
|
+
function updateTraceEdge(xml, request) {
|
|
3381
|
+
const parsed = parse(xml);
|
|
3382
|
+
if (!parsed.ok) {
|
|
3383
|
+
return { ok: false, error: `document does not parse: ${parsed.error.message}` };
|
|
3384
|
+
}
|
|
3385
|
+
if (request.uri.trim() === "") {
|
|
3386
|
+
return { ok: false, error: "locator uri must not be empty" };
|
|
3387
|
+
}
|
|
3388
|
+
const prefix = request.type === "implements" ? "E-IMPL-" : "E-VER-";
|
|
3389
|
+
const edgeId = request.edgeId ?? prefix + request.artifactId.replace(/^REQ-/, "");
|
|
3390
|
+
const edge = parsed.document.trace.find((e) => e.id === edgeId);
|
|
3391
|
+
if (edge === void 0) {
|
|
3392
|
+
return {
|
|
3393
|
+
ok: false,
|
|
3394
|
+
error: request.edgeId !== void 0 ? `no trace edge with id "${edgeId}" exists` : `no trace edge with the derived id "${edgeId}" exists; pass --id for an explicitly named edge`
|
|
3395
|
+
};
|
|
3396
|
+
}
|
|
3397
|
+
if (edge.type !== request.type) {
|
|
3398
|
+
return {
|
|
3399
|
+
ok: false,
|
|
3400
|
+
error: `edge "${edgeId}" has type "${edge.type}", not "${request.type}"`
|
|
3401
|
+
};
|
|
3402
|
+
}
|
|
3403
|
+
const local = edge.from.kind === "local" ? edge.from : edge.to.kind === "local" ? edge.to : void 0;
|
|
3404
|
+
if (local === void 0 || local.id !== request.artifactId) {
|
|
3405
|
+
return {
|
|
3406
|
+
ok: false,
|
|
3407
|
+
error: `edge "${edgeId}" does not link artifact "${request.artifactId}"`
|
|
3408
|
+
};
|
|
3409
|
+
}
|
|
3410
|
+
const external = edge.from.kind === "external" ? edge.from : edge.to.kind === "external" ? edge.to : void 0;
|
|
3411
|
+
if (external === void 0) {
|
|
3412
|
+
return { ok: false, error: `edge "${edgeId}" has no external locator to update` };
|
|
3413
|
+
}
|
|
3414
|
+
const openTag = new RegExp(`<edge\\b[^>]*\\bid="${escapeRegExp2(edgeId)}"[^>]*>`);
|
|
3415
|
+
const open = openTag.exec(xml);
|
|
3416
|
+
const closeIdx = open === null ? -1 : xml.indexOf("</edge>", open.index);
|
|
3417
|
+
if (open === null || closeIdx < 0) {
|
|
3418
|
+
return { ok: false, error: `could not locate edge "${edgeId}" in the document text` };
|
|
3419
|
+
}
|
|
3420
|
+
const span = xml.slice(open.index, closeIdx);
|
|
3421
|
+
const occurrences = span.match(EXTERNAL_ELEMENT) ?? [];
|
|
3422
|
+
if (occurrences.length !== 1) {
|
|
3423
|
+
return {
|
|
3424
|
+
ok: false,
|
|
3425
|
+
error: `edge "${edgeId}" does not contain exactly one external locator element`
|
|
3426
|
+
};
|
|
3427
|
+
}
|
|
3428
|
+
const kind = request.kind ?? external.hintKind ?? (request.type === "implements" ? "code" : "test");
|
|
3429
|
+
const title = request.title ?? external.title;
|
|
3430
|
+
const titleAttr = title !== void 0 ? ` title="${escapeAttr(title)}"` : "";
|
|
3431
|
+
const replacement = `<external uri="${escapeAttr(request.uri)}" kind="${escapeAttr(kind)}"${titleAttr}/>`;
|
|
3432
|
+
const editedSpan = span.replace(occurrences[0], () => replacement);
|
|
3433
|
+
const updated = xml.slice(0, open.index) + editedSpan + xml.slice(closeIdx);
|
|
3434
|
+
const before = checkIntegrity(xml).length;
|
|
3435
|
+
const reparsed = parse(updated);
|
|
3436
|
+
if (!reparsed.ok) {
|
|
3437
|
+
return {
|
|
3438
|
+
ok: false,
|
|
3439
|
+
error: `edit produced an unparseable document: ${reparsed.error.message}`
|
|
3440
|
+
};
|
|
3441
|
+
}
|
|
3442
|
+
if (checkIntegrity(updated).length > before) {
|
|
3443
|
+
return {
|
|
3444
|
+
ok: false,
|
|
3445
|
+
error: "edit introduced an integrity violation; document left unchanged"
|
|
3446
|
+
};
|
|
3447
|
+
}
|
|
3448
|
+
return {
|
|
3449
|
+
ok: true,
|
|
3450
|
+
xml: updated,
|
|
3451
|
+
edgeId,
|
|
3452
|
+
edgeXml: `${editedSpan}</edge>`.trim(),
|
|
3453
|
+
previousUri: external.uri
|
|
3454
|
+
};
|
|
3455
|
+
}
|
|
3456
|
+
function collectTitles(doc) {
|
|
3457
|
+
const m = /* @__PURE__ */ new Map();
|
|
3458
|
+
const set = (id, title) => {
|
|
3459
|
+
if (id !== void 0 && title !== void 0 && title !== "" && !m.has(id)) {
|
|
3460
|
+
m.set(id, title);
|
|
3461
|
+
}
|
|
3462
|
+
};
|
|
3463
|
+
for (const p of doc.meta.profiles ?? []) set(p.id, p.type);
|
|
3464
|
+
const c = doc.catalogs;
|
|
3465
|
+
if (c) {
|
|
3466
|
+
for (const t of c.glossary ?? []) set(t.id, t.name);
|
|
3467
|
+
for (const a of c.actors ?? []) set(a.id, a.name);
|
|
3468
|
+
for (const s of c.stakeholders ?? []) set(s.id, s.name);
|
|
3469
|
+
for (const x of c.constraints ?? []) set(x.id, x.statement);
|
|
3470
|
+
for (const x of c.policies ?? []) set(x.id, x.obligation);
|
|
3471
|
+
for (const x of c.decisions ?? []) set(x.id, x.decision);
|
|
3472
|
+
for (const x of c.risks ?? []) set(x.id, x.statement);
|
|
3473
|
+
}
|
|
3474
|
+
const d = doc.domain;
|
|
3475
|
+
if (d) {
|
|
3476
|
+
for (const e of d.entities ?? []) {
|
|
3477
|
+
set(e.id, e.name);
|
|
3478
|
+
for (const a of e.attrs ?? []) set(a.id, a.name);
|
|
3479
|
+
}
|
|
3480
|
+
for (const r of d.businessRules ?? []) set(r.id, r.statement);
|
|
3481
|
+
}
|
|
3482
|
+
const g = doc.goals;
|
|
3483
|
+
if (g) {
|
|
3484
|
+
for (const x of g.goals ?? []) set(x.id, x.title);
|
|
3485
|
+
for (const x of g.qualityGoals ?? []) set(x.id, x.title);
|
|
3486
|
+
for (const x of g.obstacles ?? []) set(x.id, x.title);
|
|
3487
|
+
for (const x of g.goalLinks ?? []) set(x.id, x.id);
|
|
3488
|
+
}
|
|
3489
|
+
const sc = doc.scenarios;
|
|
3490
|
+
if (sc) {
|
|
3491
|
+
for (const x of sc.scenarios ?? []) set(x.id, x.title);
|
|
3492
|
+
for (const x of sc.misuseCases ?? []) set(x.id, x.title);
|
|
3493
|
+
for (const x of sc.edgeCases ?? []) set(x.id, x.title);
|
|
3494
|
+
}
|
|
3495
|
+
for (const p of doc.packages) {
|
|
3496
|
+
set(p.id, p.title);
|
|
3497
|
+
for (const r of p.requirements) set(r.id, r.title);
|
|
3498
|
+
}
|
|
3499
|
+
for (const r of doc.looseRequirements) set(r.id, r.title);
|
|
3500
|
+
const b = doc.behavior;
|
|
3501
|
+
if (b) {
|
|
3502
|
+
for (const sm of b.stateMachines ?? []) {
|
|
3503
|
+
set(sm.id, sm.name);
|
|
3504
|
+
for (const st of sm.states) set(st.id, st.name);
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
const it = doc.interfaces;
|
|
3508
|
+
if (it) {
|
|
3509
|
+
for (const api of it.apis ?? []) {
|
|
3510
|
+
set(api.id, api.name);
|
|
3511
|
+
for (const ep of api.endpoints ?? []) set(ep.id, `${ep.method} ${ep.path}`);
|
|
3512
|
+
}
|
|
3513
|
+
for (const ev of it.events ?? []) set(ev.id, ev.name);
|
|
3514
|
+
}
|
|
3515
|
+
const v = doc.verification;
|
|
3516
|
+
if (v) {
|
|
3517
|
+
for (const ts of v.testSuites ?? []) set(ts.id, ts.title);
|
|
3518
|
+
for (const tc of v.testCases ?? []) set(tc.id, tc.title);
|
|
3519
|
+
}
|
|
3520
|
+
const gv = doc.governance;
|
|
3521
|
+
if (gv) {
|
|
3522
|
+
for (const x of gv.issues ?? []) set(x.id, x.statement);
|
|
3523
|
+
for (const x of gv.approvals ?? []) set(x.id, x.role);
|
|
3524
|
+
}
|
|
3525
|
+
return m;
|
|
3526
|
+
}
|
|
3375
3527
|
function endpointKey2(locator) {
|
|
3376
3528
|
return locator.kind === "local" ? locator.id : locator.uri;
|
|
3377
3529
|
}
|
|
@@ -3713,6 +3865,131 @@ function computeCoverage(doc) {
|
|
|
3713
3865
|
function finding(source, rule, message) {
|
|
3714
3866
|
return { source, severity: "warning", rule, message };
|
|
3715
3867
|
}
|
|
3868
|
+
function refFromEndpoint(ep, titleById) {
|
|
3869
|
+
const loc = ep.locator;
|
|
3870
|
+
if (loc.kind === "local") {
|
|
3871
|
+
const ref2 = { id: loc.id };
|
|
3872
|
+
const title = ep.requirement?.title ?? loc.title ?? titleById.get(loc.id);
|
|
3873
|
+
if (title !== void 0) ref2.title = title;
|
|
3874
|
+
if (ep.target === void 0) ref2.broken = true;
|
|
3875
|
+
return ref2;
|
|
3876
|
+
}
|
|
3877
|
+
const ref = {
|
|
3878
|
+
id: loc.kind === "doc" ? `${loc.uri}#${loc.id}` : loc.uri,
|
|
3879
|
+
external: true
|
|
3880
|
+
};
|
|
3881
|
+
if (loc.title !== void 0) ref.title = loc.title;
|
|
3882
|
+
return ref;
|
|
3883
|
+
}
|
|
3884
|
+
function matches(row, filter) {
|
|
3885
|
+
if (filter === void 0) return true;
|
|
3886
|
+
if (filter.status && !filter.status.includes(row.status)) return false;
|
|
3887
|
+
if (filter.type && !filter.type.includes(row.type)) return false;
|
|
3888
|
+
if (filter.warning && !filter.warning.some((w) => row.warnings.includes(w)))
|
|
3889
|
+
return false;
|
|
3890
|
+
return true;
|
|
3891
|
+
}
|
|
3892
|
+
function buildMatrix(doc, filter) {
|
|
3893
|
+
const cov = computeCoverage(doc);
|
|
3894
|
+
const reqById = new Map(allRequirements(doc).map((r) => [r.id, r]));
|
|
3895
|
+
const titleById = collectTitles(doc);
|
|
3896
|
+
const edgeById = new Map(resolveTrace(doc).edges.map((re) => [re.edge.id, re]));
|
|
3897
|
+
const unverified = new Set(cov.unverifiedRequirements);
|
|
3898
|
+
const unimplemented = new Set(cov.unimplementedRequirements);
|
|
3899
|
+
const orphans = new Set(cov.orphanRequirements);
|
|
3900
|
+
const premature = new Set(cov.prematureImplementations.map((p) => p.requirementId));
|
|
3901
|
+
const rows = [];
|
|
3902
|
+
for (const ac of cov.requirements) {
|
|
3903
|
+
const req = reqById.get(ac.id);
|
|
3904
|
+
if (req === void 0) continue;
|
|
3905
|
+
const goals = (ac.outgoing.satisfies ?? []).flatMap((edgeId) => {
|
|
3906
|
+
const re = edgeById.get(edgeId);
|
|
3907
|
+
return re ? [refFromEndpoint(re.to, titleById)] : [];
|
|
3908
|
+
});
|
|
3909
|
+
const implementations = (ac.incoming.implements ?? []).flatMap((edgeId) => {
|
|
3910
|
+
const re = edgeById.get(edgeId);
|
|
3911
|
+
return re ? [refFromEndpoint(re.from, titleById)] : [];
|
|
3912
|
+
});
|
|
3913
|
+
const tests = (ac.outgoing.verifiedBy ?? []).flatMap((edgeId) => {
|
|
3914
|
+
const re = edgeById.get(edgeId);
|
|
3915
|
+
return re ? [refFromEndpoint(re.to, titleById)] : [];
|
|
3916
|
+
});
|
|
3917
|
+
const verification = unverified.has(ac.id) ? "unverified" : "verified";
|
|
3918
|
+
const implementation = premature.has(ac.id) ? "premature" : unimplemented.has(ac.id) ? "unimplemented" : "implemented";
|
|
3919
|
+
const warnings = [];
|
|
3920
|
+
if (unverified.has(ac.id)) warnings.push("unverified");
|
|
3921
|
+
if (unimplemented.has(ac.id)) warnings.push("unimplemented");
|
|
3922
|
+
if (orphans.has(ac.id)) warnings.push("orphan");
|
|
3923
|
+
if (premature.has(ac.id)) warnings.push("premature");
|
|
3924
|
+
if ([...goals, ...implementations, ...tests].some((r) => r.broken))
|
|
3925
|
+
warnings.push("broken-trace");
|
|
3926
|
+
const row = {
|
|
3927
|
+
id: ac.id,
|
|
3928
|
+
title: req.title,
|
|
3929
|
+
type: req.type,
|
|
3930
|
+
status: req.status ?? "draft",
|
|
3931
|
+
goals,
|
|
3932
|
+
implementations,
|
|
3933
|
+
tests,
|
|
3934
|
+
verification,
|
|
3935
|
+
implementation,
|
|
3936
|
+
warnings
|
|
3937
|
+
};
|
|
3938
|
+
if (req.priority !== void 0) row.priority = req.priority;
|
|
3939
|
+
rows.push(row);
|
|
3940
|
+
}
|
|
3941
|
+
const kept = rows.filter((r) => matches(r, filter));
|
|
3942
|
+
const summary = {
|
|
3943
|
+
total: kept.length,
|
|
3944
|
+
verified: kept.filter((r) => r.verification === "verified").length,
|
|
3945
|
+
unverified: kept.filter((r) => r.verification === "unverified").length,
|
|
3946
|
+
implemented: kept.filter((r) => r.implementation === "implemented").length,
|
|
3947
|
+
unimplemented: kept.filter((r) => r.implementation === "unimplemented").length,
|
|
3948
|
+
premature: kept.filter((r) => r.implementation === "premature").length,
|
|
3949
|
+
orphans: kept.filter((r) => r.warnings.includes("orphan")).length,
|
|
3950
|
+
brokenTraces: kept.filter((r) => r.warnings.includes("broken-trace")).length
|
|
3951
|
+
};
|
|
3952
|
+
return { rows: kept, summary };
|
|
3953
|
+
}
|
|
3954
|
+
function escapeCell2(value) {
|
|
3955
|
+
return value.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
3956
|
+
}
|
|
3957
|
+
function refLabel(r) {
|
|
3958
|
+
const base = r.title !== void 0 ? `${r.id} (${r.title})` : r.id;
|
|
3959
|
+
return r.broken ? `${base} [broken]` : base;
|
|
3960
|
+
}
|
|
3961
|
+
function matrixToMarkdown(report) {
|
|
3962
|
+
const s = report.summary;
|
|
3963
|
+
const lines = [
|
|
3964
|
+
"# Traceability matrix",
|
|
3965
|
+
"",
|
|
3966
|
+
`- **Requirements:** ${s.total}`,
|
|
3967
|
+
`- **Verified:** ${s.verified} \xB7 **Unverified:** ${s.unverified}`,
|
|
3968
|
+
`- **Implemented:** ${s.implemented} \xB7 **Unimplemented:** ${s.unimplemented} \xB7 **Premature:** ${s.premature}`,
|
|
3969
|
+
`- **Orphans:** ${s.orphans} \xB7 **Broken traces:** ${s.brokenTraces}`,
|
|
3970
|
+
"",
|
|
3971
|
+
"| ID | Title | Type | Status | Verify | Impl | Goals | Implemented by | Verified by | Warnings |",
|
|
3972
|
+
"| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |"
|
|
3973
|
+
];
|
|
3974
|
+
for (const row of report.rows) {
|
|
3975
|
+
const cells = [
|
|
3976
|
+
row.id,
|
|
3977
|
+
row.title,
|
|
3978
|
+
row.type,
|
|
3979
|
+
row.status,
|
|
3980
|
+
row.verification,
|
|
3981
|
+
row.implementation,
|
|
3982
|
+
row.goals.map(refLabel).join(", "),
|
|
3983
|
+
row.implementations.map(refLabel).join(", "),
|
|
3984
|
+
row.tests.map(refLabel).join(", "),
|
|
3985
|
+
row.warnings.join(", ")
|
|
3986
|
+
].map(escapeCell2);
|
|
3987
|
+
lines.push(`| ${cells.join(" | ")} |`);
|
|
3988
|
+
}
|
|
3989
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
3990
|
+
return `${lines.join("\n")}
|
|
3991
|
+
`;
|
|
3992
|
+
}
|
|
3716
3993
|
var BASELINE_PATH = ".rqml/baseline.json";
|
|
3717
3994
|
function hashFileAt(filePath) {
|
|
3718
3995
|
try {
|
|
@@ -4010,7 +4287,7 @@ async function runImpact(rest) {
|
|
|
4010
4287
|
}
|
|
4011
4288
|
|
|
4012
4289
|
// ../schema/dist/index.js
|
|
4013
|
-
var AGENTS_default = '# RQML Agent Guidelines\n\n## Strictness: `standard`\n\n| Level | Description |\n|-------|-------------|\n| `relaxed` | Prototyping. Spec is advisory. Quick iteration allowed. |\n| `standard` | Production default. Spec-first for features. Core traces. |\n| `strict` | Full traceability. All behavior specified. No ghost features. |\n| `certified` | Regulated/safety-critical. Audit-grade traces with metadata. |\n\n---\n\nThis project uses **RQML** as the single source of truth for system intent. Familiarize yourself with the documentation at https://rqml.org/docs/user-guide/\n\n**Specification file:** Specification lives in a single .rqml file in the root of the project - convention is `requirements.rqml`. Multiple .rqml files may be employed in multirepo projects, in such cases a .rqml spec applies to everything that is higher in the project tree, unless overridden by another .rqml file.\n\n**Schema file:**\nThe RQML XSD schema is at https://rqml.org/schema/rqml-2.1.0.xsd (insert correct version number). Make sure to adhere to the schema at all times and follow guidelines in schema comments. Use as much of the RQML tagset as is necessary to capture and describe high quality requirements.\n\n---\n\n## Toolchain\n\nThe spec-first loop is enforced by the `rqml` CLI (npm: `@rqml/cli`; the `@rqml/mcp` server exposes the same engine as agent tools):\n\n```bash\nrqml check # deterministic gate: validation + coverage + drift (exit 0 = pass)\nrqml status # re-anchor: spec, coverage, and drift state\nrqml show <REQ-ID> # one requirement: statement, acceptance criteria, trace neighborhood\nrqml impact <ID> # what is affected, transitively, if this artifact changes\nrqml link <REQ-ID> <path> # record an implements edge + drift baseline (--type verifiedBy for tests)\nrqml skeleton <kind> # schema-valid snippet: req | edge | testCase | stateMachine\n```\n\nRun `rqml status` when you start a session to re-anchor on the spec. Run `rqml check` before finishing any task \u2014 it must exit 0.\n\n---\n\n## Core Principle: Spec-First Development\n\
|
|
4290
|
+
var AGENTS_default = '# RQML Agent Guidelines\n\n## Strictness: `standard`\n\n| Level | Description |\n|-------|-------------|\n| `relaxed` | Prototyping. Spec is advisory. Quick iteration allowed. |\n| `standard` | Production default. Spec-first for features. Core traces. |\n| `strict` | Full traceability. All behavior specified. No ghost features. |\n| `certified` | Regulated/safety-critical. Audit-grade traces with metadata. |\n\n---\n\nThis project uses **RQML** as the single source of truth for system intent. Familiarize yourself with the documentation at https://rqml.org/docs/user-guide/ and the development process at https://rqml.org/docs/development-process/\n\n**Specification file:** Specification lives in a single .rqml file in the root of the project - convention is `requirements.rqml`. Multiple .rqml files may be employed in multirepo projects, in such cases a .rqml spec applies to everything that is higher in the project tree, unless overridden by another .rqml file.\n\n**Schema file:**\nThe RQML XSD schema is at https://rqml.org/schema/rqml-2.1.0.xsd (insert correct version number). Make sure to adhere to the schema at all times and follow guidelines in schema comments. Use as much of the RQML tagset as is necessary to capture and describe high quality requirements.\n\n---\n\n## Toolchain\n\nThe spec-first loop is enforced by the `rqml` CLI (npm: `@rqml/cli`; the `@rqml/mcp` server exposes the same engine as agent tools):\n\n```bash\nrqml check # deterministic gate: validation + coverage + drift (exit 0 = pass)\nrqml status # re-anchor: spec, coverage, and drift state\nrqml show <REQ-ID> # one requirement: statement, acceptance criteria, trace neighborhood\nrqml impact <ID> # what is affected, transitively, if this artifact changes\nrqml link <REQ-ID> <path> # record an implements edge + drift baseline (--type verifiedBy for tests)\nrqml skeleton <kind> # schema-valid snippet: req | edge | testCase | stateMachine\n```\n\nRun `rqml status` when you start a session to re-anchor on the spec. Run `rqml check` before finishing any task \u2014 it must exit 0.\n\n---\n\n## Core Principle: Spec-First Development\n\nCode follows specification, not the reverse. If code and spec diverge, the spec is authoritative\u2014update the code or negotiate a spec change with the developer.\n\nRQML organizes work into a **five-stage process** (https://rqml.org/docs/development-process/). Each stage produces a durable artifact in version control; verification feeds back to the spec, so it is a loop:\n\n| Stage | Task | Output |\n|-------|------|--------|\n| **Spec** | Capture intent as requirements | `requirements.rqml` |\n| **Design** | Decide architecture, record decisions | ADRs in `.rqml/adr/` |\n| **Plan** | Break work into agent-sized stages | `.rqml/plan.md` |\n| **Code** | Implement specified behavior, keep traces current | code + tests |\n| **Verify** | Prove coverage and catch drift | trace graph + `rqml check` |\n\nNever skip ahead: do not implement behavior that is not specified, and do not make a significant architectural choice without recording it as an ADR.\n\n---\n\n## Workflow\n\n### 1. Spec\nAsk clarifying questions until you understand the goal, scope, acceptance criteria, and constraints. Don\'t assume\u2014capture assumptions as `<notes>` or `<issue>` elements. **Never implement unspecified behavior.** Update the `.rqml` file before coding:\n- Add a `<req>` with statement and acceptance criteria\n- Set appropriate `type`, `priority`, and `status="draft"`\n- Get developer confirmation; only `status="approved"` requirements drive implementation\n\n### 2. Design\nBefore building, decide *how*. Record each significant architectural decision as an **Architecture Decision Record (ADR)** in `.rqml/adr/`, following the canonical format (https://rqml.org/docs/development-process/design): `NNNN-kebab-case-slug.md`, with Status, Classification, Context, Options considered, Decision, and Consequences. A decision is ADR-worthy when there are real alternatives or the choice constrains future work; skip ADRs for low-level implementation details. ADRs are immutable once accepted\u2014supersede, don\'t edit.\n\n### 3. Plan\nBreak approved requirements into a staged implementation plan at `.rqml/plan.md`, framed for coding agents: each stage names its goal, the requirement IDs it addresses, the files it touches, and how to verify it.\n\n### 4. Code (Implement)\nRead the requirement first: `rqml show REQ-XXX`. Check blast radius before changing existing artifacts: `rqml impact REQ-XXX`. Honor the ADRs. If you discover missing requirements, stop and add them to the spec first. After implementing, record the trace link:\n\n```bash\nrqml link REQ-XXX src/path/to/implementation.ts\n```\n\n### 5. Verify\nAdd tests that reference requirement IDs, then record verification and run the gate:\n\n```bash\nrqml link REQ-XXX test/path/to/test.ts --type verifiedBy\nrqml check # must exit 0 before you are done\n```\n\n---\n\n## When Code and Spec Diverge\n\n1. **Spec gap** (code has behavior not in spec): Propose adding the requirement, mark as `status="review"`\n2. **Code bug** (code doesn\'t match spec): Fix the code\n3. **Spec bug** (spec is wrong): Propose correction, wait for developer confirmation\n\n**Never silently change the spec to match code.**\n\n---\n\n## Strictness Reference\n\n| Aspect | relaxed | standard | strict | certified |\n|--------|---------|----------|--------|-----------|\n| Spec (elicitation) | Major features | Testable reqs | Edge cases | Formal |\n| Spec-first | Recommended | Required | Required | Approved first |\n| Design (ADRs) | Optional | Significant choices | All architectural choices | With approval |\n| Plan | Optional | For multi-stage work | Required | Required |\n| Code traces | Optional | New features | All changes | With metadata |\n| Verify (test traces) | Optional | New reqs | All reqs | Full matrix |\n| Ghost features | Allowed | Blocked | Blocked | Blocked |\n\n---\n\n## Change Summary Template\n\nFor PRs and commits:\n\n```\n## RQML Trace Summary\n\n**Requirements:** REQ-xxx (added/modified/implemented)\n**Design:** ADR-xxxx \u2014 decision recorded (if any)\n**Implementation:** `path/to/file` \u2014 what changed\n**Verification:** `path/to/test` \u2014 what it verifies\n**Open items:** gaps, assumptions, follow-ups\n```\n\n---\n\n## Schema Validation\n\nThe `.rqml` file must remain valid XML conforming to the version of RQML referenced in the version attribute in the spec document.\n\n**To validate:** Use the toolchain \u2014 it validates offline against the bundled schema and also checks referential integrity the XSD alone cannot enforce:\n```bash\nrqml validate\n```\n\nIf the `rqml` CLI is not installed, `npx @rqml/cli validate` works without installation. As a last resort, xmllint (pre-installed on macOS/Linux) checks XSD validity only:\n```bash\nxmllint --schema https://rqml.org/schema/rqml-2.1.0.xsd <rqml-file-name> --noout\n```\n\n**IDE validation:** If the `.rqml` file includes `xsi:schemaLocation`, XML-aware editors (VS Code with XML extension, IntelliJ) validate automatically.\n\nThe schema comments contain detailed guidance on document structure, ID conventions, and requirement quality criteria.\n\n**If unsure:** Ask the developer before making structural changes to the spec.\n';
|
|
4014
4291
|
var AGENTS_TEMPLATE = AGENTS_default;
|
|
4015
4292
|
|
|
4016
4293
|
// src/commands/init.ts
|
|
@@ -4053,15 +4330,21 @@ async function runInit(rest) {
|
|
|
4053
4330
|
if (!wrote) process.stdout.write("nothing to do; project already initialized\n");
|
|
4054
4331
|
return EXIT.OK;
|
|
4055
4332
|
}
|
|
4056
|
-
var USAGE = "usage: rqml link <artifact-id> <uri> [--type implements|verifiedBy] [--id <edge-id>] [--kind <k>] [--title <t>] [--spec <path>]";
|
|
4333
|
+
var USAGE = "usage: rqml link <artifact-id> <uri> [--update] [--type implements|verifiedBy] [--id <edge-id>] [--kind <k>] [--title <t>] [--spec <path>]\n rqml link --refresh <edge-id> [--spec <path>]";
|
|
4057
4334
|
async function runLink(rest) {
|
|
4058
4335
|
const args = parseArgs(rest);
|
|
4336
|
+
if (args.flags.has("refresh")) {
|
|
4337
|
+
const edgeId2 = flagString(args, "refresh");
|
|
4338
|
+
if (edgeId2 === void 0) throw new UsageError(USAGE);
|
|
4339
|
+
return runRefresh(args, edgeId2);
|
|
4340
|
+
}
|
|
4059
4341
|
const [artifactId, uri] = args.positionals;
|
|
4060
4342
|
if (artifactId === void 0 || uri === void 0) throw new UsageError(USAGE);
|
|
4061
4343
|
const type = flagString(args, "type") ?? "implements";
|
|
4062
4344
|
if (type !== "implements" && type !== "verifiedBy") {
|
|
4063
4345
|
throw new UsageError(`unknown link type "${type}" (implements|verifiedBy)`);
|
|
4064
4346
|
}
|
|
4347
|
+
const update = args.flags.get("update") === true || args.flags.get("update") === "true";
|
|
4065
4348
|
const { path, xml } = readSpec(specArgs(args));
|
|
4066
4349
|
const request = { artifactId, uri, type };
|
|
4067
4350
|
const edgeId = flagString(args, "id");
|
|
@@ -4070,7 +4353,7 @@ async function runLink(rest) {
|
|
|
4070
4353
|
if (kind !== void 0) request.kind = kind;
|
|
4071
4354
|
const title = flagString(args, "title");
|
|
4072
4355
|
if (title !== void 0) request.title = title;
|
|
4073
|
-
const result = appendTraceEdge(xml, request);
|
|
4356
|
+
const result = update ? updateTraceEdge(xml, request) : appendTraceEdge(xml, request);
|
|
4074
4357
|
if (!result.ok) {
|
|
4075
4358
|
process.stderr.write(`\u2717 link failed: ${result.error}
|
|
4076
4359
|
`);
|
|
@@ -4099,6 +4382,7 @@ async function runLink(rest) {
|
|
|
4099
4382
|
if (args.json) {
|
|
4100
4383
|
const report = {
|
|
4101
4384
|
spec: path,
|
|
4385
|
+
mode: update ? "update" : "append",
|
|
4102
4386
|
edgeId: result.edgeId,
|
|
4103
4387
|
type,
|
|
4104
4388
|
artifactId,
|
|
@@ -4109,12 +4393,81 @@ async function runLink(rest) {
|
|
|
4109
4393
|
`);
|
|
4110
4394
|
} else {
|
|
4111
4395
|
const arrow = type === "implements" ? "\u2190" : "\u2192";
|
|
4396
|
+
const mode = update ? ", updated" : "";
|
|
4112
4397
|
const baseline = baselineRecorded ? ", baseline recorded" : "";
|
|
4113
4398
|
process.stdout.write(
|
|
4114
|
-
`\u2713 ${artifactId} ${arrow} ${uri} (${result.edgeId}, ${type}${baseline})
|
|
4399
|
+
`\u2713 ${artifactId} ${arrow} ${uri} (${result.edgeId}, ${type}${mode}${baseline})
|
|
4400
|
+
`
|
|
4401
|
+
);
|
|
4402
|
+
}
|
|
4403
|
+
return EXIT.OK;
|
|
4404
|
+
}
|
|
4405
|
+
function runRefresh(args, edgeId) {
|
|
4406
|
+
const { path, xml } = readSpec(specArgs(args));
|
|
4407
|
+
const parsed = parse(xml);
|
|
4408
|
+
if (!parsed.ok) {
|
|
4409
|
+
process.stderr.write(`\u2717 refresh failed: ${parsed.error.message}
|
|
4410
|
+
`);
|
|
4411
|
+
return EXIT.VALIDATION;
|
|
4412
|
+
}
|
|
4413
|
+
const link = implementsLinks(parsed.document).find((l) => l.edgeId === edgeId);
|
|
4414
|
+
if (link === void 0) {
|
|
4415
|
+
process.stderr.write(
|
|
4416
|
+
`\u2717 refresh failed: no implements edge "${edgeId}" with an external locator exists (only implements edges carry baselines)
|
|
4115
4417
|
`
|
|
4116
4418
|
);
|
|
4419
|
+
return EXIT.VALIDATION;
|
|
4117
4420
|
}
|
|
4421
|
+
const hash = computeBaseline(parsed.document, { baseDir: args.baseDir })[edgeId];
|
|
4422
|
+
if (hash === void 0) {
|
|
4423
|
+
process.stderr.write(
|
|
4424
|
+
`\u2717 refresh failed: "${link.uri}" cannot be hashed (missing file or non-filesystem URI)
|
|
4425
|
+
`
|
|
4426
|
+
);
|
|
4427
|
+
return EXIT.VALIDATION;
|
|
4428
|
+
}
|
|
4429
|
+
const baseline = loadBaseline(args.baseDir) ?? {};
|
|
4430
|
+
baseline[edgeId] = hash;
|
|
4431
|
+
saveBaseline(args.baseDir, baseline);
|
|
4432
|
+
if (args.json) {
|
|
4433
|
+
const report = { spec: path, mode: "refresh", edgeId, uri: link.uri, hash };
|
|
4434
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}
|
|
4435
|
+
`);
|
|
4436
|
+
} else {
|
|
4437
|
+
process.stdout.write(`\u2713 baseline refreshed for ${edgeId} (${link.uri})
|
|
4438
|
+
`);
|
|
4439
|
+
}
|
|
4440
|
+
return EXIT.OK;
|
|
4441
|
+
}
|
|
4442
|
+
|
|
4443
|
+
// src/commands/matrix.ts
|
|
4444
|
+
function list(value) {
|
|
4445
|
+
if (value === void 0) return void 0;
|
|
4446
|
+
const items = value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
4447
|
+
return items.length > 0 ? items : void 0;
|
|
4448
|
+
}
|
|
4449
|
+
async function runMatrix(rest) {
|
|
4450
|
+
const args = parseArgs(rest);
|
|
4451
|
+
const { path, xml } = readSpec(specArgs(args));
|
|
4452
|
+
const parsed = parse(xml);
|
|
4453
|
+
if (!parsed.ok) {
|
|
4454
|
+
process.stderr.write(`\u2717 ${path}: ${parsed.error.message}
|
|
4455
|
+
`);
|
|
4456
|
+
return EXIT.VALIDATION;
|
|
4457
|
+
}
|
|
4458
|
+
const status = list(flagString(args, "status"));
|
|
4459
|
+
const type = list(flagString(args, "type"));
|
|
4460
|
+
const warning = list(flagString(args, "warning"));
|
|
4461
|
+
const filter = {};
|
|
4462
|
+
if (status) filter.status = status;
|
|
4463
|
+
if (type) filter.type = type;
|
|
4464
|
+
if (warning) filter.warning = warning;
|
|
4465
|
+
const filtered = status !== void 0 || type !== void 0 || warning !== void 0;
|
|
4466
|
+
const matrix = buildMatrix(parsed.document, filtered ? filter : void 0);
|
|
4467
|
+
process.stdout.write(
|
|
4468
|
+
args.json ? `${JSON.stringify(matrix, null, 2)}
|
|
4469
|
+
` : matrixToMarkdown(matrix)
|
|
4470
|
+
);
|
|
4118
4471
|
return EXIT.OK;
|
|
4119
4472
|
}
|
|
4120
4473
|
|
|
@@ -4234,7 +4587,7 @@ async function runValidate(rest) {
|
|
|
4234
4587
|
}
|
|
4235
4588
|
|
|
4236
4589
|
// src/index.ts
|
|
4237
|
-
var VERSION = "
|
|
4590
|
+
var VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
4238
4591
|
var HELP = `rqml \u2014 RQML reference CLI (v${VERSION})
|
|
4239
4592
|
|
|
4240
4593
|
Usage:
|
|
@@ -4246,12 +4599,16 @@ Commands:
|
|
|
4246
4599
|
status [path] Show spec, coverage, and lint summary
|
|
4247
4600
|
check [path] Deterministic enforcement gate (validation + coverage + drift)
|
|
4248
4601
|
link <id> <uri> Record an implements/verifiedBy edge and its drift baseline
|
|
4602
|
+
(--update repoints an existing edge; --refresh <edge-id>
|
|
4603
|
+
re-records only the baseline for an intentional change)
|
|
4249
4604
|
show <id> Extract one artifact with its trace neighborhood
|
|
4250
4605
|
impact <id> What is affected, transitively, if this artifact changes
|
|
4606
|
+
matrix [path] Traceability matrix: status, goals, code, tests, warnings
|
|
4251
4607
|
skeleton <kind> Print a schema-valid snippet (req|edge|testCase|stateMachine)
|
|
4252
4608
|
|
|
4253
4609
|
Options:
|
|
4254
|
-
--json Emit machine-readable JSON (status, check, validate, link, show, impact)
|
|
4610
|
+
--json Emit machine-readable JSON (status, check, validate, link, show, impact, matrix)
|
|
4611
|
+
--status/--type/--warning Filter matrix rows (comma-separated, e.g. --warning unverified)
|
|
4255
4612
|
--strictness <level> relaxed | standard | strict | certified (default: standard)
|
|
4256
4613
|
--base-dir <dir> Directory to resolve the spec and code links against
|
|
4257
4614
|
--spec <path> Explicit spec file (link, show, impact)
|
|
@@ -4259,6 +4616,8 @@ Options:
|
|
|
4259
4616
|
--id <id> Explicit edge id (link) or skeleton root id
|
|
4260
4617
|
--kind <kind> Locator kind hint for link (default: code/test by type)
|
|
4261
4618
|
--title <title> Locator title hint for link
|
|
4619
|
+
--update Replace the external locator of an existing edge (link)
|
|
4620
|
+
--refresh <edge-id> Re-record the drift baseline for one edge (link)
|
|
4262
4621
|
-h, --help Show this help
|
|
4263
4622
|
-v, --version Show version
|
|
4264
4623
|
|
|
@@ -4281,6 +4640,8 @@ async function main(argv) {
|
|
|
4281
4640
|
return runShow(rest);
|
|
4282
4641
|
case "impact":
|
|
4283
4642
|
return runImpact(rest);
|
|
4643
|
+
case "matrix":
|
|
4644
|
+
return runMatrix(rest);
|
|
4284
4645
|
case "skeleton":
|
|
4285
4646
|
return runSkeleton(rest);
|
|
4286
4647
|
case "-v":
|