@rqml/cli 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +707 -20
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { __commonJS, __toESM } from './chunk-5WRI5ZAA.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { createHash } from 'crypto';
|
|
4
|
+
import { writeFileSync, existsSync, readFileSync, mkdirSync, statSync, readdirSync } from 'fs';
|
|
5
|
+
import { resolve, join, dirname, isAbsolute } from 'path';
|
|
5
6
|
import { fileURLToPath } from 'url';
|
|
6
7
|
|
|
7
8
|
// ../../node_modules/.pnpm/fast-xml-parser@4.5.6/node_modules/fast-xml-parser/src/util.js
|
|
@@ -3008,6 +3009,64 @@ function resolveTrace(doc) {
|
|
|
3008
3009
|
}));
|
|
3009
3010
|
return { edges, diagnostics };
|
|
3010
3011
|
}
|
|
3012
|
+
function endpointKey(locator) {
|
|
3013
|
+
return locator.kind === "local" ? locator.id : locator.uri;
|
|
3014
|
+
}
|
|
3015
|
+
function impactOf(doc, id) {
|
|
3016
|
+
const idIndex = declaredIdIndex(doc);
|
|
3017
|
+
const visited = /* @__PURE__ */ new Set([id]);
|
|
3018
|
+
const affected = [];
|
|
3019
|
+
let frontier = [{ key: id, path: [] }];
|
|
3020
|
+
let distance = 0;
|
|
3021
|
+
while (frontier.length > 0) {
|
|
3022
|
+
distance += 1;
|
|
3023
|
+
const next = [];
|
|
3024
|
+
for (const node of frontier) {
|
|
3025
|
+
for (const edge of doc.trace) {
|
|
3026
|
+
const fromKey = endpointKey(edge.from);
|
|
3027
|
+
const toKey = endpointKey(edge.to);
|
|
3028
|
+
const hops = [
|
|
3029
|
+
{ direction: "outgoing", here: fromKey, far: edge.to },
|
|
3030
|
+
{ direction: "incoming", here: toKey, far: edge.from }
|
|
3031
|
+
];
|
|
3032
|
+
for (const hop of hops) {
|
|
3033
|
+
const farKey = endpointKey(hop.far);
|
|
3034
|
+
if (hop.here !== node.key || visited.has(farKey)) continue;
|
|
3035
|
+
visited.add(farKey);
|
|
3036
|
+
const local = hop.far.kind === "local";
|
|
3037
|
+
const kind = local ? idIndex.get(farKey)?.kind ?? "unknown" : hop.far.kind;
|
|
3038
|
+
const step = {
|
|
3039
|
+
edgeId: edge.id,
|
|
3040
|
+
type: edge.type,
|
|
3041
|
+
direction: hop.direction,
|
|
3042
|
+
target: farKey
|
|
3043
|
+
};
|
|
3044
|
+
const path = [...node.path, step];
|
|
3045
|
+
affected.push({ id: farKey, kind, distance, path });
|
|
3046
|
+
if (local) next.push({ key: farKey, path });
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
frontier = next;
|
|
3051
|
+
}
|
|
3052
|
+
affected.sort((a, b) => a.distance - b.distance || a.id.localeCompare(b.id));
|
|
3053
|
+
const groupIndex = /* @__PURE__ */ new Map();
|
|
3054
|
+
for (const artifact of affected) {
|
|
3055
|
+
const last = artifact.path[artifact.path.length - 1];
|
|
3056
|
+
if (last === void 0) continue;
|
|
3057
|
+
const key = `${last.direction}:${last.type}`;
|
|
3058
|
+
let group = groupIndex.get(key);
|
|
3059
|
+
if (group === void 0) {
|
|
3060
|
+
group = { direction: last.direction, type: last.type, ids: [] };
|
|
3061
|
+
groupIndex.set(key, group);
|
|
3062
|
+
}
|
|
3063
|
+
group.ids.push(artifact.id);
|
|
3064
|
+
}
|
|
3065
|
+
const groups = [...groupIndex.values()].map((g) => ({ ...g, ids: [...new Set(g.ids)].sort() })).sort(
|
|
3066
|
+
(a, b) => a.direction.localeCompare(b.direction) || a.type.localeCompare(b.type)
|
|
3067
|
+
);
|
|
3068
|
+
return { id, affected, groups };
|
|
3069
|
+
}
|
|
3011
3070
|
var ATTR_PREFIX3 = "@_";
|
|
3012
3071
|
var REFERENCE_ELEMENTS = /* @__PURE__ */ new Set(["local", "doc"]);
|
|
3013
3072
|
var parser2 = new import_fast_xml_parser.XMLParser({
|
|
@@ -3047,7 +3106,7 @@ function flatRefs(edge, out) {
|
|
|
3047
3106
|
if (from !== void 0) out.push({ refId: from, edgeId, side: "from", flat: true });
|
|
3048
3107
|
if (to !== void 0) out.push({ refId: to, edgeId, side: "to", flat: true });
|
|
3049
3108
|
}
|
|
3050
|
-
function walk(node, declared, refs) {
|
|
3109
|
+
function walk(node, declared, refs, machines) {
|
|
3051
3110
|
for (const [key, value] of Object.entries(node)) {
|
|
3052
3111
|
if (key.startsWith(ATTR_PREFIX3) || key === "#text") continue;
|
|
3053
3112
|
for (const item of asArray2(value)) {
|
|
@@ -3056,7 +3115,8 @@ function walk(node, declared, refs) {
|
|
|
3056
3115
|
if (id !== void 0 && !REFERENCE_ELEMENTS.has(key)) declared.push(id);
|
|
3057
3116
|
if (key === "edge") nestedRefs(item, refs);
|
|
3058
3117
|
else if (key === "traceEdge") flatRefs(item, refs);
|
|
3059
|
-
|
|
3118
|
+
else if (key === "stateMachine") machines.push(item);
|
|
3119
|
+
walk(item, declared, refs, machines);
|
|
3060
3120
|
}
|
|
3061
3121
|
}
|
|
3062
3122
|
}
|
|
@@ -3105,7 +3165,8 @@ function checkIntegrity(xml) {
|
|
|
3105
3165
|
if (!root) return [];
|
|
3106
3166
|
const declared = [];
|
|
3107
3167
|
const refs = [];
|
|
3108
|
-
|
|
3168
|
+
const machines = [];
|
|
3169
|
+
walk(root, declared, refs, machines);
|
|
3109
3170
|
const starts = lineStarts(xml);
|
|
3110
3171
|
const diagnostics = [];
|
|
3111
3172
|
const counts = /* @__PURE__ */ new Map();
|
|
@@ -3146,8 +3207,372 @@ function checkIntegrity(xml) {
|
|
|
3146
3207
|
if (lines.length > 0) diag.line = lines[0];
|
|
3147
3208
|
diagnostics.push(diag);
|
|
3148
3209
|
}
|
|
3210
|
+
for (const sm of machines) {
|
|
3211
|
+
const smId = attr2(sm, "id") ?? "";
|
|
3212
|
+
const stateIds = /* @__PURE__ */ new Set();
|
|
3213
|
+
const finalStates = /* @__PURE__ */ new Set();
|
|
3214
|
+
for (const st of asArray2(sm.state).filter(isNode2)) {
|
|
3215
|
+
const sid = attr2(st, "id");
|
|
3216
|
+
if (sid === void 0) continue;
|
|
3217
|
+
stateIds.add(sid);
|
|
3218
|
+
if (attr2(st, "type") === "final") finalStates.add(sid);
|
|
3219
|
+
}
|
|
3220
|
+
const initial = attr2(sm, "initial");
|
|
3221
|
+
if (initial !== void 0 && !stateIds.has(initial)) {
|
|
3222
|
+
const re = new RegExp(
|
|
3223
|
+
`<stateMachine\\b[^>]*?\\sinitial\\s*=\\s*"${escapeRegExp(initial)}"`,
|
|
3224
|
+
"g"
|
|
3225
|
+
);
|
|
3226
|
+
const lines = matchLines(xml, starts, re);
|
|
3227
|
+
const diag = {
|
|
3228
|
+
source: "validate",
|
|
3229
|
+
severity: "error",
|
|
3230
|
+
rule: "unresolved-state-ref",
|
|
3231
|
+
message: `State machine "${smId}" initial state "${initial}" is not a declared state of the machine.`
|
|
3232
|
+
};
|
|
3233
|
+
if (lines.length > 0) diag.line = lines[0];
|
|
3234
|
+
diagnostics.push(diag);
|
|
3235
|
+
}
|
|
3236
|
+
for (const tr of asArray2(sm.transition).filter(isNode2)) {
|
|
3237
|
+
const trId = attr2(tr, "id") ?? "";
|
|
3238
|
+
for (const side of ["from", "to"]) {
|
|
3239
|
+
const refId = attr2(tr, side);
|
|
3240
|
+
if (refId === void 0 || stateIds.has(refId)) continue;
|
|
3241
|
+
const re = new RegExp(
|
|
3242
|
+
`<transition\\b[^>]*?\\s${side}\\s*=\\s*"${escapeRegExp(refId)}"`,
|
|
3243
|
+
"g"
|
|
3244
|
+
);
|
|
3245
|
+
const lines = matchLines(xml, starts, re);
|
|
3246
|
+
const diag = {
|
|
3247
|
+
source: "validate",
|
|
3248
|
+
severity: "error",
|
|
3249
|
+
rule: "unresolved-state-ref",
|
|
3250
|
+
message: `Transition "${trId}" (${side}) references unknown state "${refId}" in state machine "${smId}".`
|
|
3251
|
+
};
|
|
3252
|
+
if (lines.length > 0) diag.line = lines[0];
|
|
3253
|
+
diagnostics.push(diag);
|
|
3254
|
+
}
|
|
3255
|
+
const from = attr2(tr, "from");
|
|
3256
|
+
if (from !== void 0 && finalStates.has(from)) {
|
|
3257
|
+
const re = new RegExp(
|
|
3258
|
+
`<transition\\b[^>]*?\\sid\\s*=\\s*"${escapeRegExp(trId)}"`,
|
|
3259
|
+
"g"
|
|
3260
|
+
);
|
|
3261
|
+
const lines = matchLines(xml, starts, re);
|
|
3262
|
+
const diag = {
|
|
3263
|
+
source: "validate",
|
|
3264
|
+
severity: "error",
|
|
3265
|
+
rule: "final-state-outgoing",
|
|
3266
|
+
message: `Final state "${from}" has outgoing transition "${trId}" in state machine "${smId}".`
|
|
3267
|
+
};
|
|
3268
|
+
if (lines.length > 0) diag.line = lines[0];
|
|
3269
|
+
diagnostics.push(diag);
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3149
3273
|
return diagnostics;
|
|
3150
3274
|
}
|
|
3275
|
+
var ID_PATTERN = /^[A-Za-z][A-Za-z0-9._-]{1,79}$/;
|
|
3276
|
+
function escapeAttr(value) {
|
|
3277
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
3278
|
+
}
|
|
3279
|
+
function deriveEdgeId(type, artifactId, taken) {
|
|
3280
|
+
const prefix = type === "implements" ? "E-IMPL-" : "E-VER-";
|
|
3281
|
+
const base = prefix + artifactId.replace(/^REQ-/, "");
|
|
3282
|
+
if (!taken(base)) return base;
|
|
3283
|
+
for (let n = 2; ; n++) {
|
|
3284
|
+
const candidate = `${base}-${n}`;
|
|
3285
|
+
if (!taken(candidate)) return candidate;
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
function edgeBlock(request, edgeId, indent) {
|
|
3289
|
+
const kind = request.kind ?? (request.type === "implements" ? "code" : "test");
|
|
3290
|
+
const titleAttr = request.title !== void 0 ? ` title="${escapeAttr(request.title)}"` : "";
|
|
3291
|
+
const external = `<external uri="${escapeAttr(request.uri)}" kind="${escapeAttr(kind)}"${titleAttr}/>`;
|
|
3292
|
+
const local = `<local id="${request.artifactId}"/>`;
|
|
3293
|
+
const [from, to] = request.type === "implements" ? [external, local] : [local, external];
|
|
3294
|
+
return [
|
|
3295
|
+
`${indent}<edge id="${edgeId}" type="${request.type}">`,
|
|
3296
|
+
`${indent} <from><locator>${from}</locator></from>`,
|
|
3297
|
+
`${indent} <to><locator>${to}</locator></to>`,
|
|
3298
|
+
`${indent}</edge>`
|
|
3299
|
+
].join("\n");
|
|
3300
|
+
}
|
|
3301
|
+
function lineInfo(xml, index) {
|
|
3302
|
+
const start = xml.lastIndexOf("\n", index - 1) + 1;
|
|
3303
|
+
const prefix = xml.slice(start, index);
|
|
3304
|
+
return { start, indent: /^[ \t]*$/.test(prefix) ? prefix : "" };
|
|
3305
|
+
}
|
|
3306
|
+
function appendTraceEdge(xml, request) {
|
|
3307
|
+
const parsed = parse(xml);
|
|
3308
|
+
if (!parsed.ok) {
|
|
3309
|
+
return { ok: false, error: `document does not parse: ${parsed.error.message}` };
|
|
3310
|
+
}
|
|
3311
|
+
const idIndex = declaredIdIndex(parsed.document);
|
|
3312
|
+
if (!idIndex.has(request.artifactId)) {
|
|
3313
|
+
return {
|
|
3314
|
+
ok: false,
|
|
3315
|
+
error: `artifact "${request.artifactId}" is not declared in the document`
|
|
3316
|
+
};
|
|
3317
|
+
}
|
|
3318
|
+
if (request.uri.trim() === "") {
|
|
3319
|
+
return { ok: false, error: "locator uri must not be empty" };
|
|
3320
|
+
}
|
|
3321
|
+
let edgeId;
|
|
3322
|
+
if (request.edgeId !== void 0) {
|
|
3323
|
+
if (!ID_PATTERN.test(request.edgeId)) {
|
|
3324
|
+
return {
|
|
3325
|
+
ok: false,
|
|
3326
|
+
error: `edge id "${request.edgeId}" does not match the RQML id pattern`
|
|
3327
|
+
};
|
|
3328
|
+
}
|
|
3329
|
+
if (idIndex.has(request.edgeId)) {
|
|
3330
|
+
return { ok: false, error: `edge id "${request.edgeId}" is already declared` };
|
|
3331
|
+
}
|
|
3332
|
+
edgeId = request.edgeId;
|
|
3333
|
+
} else {
|
|
3334
|
+
edgeId = deriveEdgeId(request.type, request.artifactId, (id) => idIndex.has(id));
|
|
3335
|
+
}
|
|
3336
|
+
let updated;
|
|
3337
|
+
let edgeXml;
|
|
3338
|
+
const closeIdx = xml.lastIndexOf("</trace>");
|
|
3339
|
+
if (closeIdx >= 0) {
|
|
3340
|
+
const { start, indent } = lineInfo(xml, closeIdx);
|
|
3341
|
+
edgeXml = edgeBlock(request, edgeId, `${indent} `);
|
|
3342
|
+
updated = `${xml.slice(0, start)}${edgeXml}
|
|
3343
|
+
${xml.slice(start)}`;
|
|
3344
|
+
} else {
|
|
3345
|
+
const anchorIdx = (() => {
|
|
3346
|
+
const gov = xml.indexOf("<governance");
|
|
3347
|
+
return gov >= 0 ? gov : xml.lastIndexOf("</rqml>");
|
|
3348
|
+
})();
|
|
3349
|
+
if (anchorIdx < 0) return { ok: false, error: "no </rqml> close tag found" };
|
|
3350
|
+
const { start, indent } = lineInfo(xml, anchorIdx);
|
|
3351
|
+
const sectionIndent = indent === "" ? " " : indent;
|
|
3352
|
+
edgeXml = edgeBlock(request, edgeId, `${sectionIndent} `);
|
|
3353
|
+
const section = `${sectionIndent}<trace>
|
|
3354
|
+
${edgeXml}
|
|
3355
|
+
${sectionIndent}</trace>
|
|
3356
|
+
`;
|
|
3357
|
+
updated = `${xml.slice(0, start)}${section}${xml.slice(start)}`;
|
|
3358
|
+
}
|
|
3359
|
+
const before = checkIntegrity(xml).length;
|
|
3360
|
+
const reparsed = parse(updated);
|
|
3361
|
+
if (!reparsed.ok) {
|
|
3362
|
+
return {
|
|
3363
|
+
ok: false,
|
|
3364
|
+
error: `edit produced an unparseable document: ${reparsed.error.message}`
|
|
3365
|
+
};
|
|
3366
|
+
}
|
|
3367
|
+
if (checkIntegrity(updated).length > before) {
|
|
3368
|
+
return {
|
|
3369
|
+
ok: false,
|
|
3370
|
+
error: "edit introduced an integrity violation; document left unchanged"
|
|
3371
|
+
};
|
|
3372
|
+
}
|
|
3373
|
+
return { ok: true, xml: updated, edgeId, edgeXml: edgeXml.trim() };
|
|
3374
|
+
}
|
|
3375
|
+
function endpointKey2(locator) {
|
|
3376
|
+
return locator.kind === "local" ? locator.id : locator.uri;
|
|
3377
|
+
}
|
|
3378
|
+
function details(doc, id, kind) {
|
|
3379
|
+
switch (kind) {
|
|
3380
|
+
case "goal": {
|
|
3381
|
+
const g = doc.goals?.goals?.find((x) => x.id === id);
|
|
3382
|
+
return g ? {
|
|
3383
|
+
title: g.title,
|
|
3384
|
+
statement: g.statement,
|
|
3385
|
+
status: g.status,
|
|
3386
|
+
priority: g.priority,
|
|
3387
|
+
rationale: g.rationale
|
|
3388
|
+
} : {};
|
|
3389
|
+
}
|
|
3390
|
+
case "qgoal": {
|
|
3391
|
+
const g = doc.goals?.qualityGoals?.find((x) => x.id === id);
|
|
3392
|
+
return g ? {
|
|
3393
|
+
title: g.title,
|
|
3394
|
+
statement: g.statement,
|
|
3395
|
+
status: g.status,
|
|
3396
|
+
priority: g.priority,
|
|
3397
|
+
notes: g.metric
|
|
3398
|
+
} : {};
|
|
3399
|
+
}
|
|
3400
|
+
case "obstacle": {
|
|
3401
|
+
const o = doc.goals?.obstacles?.find((x) => x.id === id);
|
|
3402
|
+
return o ? { title: o.title, statement: o.statement, notes: o.mitigation } : {};
|
|
3403
|
+
}
|
|
3404
|
+
case "scenario":
|
|
3405
|
+
case "misuseCase":
|
|
3406
|
+
case "edgeCase": {
|
|
3407
|
+
const all = [
|
|
3408
|
+
...doc.scenarios?.scenarios ?? [],
|
|
3409
|
+
...doc.scenarios?.misuseCases ?? [],
|
|
3410
|
+
...doc.scenarios?.edgeCases ?? []
|
|
3411
|
+
];
|
|
3412
|
+
const s = all.find((x) => x.id === id);
|
|
3413
|
+
return s ? { title: s.title, statement: s.narrative } : {};
|
|
3414
|
+
}
|
|
3415
|
+
case "testCase": {
|
|
3416
|
+
const t = doc.verification?.testCases?.find((x) => x.id === id);
|
|
3417
|
+
return t ? { title: t.title, statement: t.purpose, notes: t.expected } : {};
|
|
3418
|
+
}
|
|
3419
|
+
case "testSuite": {
|
|
3420
|
+
const t = doc.verification?.testSuites?.find((x) => x.id === id);
|
|
3421
|
+
return t ? { title: t.title, statement: t.description } : {};
|
|
3422
|
+
}
|
|
3423
|
+
case "risk": {
|
|
3424
|
+
const r = doc.catalogs?.risks?.find((x) => x.id === id);
|
|
3425
|
+
return r ? { statement: r.statement, notes: r.mitigation } : {};
|
|
3426
|
+
}
|
|
3427
|
+
case "constraint": {
|
|
3428
|
+
const c = doc.catalogs?.constraints?.find((x) => x.id === id);
|
|
3429
|
+
return c ? { statement: c.statement } : {};
|
|
3430
|
+
}
|
|
3431
|
+
case "decision": {
|
|
3432
|
+
const d = doc.catalogs?.decisions?.find((x) => x.id === id);
|
|
3433
|
+
return d ? { statement: d.decision, rationale: d.context, status: d.status } : {};
|
|
3434
|
+
}
|
|
3435
|
+
case "term": {
|
|
3436
|
+
const t = doc.catalogs?.glossary?.find((x) => x.id === id);
|
|
3437
|
+
return t ? { title: t.name, statement: t.definition } : {};
|
|
3438
|
+
}
|
|
3439
|
+
case "rule": {
|
|
3440
|
+
const r = doc.domain?.businessRules?.find((x) => x.id === id);
|
|
3441
|
+
return r ? { statement: r.statement, notes: r.examples } : {};
|
|
3442
|
+
}
|
|
3443
|
+
case "entity": {
|
|
3444
|
+
const e = doc.domain?.entities?.find((x) => x.id === id);
|
|
3445
|
+
return e ? { title: e.name, statement: e.description } : {};
|
|
3446
|
+
}
|
|
3447
|
+
case "stateMachine": {
|
|
3448
|
+
const sm = doc.behavior?.stateMachines?.find((x) => x.id === id);
|
|
3449
|
+
return sm ? { title: sm.name, statement: sm.description } : {};
|
|
3450
|
+
}
|
|
3451
|
+
default:
|
|
3452
|
+
return {};
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
function extractArtifact(doc, id) {
|
|
3456
|
+
const idIndex = declaredIdIndex(doc);
|
|
3457
|
+
const ref = idIndex.get(id);
|
|
3458
|
+
if (ref === void 0) return void 0;
|
|
3459
|
+
const slice = { id, kind: ref.kind, edges: [] };
|
|
3460
|
+
const req = requirementIndex(doc).get(id);
|
|
3461
|
+
if (req !== void 0) {
|
|
3462
|
+
slice.title = req.title;
|
|
3463
|
+
slice.statement = req.statement;
|
|
3464
|
+
slice.reqType = req.type;
|
|
3465
|
+
if (req.status !== void 0) slice.status = req.status;
|
|
3466
|
+
if (req.priority !== void 0) slice.priority = req.priority;
|
|
3467
|
+
if (req.rationale !== void 0) slice.rationale = req.rationale;
|
|
3468
|
+
if (req.notes !== void 0) slice.notes = req.notes;
|
|
3469
|
+
if (req.acceptance.length > 0) slice.acceptance = req.acceptance;
|
|
3470
|
+
} else {
|
|
3471
|
+
const extra = Object.fromEntries(
|
|
3472
|
+
Object.entries(details(doc, id, ref.kind)).filter(([, v]) => v !== void 0)
|
|
3473
|
+
);
|
|
3474
|
+
Object.assign(slice, extra);
|
|
3475
|
+
}
|
|
3476
|
+
for (const edge of doc.trace) {
|
|
3477
|
+
const fromKey = endpointKey2(edge.from);
|
|
3478
|
+
const toKey = endpointKey2(edge.to);
|
|
3479
|
+
if (fromKey !== id && toKey !== id) continue;
|
|
3480
|
+
const direction = fromKey === id ? "outgoing" : "incoming";
|
|
3481
|
+
const far = direction === "outgoing" ? edge.to : edge.from;
|
|
3482
|
+
const target = endpointKey2(far);
|
|
3483
|
+
const targetKind = far.kind === "local" ? idIndex.get(target)?.kind ?? "unknown" : far.kind;
|
|
3484
|
+
const sliceEdge = {
|
|
3485
|
+
edgeId: edge.id,
|
|
3486
|
+
type: edge.type,
|
|
3487
|
+
direction,
|
|
3488
|
+
target,
|
|
3489
|
+
targetKind
|
|
3490
|
+
};
|
|
3491
|
+
if (far.kind !== "local" && far.title !== void 0) sliceEdge.title = far.title;
|
|
3492
|
+
slice.edges.push(sliceEdge);
|
|
3493
|
+
}
|
|
3494
|
+
return slice;
|
|
3495
|
+
}
|
|
3496
|
+
function sliceToMarkdown(slice) {
|
|
3497
|
+
const lines = [];
|
|
3498
|
+
const heading = slice.title !== void 0 ? `${slice.id} \u2014 ${slice.title}` : slice.id;
|
|
3499
|
+
lines.push(`## ${heading}`);
|
|
3500
|
+
const facts = [
|
|
3501
|
+
`kind: ${slice.kind}${slice.reqType !== void 0 ? ` (${slice.reqType})` : ""}`
|
|
3502
|
+
];
|
|
3503
|
+
if (slice.status !== void 0) facts.push(`status: ${slice.status}`);
|
|
3504
|
+
if (slice.priority !== void 0) facts.push(`priority: ${slice.priority}`);
|
|
3505
|
+
lines.push(facts.join(" \xB7 "), "");
|
|
3506
|
+
if (slice.statement !== void 0) lines.push(slice.statement.trim(), "");
|
|
3507
|
+
if (slice.rationale !== void 0) {
|
|
3508
|
+
lines.push(`**Rationale:** ${slice.rationale.trim()}`, "");
|
|
3509
|
+
}
|
|
3510
|
+
if (slice.notes !== void 0) lines.push(`**Notes:** ${slice.notes.trim()}`, "");
|
|
3511
|
+
if (slice.acceptance !== void 0 && slice.acceptance.length > 0) {
|
|
3512
|
+
lines.push("### Acceptance");
|
|
3513
|
+
for (const c of slice.acceptance) {
|
|
3514
|
+
const parts = [];
|
|
3515
|
+
if (c.given !== void 0) parts.push(`GIVEN ${c.given.trim()}`);
|
|
3516
|
+
if (c.when !== void 0) parts.push(`WHEN ${c.when.trim()}`);
|
|
3517
|
+
parts.push(`THEN ${c.then.trim()}`);
|
|
3518
|
+
lines.push(`- ${c.id !== void 0 ? `\`${c.id}\` ` : ""}${parts.join(" ")}`);
|
|
3519
|
+
}
|
|
3520
|
+
lines.push("");
|
|
3521
|
+
}
|
|
3522
|
+
if (slice.edges.length > 0) {
|
|
3523
|
+
lines.push("### Trace");
|
|
3524
|
+
for (const e of slice.edges) {
|
|
3525
|
+
const arrow = e.direction === "outgoing" ? "\u2192" : "\u2190";
|
|
3526
|
+
lines.push(`- ${arrow} ${e.type} ${e.target} (${e.targetKind}, \`${e.edgeId}\`)`);
|
|
3527
|
+
}
|
|
3528
|
+
lines.push("");
|
|
3529
|
+
}
|
|
3530
|
+
return `${lines.join("\n").trimEnd()}
|
|
3531
|
+
`;
|
|
3532
|
+
}
|
|
3533
|
+
var SKELETON_KINDS = [
|
|
3534
|
+
"req",
|
|
3535
|
+
"edge",
|
|
3536
|
+
"testCase",
|
|
3537
|
+
"stateMachine"
|
|
3538
|
+
];
|
|
3539
|
+
var TEMPLATES = {
|
|
3540
|
+
req: (id) => `<req id="${id}" type="FR" title="Title" status="draft" priority="must">
|
|
3541
|
+
<statement>The system SHALL ...</statement>
|
|
3542
|
+
<acceptance>
|
|
3543
|
+
<criterion id="${id}-CRIT-1">
|
|
3544
|
+
<given>...</given>
|
|
3545
|
+
<when>...</when>
|
|
3546
|
+
<then>...</then>
|
|
3547
|
+
</criterion>
|
|
3548
|
+
</acceptance>
|
|
3549
|
+
</req>`,
|
|
3550
|
+
edge: (id) => `<edge id="${id}" type="satisfies">
|
|
3551
|
+
<from><locator><local id="REQ-AREA-001"/></locator></from>
|
|
3552
|
+
<to><locator><local id="GOAL-NAME"/></locator></to>
|
|
3553
|
+
</edge>`,
|
|
3554
|
+
testCase: (id) => `<testCase id="${id}" type="unit" title="Title">
|
|
3555
|
+
<purpose>...</purpose>
|
|
3556
|
+
<steps>...</steps>
|
|
3557
|
+
<expected>...</expected>
|
|
3558
|
+
</testCase>`,
|
|
3559
|
+
stateMachine: (id) => `<stateMachine id="${id}" name="Name" initial="ST-START">
|
|
3560
|
+
<state id="ST-START" name="Start" type="initial"/>
|
|
3561
|
+
<state id="ST-DONE" name="Done" type="final"/>
|
|
3562
|
+
<transition id="TR-FINISH" from="ST-START" to="ST-DONE" event="finish"/>
|
|
3563
|
+
</stateMachine>`
|
|
3564
|
+
};
|
|
3565
|
+
var DEFAULT_IDS = {
|
|
3566
|
+
req: "REQ-AREA-001",
|
|
3567
|
+
edge: "E-AREA-001",
|
|
3568
|
+
testCase: "TC-NAME",
|
|
3569
|
+
stateMachine: "SM-NAME"
|
|
3570
|
+
};
|
|
3571
|
+
function skeleton(kind, options = {}) {
|
|
3572
|
+
const template = TEMPLATES[kind];
|
|
3573
|
+
return `${template(options.id ?? DEFAULT_IDS[kind])}
|
|
3574
|
+
`;
|
|
3575
|
+
}
|
|
3151
3576
|
var UPWARD_TARGET_KINDS = /* @__PURE__ */ new Set([
|
|
3152
3577
|
"goal",
|
|
3153
3578
|
"qgoal",
|
|
@@ -3200,15 +3625,20 @@ function computeCoverage(doc) {
|
|
|
3200
3625
|
if (!(incoming.get(id)?.satisfies?.length ?? 0)) uncoveredGoals.push(id);
|
|
3201
3626
|
}
|
|
3202
3627
|
uncoveredGoals.sort();
|
|
3628
|
+
const statusOf = new Map(reqs.map((r) => [r.id, r.status]));
|
|
3203
3629
|
const unverifiedRequirements = [];
|
|
3204
3630
|
const unimplementedRequirements = [];
|
|
3631
|
+
const unimplementedApprovedRequirements = [];
|
|
3205
3632
|
const orphanRequirements = [];
|
|
3206
3633
|
for (const id of [...reqIds].sort()) {
|
|
3207
3634
|
const out = outgoing.get(id) ?? {};
|
|
3208
3635
|
const inc = incoming.get(id) ?? {};
|
|
3209
3636
|
const verified = (out.verifiedBy?.length ?? 0) > 0 || (inc.covers?.length ?? 0) > 0;
|
|
3210
3637
|
if (!verified) unverifiedRequirements.push(id);
|
|
3211
|
-
if (!(inc.implements?.length ?? 0))
|
|
3638
|
+
if (!(inc.implements?.length ?? 0)) {
|
|
3639
|
+
unimplementedRequirements.push(id);
|
|
3640
|
+
if (statusOf.get(id) === "approved") unimplementedApprovedRequirements.push(id);
|
|
3641
|
+
}
|
|
3212
3642
|
const satisfiesUpward = (out.satisfies ?? []).some((edgeId) => {
|
|
3213
3643
|
const edge = doc.trace.find((e) => e.id === edgeId);
|
|
3214
3644
|
const to = edge?.to;
|
|
@@ -3218,6 +3648,15 @@ function computeCoverage(doc) {
|
|
|
3218
3648
|
});
|
|
3219
3649
|
if (!satisfiesUpward) orphanRequirements.push(id);
|
|
3220
3650
|
}
|
|
3651
|
+
const prematureImplementations = [];
|
|
3652
|
+
for (const edge of doc.trace) {
|
|
3653
|
+
if (edge.type !== "implements" || edge.to.kind !== "local") continue;
|
|
3654
|
+
const requirementId = edge.to.id;
|
|
3655
|
+
if (!reqIds.has(requirementId)) continue;
|
|
3656
|
+
if (statusOf.get(requirementId) === "approved") continue;
|
|
3657
|
+
prematureImplementations.push({ edgeId: edge.id, requirementId });
|
|
3658
|
+
}
|
|
3659
|
+
prematureImplementations.sort((a, b) => a.edgeId.localeCompare(b.edgeId));
|
|
3221
3660
|
const diagnostics = [];
|
|
3222
3661
|
for (const id of uncoveredGoals)
|
|
3223
3662
|
diagnostics.push(
|
|
@@ -3251,12 +3690,22 @@ function computeCoverage(doc) {
|
|
|
3251
3690
|
`Requirement "${id}" satisfies no goal or scenario.`
|
|
3252
3691
|
)
|
|
3253
3692
|
);
|
|
3693
|
+
for (const p of prematureImplementations)
|
|
3694
|
+
diagnostics.push(
|
|
3695
|
+
finding(
|
|
3696
|
+
"coverage",
|
|
3697
|
+
"premature-implementation",
|
|
3698
|
+
`implements edge "${p.edgeId}" targets requirement "${p.requirementId}", which is not approved.`
|
|
3699
|
+
)
|
|
3700
|
+
);
|
|
3254
3701
|
diagnostics.push(...resolveTrace(doc).diagnostics);
|
|
3255
3702
|
return {
|
|
3256
3703
|
requirements,
|
|
3257
3704
|
uncoveredGoals,
|
|
3258
3705
|
unverifiedRequirements,
|
|
3259
3706
|
unimplementedRequirements,
|
|
3707
|
+
unimplementedApprovedRequirements,
|
|
3708
|
+
prematureImplementations,
|
|
3260
3709
|
orphanRequirements,
|
|
3261
3710
|
diagnostics
|
|
3262
3711
|
};
|
|
@@ -3264,6 +3713,51 @@ function computeCoverage(doc) {
|
|
|
3264
3713
|
function finding(source, rule, message) {
|
|
3265
3714
|
return { source, severity: "warning", rule, message };
|
|
3266
3715
|
}
|
|
3716
|
+
var BASELINE_PATH = ".rqml/baseline.json";
|
|
3717
|
+
function hashFileAt(filePath) {
|
|
3718
|
+
try {
|
|
3719
|
+
return createHash("sha256").update(readFileSync(filePath)).digest("hex");
|
|
3720
|
+
} catch {
|
|
3721
|
+
return void 0;
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
function computeBaseline(doc, options = {}) {
|
|
3725
|
+
const baseDir = options.baseDir ?? process.cwd();
|
|
3726
|
+
const baseline = {};
|
|
3727
|
+
for (const link of implementsLinks(doc)) {
|
|
3728
|
+
const filePath = filePathFromUri(link.uri, baseDir);
|
|
3729
|
+
if (filePath === void 0) continue;
|
|
3730
|
+
const hash = hashFileAt(filePath);
|
|
3731
|
+
if (hash !== void 0) baseline[link.edgeId] = hash;
|
|
3732
|
+
}
|
|
3733
|
+
return baseline;
|
|
3734
|
+
}
|
|
3735
|
+
function loadBaseline(baseDir) {
|
|
3736
|
+
try {
|
|
3737
|
+
const parsed = JSON.parse(
|
|
3738
|
+
readFileSync(join(baseDir, BASELINE_PATH), "utf8")
|
|
3739
|
+
);
|
|
3740
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
3741
|
+
return void 0;
|
|
3742
|
+
}
|
|
3743
|
+
const out = {};
|
|
3744
|
+
for (const [edgeId, hash] of Object.entries(parsed)) {
|
|
3745
|
+
if (typeof hash === "string") out[edgeId] = hash;
|
|
3746
|
+
}
|
|
3747
|
+
return out;
|
|
3748
|
+
} catch {
|
|
3749
|
+
return void 0;
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
function saveBaseline(baseDir, baseline) {
|
|
3753
|
+
const path = join(baseDir, BASELINE_PATH);
|
|
3754
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
3755
|
+
const sorted = Object.fromEntries(
|
|
3756
|
+
Object.entries(baseline).sort(([a], [b]) => a.localeCompare(b))
|
|
3757
|
+
);
|
|
3758
|
+
writeFileSync(path, `${JSON.stringify(sorted, null, 2)}
|
|
3759
|
+
`);
|
|
3760
|
+
}
|
|
3267
3761
|
function implementsLinks(doc) {
|
|
3268
3762
|
const out = [];
|
|
3269
3763
|
for (const edge of doc.trace) {
|
|
@@ -3289,16 +3783,22 @@ function filePathFromUri(uri, baseDir) {
|
|
|
3289
3783
|
}
|
|
3290
3784
|
return void 0;
|
|
3291
3785
|
}
|
|
3292
|
-
function filesystemResolver(baseDir) {
|
|
3786
|
+
function filesystemResolver(baseDir, baseline) {
|
|
3293
3787
|
return (link) => {
|
|
3294
3788
|
const filePath = filePathFromUri(link.uri, baseDir);
|
|
3295
3789
|
if (filePath === void 0) return "present";
|
|
3296
|
-
|
|
3790
|
+
if (!existsSync(filePath)) return "missing";
|
|
3791
|
+
const recorded = baseline?.[link.edgeId];
|
|
3792
|
+
if (recorded !== void 0) {
|
|
3793
|
+
const current = hashFileAt(filePath);
|
|
3794
|
+
if (current !== void 0 && current !== recorded) return "changed";
|
|
3795
|
+
}
|
|
3796
|
+
return "present";
|
|
3297
3797
|
};
|
|
3298
3798
|
}
|
|
3299
3799
|
function detectDrift(doc, options = {}) {
|
|
3300
3800
|
const baseDir = options.baseDir ?? process.cwd();
|
|
3301
|
-
const resolve4 = options.resolve ?? filesystemResolver(baseDir);
|
|
3801
|
+
const resolve4 = options.resolve ?? filesystemResolver(baseDir, options.baseline);
|
|
3302
3802
|
const links = implementsLinks(doc);
|
|
3303
3803
|
const drifted = [];
|
|
3304
3804
|
const diagnostics = [];
|
|
@@ -3370,17 +3870,29 @@ function parseArgs(rest) {
|
|
|
3370
3870
|
positionals,
|
|
3371
3871
|
json: flags.get("json") === true || flags.get("json") === "true",
|
|
3372
3872
|
strictness,
|
|
3373
|
-
baseDir: resolve(String(flags.get("base-dir") ?? process.cwd()))
|
|
3873
|
+
baseDir: resolve(String(flags.get("base-dir") ?? process.cwd())),
|
|
3874
|
+
flags
|
|
3374
3875
|
};
|
|
3375
3876
|
}
|
|
3877
|
+
function flagString(args, name) {
|
|
3878
|
+
const value = args.flags.get(name);
|
|
3879
|
+
return typeof value === "string" ? value : void 0;
|
|
3880
|
+
}
|
|
3881
|
+
function specArgs(args) {
|
|
3882
|
+
const override = flagString(args, "spec");
|
|
3883
|
+
return { ...args, positionals: override !== void 0 ? [override] : [] };
|
|
3884
|
+
}
|
|
3376
3885
|
function resolveSpecPath(args) {
|
|
3377
3886
|
const explicit = args.positionals[0];
|
|
3378
3887
|
if (explicit !== void 0) {
|
|
3379
3888
|
const p = resolve(args.baseDir, explicit);
|
|
3380
3889
|
if (!existsSync(p)) throw new UsageError(`spec file not found: ${explicit}`);
|
|
3890
|
+
if (statSync(p).isDirectory()) {
|
|
3891
|
+
throw new UsageError(`"${explicit}" is a directory, not an .rqml file`);
|
|
3892
|
+
}
|
|
3381
3893
|
return p;
|
|
3382
3894
|
}
|
|
3383
|
-
const candidates = readdirSync(args.baseDir).filter((
|
|
3895
|
+
const candidates = readdirSync(args.baseDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".rqml")).map((e) => e.name).sort();
|
|
3384
3896
|
if (candidates.length === 0) {
|
|
3385
3897
|
throw new UsageError("no .rqml document found in this directory; pass a path");
|
|
3386
3898
|
}
|
|
@@ -3404,10 +3916,13 @@ async function runCheck(rest) {
|
|
|
3404
3916
|
const integrity = validation.valid ? checkIntegrity(xml) : [];
|
|
3405
3917
|
const parsed = parse(xml);
|
|
3406
3918
|
const coverage = parsed.ok ? computeCoverage(parsed.document) : void 0;
|
|
3407
|
-
const
|
|
3919
|
+
const driftOptions = { baseDir: args.baseDir };
|
|
3920
|
+
const baseline = loadBaseline(args.baseDir);
|
|
3921
|
+
if (baseline !== void 0) driftOptions.baseline = baseline;
|
|
3922
|
+
const drift = parsed.ok ? detectDrift(parsed.document, driftOptions) : void 0;
|
|
3408
3923
|
const validationFailed = !validation.valid || integrity.length > 0;
|
|
3409
3924
|
const driftFailed = (drift?.drifted.length ?? 0) > 0;
|
|
3410
|
-
const coverageProblemCount = (coverage?.uncoveredGoals.length ?? 0) + (coverage?.unverifiedRequirements.length ?? 0) + (coverage?.orphanRequirements.length ?? 0);
|
|
3925
|
+
const coverageProblemCount = (coverage?.uncoveredGoals.length ?? 0) + (coverage?.unverifiedRequirements.length ?? 0) + (coverage?.orphanRequirements.length ?? 0) + (coverage?.unimplementedApprovedRequirements.length ?? 0) + (coverage?.prematureImplementations.length ?? 0);
|
|
3411
3926
|
const coverageFailed = coverageBlocks(args.strictness) && coverageProblemCount > 0;
|
|
3412
3927
|
const verdict = validationFailed || driftFailed || coverageFailed ? "fail" : "pass";
|
|
3413
3928
|
const diagnostics = [
|
|
@@ -3427,6 +3942,8 @@ async function runCheck(rest) {
|
|
|
3427
3942
|
uncoveredGoals: coverage.uncoveredGoals,
|
|
3428
3943
|
unverifiedRequirements: coverage.unverifiedRequirements,
|
|
3429
3944
|
unimplementedRequirements: coverage.unimplementedRequirements,
|
|
3945
|
+
unimplementedApprovedRequirements: coverage.unimplementedApprovedRequirements,
|
|
3946
|
+
prematureImplementations: coverage.prematureImplementations,
|
|
3430
3947
|
orphanRequirements: coverage.orphanRequirements
|
|
3431
3948
|
} : null,
|
|
3432
3949
|
diagnostics
|
|
@@ -3446,8 +3963,54 @@ async function runCheck(rest) {
|
|
|
3446
3963
|
return EXIT.OK;
|
|
3447
3964
|
}
|
|
3448
3965
|
|
|
3966
|
+
// src/commands/impact.ts
|
|
3967
|
+
async function runImpact(rest) {
|
|
3968
|
+
const args = parseArgs(rest);
|
|
3969
|
+
const id = args.positionals[0];
|
|
3970
|
+
if (id === void 0) {
|
|
3971
|
+
throw new UsageError("usage: rqml impact <id> [--json] [--spec <path>]");
|
|
3972
|
+
}
|
|
3973
|
+
const { path, xml } = readSpec(specArgs(args));
|
|
3974
|
+
const parsed = parse(xml);
|
|
3975
|
+
if (!parsed.ok) {
|
|
3976
|
+
process.stderr.write(`\u2717 ${path}: ${parsed.error.message}
|
|
3977
|
+
`);
|
|
3978
|
+
return EXIT.VALIDATION;
|
|
3979
|
+
}
|
|
3980
|
+
if (!declaredIdIndex(parsed.document).has(id)) {
|
|
3981
|
+
process.stderr.write(`\u2717 no artifact with id "${id}" in ${path}
|
|
3982
|
+
`);
|
|
3983
|
+
return EXIT.USAGE;
|
|
3984
|
+
}
|
|
3985
|
+
const report = impactOf(parsed.document, id);
|
|
3986
|
+
if (args.json) {
|
|
3987
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}
|
|
3988
|
+
`);
|
|
3989
|
+
return EXIT.OK;
|
|
3990
|
+
}
|
|
3991
|
+
process.stdout.write(`Impact of ${id} \u2014 ${report.affected.length} affected
|
|
3992
|
+
`);
|
|
3993
|
+
for (const group of report.groups) {
|
|
3994
|
+
const arrow = group.direction === "outgoing" ? "\u2192" : "\u2190";
|
|
3995
|
+
process.stdout.write(` ${arrow} ${group.type}: ${group.ids.join(", ")}
|
|
3996
|
+
`);
|
|
3997
|
+
}
|
|
3998
|
+
const transitive = report.affected.filter((a) => a.distance > 1);
|
|
3999
|
+
if (transitive.length > 0) {
|
|
4000
|
+
process.stdout.write(" transitively:\n");
|
|
4001
|
+
for (const a of transitive) {
|
|
4002
|
+
const path2 = a.path.map((s) => s.edgeId).join(" \u203A ");
|
|
4003
|
+
process.stdout.write(
|
|
4004
|
+
` ${a.id} (${a.kind}, distance ${a.distance} via ${path2})
|
|
4005
|
+
`
|
|
4006
|
+
);
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
return EXIT.OK;
|
|
4010
|
+
}
|
|
4011
|
+
|
|
3449
4012
|
// ../schema/dist/index.js
|
|
3450
|
-
var AGENTS_default =
|
|
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\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';
|
|
3451
4014
|
var AGENTS_TEMPLATE = AGENTS_default;
|
|
3452
4015
|
|
|
3453
4016
|
// src/commands/init.ts
|
|
@@ -3490,6 +4053,110 @@ async function runInit(rest) {
|
|
|
3490
4053
|
if (!wrote) process.stdout.write("nothing to do; project already initialized\n");
|
|
3491
4054
|
return EXIT.OK;
|
|
3492
4055
|
}
|
|
4056
|
+
var USAGE = "usage: rqml link <artifact-id> <uri> [--type implements|verifiedBy] [--id <edge-id>] [--kind <k>] [--title <t>] [--spec <path>]";
|
|
4057
|
+
async function runLink(rest) {
|
|
4058
|
+
const args = parseArgs(rest);
|
|
4059
|
+
const [artifactId, uri] = args.positionals;
|
|
4060
|
+
if (artifactId === void 0 || uri === void 0) throw new UsageError(USAGE);
|
|
4061
|
+
const type = flagString(args, "type") ?? "implements";
|
|
4062
|
+
if (type !== "implements" && type !== "verifiedBy") {
|
|
4063
|
+
throw new UsageError(`unknown link type "${type}" (implements|verifiedBy)`);
|
|
4064
|
+
}
|
|
4065
|
+
const { path, xml } = readSpec(specArgs(args));
|
|
4066
|
+
const request = { artifactId, uri, type };
|
|
4067
|
+
const edgeId = flagString(args, "id");
|
|
4068
|
+
if (edgeId !== void 0) request.edgeId = edgeId;
|
|
4069
|
+
const kind = flagString(args, "kind");
|
|
4070
|
+
if (kind !== void 0) request.kind = kind;
|
|
4071
|
+
const title = flagString(args, "title");
|
|
4072
|
+
if (title !== void 0) request.title = title;
|
|
4073
|
+
const result = appendTraceEdge(xml, request);
|
|
4074
|
+
if (!result.ok) {
|
|
4075
|
+
process.stderr.write(`\u2717 link failed: ${result.error}
|
|
4076
|
+
`);
|
|
4077
|
+
return EXIT.VALIDATION;
|
|
4078
|
+
}
|
|
4079
|
+
const { validate } = await import('./validate-O3LLP44J.js');
|
|
4080
|
+
const validation = validate(result.xml);
|
|
4081
|
+
if (!validation.valid) {
|
|
4082
|
+
printDiagnostics(validation.diagnostics);
|
|
4083
|
+
process.stderr.write("\u2717 link would invalidate the document; nothing written\n");
|
|
4084
|
+
return EXIT.VALIDATION;
|
|
4085
|
+
}
|
|
4086
|
+
writeFileSync(path, result.xml);
|
|
4087
|
+
let baselineRecorded = false;
|
|
4088
|
+
const parsed = parse(result.xml);
|
|
4089
|
+
if (parsed.ok) {
|
|
4090
|
+
const fresh = computeBaseline(parsed.document, { baseDir: args.baseDir });
|
|
4091
|
+
const hash = fresh[result.edgeId];
|
|
4092
|
+
if (hash !== void 0) {
|
|
4093
|
+
const baseline = loadBaseline(args.baseDir) ?? {};
|
|
4094
|
+
baseline[result.edgeId] = hash;
|
|
4095
|
+
saveBaseline(args.baseDir, baseline);
|
|
4096
|
+
baselineRecorded = true;
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
4099
|
+
if (args.json) {
|
|
4100
|
+
const report = {
|
|
4101
|
+
spec: path,
|
|
4102
|
+
edgeId: result.edgeId,
|
|
4103
|
+
type,
|
|
4104
|
+
artifactId,
|
|
4105
|
+
uri,
|
|
4106
|
+
baselineRecorded
|
|
4107
|
+
};
|
|
4108
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}
|
|
4109
|
+
`);
|
|
4110
|
+
} else {
|
|
4111
|
+
const arrow = type === "implements" ? "\u2190" : "\u2192";
|
|
4112
|
+
const baseline = baselineRecorded ? ", baseline recorded" : "";
|
|
4113
|
+
process.stdout.write(
|
|
4114
|
+
`\u2713 ${artifactId} ${arrow} ${uri} (${result.edgeId}, ${type}${baseline})
|
|
4115
|
+
`
|
|
4116
|
+
);
|
|
4117
|
+
}
|
|
4118
|
+
return EXIT.OK;
|
|
4119
|
+
}
|
|
4120
|
+
|
|
4121
|
+
// src/commands/show.ts
|
|
4122
|
+
async function runShow(rest) {
|
|
4123
|
+
const args = parseArgs(rest);
|
|
4124
|
+
const id = args.positionals[0];
|
|
4125
|
+
if (id === void 0) {
|
|
4126
|
+
throw new UsageError("usage: rqml show <id> [--json] [--spec <path>]");
|
|
4127
|
+
}
|
|
4128
|
+
const { path, xml } = readSpec(specArgs(args));
|
|
4129
|
+
const parsed = parse(xml);
|
|
4130
|
+
if (!parsed.ok) {
|
|
4131
|
+
process.stderr.write(`\u2717 ${path}: ${parsed.error.message}
|
|
4132
|
+
`);
|
|
4133
|
+
return EXIT.VALIDATION;
|
|
4134
|
+
}
|
|
4135
|
+
const slice = extractArtifact(parsed.document, id);
|
|
4136
|
+
if (slice === void 0) {
|
|
4137
|
+
process.stderr.write(`\u2717 no artifact with id "${id}" in ${path}
|
|
4138
|
+
`);
|
|
4139
|
+
return EXIT.USAGE;
|
|
4140
|
+
}
|
|
4141
|
+
process.stdout.write(
|
|
4142
|
+
args.json ? `${JSON.stringify(slice, null, 2)}
|
|
4143
|
+
` : sliceToMarkdown(slice)
|
|
4144
|
+
);
|
|
4145
|
+
return EXIT.OK;
|
|
4146
|
+
}
|
|
4147
|
+
|
|
4148
|
+
// src/commands/skeleton.ts
|
|
4149
|
+
async function runSkeleton(rest) {
|
|
4150
|
+
const args = parseArgs(rest);
|
|
4151
|
+
const kind = args.positionals[0];
|
|
4152
|
+
const kinds = SKELETON_KINDS.join("|");
|
|
4153
|
+
if (kind === void 0 || !SKELETON_KINDS.includes(kind)) {
|
|
4154
|
+
throw new UsageError(`usage: rqml skeleton <${kinds}> [--id <id>]`);
|
|
4155
|
+
}
|
|
4156
|
+
const id = flagString(args, "id");
|
|
4157
|
+
process.stdout.write(skeleton(kind, id !== void 0 ? { id } : {}));
|
|
4158
|
+
return EXIT.OK;
|
|
4159
|
+
}
|
|
3493
4160
|
|
|
3494
4161
|
// src/commands/status.ts
|
|
3495
4162
|
async function runStatus(rest) {
|
|
@@ -3516,6 +4183,8 @@ async function runStatus(rest) {
|
|
|
3516
4183
|
uncoveredGoals: coverage.uncoveredGoals,
|
|
3517
4184
|
unverifiedRequirements: coverage.unverifiedRequirements,
|
|
3518
4185
|
unimplementedRequirements: coverage.unimplementedRequirements,
|
|
4186
|
+
unimplementedApprovedRequirements: coverage.unimplementedApprovedRequirements,
|
|
4187
|
+
prematureImplementations: coverage.prematureImplementations,
|
|
3519
4188
|
orphanRequirements: coverage.orphanRequirements,
|
|
3520
4189
|
danglingReferences: trace.diagnostics.length,
|
|
3521
4190
|
lintFindings: lintDiags.length
|
|
@@ -3530,7 +4199,8 @@ async function runStatus(rest) {
|
|
|
3530
4199
|
requirements: ${reqCount} trace edges: ${doc.trace.length}
|
|
3531
4200
|
uncovered goals: ${coverage.uncoveredGoals.length}
|
|
3532
4201
|
unverified reqs: ${coverage.unverifiedRequirements.length}
|
|
3533
|
-
unimplemented reqs: ${coverage.unimplementedRequirements.length}
|
|
4202
|
+
unimplemented reqs: ${coverage.unimplementedRequirements.length} (approved: ${coverage.unimplementedApprovedRequirements.length})
|
|
4203
|
+
premature implementations: ${coverage.prematureImplementations.length}
|
|
3534
4204
|
dangling refs: ${trace.diagnostics.length} lint findings: ${lintDiags.length}
|
|
3535
4205
|
`
|
|
3536
4206
|
);
|
|
@@ -3571,15 +4241,24 @@ Usage:
|
|
|
3571
4241
|
rqml <command> [spec.rqml] [options]
|
|
3572
4242
|
|
|
3573
4243
|
Commands:
|
|
3574
|
-
init [path]
|
|
3575
|
-
validate [path]
|
|
3576
|
-
status [path]
|
|
3577
|
-
check [path]
|
|
4244
|
+
init [path] Scaffold a starter spec and AGENTS.md project marker
|
|
4245
|
+
validate [path] Validate XML well-formedness, XSD, and referential integrity
|
|
4246
|
+
status [path] Show spec, coverage, and lint summary
|
|
4247
|
+
check [path] Deterministic enforcement gate (validation + coverage + drift)
|
|
4248
|
+
link <id> <uri> Record an implements/verifiedBy edge and its drift baseline
|
|
4249
|
+
show <id> Extract one artifact with its trace neighborhood
|
|
4250
|
+
impact <id> What is affected, transitively, if this artifact changes
|
|
4251
|
+
skeleton <kind> Print a schema-valid snippet (req|edge|testCase|stateMachine)
|
|
3578
4252
|
|
|
3579
4253
|
Options:
|
|
3580
|
-
--json Emit machine-readable JSON (status, check, validate)
|
|
4254
|
+
--json Emit machine-readable JSON (status, check, validate, link, show, impact)
|
|
3581
4255
|
--strictness <level> relaxed | standard | strict | certified (default: standard)
|
|
3582
4256
|
--base-dir <dir> Directory to resolve the spec and code links against
|
|
4257
|
+
--spec <path> Explicit spec file (link, show, impact)
|
|
4258
|
+
--type <type> Link type: implements | verifiedBy (default: implements)
|
|
4259
|
+
--id <id> Explicit edge id (link) or skeleton root id
|
|
4260
|
+
--kind <kind> Locator kind hint for link (default: code/test by type)
|
|
4261
|
+
--title <title> Locator title hint for link
|
|
3583
4262
|
-h, --help Show this help
|
|
3584
4263
|
-v, --version Show version
|
|
3585
4264
|
|
|
@@ -3596,6 +4275,14 @@ async function main(argv) {
|
|
|
3596
4275
|
return runCheck(rest);
|
|
3597
4276
|
case "init":
|
|
3598
4277
|
return runInit(rest);
|
|
4278
|
+
case "link":
|
|
4279
|
+
return runLink(rest);
|
|
4280
|
+
case "show":
|
|
4281
|
+
return runShow(rest);
|
|
4282
|
+
case "impact":
|
|
4283
|
+
return runImpact(rest);
|
|
4284
|
+
case "skeleton":
|
|
4285
|
+
return runSkeleton(rest);
|
|
3599
4286
|
case "-v":
|
|
3600
4287
|
case "--version":
|
|
3601
4288
|
process.stdout.write(`${VERSION}
|