@rqml/cli 0.2.0 → 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 +134 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { __commonJS, __toESM } from './chunk-5WRI5ZAA.js';
|
|
3
|
+
import { createRequire } from 'module';
|
|
3
4
|
import { createHash } from 'crypto';
|
|
4
5
|
import { writeFileSync, existsSync, readFileSync, mkdirSync, statSync, readdirSync } from 'fs';
|
|
5
6
|
import { resolve, join, dirname, isAbsolute } from 'path';
|
|
@@ -3372,6 +3373,86 @@ ${sectionIndent}</trace>
|
|
|
3372
3373
|
}
|
|
3373
3374
|
return { ok: true, xml: updated, edgeId, edgeXml: edgeXml.trim() };
|
|
3374
3375
|
}
|
|
3376
|
+
function escapeRegExp2(value) {
|
|
3377
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3378
|
+
}
|
|
3379
|
+
var EXTERNAL_ELEMENT = /<external\b[^>]*(?:\/>|>[\s\S]*?<\/external>)/g;
|
|
3380
|
+
function updateTraceEdge(xml, request) {
|
|
3381
|
+
const parsed = parse(xml);
|
|
3382
|
+
if (!parsed.ok) {
|
|
3383
|
+
return { ok: false, error: `document does not parse: ${parsed.error.message}` };
|
|
3384
|
+
}
|
|
3385
|
+
if (request.uri.trim() === "") {
|
|
3386
|
+
return { ok: false, error: "locator uri must not be empty" };
|
|
3387
|
+
}
|
|
3388
|
+
const prefix = request.type === "implements" ? "E-IMPL-" : "E-VER-";
|
|
3389
|
+
const edgeId = request.edgeId ?? prefix + request.artifactId.replace(/^REQ-/, "");
|
|
3390
|
+
const edge = parsed.document.trace.find((e) => e.id === edgeId);
|
|
3391
|
+
if (edge === void 0) {
|
|
3392
|
+
return {
|
|
3393
|
+
ok: false,
|
|
3394
|
+
error: request.edgeId !== void 0 ? `no trace edge with id "${edgeId}" exists` : `no trace edge with the derived id "${edgeId}" exists; pass --id for an explicitly named edge`
|
|
3395
|
+
};
|
|
3396
|
+
}
|
|
3397
|
+
if (edge.type !== request.type) {
|
|
3398
|
+
return {
|
|
3399
|
+
ok: false,
|
|
3400
|
+
error: `edge "${edgeId}" has type "${edge.type}", not "${request.type}"`
|
|
3401
|
+
};
|
|
3402
|
+
}
|
|
3403
|
+
const local = edge.from.kind === "local" ? edge.from : edge.to.kind === "local" ? edge.to : void 0;
|
|
3404
|
+
if (local === void 0 || local.id !== request.artifactId) {
|
|
3405
|
+
return {
|
|
3406
|
+
ok: false,
|
|
3407
|
+
error: `edge "${edgeId}" does not link artifact "${request.artifactId}"`
|
|
3408
|
+
};
|
|
3409
|
+
}
|
|
3410
|
+
const external = edge.from.kind === "external" ? edge.from : edge.to.kind === "external" ? edge.to : void 0;
|
|
3411
|
+
if (external === void 0) {
|
|
3412
|
+
return { ok: false, error: `edge "${edgeId}" has no external locator to update` };
|
|
3413
|
+
}
|
|
3414
|
+
const openTag = new RegExp(`<edge\\b[^>]*\\bid="${escapeRegExp2(edgeId)}"[^>]*>`);
|
|
3415
|
+
const open = openTag.exec(xml);
|
|
3416
|
+
const closeIdx = open === null ? -1 : xml.indexOf("</edge>", open.index);
|
|
3417
|
+
if (open === null || closeIdx < 0) {
|
|
3418
|
+
return { ok: false, error: `could not locate edge "${edgeId}" in the document text` };
|
|
3419
|
+
}
|
|
3420
|
+
const span = xml.slice(open.index, closeIdx);
|
|
3421
|
+
const occurrences = span.match(EXTERNAL_ELEMENT) ?? [];
|
|
3422
|
+
if (occurrences.length !== 1) {
|
|
3423
|
+
return {
|
|
3424
|
+
ok: false,
|
|
3425
|
+
error: `edge "${edgeId}" does not contain exactly one external locator element`
|
|
3426
|
+
};
|
|
3427
|
+
}
|
|
3428
|
+
const kind = request.kind ?? external.hintKind ?? (request.type === "implements" ? "code" : "test");
|
|
3429
|
+
const title = request.title ?? external.title;
|
|
3430
|
+
const titleAttr = title !== void 0 ? ` title="${escapeAttr(title)}"` : "";
|
|
3431
|
+
const replacement = `<external uri="${escapeAttr(request.uri)}" kind="${escapeAttr(kind)}"${titleAttr}/>`;
|
|
3432
|
+
const editedSpan = span.replace(occurrences[0], () => replacement);
|
|
3433
|
+
const updated = xml.slice(0, open.index) + editedSpan + xml.slice(closeIdx);
|
|
3434
|
+
const before = checkIntegrity(xml).length;
|
|
3435
|
+
const reparsed = parse(updated);
|
|
3436
|
+
if (!reparsed.ok) {
|
|
3437
|
+
return {
|
|
3438
|
+
ok: false,
|
|
3439
|
+
error: `edit produced an unparseable document: ${reparsed.error.message}`
|
|
3440
|
+
};
|
|
3441
|
+
}
|
|
3442
|
+
if (checkIntegrity(updated).length > before) {
|
|
3443
|
+
return {
|
|
3444
|
+
ok: false,
|
|
3445
|
+
error: "edit introduced an integrity violation; document left unchanged"
|
|
3446
|
+
};
|
|
3447
|
+
}
|
|
3448
|
+
return {
|
|
3449
|
+
ok: true,
|
|
3450
|
+
xml: updated,
|
|
3451
|
+
edgeId,
|
|
3452
|
+
edgeXml: `${editedSpan}</edge>`.trim(),
|
|
3453
|
+
previousUri: external.uri
|
|
3454
|
+
};
|
|
3455
|
+
}
|
|
3375
3456
|
function endpointKey2(locator) {
|
|
3376
3457
|
return locator.kind === "local" ? locator.id : locator.uri;
|
|
3377
3458
|
}
|
|
@@ -4053,15 +4134,21 @@ async function runInit(rest) {
|
|
|
4053
4134
|
if (!wrote) process.stdout.write("nothing to do; project already initialized\n");
|
|
4054
4135
|
return EXIT.OK;
|
|
4055
4136
|
}
|
|
4056
|
-
var USAGE = "usage: rqml link <artifact-id> <uri> [--type implements|verifiedBy] [--id <edge-id>] [--kind <k>] [--title <t>] [--spec <path>]";
|
|
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>]";
|
|
4057
4138
|
async function runLink(rest) {
|
|
4058
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
|
+
}
|
|
4059
4145
|
const [artifactId, uri] = args.positionals;
|
|
4060
4146
|
if (artifactId === void 0 || uri === void 0) throw new UsageError(USAGE);
|
|
4061
4147
|
const type = flagString(args, "type") ?? "implements";
|
|
4062
4148
|
if (type !== "implements" && type !== "verifiedBy") {
|
|
4063
4149
|
throw new UsageError(`unknown link type "${type}" (implements|verifiedBy)`);
|
|
4064
4150
|
}
|
|
4151
|
+
const update = args.flags.get("update") === true || args.flags.get("update") === "true";
|
|
4065
4152
|
const { path, xml } = readSpec(specArgs(args));
|
|
4066
4153
|
const request = { artifactId, uri, type };
|
|
4067
4154
|
const edgeId = flagString(args, "id");
|
|
@@ -4070,7 +4157,7 @@ async function runLink(rest) {
|
|
|
4070
4157
|
if (kind !== void 0) request.kind = kind;
|
|
4071
4158
|
const title = flagString(args, "title");
|
|
4072
4159
|
if (title !== void 0) request.title = title;
|
|
4073
|
-
const result = appendTraceEdge(xml, request);
|
|
4160
|
+
const result = update ? updateTraceEdge(xml, request) : appendTraceEdge(xml, request);
|
|
4074
4161
|
if (!result.ok) {
|
|
4075
4162
|
process.stderr.write(`\u2717 link failed: ${result.error}
|
|
4076
4163
|
`);
|
|
@@ -4099,6 +4186,7 @@ async function runLink(rest) {
|
|
|
4099
4186
|
if (args.json) {
|
|
4100
4187
|
const report = {
|
|
4101
4188
|
spec: path,
|
|
4189
|
+
mode: update ? "update" : "append",
|
|
4102
4190
|
edgeId: result.edgeId,
|
|
4103
4191
|
type,
|
|
4104
4192
|
artifactId,
|
|
@@ -4109,14 +4197,52 @@ async function runLink(rest) {
|
|
|
4109
4197
|
`);
|
|
4110
4198
|
} else {
|
|
4111
4199
|
const arrow = type === "implements" ? "\u2190" : "\u2192";
|
|
4200
|
+
const mode = update ? ", updated" : "";
|
|
4112
4201
|
const baseline = baselineRecorded ? ", baseline recorded" : "";
|
|
4113
4202
|
process.stdout.write(
|
|
4114
|
-
`\u2713 ${artifactId} ${arrow} ${uri} (${result.edgeId}, ${type}${baseline})
|
|
4203
|
+
`\u2713 ${artifactId} ${arrow} ${uri} (${result.edgeId}, ${type}${mode}${baseline})
|
|
4115
4204
|
`
|
|
4116
4205
|
);
|
|
4117
4206
|
}
|
|
4118
4207
|
return EXIT.OK;
|
|
4119
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
|
+
}
|
|
4120
4246
|
|
|
4121
4247
|
// src/commands/show.ts
|
|
4122
4248
|
async function runShow(rest) {
|
|
@@ -4234,7 +4360,7 @@ async function runValidate(rest) {
|
|
|
4234
4360
|
}
|
|
4235
4361
|
|
|
4236
4362
|
// src/index.ts
|
|
4237
|
-
var VERSION = "
|
|
4363
|
+
var VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
4238
4364
|
var HELP = `rqml \u2014 RQML reference CLI (v${VERSION})
|
|
4239
4365
|
|
|
4240
4366
|
Usage:
|
|
@@ -4246,6 +4372,8 @@ Commands:
|
|
|
4246
4372
|
status [path] Show spec, coverage, and lint summary
|
|
4247
4373
|
check [path] Deterministic enforcement gate (validation + coverage + drift)
|
|
4248
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)
|
|
4249
4377
|
show <id> Extract one artifact with its trace neighborhood
|
|
4250
4378
|
impact <id> What is affected, transitively, if this artifact changes
|
|
4251
4379
|
skeleton <kind> Print a schema-valid snippet (req|edge|testCase|stateMachine)
|
|
@@ -4259,6 +4387,8 @@ Options:
|
|
|
4259
4387
|
--id <id> Explicit edge id (link) or skeleton root id
|
|
4260
4388
|
--kind <kind> Locator kind hint for link (default: code/test by type)
|
|
4261
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)
|
|
4262
4392
|
-h, --help Show this help
|
|
4263
4393
|
-v, --version Show version
|
|
4264
4394
|
|