@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 CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { __commonJS, __toESM } from './chunk-5WRI5ZAA.js';
3
- import { existsSync, writeFileSync, readFileSync, statSync, readdirSync } from 'fs';
4
- import { resolve, isAbsolute } from 'path';
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
- walk(item, declared, refs);
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
- walk(root, declared, refs);
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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)) unimplementedRequirements.push(id);
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
- return existsSync(filePath) ? "present" : "missing";
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 drift = parsed.ok ? detectDrift(parsed.document, { baseDir: args.baseDir }) : void 0;
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 = "# 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## 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\nReference requirement IDs in code comments. If you discover missing requirements, stop and add them to the spec first.\n\n### 4. Verify\nAdd tests that reference requirement IDs. Update `<trace>` section with verification links.\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:** Try xmllint first (pre-installed on macOS/Linux):\n```bash\nxmllint --schema https://rqml.org/schema/rqml-2.1.0.xsd <rqml-file-name> --noout\n```\n\nIf xmllint is unavailable, use Python with lxml:\n```bash\npip install lxml\npython -c \"from lxml import etree; s=etree.XMLSchema(etree.parse('https://rqml.org/schema/rqml-2.1.0.xsd')); print('Valid' if s.validate(etree.parse('<rqml-file-name>')) else s.error_log)\"\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";
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 = "0.1.0";
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] Scaffold a starter spec and AGENTS.md project marker
3578
- validate [path] Validate XML well-formedness, XSD, and referential integrity
3579
- status [path] Show spec, coverage, and lint summary
3580
- check [path] Deterministic enforcement gate (validation + coverage + drift)
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}