@rqml/cli 0.1.1 → 0.3.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/index.js +834 -20
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { __commonJS, __toESM } from './chunk-5WRI5ZAA.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
import { createHash } from 'crypto';
|
|
5
|
+
import { writeFileSync, existsSync, readFileSync, mkdirSync, statSync, readdirSync } from 'fs';
|
|
6
|
+
import { resolve, join, dirname, isAbsolute } from 'path';
|
|
5
7
|
import { fileURLToPath } from 'url';
|
|
6
8
|
|
|
7
9
|
// ../../node_modules/.pnpm/fast-xml-parser@4.5.6/node_modules/fast-xml-parser/src/util.js
|
|
@@ -3008,6 +3010,64 @@ function resolveTrace(doc) {
|
|
|
3008
3010
|
}));
|
|
3009
3011
|
return { edges, diagnostics };
|
|
3010
3012
|
}
|
|
3013
|
+
function endpointKey(locator) {
|
|
3014
|
+
return locator.kind === "local" ? locator.id : locator.uri;
|
|
3015
|
+
}
|
|
3016
|
+
function impactOf(doc, id) {
|
|
3017
|
+
const idIndex = declaredIdIndex(doc);
|
|
3018
|
+
const visited = /* @__PURE__ */ new Set([id]);
|
|
3019
|
+
const affected = [];
|
|
3020
|
+
let frontier = [{ key: id, path: [] }];
|
|
3021
|
+
let distance = 0;
|
|
3022
|
+
while (frontier.length > 0) {
|
|
3023
|
+
distance += 1;
|
|
3024
|
+
const next = [];
|
|
3025
|
+
for (const node of frontier) {
|
|
3026
|
+
for (const edge of doc.trace) {
|
|
3027
|
+
const fromKey = endpointKey(edge.from);
|
|
3028
|
+
const toKey = endpointKey(edge.to);
|
|
3029
|
+
const hops = [
|
|
3030
|
+
{ direction: "outgoing", here: fromKey, far: edge.to },
|
|
3031
|
+
{ direction: "incoming", here: toKey, far: edge.from }
|
|
3032
|
+
];
|
|
3033
|
+
for (const hop of hops) {
|
|
3034
|
+
const farKey = endpointKey(hop.far);
|
|
3035
|
+
if (hop.here !== node.key || visited.has(farKey)) continue;
|
|
3036
|
+
visited.add(farKey);
|
|
3037
|
+
const local = hop.far.kind === "local";
|
|
3038
|
+
const kind = local ? idIndex.get(farKey)?.kind ?? "unknown" : hop.far.kind;
|
|
3039
|
+
const step = {
|
|
3040
|
+
edgeId: edge.id,
|
|
3041
|
+
type: edge.type,
|
|
3042
|
+
direction: hop.direction,
|
|
3043
|
+
target: farKey
|
|
3044
|
+
};
|
|
3045
|
+
const path = [...node.path, step];
|
|
3046
|
+
affected.push({ id: farKey, kind, distance, path });
|
|
3047
|
+
if (local) next.push({ key: farKey, path });
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
frontier = next;
|
|
3052
|
+
}
|
|
3053
|
+
affected.sort((a, b) => a.distance - b.distance || a.id.localeCompare(b.id));
|
|
3054
|
+
const groupIndex = /* @__PURE__ */ new Map();
|
|
3055
|
+
for (const artifact of affected) {
|
|
3056
|
+
const last = artifact.path[artifact.path.length - 1];
|
|
3057
|
+
if (last === void 0) continue;
|
|
3058
|
+
const key = `${last.direction}:${last.type}`;
|
|
3059
|
+
let group = groupIndex.get(key);
|
|
3060
|
+
if (group === void 0) {
|
|
3061
|
+
group = { direction: last.direction, type: last.type, ids: [] };
|
|
3062
|
+
groupIndex.set(key, group);
|
|
3063
|
+
}
|
|
3064
|
+
group.ids.push(artifact.id);
|
|
3065
|
+
}
|
|
3066
|
+
const groups = [...groupIndex.values()].map((g) => ({ ...g, ids: [...new Set(g.ids)].sort() })).sort(
|
|
3067
|
+
(a, b) => a.direction.localeCompare(b.direction) || a.type.localeCompare(b.type)
|
|
3068
|
+
);
|
|
3069
|
+
return { id, affected, groups };
|
|
3070
|
+
}
|
|
3011
3071
|
var ATTR_PREFIX3 = "@_";
|
|
3012
3072
|
var REFERENCE_ELEMENTS = /* @__PURE__ */ new Set(["local", "doc"]);
|
|
3013
3073
|
var parser2 = new import_fast_xml_parser.XMLParser({
|
|
@@ -3047,7 +3107,7 @@ function flatRefs(edge, out) {
|
|
|
3047
3107
|
if (from !== void 0) out.push({ refId: from, edgeId, side: "from", flat: true });
|
|
3048
3108
|
if (to !== void 0) out.push({ refId: to, edgeId, side: "to", flat: true });
|
|
3049
3109
|
}
|
|
3050
|
-
function walk(node, declared, refs) {
|
|
3110
|
+
function walk(node, declared, refs, machines) {
|
|
3051
3111
|
for (const [key, value] of Object.entries(node)) {
|
|
3052
3112
|
if (key.startsWith(ATTR_PREFIX3) || key === "#text") continue;
|
|
3053
3113
|
for (const item of asArray2(value)) {
|
|
@@ -3056,7 +3116,8 @@ function walk(node, declared, refs) {
|
|
|
3056
3116
|
if (id !== void 0 && !REFERENCE_ELEMENTS.has(key)) declared.push(id);
|
|
3057
3117
|
if (key === "edge") nestedRefs(item, refs);
|
|
3058
3118
|
else if (key === "traceEdge") flatRefs(item, refs);
|
|
3059
|
-
|
|
3119
|
+
else if (key === "stateMachine") machines.push(item);
|
|
3120
|
+
walk(item, declared, refs, machines);
|
|
3060
3121
|
}
|
|
3061
3122
|
}
|
|
3062
3123
|
}
|
|
@@ -3105,7 +3166,8 @@ function checkIntegrity(xml) {
|
|
|
3105
3166
|
if (!root) return [];
|
|
3106
3167
|
const declared = [];
|
|
3107
3168
|
const refs = [];
|
|
3108
|
-
|
|
3169
|
+
const machines = [];
|
|
3170
|
+
walk(root, declared, refs, machines);
|
|
3109
3171
|
const starts = lineStarts(xml);
|
|
3110
3172
|
const diagnostics = [];
|
|
3111
3173
|
const counts = /* @__PURE__ */ new Map();
|
|
@@ -3146,8 +3208,452 @@ function checkIntegrity(xml) {
|
|
|
3146
3208
|
if (lines.length > 0) diag.line = lines[0];
|
|
3147
3209
|
diagnostics.push(diag);
|
|
3148
3210
|
}
|
|
3211
|
+
for (const sm of machines) {
|
|
3212
|
+
const smId = attr2(sm, "id") ?? "";
|
|
3213
|
+
const stateIds = /* @__PURE__ */ new Set();
|
|
3214
|
+
const finalStates = /* @__PURE__ */ new Set();
|
|
3215
|
+
for (const st of asArray2(sm.state).filter(isNode2)) {
|
|
3216
|
+
const sid = attr2(st, "id");
|
|
3217
|
+
if (sid === void 0) continue;
|
|
3218
|
+
stateIds.add(sid);
|
|
3219
|
+
if (attr2(st, "type") === "final") finalStates.add(sid);
|
|
3220
|
+
}
|
|
3221
|
+
const initial = attr2(sm, "initial");
|
|
3222
|
+
if (initial !== void 0 && !stateIds.has(initial)) {
|
|
3223
|
+
const re = new RegExp(
|
|
3224
|
+
`<stateMachine\\b[^>]*?\\sinitial\\s*=\\s*"${escapeRegExp(initial)}"`,
|
|
3225
|
+
"g"
|
|
3226
|
+
);
|
|
3227
|
+
const lines = matchLines(xml, starts, re);
|
|
3228
|
+
const diag = {
|
|
3229
|
+
source: "validate",
|
|
3230
|
+
severity: "error",
|
|
3231
|
+
rule: "unresolved-state-ref",
|
|
3232
|
+
message: `State machine "${smId}" initial state "${initial}" is not a declared state of the machine.`
|
|
3233
|
+
};
|
|
3234
|
+
if (lines.length > 0) diag.line = lines[0];
|
|
3235
|
+
diagnostics.push(diag);
|
|
3236
|
+
}
|
|
3237
|
+
for (const tr of asArray2(sm.transition).filter(isNode2)) {
|
|
3238
|
+
const trId = attr2(tr, "id") ?? "";
|
|
3239
|
+
for (const side of ["from", "to"]) {
|
|
3240
|
+
const refId = attr2(tr, side);
|
|
3241
|
+
if (refId === void 0 || stateIds.has(refId)) continue;
|
|
3242
|
+
const re = new RegExp(
|
|
3243
|
+
`<transition\\b[^>]*?\\s${side}\\s*=\\s*"${escapeRegExp(refId)}"`,
|
|
3244
|
+
"g"
|
|
3245
|
+
);
|
|
3246
|
+
const lines = matchLines(xml, starts, re);
|
|
3247
|
+
const diag = {
|
|
3248
|
+
source: "validate",
|
|
3249
|
+
severity: "error",
|
|
3250
|
+
rule: "unresolved-state-ref",
|
|
3251
|
+
message: `Transition "${trId}" (${side}) references unknown state "${refId}" in state machine "${smId}".`
|
|
3252
|
+
};
|
|
3253
|
+
if (lines.length > 0) diag.line = lines[0];
|
|
3254
|
+
diagnostics.push(diag);
|
|
3255
|
+
}
|
|
3256
|
+
const from = attr2(tr, "from");
|
|
3257
|
+
if (from !== void 0 && finalStates.has(from)) {
|
|
3258
|
+
const re = new RegExp(
|
|
3259
|
+
`<transition\\b[^>]*?\\sid\\s*=\\s*"${escapeRegExp(trId)}"`,
|
|
3260
|
+
"g"
|
|
3261
|
+
);
|
|
3262
|
+
const lines = matchLines(xml, starts, re);
|
|
3263
|
+
const diag = {
|
|
3264
|
+
source: "validate",
|
|
3265
|
+
severity: "error",
|
|
3266
|
+
rule: "final-state-outgoing",
|
|
3267
|
+
message: `Final state "${from}" has outgoing transition "${trId}" in state machine "${smId}".`
|
|
3268
|
+
};
|
|
3269
|
+
if (lines.length > 0) diag.line = lines[0];
|
|
3270
|
+
diagnostics.push(diag);
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3149
3274
|
return diagnostics;
|
|
3150
3275
|
}
|
|
3276
|
+
var ID_PATTERN = /^[A-Za-z][A-Za-z0-9._-]{1,79}$/;
|
|
3277
|
+
function escapeAttr(value) {
|
|
3278
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
3279
|
+
}
|
|
3280
|
+
function deriveEdgeId(type, artifactId, taken) {
|
|
3281
|
+
const prefix = type === "implements" ? "E-IMPL-" : "E-VER-";
|
|
3282
|
+
const base = prefix + artifactId.replace(/^REQ-/, "");
|
|
3283
|
+
if (!taken(base)) return base;
|
|
3284
|
+
for (let n = 2; ; n++) {
|
|
3285
|
+
const candidate = `${base}-${n}`;
|
|
3286
|
+
if (!taken(candidate)) return candidate;
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
function edgeBlock(request, edgeId, indent) {
|
|
3290
|
+
const kind = request.kind ?? (request.type === "implements" ? "code" : "test");
|
|
3291
|
+
const titleAttr = request.title !== void 0 ? ` title="${escapeAttr(request.title)}"` : "";
|
|
3292
|
+
const external = `<external uri="${escapeAttr(request.uri)}" kind="${escapeAttr(kind)}"${titleAttr}/>`;
|
|
3293
|
+
const local = `<local id="${request.artifactId}"/>`;
|
|
3294
|
+
const [from, to] = request.type === "implements" ? [external, local] : [local, external];
|
|
3295
|
+
return [
|
|
3296
|
+
`${indent}<edge id="${edgeId}" type="${request.type}">`,
|
|
3297
|
+
`${indent} <from><locator>${from}</locator></from>`,
|
|
3298
|
+
`${indent} <to><locator>${to}</locator></to>`,
|
|
3299
|
+
`${indent}</edge>`
|
|
3300
|
+
].join("\n");
|
|
3301
|
+
}
|
|
3302
|
+
function lineInfo(xml, index) {
|
|
3303
|
+
const start = xml.lastIndexOf("\n", index - 1) + 1;
|
|
3304
|
+
const prefix = xml.slice(start, index);
|
|
3305
|
+
return { start, indent: /^[ \t]*$/.test(prefix) ? prefix : "" };
|
|
3306
|
+
}
|
|
3307
|
+
function appendTraceEdge(xml, request) {
|
|
3308
|
+
const parsed = parse(xml);
|
|
3309
|
+
if (!parsed.ok) {
|
|
3310
|
+
return { ok: false, error: `document does not parse: ${parsed.error.message}` };
|
|
3311
|
+
}
|
|
3312
|
+
const idIndex = declaredIdIndex(parsed.document);
|
|
3313
|
+
if (!idIndex.has(request.artifactId)) {
|
|
3314
|
+
return {
|
|
3315
|
+
ok: false,
|
|
3316
|
+
error: `artifact "${request.artifactId}" is not declared in the document`
|
|
3317
|
+
};
|
|
3318
|
+
}
|
|
3319
|
+
if (request.uri.trim() === "") {
|
|
3320
|
+
return { ok: false, error: "locator uri must not be empty" };
|
|
3321
|
+
}
|
|
3322
|
+
let edgeId;
|
|
3323
|
+
if (request.edgeId !== void 0) {
|
|
3324
|
+
if (!ID_PATTERN.test(request.edgeId)) {
|
|
3325
|
+
return {
|
|
3326
|
+
ok: false,
|
|
3327
|
+
error: `edge id "${request.edgeId}" does not match the RQML id pattern`
|
|
3328
|
+
};
|
|
3329
|
+
}
|
|
3330
|
+
if (idIndex.has(request.edgeId)) {
|
|
3331
|
+
return { ok: false, error: `edge id "${request.edgeId}" is already declared` };
|
|
3332
|
+
}
|
|
3333
|
+
edgeId = request.edgeId;
|
|
3334
|
+
} else {
|
|
3335
|
+
edgeId = deriveEdgeId(request.type, request.artifactId, (id) => idIndex.has(id));
|
|
3336
|
+
}
|
|
3337
|
+
let updated;
|
|
3338
|
+
let edgeXml;
|
|
3339
|
+
const closeIdx = xml.lastIndexOf("</trace>");
|
|
3340
|
+
if (closeIdx >= 0) {
|
|
3341
|
+
const { start, indent } = lineInfo(xml, closeIdx);
|
|
3342
|
+
edgeXml = edgeBlock(request, edgeId, `${indent} `);
|
|
3343
|
+
updated = `${xml.slice(0, start)}${edgeXml}
|
|
3344
|
+
${xml.slice(start)}`;
|
|
3345
|
+
} else {
|
|
3346
|
+
const anchorIdx = (() => {
|
|
3347
|
+
const gov = xml.indexOf("<governance");
|
|
3348
|
+
return gov >= 0 ? gov : xml.lastIndexOf("</rqml>");
|
|
3349
|
+
})();
|
|
3350
|
+
if (anchorIdx < 0) return { ok: false, error: "no </rqml> close tag found" };
|
|
3351
|
+
const { start, indent } = lineInfo(xml, anchorIdx);
|
|
3352
|
+
const sectionIndent = indent === "" ? " " : indent;
|
|
3353
|
+
edgeXml = edgeBlock(request, edgeId, `${sectionIndent} `);
|
|
3354
|
+
const section = `${sectionIndent}<trace>
|
|
3355
|
+
${edgeXml}
|
|
3356
|
+
${sectionIndent}</trace>
|
|
3357
|
+
`;
|
|
3358
|
+
updated = `${xml.slice(0, start)}${section}${xml.slice(start)}`;
|
|
3359
|
+
}
|
|
3360
|
+
const before = checkIntegrity(xml).length;
|
|
3361
|
+
const reparsed = parse(updated);
|
|
3362
|
+
if (!reparsed.ok) {
|
|
3363
|
+
return {
|
|
3364
|
+
ok: false,
|
|
3365
|
+
error: `edit produced an unparseable document: ${reparsed.error.message}`
|
|
3366
|
+
};
|
|
3367
|
+
}
|
|
3368
|
+
if (checkIntegrity(updated).length > before) {
|
|
3369
|
+
return {
|
|
3370
|
+
ok: false,
|
|
3371
|
+
error: "edit introduced an integrity violation; document left unchanged"
|
|
3372
|
+
};
|
|
3373
|
+
}
|
|
3374
|
+
return { ok: true, xml: updated, edgeId, edgeXml: edgeXml.trim() };
|
|
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 endpointKey2(locator) {
|
|
3457
|
+
return locator.kind === "local" ? locator.id : locator.uri;
|
|
3458
|
+
}
|
|
3459
|
+
function details(doc, id, kind) {
|
|
3460
|
+
switch (kind) {
|
|
3461
|
+
case "goal": {
|
|
3462
|
+
const g = doc.goals?.goals?.find((x) => x.id === id);
|
|
3463
|
+
return g ? {
|
|
3464
|
+
title: g.title,
|
|
3465
|
+
statement: g.statement,
|
|
3466
|
+
status: g.status,
|
|
3467
|
+
priority: g.priority,
|
|
3468
|
+
rationale: g.rationale
|
|
3469
|
+
} : {};
|
|
3470
|
+
}
|
|
3471
|
+
case "qgoal": {
|
|
3472
|
+
const g = doc.goals?.qualityGoals?.find((x) => x.id === id);
|
|
3473
|
+
return g ? {
|
|
3474
|
+
title: g.title,
|
|
3475
|
+
statement: g.statement,
|
|
3476
|
+
status: g.status,
|
|
3477
|
+
priority: g.priority,
|
|
3478
|
+
notes: g.metric
|
|
3479
|
+
} : {};
|
|
3480
|
+
}
|
|
3481
|
+
case "obstacle": {
|
|
3482
|
+
const o = doc.goals?.obstacles?.find((x) => x.id === id);
|
|
3483
|
+
return o ? { title: o.title, statement: o.statement, notes: o.mitigation } : {};
|
|
3484
|
+
}
|
|
3485
|
+
case "scenario":
|
|
3486
|
+
case "misuseCase":
|
|
3487
|
+
case "edgeCase": {
|
|
3488
|
+
const all = [
|
|
3489
|
+
...doc.scenarios?.scenarios ?? [],
|
|
3490
|
+
...doc.scenarios?.misuseCases ?? [],
|
|
3491
|
+
...doc.scenarios?.edgeCases ?? []
|
|
3492
|
+
];
|
|
3493
|
+
const s = all.find((x) => x.id === id);
|
|
3494
|
+
return s ? { title: s.title, statement: s.narrative } : {};
|
|
3495
|
+
}
|
|
3496
|
+
case "testCase": {
|
|
3497
|
+
const t = doc.verification?.testCases?.find((x) => x.id === id);
|
|
3498
|
+
return t ? { title: t.title, statement: t.purpose, notes: t.expected } : {};
|
|
3499
|
+
}
|
|
3500
|
+
case "testSuite": {
|
|
3501
|
+
const t = doc.verification?.testSuites?.find((x) => x.id === id);
|
|
3502
|
+
return t ? { title: t.title, statement: t.description } : {};
|
|
3503
|
+
}
|
|
3504
|
+
case "risk": {
|
|
3505
|
+
const r = doc.catalogs?.risks?.find((x) => x.id === id);
|
|
3506
|
+
return r ? { statement: r.statement, notes: r.mitigation } : {};
|
|
3507
|
+
}
|
|
3508
|
+
case "constraint": {
|
|
3509
|
+
const c = doc.catalogs?.constraints?.find((x) => x.id === id);
|
|
3510
|
+
return c ? { statement: c.statement } : {};
|
|
3511
|
+
}
|
|
3512
|
+
case "decision": {
|
|
3513
|
+
const d = doc.catalogs?.decisions?.find((x) => x.id === id);
|
|
3514
|
+
return d ? { statement: d.decision, rationale: d.context, status: d.status } : {};
|
|
3515
|
+
}
|
|
3516
|
+
case "term": {
|
|
3517
|
+
const t = doc.catalogs?.glossary?.find((x) => x.id === id);
|
|
3518
|
+
return t ? { title: t.name, statement: t.definition } : {};
|
|
3519
|
+
}
|
|
3520
|
+
case "rule": {
|
|
3521
|
+
const r = doc.domain?.businessRules?.find((x) => x.id === id);
|
|
3522
|
+
return r ? { statement: r.statement, notes: r.examples } : {};
|
|
3523
|
+
}
|
|
3524
|
+
case "entity": {
|
|
3525
|
+
const e = doc.domain?.entities?.find((x) => x.id === id);
|
|
3526
|
+
return e ? { title: e.name, statement: e.description } : {};
|
|
3527
|
+
}
|
|
3528
|
+
case "stateMachine": {
|
|
3529
|
+
const sm = doc.behavior?.stateMachines?.find((x) => x.id === id);
|
|
3530
|
+
return sm ? { title: sm.name, statement: sm.description } : {};
|
|
3531
|
+
}
|
|
3532
|
+
default:
|
|
3533
|
+
return {};
|
|
3534
|
+
}
|
|
3535
|
+
}
|
|
3536
|
+
function extractArtifact(doc, id) {
|
|
3537
|
+
const idIndex = declaredIdIndex(doc);
|
|
3538
|
+
const ref = idIndex.get(id);
|
|
3539
|
+
if (ref === void 0) return void 0;
|
|
3540
|
+
const slice = { id, kind: ref.kind, edges: [] };
|
|
3541
|
+
const req = requirementIndex(doc).get(id);
|
|
3542
|
+
if (req !== void 0) {
|
|
3543
|
+
slice.title = req.title;
|
|
3544
|
+
slice.statement = req.statement;
|
|
3545
|
+
slice.reqType = req.type;
|
|
3546
|
+
if (req.status !== void 0) slice.status = req.status;
|
|
3547
|
+
if (req.priority !== void 0) slice.priority = req.priority;
|
|
3548
|
+
if (req.rationale !== void 0) slice.rationale = req.rationale;
|
|
3549
|
+
if (req.notes !== void 0) slice.notes = req.notes;
|
|
3550
|
+
if (req.acceptance.length > 0) slice.acceptance = req.acceptance;
|
|
3551
|
+
} else {
|
|
3552
|
+
const extra = Object.fromEntries(
|
|
3553
|
+
Object.entries(details(doc, id, ref.kind)).filter(([, v]) => v !== void 0)
|
|
3554
|
+
);
|
|
3555
|
+
Object.assign(slice, extra);
|
|
3556
|
+
}
|
|
3557
|
+
for (const edge of doc.trace) {
|
|
3558
|
+
const fromKey = endpointKey2(edge.from);
|
|
3559
|
+
const toKey = endpointKey2(edge.to);
|
|
3560
|
+
if (fromKey !== id && toKey !== id) continue;
|
|
3561
|
+
const direction = fromKey === id ? "outgoing" : "incoming";
|
|
3562
|
+
const far = direction === "outgoing" ? edge.to : edge.from;
|
|
3563
|
+
const target = endpointKey2(far);
|
|
3564
|
+
const targetKind = far.kind === "local" ? idIndex.get(target)?.kind ?? "unknown" : far.kind;
|
|
3565
|
+
const sliceEdge = {
|
|
3566
|
+
edgeId: edge.id,
|
|
3567
|
+
type: edge.type,
|
|
3568
|
+
direction,
|
|
3569
|
+
target,
|
|
3570
|
+
targetKind
|
|
3571
|
+
};
|
|
3572
|
+
if (far.kind !== "local" && far.title !== void 0) sliceEdge.title = far.title;
|
|
3573
|
+
slice.edges.push(sliceEdge);
|
|
3574
|
+
}
|
|
3575
|
+
return slice;
|
|
3576
|
+
}
|
|
3577
|
+
function sliceToMarkdown(slice) {
|
|
3578
|
+
const lines = [];
|
|
3579
|
+
const heading = slice.title !== void 0 ? `${slice.id} \u2014 ${slice.title}` : slice.id;
|
|
3580
|
+
lines.push(`## ${heading}`);
|
|
3581
|
+
const facts = [
|
|
3582
|
+
`kind: ${slice.kind}${slice.reqType !== void 0 ? ` (${slice.reqType})` : ""}`
|
|
3583
|
+
];
|
|
3584
|
+
if (slice.status !== void 0) facts.push(`status: ${slice.status}`);
|
|
3585
|
+
if (slice.priority !== void 0) facts.push(`priority: ${slice.priority}`);
|
|
3586
|
+
lines.push(facts.join(" \xB7 "), "");
|
|
3587
|
+
if (slice.statement !== void 0) lines.push(slice.statement.trim(), "");
|
|
3588
|
+
if (slice.rationale !== void 0) {
|
|
3589
|
+
lines.push(`**Rationale:** ${slice.rationale.trim()}`, "");
|
|
3590
|
+
}
|
|
3591
|
+
if (slice.notes !== void 0) lines.push(`**Notes:** ${slice.notes.trim()}`, "");
|
|
3592
|
+
if (slice.acceptance !== void 0 && slice.acceptance.length > 0) {
|
|
3593
|
+
lines.push("### Acceptance");
|
|
3594
|
+
for (const c of slice.acceptance) {
|
|
3595
|
+
const parts = [];
|
|
3596
|
+
if (c.given !== void 0) parts.push(`GIVEN ${c.given.trim()}`);
|
|
3597
|
+
if (c.when !== void 0) parts.push(`WHEN ${c.when.trim()}`);
|
|
3598
|
+
parts.push(`THEN ${c.then.trim()}`);
|
|
3599
|
+
lines.push(`- ${c.id !== void 0 ? `\`${c.id}\` ` : ""}${parts.join(" ")}`);
|
|
3600
|
+
}
|
|
3601
|
+
lines.push("");
|
|
3602
|
+
}
|
|
3603
|
+
if (slice.edges.length > 0) {
|
|
3604
|
+
lines.push("### Trace");
|
|
3605
|
+
for (const e of slice.edges) {
|
|
3606
|
+
const arrow = e.direction === "outgoing" ? "\u2192" : "\u2190";
|
|
3607
|
+
lines.push(`- ${arrow} ${e.type} ${e.target} (${e.targetKind}, \`${e.edgeId}\`)`);
|
|
3608
|
+
}
|
|
3609
|
+
lines.push("");
|
|
3610
|
+
}
|
|
3611
|
+
return `${lines.join("\n").trimEnd()}
|
|
3612
|
+
`;
|
|
3613
|
+
}
|
|
3614
|
+
var SKELETON_KINDS = [
|
|
3615
|
+
"req",
|
|
3616
|
+
"edge",
|
|
3617
|
+
"testCase",
|
|
3618
|
+
"stateMachine"
|
|
3619
|
+
];
|
|
3620
|
+
var TEMPLATES = {
|
|
3621
|
+
req: (id) => `<req id="${id}" type="FR" title="Title" status="draft" priority="must">
|
|
3622
|
+
<statement>The system SHALL ...</statement>
|
|
3623
|
+
<acceptance>
|
|
3624
|
+
<criterion id="${id}-CRIT-1">
|
|
3625
|
+
<given>...</given>
|
|
3626
|
+
<when>...</when>
|
|
3627
|
+
<then>...</then>
|
|
3628
|
+
</criterion>
|
|
3629
|
+
</acceptance>
|
|
3630
|
+
</req>`,
|
|
3631
|
+
edge: (id) => `<edge id="${id}" type="satisfies">
|
|
3632
|
+
<from><locator><local id="REQ-AREA-001"/></locator></from>
|
|
3633
|
+
<to><locator><local id="GOAL-NAME"/></locator></to>
|
|
3634
|
+
</edge>`,
|
|
3635
|
+
testCase: (id) => `<testCase id="${id}" type="unit" title="Title">
|
|
3636
|
+
<purpose>...</purpose>
|
|
3637
|
+
<steps>...</steps>
|
|
3638
|
+
<expected>...</expected>
|
|
3639
|
+
</testCase>`,
|
|
3640
|
+
stateMachine: (id) => `<stateMachine id="${id}" name="Name" initial="ST-START">
|
|
3641
|
+
<state id="ST-START" name="Start" type="initial"/>
|
|
3642
|
+
<state id="ST-DONE" name="Done" type="final"/>
|
|
3643
|
+
<transition id="TR-FINISH" from="ST-START" to="ST-DONE" event="finish"/>
|
|
3644
|
+
</stateMachine>`
|
|
3645
|
+
};
|
|
3646
|
+
var DEFAULT_IDS = {
|
|
3647
|
+
req: "REQ-AREA-001",
|
|
3648
|
+
edge: "E-AREA-001",
|
|
3649
|
+
testCase: "TC-NAME",
|
|
3650
|
+
stateMachine: "SM-NAME"
|
|
3651
|
+
};
|
|
3652
|
+
function skeleton(kind, options = {}) {
|
|
3653
|
+
const template = TEMPLATES[kind];
|
|
3654
|
+
return `${template(options.id ?? DEFAULT_IDS[kind])}
|
|
3655
|
+
`;
|
|
3656
|
+
}
|
|
3151
3657
|
var UPWARD_TARGET_KINDS = /* @__PURE__ */ new Set([
|
|
3152
3658
|
"goal",
|
|
3153
3659
|
"qgoal",
|
|
@@ -3200,15 +3706,20 @@ function computeCoverage(doc) {
|
|
|
3200
3706
|
if (!(incoming.get(id)?.satisfies?.length ?? 0)) uncoveredGoals.push(id);
|
|
3201
3707
|
}
|
|
3202
3708
|
uncoveredGoals.sort();
|
|
3709
|
+
const statusOf = new Map(reqs.map((r) => [r.id, r.status]));
|
|
3203
3710
|
const unverifiedRequirements = [];
|
|
3204
3711
|
const unimplementedRequirements = [];
|
|
3712
|
+
const unimplementedApprovedRequirements = [];
|
|
3205
3713
|
const orphanRequirements = [];
|
|
3206
3714
|
for (const id of [...reqIds].sort()) {
|
|
3207
3715
|
const out = outgoing.get(id) ?? {};
|
|
3208
3716
|
const inc = incoming.get(id) ?? {};
|
|
3209
3717
|
const verified = (out.verifiedBy?.length ?? 0) > 0 || (inc.covers?.length ?? 0) > 0;
|
|
3210
3718
|
if (!verified) unverifiedRequirements.push(id);
|
|
3211
|
-
if (!(inc.implements?.length ?? 0))
|
|
3719
|
+
if (!(inc.implements?.length ?? 0)) {
|
|
3720
|
+
unimplementedRequirements.push(id);
|
|
3721
|
+
if (statusOf.get(id) === "approved") unimplementedApprovedRequirements.push(id);
|
|
3722
|
+
}
|
|
3212
3723
|
const satisfiesUpward = (out.satisfies ?? []).some((edgeId) => {
|
|
3213
3724
|
const edge = doc.trace.find((e) => e.id === edgeId);
|
|
3214
3725
|
const to = edge?.to;
|
|
@@ -3218,6 +3729,15 @@ function computeCoverage(doc) {
|
|
|
3218
3729
|
});
|
|
3219
3730
|
if (!satisfiesUpward) orphanRequirements.push(id);
|
|
3220
3731
|
}
|
|
3732
|
+
const prematureImplementations = [];
|
|
3733
|
+
for (const edge of doc.trace) {
|
|
3734
|
+
if (edge.type !== "implements" || edge.to.kind !== "local") continue;
|
|
3735
|
+
const requirementId = edge.to.id;
|
|
3736
|
+
if (!reqIds.has(requirementId)) continue;
|
|
3737
|
+
if (statusOf.get(requirementId) === "approved") continue;
|
|
3738
|
+
prematureImplementations.push({ edgeId: edge.id, requirementId });
|
|
3739
|
+
}
|
|
3740
|
+
prematureImplementations.sort((a, b) => a.edgeId.localeCompare(b.edgeId));
|
|
3221
3741
|
const diagnostics = [];
|
|
3222
3742
|
for (const id of uncoveredGoals)
|
|
3223
3743
|
diagnostics.push(
|
|
@@ -3251,12 +3771,22 @@ function computeCoverage(doc) {
|
|
|
3251
3771
|
`Requirement "${id}" satisfies no goal or scenario.`
|
|
3252
3772
|
)
|
|
3253
3773
|
);
|
|
3774
|
+
for (const p of prematureImplementations)
|
|
3775
|
+
diagnostics.push(
|
|
3776
|
+
finding(
|
|
3777
|
+
"coverage",
|
|
3778
|
+
"premature-implementation",
|
|
3779
|
+
`implements edge "${p.edgeId}" targets requirement "${p.requirementId}", which is not approved.`
|
|
3780
|
+
)
|
|
3781
|
+
);
|
|
3254
3782
|
diagnostics.push(...resolveTrace(doc).diagnostics);
|
|
3255
3783
|
return {
|
|
3256
3784
|
requirements,
|
|
3257
3785
|
uncoveredGoals,
|
|
3258
3786
|
unverifiedRequirements,
|
|
3259
3787
|
unimplementedRequirements,
|
|
3788
|
+
unimplementedApprovedRequirements,
|
|
3789
|
+
prematureImplementations,
|
|
3260
3790
|
orphanRequirements,
|
|
3261
3791
|
diagnostics
|
|
3262
3792
|
};
|
|
@@ -3264,6 +3794,51 @@ function computeCoverage(doc) {
|
|
|
3264
3794
|
function finding(source, rule, message) {
|
|
3265
3795
|
return { source, severity: "warning", rule, message };
|
|
3266
3796
|
}
|
|
3797
|
+
var BASELINE_PATH = ".rqml/baseline.json";
|
|
3798
|
+
function hashFileAt(filePath) {
|
|
3799
|
+
try {
|
|
3800
|
+
return createHash("sha256").update(readFileSync(filePath)).digest("hex");
|
|
3801
|
+
} catch {
|
|
3802
|
+
return void 0;
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3805
|
+
function computeBaseline(doc, options = {}) {
|
|
3806
|
+
const baseDir = options.baseDir ?? process.cwd();
|
|
3807
|
+
const baseline = {};
|
|
3808
|
+
for (const link of implementsLinks(doc)) {
|
|
3809
|
+
const filePath = filePathFromUri(link.uri, baseDir);
|
|
3810
|
+
if (filePath === void 0) continue;
|
|
3811
|
+
const hash = hashFileAt(filePath);
|
|
3812
|
+
if (hash !== void 0) baseline[link.edgeId] = hash;
|
|
3813
|
+
}
|
|
3814
|
+
return baseline;
|
|
3815
|
+
}
|
|
3816
|
+
function loadBaseline(baseDir) {
|
|
3817
|
+
try {
|
|
3818
|
+
const parsed = JSON.parse(
|
|
3819
|
+
readFileSync(join(baseDir, BASELINE_PATH), "utf8")
|
|
3820
|
+
);
|
|
3821
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
3822
|
+
return void 0;
|
|
3823
|
+
}
|
|
3824
|
+
const out = {};
|
|
3825
|
+
for (const [edgeId, hash] of Object.entries(parsed)) {
|
|
3826
|
+
if (typeof hash === "string") out[edgeId] = hash;
|
|
3827
|
+
}
|
|
3828
|
+
return out;
|
|
3829
|
+
} catch {
|
|
3830
|
+
return void 0;
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3833
|
+
function saveBaseline(baseDir, baseline) {
|
|
3834
|
+
const path = join(baseDir, BASELINE_PATH);
|
|
3835
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
3836
|
+
const sorted = Object.fromEntries(
|
|
3837
|
+
Object.entries(baseline).sort(([a], [b]) => a.localeCompare(b))
|
|
3838
|
+
);
|
|
3839
|
+
writeFileSync(path, `${JSON.stringify(sorted, null, 2)}
|
|
3840
|
+
`);
|
|
3841
|
+
}
|
|
3267
3842
|
function implementsLinks(doc) {
|
|
3268
3843
|
const out = [];
|
|
3269
3844
|
for (const edge of doc.trace) {
|
|
@@ -3289,16 +3864,22 @@ function filePathFromUri(uri, baseDir) {
|
|
|
3289
3864
|
}
|
|
3290
3865
|
return void 0;
|
|
3291
3866
|
}
|
|
3292
|
-
function filesystemResolver(baseDir) {
|
|
3867
|
+
function filesystemResolver(baseDir, baseline) {
|
|
3293
3868
|
return (link) => {
|
|
3294
3869
|
const filePath = filePathFromUri(link.uri, baseDir);
|
|
3295
3870
|
if (filePath === void 0) return "present";
|
|
3296
|
-
|
|
3871
|
+
if (!existsSync(filePath)) return "missing";
|
|
3872
|
+
const recorded = baseline?.[link.edgeId];
|
|
3873
|
+
if (recorded !== void 0) {
|
|
3874
|
+
const current = hashFileAt(filePath);
|
|
3875
|
+
if (current !== void 0 && current !== recorded) return "changed";
|
|
3876
|
+
}
|
|
3877
|
+
return "present";
|
|
3297
3878
|
};
|
|
3298
3879
|
}
|
|
3299
3880
|
function detectDrift(doc, options = {}) {
|
|
3300
3881
|
const baseDir = options.baseDir ?? process.cwd();
|
|
3301
|
-
const resolve4 = options.resolve ?? filesystemResolver(baseDir);
|
|
3882
|
+
const resolve4 = options.resolve ?? filesystemResolver(baseDir, options.baseline);
|
|
3302
3883
|
const links = implementsLinks(doc);
|
|
3303
3884
|
const drifted = [];
|
|
3304
3885
|
const diagnostics = [];
|
|
@@ -3370,9 +3951,18 @@ function parseArgs(rest) {
|
|
|
3370
3951
|
positionals,
|
|
3371
3952
|
json: flags.get("json") === true || flags.get("json") === "true",
|
|
3372
3953
|
strictness,
|
|
3373
|
-
baseDir: resolve(String(flags.get("base-dir") ?? process.cwd()))
|
|
3954
|
+
baseDir: resolve(String(flags.get("base-dir") ?? process.cwd())),
|
|
3955
|
+
flags
|
|
3374
3956
|
};
|
|
3375
3957
|
}
|
|
3958
|
+
function flagString(args, name) {
|
|
3959
|
+
const value = args.flags.get(name);
|
|
3960
|
+
return typeof value === "string" ? value : void 0;
|
|
3961
|
+
}
|
|
3962
|
+
function specArgs(args) {
|
|
3963
|
+
const override = flagString(args, "spec");
|
|
3964
|
+
return { ...args, positionals: override !== void 0 ? [override] : [] };
|
|
3965
|
+
}
|
|
3376
3966
|
function resolveSpecPath(args) {
|
|
3377
3967
|
const explicit = args.positionals[0];
|
|
3378
3968
|
if (explicit !== void 0) {
|
|
@@ -3407,10 +3997,13 @@ async function runCheck(rest) {
|
|
|
3407
3997
|
const integrity = validation.valid ? checkIntegrity(xml) : [];
|
|
3408
3998
|
const parsed = parse(xml);
|
|
3409
3999
|
const coverage = parsed.ok ? computeCoverage(parsed.document) : void 0;
|
|
3410
|
-
const
|
|
4000
|
+
const driftOptions = { baseDir: args.baseDir };
|
|
4001
|
+
const baseline = loadBaseline(args.baseDir);
|
|
4002
|
+
if (baseline !== void 0) driftOptions.baseline = baseline;
|
|
4003
|
+
const drift = parsed.ok ? detectDrift(parsed.document, driftOptions) : void 0;
|
|
3411
4004
|
const validationFailed = !validation.valid || integrity.length > 0;
|
|
3412
4005
|
const driftFailed = (drift?.drifted.length ?? 0) > 0;
|
|
3413
|
-
const coverageProblemCount = (coverage?.uncoveredGoals.length ?? 0) + (coverage?.unverifiedRequirements.length ?? 0) + (coverage?.orphanRequirements.length ?? 0);
|
|
4006
|
+
const coverageProblemCount = (coverage?.uncoveredGoals.length ?? 0) + (coverage?.unverifiedRequirements.length ?? 0) + (coverage?.orphanRequirements.length ?? 0) + (coverage?.unimplementedApprovedRequirements.length ?? 0) + (coverage?.prematureImplementations.length ?? 0);
|
|
3414
4007
|
const coverageFailed = coverageBlocks(args.strictness) && coverageProblemCount > 0;
|
|
3415
4008
|
const verdict = validationFailed || driftFailed || coverageFailed ? "fail" : "pass";
|
|
3416
4009
|
const diagnostics = [
|
|
@@ -3430,6 +4023,8 @@ async function runCheck(rest) {
|
|
|
3430
4023
|
uncoveredGoals: coverage.uncoveredGoals,
|
|
3431
4024
|
unverifiedRequirements: coverage.unverifiedRequirements,
|
|
3432
4025
|
unimplementedRequirements: coverage.unimplementedRequirements,
|
|
4026
|
+
unimplementedApprovedRequirements: coverage.unimplementedApprovedRequirements,
|
|
4027
|
+
prematureImplementations: coverage.prematureImplementations,
|
|
3433
4028
|
orphanRequirements: coverage.orphanRequirements
|
|
3434
4029
|
} : null,
|
|
3435
4030
|
diagnostics
|
|
@@ -3449,8 +4044,54 @@ async function runCheck(rest) {
|
|
|
3449
4044
|
return EXIT.OK;
|
|
3450
4045
|
}
|
|
3451
4046
|
|
|
4047
|
+
// src/commands/impact.ts
|
|
4048
|
+
async function runImpact(rest) {
|
|
4049
|
+
const args = parseArgs(rest);
|
|
4050
|
+
const id = args.positionals[0];
|
|
4051
|
+
if (id === void 0) {
|
|
4052
|
+
throw new UsageError("usage: rqml impact <id> [--json] [--spec <path>]");
|
|
4053
|
+
}
|
|
4054
|
+
const { path, xml } = readSpec(specArgs(args));
|
|
4055
|
+
const parsed = parse(xml);
|
|
4056
|
+
if (!parsed.ok) {
|
|
4057
|
+
process.stderr.write(`\u2717 ${path}: ${parsed.error.message}
|
|
4058
|
+
`);
|
|
4059
|
+
return EXIT.VALIDATION;
|
|
4060
|
+
}
|
|
4061
|
+
if (!declaredIdIndex(parsed.document).has(id)) {
|
|
4062
|
+
process.stderr.write(`\u2717 no artifact with id "${id}" in ${path}
|
|
4063
|
+
`);
|
|
4064
|
+
return EXIT.USAGE;
|
|
4065
|
+
}
|
|
4066
|
+
const report = impactOf(parsed.document, id);
|
|
4067
|
+
if (args.json) {
|
|
4068
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}
|
|
4069
|
+
`);
|
|
4070
|
+
return EXIT.OK;
|
|
4071
|
+
}
|
|
4072
|
+
process.stdout.write(`Impact of ${id} \u2014 ${report.affected.length} affected
|
|
4073
|
+
`);
|
|
4074
|
+
for (const group of report.groups) {
|
|
4075
|
+
const arrow = group.direction === "outgoing" ? "\u2192" : "\u2190";
|
|
4076
|
+
process.stdout.write(` ${arrow} ${group.type}: ${group.ids.join(", ")}
|
|
4077
|
+
`);
|
|
4078
|
+
}
|
|
4079
|
+
const transitive = report.affected.filter((a) => a.distance > 1);
|
|
4080
|
+
if (transitive.length > 0) {
|
|
4081
|
+
process.stdout.write(" transitively:\n");
|
|
4082
|
+
for (const a of transitive) {
|
|
4083
|
+
const path2 = a.path.map((s) => s.edgeId).join(" \u203A ");
|
|
4084
|
+
process.stdout.write(
|
|
4085
|
+
` ${a.id} (${a.kind}, distance ${a.distance} via ${path2})
|
|
4086
|
+
`
|
|
4087
|
+
);
|
|
4088
|
+
}
|
|
4089
|
+
}
|
|
4090
|
+
return EXIT.OK;
|
|
4091
|
+
}
|
|
4092
|
+
|
|
3452
4093
|
// ../schema/dist/index.js
|
|
3453
|
-
var AGENTS_default =
|
|
4094
|
+
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\n```\n[Elicit] \u2192 [Specify] \u2192 [Implement] \u2192 [Verify] \u2192 [Trace]\n \u2191____________________\u2190______________________|\n```\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\n---\n\n## Workflow\n\n### 1. Elicit\nAsk clarifying questions until you understand the goal, scope, acceptance criteria, and constraints. Don\'t assume\u2014capture assumptions as `<notes>` or `<issue>` elements.\n\n### 2. Specify\n**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 before proceeding\n\n### 3. Implement\nRead the requirement first: `rqml show REQ-XXX`. Check blast radius before changing existing artifacts: `rqml impact REQ-XXX`. 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### 4. Verify\nAdd tests that reference requirement IDs, then record verification:\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| Elicitation | Major features | Testable reqs | Edge cases | Formal |\n| Spec-first | Recommended | Required | Required | Approved first |\n| Code traces | Optional | New features | All changes | With metadata |\n| 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**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';
|
|
3454
4095
|
var AGENTS_TEMPLATE = AGENTS_default;
|
|
3455
4096
|
|
|
3456
4097
|
// src/commands/init.ts
|
|
@@ -3493,6 +4134,155 @@ async function runInit(rest) {
|
|
|
3493
4134
|
if (!wrote) process.stdout.write("nothing to do; project already initialized\n");
|
|
3494
4135
|
return EXIT.OK;
|
|
3495
4136
|
}
|
|
4137
|
+
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>]";
|
|
4138
|
+
async function runLink(rest) {
|
|
4139
|
+
const args = parseArgs(rest);
|
|
4140
|
+
if (args.flags.has("refresh")) {
|
|
4141
|
+
const edgeId2 = flagString(args, "refresh");
|
|
4142
|
+
if (edgeId2 === void 0) throw new UsageError(USAGE);
|
|
4143
|
+
return runRefresh(args, edgeId2);
|
|
4144
|
+
}
|
|
4145
|
+
const [artifactId, uri] = args.positionals;
|
|
4146
|
+
if (artifactId === void 0 || uri === void 0) throw new UsageError(USAGE);
|
|
4147
|
+
const type = flagString(args, "type") ?? "implements";
|
|
4148
|
+
if (type !== "implements" && type !== "verifiedBy") {
|
|
4149
|
+
throw new UsageError(`unknown link type "${type}" (implements|verifiedBy)`);
|
|
4150
|
+
}
|
|
4151
|
+
const update = args.flags.get("update") === true || args.flags.get("update") === "true";
|
|
4152
|
+
const { path, xml } = readSpec(specArgs(args));
|
|
4153
|
+
const request = { artifactId, uri, type };
|
|
4154
|
+
const edgeId = flagString(args, "id");
|
|
4155
|
+
if (edgeId !== void 0) request.edgeId = edgeId;
|
|
4156
|
+
const kind = flagString(args, "kind");
|
|
4157
|
+
if (kind !== void 0) request.kind = kind;
|
|
4158
|
+
const title = flagString(args, "title");
|
|
4159
|
+
if (title !== void 0) request.title = title;
|
|
4160
|
+
const result = update ? updateTraceEdge(xml, request) : appendTraceEdge(xml, request);
|
|
4161
|
+
if (!result.ok) {
|
|
4162
|
+
process.stderr.write(`\u2717 link failed: ${result.error}
|
|
4163
|
+
`);
|
|
4164
|
+
return EXIT.VALIDATION;
|
|
4165
|
+
}
|
|
4166
|
+
const { validate } = await import('./validate-O3LLP44J.js');
|
|
4167
|
+
const validation = validate(result.xml);
|
|
4168
|
+
if (!validation.valid) {
|
|
4169
|
+
printDiagnostics(validation.diagnostics);
|
|
4170
|
+
process.stderr.write("\u2717 link would invalidate the document; nothing written\n");
|
|
4171
|
+
return EXIT.VALIDATION;
|
|
4172
|
+
}
|
|
4173
|
+
writeFileSync(path, result.xml);
|
|
4174
|
+
let baselineRecorded = false;
|
|
4175
|
+
const parsed = parse(result.xml);
|
|
4176
|
+
if (parsed.ok) {
|
|
4177
|
+
const fresh = computeBaseline(parsed.document, { baseDir: args.baseDir });
|
|
4178
|
+
const hash = fresh[result.edgeId];
|
|
4179
|
+
if (hash !== void 0) {
|
|
4180
|
+
const baseline = loadBaseline(args.baseDir) ?? {};
|
|
4181
|
+
baseline[result.edgeId] = hash;
|
|
4182
|
+
saveBaseline(args.baseDir, baseline);
|
|
4183
|
+
baselineRecorded = true;
|
|
4184
|
+
}
|
|
4185
|
+
}
|
|
4186
|
+
if (args.json) {
|
|
4187
|
+
const report = {
|
|
4188
|
+
spec: path,
|
|
4189
|
+
mode: update ? "update" : "append",
|
|
4190
|
+
edgeId: result.edgeId,
|
|
4191
|
+
type,
|
|
4192
|
+
artifactId,
|
|
4193
|
+
uri,
|
|
4194
|
+
baselineRecorded
|
|
4195
|
+
};
|
|
4196
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}
|
|
4197
|
+
`);
|
|
4198
|
+
} else {
|
|
4199
|
+
const arrow = type === "implements" ? "\u2190" : "\u2192";
|
|
4200
|
+
const mode = update ? ", updated" : "";
|
|
4201
|
+
const baseline = baselineRecorded ? ", baseline recorded" : "";
|
|
4202
|
+
process.stdout.write(
|
|
4203
|
+
`\u2713 ${artifactId} ${arrow} ${uri} (${result.edgeId}, ${type}${mode}${baseline})
|
|
4204
|
+
`
|
|
4205
|
+
);
|
|
4206
|
+
}
|
|
4207
|
+
return EXIT.OK;
|
|
4208
|
+
}
|
|
4209
|
+
function runRefresh(args, edgeId) {
|
|
4210
|
+
const { path, xml } = readSpec(specArgs(args));
|
|
4211
|
+
const parsed = parse(xml);
|
|
4212
|
+
if (!parsed.ok) {
|
|
4213
|
+
process.stderr.write(`\u2717 refresh failed: ${parsed.error.message}
|
|
4214
|
+
`);
|
|
4215
|
+
return EXIT.VALIDATION;
|
|
4216
|
+
}
|
|
4217
|
+
const link = implementsLinks(parsed.document).find((l) => l.edgeId === edgeId);
|
|
4218
|
+
if (link === void 0) {
|
|
4219
|
+
process.stderr.write(
|
|
4220
|
+
`\u2717 refresh failed: no implements edge "${edgeId}" with an external locator exists (only implements edges carry baselines)
|
|
4221
|
+
`
|
|
4222
|
+
);
|
|
4223
|
+
return EXIT.VALIDATION;
|
|
4224
|
+
}
|
|
4225
|
+
const hash = computeBaseline(parsed.document, { baseDir: args.baseDir })[edgeId];
|
|
4226
|
+
if (hash === void 0) {
|
|
4227
|
+
process.stderr.write(
|
|
4228
|
+
`\u2717 refresh failed: "${link.uri}" cannot be hashed (missing file or non-filesystem URI)
|
|
4229
|
+
`
|
|
4230
|
+
);
|
|
4231
|
+
return EXIT.VALIDATION;
|
|
4232
|
+
}
|
|
4233
|
+
const baseline = loadBaseline(args.baseDir) ?? {};
|
|
4234
|
+
baseline[edgeId] = hash;
|
|
4235
|
+
saveBaseline(args.baseDir, baseline);
|
|
4236
|
+
if (args.json) {
|
|
4237
|
+
const report = { spec: path, mode: "refresh", edgeId, uri: link.uri, hash };
|
|
4238
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}
|
|
4239
|
+
`);
|
|
4240
|
+
} else {
|
|
4241
|
+
process.stdout.write(`\u2713 baseline refreshed for ${edgeId} (${link.uri})
|
|
4242
|
+
`);
|
|
4243
|
+
}
|
|
4244
|
+
return EXIT.OK;
|
|
4245
|
+
}
|
|
4246
|
+
|
|
4247
|
+
// src/commands/show.ts
|
|
4248
|
+
async function runShow(rest) {
|
|
4249
|
+
const args = parseArgs(rest);
|
|
4250
|
+
const id = args.positionals[0];
|
|
4251
|
+
if (id === void 0) {
|
|
4252
|
+
throw new UsageError("usage: rqml show <id> [--json] [--spec <path>]");
|
|
4253
|
+
}
|
|
4254
|
+
const { path, xml } = readSpec(specArgs(args));
|
|
4255
|
+
const parsed = parse(xml);
|
|
4256
|
+
if (!parsed.ok) {
|
|
4257
|
+
process.stderr.write(`\u2717 ${path}: ${parsed.error.message}
|
|
4258
|
+
`);
|
|
4259
|
+
return EXIT.VALIDATION;
|
|
4260
|
+
}
|
|
4261
|
+
const slice = extractArtifact(parsed.document, id);
|
|
4262
|
+
if (slice === void 0) {
|
|
4263
|
+
process.stderr.write(`\u2717 no artifact with id "${id}" in ${path}
|
|
4264
|
+
`);
|
|
4265
|
+
return EXIT.USAGE;
|
|
4266
|
+
}
|
|
4267
|
+
process.stdout.write(
|
|
4268
|
+
args.json ? `${JSON.stringify(slice, null, 2)}
|
|
4269
|
+
` : sliceToMarkdown(slice)
|
|
4270
|
+
);
|
|
4271
|
+
return EXIT.OK;
|
|
4272
|
+
}
|
|
4273
|
+
|
|
4274
|
+
// src/commands/skeleton.ts
|
|
4275
|
+
async function runSkeleton(rest) {
|
|
4276
|
+
const args = parseArgs(rest);
|
|
4277
|
+
const kind = args.positionals[0];
|
|
4278
|
+
const kinds = SKELETON_KINDS.join("|");
|
|
4279
|
+
if (kind === void 0 || !SKELETON_KINDS.includes(kind)) {
|
|
4280
|
+
throw new UsageError(`usage: rqml skeleton <${kinds}> [--id <id>]`);
|
|
4281
|
+
}
|
|
4282
|
+
const id = flagString(args, "id");
|
|
4283
|
+
process.stdout.write(skeleton(kind, id !== void 0 ? { id } : {}));
|
|
4284
|
+
return EXIT.OK;
|
|
4285
|
+
}
|
|
3496
4286
|
|
|
3497
4287
|
// src/commands/status.ts
|
|
3498
4288
|
async function runStatus(rest) {
|
|
@@ -3519,6 +4309,8 @@ async function runStatus(rest) {
|
|
|
3519
4309
|
uncoveredGoals: coverage.uncoveredGoals,
|
|
3520
4310
|
unverifiedRequirements: coverage.unverifiedRequirements,
|
|
3521
4311
|
unimplementedRequirements: coverage.unimplementedRequirements,
|
|
4312
|
+
unimplementedApprovedRequirements: coverage.unimplementedApprovedRequirements,
|
|
4313
|
+
prematureImplementations: coverage.prematureImplementations,
|
|
3522
4314
|
orphanRequirements: coverage.orphanRequirements,
|
|
3523
4315
|
danglingReferences: trace.diagnostics.length,
|
|
3524
4316
|
lintFindings: lintDiags.length
|
|
@@ -3533,7 +4325,8 @@ async function runStatus(rest) {
|
|
|
3533
4325
|
requirements: ${reqCount} trace edges: ${doc.trace.length}
|
|
3534
4326
|
uncovered goals: ${coverage.uncoveredGoals.length}
|
|
3535
4327
|
unverified reqs: ${coverage.unverifiedRequirements.length}
|
|
3536
|
-
unimplemented reqs: ${coverage.unimplementedRequirements.length}
|
|
4328
|
+
unimplemented reqs: ${coverage.unimplementedRequirements.length} (approved: ${coverage.unimplementedApprovedRequirements.length})
|
|
4329
|
+
premature implementations: ${coverage.prematureImplementations.length}
|
|
3537
4330
|
dangling refs: ${trace.diagnostics.length} lint findings: ${lintDiags.length}
|
|
3538
4331
|
`
|
|
3539
4332
|
);
|
|
@@ -3567,22 +4360,35 @@ async function runValidate(rest) {
|
|
|
3567
4360
|
}
|
|
3568
4361
|
|
|
3569
4362
|
// src/index.ts
|
|
3570
|
-
var VERSION = "
|
|
4363
|
+
var VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
3571
4364
|
var HELP = `rqml \u2014 RQML reference CLI (v${VERSION})
|
|
3572
4365
|
|
|
3573
4366
|
Usage:
|
|
3574
4367
|
rqml <command> [spec.rqml] [options]
|
|
3575
4368
|
|
|
3576
4369
|
Commands:
|
|
3577
|
-
init [path]
|
|
3578
|
-
validate [path]
|
|
3579
|
-
status [path]
|
|
3580
|
-
check [path]
|
|
4370
|
+
init [path] Scaffold a starter spec and AGENTS.md project marker
|
|
4371
|
+
validate [path] Validate XML well-formedness, XSD, and referential integrity
|
|
4372
|
+
status [path] Show spec, coverage, and lint summary
|
|
4373
|
+
check [path] Deterministic enforcement gate (validation + coverage + drift)
|
|
4374
|
+
link <id> <uri> Record an implements/verifiedBy edge and its drift baseline
|
|
4375
|
+
(--update repoints an existing edge; --refresh <edge-id>
|
|
4376
|
+
re-records only the baseline for an intentional change)
|
|
4377
|
+
show <id> Extract one artifact with its trace neighborhood
|
|
4378
|
+
impact <id> What is affected, transitively, if this artifact changes
|
|
4379
|
+
skeleton <kind> Print a schema-valid snippet (req|edge|testCase|stateMachine)
|
|
3581
4380
|
|
|
3582
4381
|
Options:
|
|
3583
|
-
--json Emit machine-readable JSON (status, check, validate)
|
|
4382
|
+
--json Emit machine-readable JSON (status, check, validate, link, show, impact)
|
|
3584
4383
|
--strictness <level> relaxed | standard | strict | certified (default: standard)
|
|
3585
4384
|
--base-dir <dir> Directory to resolve the spec and code links against
|
|
4385
|
+
--spec <path> Explicit spec file (link, show, impact)
|
|
4386
|
+
--type <type> Link type: implements | verifiedBy (default: implements)
|
|
4387
|
+
--id <id> Explicit edge id (link) or skeleton root id
|
|
4388
|
+
--kind <kind> Locator kind hint for link (default: code/test by type)
|
|
4389
|
+
--title <title> Locator title hint for link
|
|
4390
|
+
--update Replace the external locator of an existing edge (link)
|
|
4391
|
+
--refresh <edge-id> Re-record the drift baseline for one edge (link)
|
|
3586
4392
|
-h, --help Show this help
|
|
3587
4393
|
-v, --version Show version
|
|
3588
4394
|
|
|
@@ -3599,6 +4405,14 @@ async function main(argv) {
|
|
|
3599
4405
|
return runCheck(rest);
|
|
3600
4406
|
case "init":
|
|
3601
4407
|
return runInit(rest);
|
|
4408
|
+
case "link":
|
|
4409
|
+
return runLink(rest);
|
|
4410
|
+
case "show":
|
|
4411
|
+
return runShow(rest);
|
|
4412
|
+
case "impact":
|
|
4413
|
+
return runImpact(rest);
|
|
4414
|
+
case "skeleton":
|
|
4415
|
+
return runSkeleton(rest);
|
|
3602
4416
|
case "-v":
|
|
3603
4417
|
case "--version":
|
|
3604
4418
|
process.stdout.write(`${VERSION}
|