@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 CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { __commonJS, __toESM } from './chunk-5WRI5ZAA.js';
3
- import { existsSync, writeFileSync, readFileSync, readdirSync } from 'fs';
4
- import { resolve, isAbsolute } from 'path';
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
- walk(item, declared, refs);
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
- walk(root, declared, refs);
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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)) unimplementedRequirements.push(id);
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
- return existsSync(filePath) ? "present" : "missing";
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((f) => f.endsWith(".rqml")).sort();
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 drift = parsed.ok ? detectDrift(parsed.document, { baseDir: args.baseDir }) : void 0;
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 = "# 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";
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] Scaffold a starter spec and AGENTS.md project marker
3575
- validate [path] Validate XML well-formedness, XSD, and referential integrity
3576
- status [path] Show spec, coverage, and lint summary
3577
- check [path] Deterministic enforcement gate (validation + coverage + drift)
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}