@spec-engine/spec-engine 0.7.2 → 0.8.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/docs/checks/index.html +11 -1
- package/dist/docs/commands/index.html +4 -4
- package/dist/docs/format/index.html +5 -1
- package/dist/docs/pagefind/fragment/en_24f76b6.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_4bd9de6.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_9cc1878.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_9cfc306.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_c3abf3d.pf_fragment +0 -0
- package/dist/docs/pagefind/index/en_6e50ed4.pf_index +0 -0
- package/dist/docs/pagefind/index/en_afc98e5.pf_index +0 -0
- package/dist/docs/pagefind/pagefind-entry.json +1 -1
- package/dist/docs/pagefind/pagefind.en_d0b5c328bc.pf_meta +0 -0
- package/dist/docs/platform-mapping/index.html +2 -2
- package/dist/docs/tags/index.html +47 -1
- package/dist/impl.js +345 -171
- package/package.json +3 -3
- package/dist/docs/pagefind/fragment/en_70e8a6f.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_907a67d.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_de9d958.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_e616a50.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_f77a622.pf_fragment +0 -0
- package/dist/docs/pagefind/index/en_19cbfc6.pf_index +0 -0
- package/dist/docs/pagefind/index/en_4be71c4.pf_index +0 -0
- package/dist/docs/pagefind/pagefind.en_f697eb692e.pf_meta +0 -0
package/dist/impl.js
CHANGED
|
@@ -20,7 +20,7 @@ var package_default;
|
|
|
20
20
|
var init_package = __esm(() => {
|
|
21
21
|
package_default = {
|
|
22
22
|
name: "@spec-engine/spec-engine",
|
|
23
|
-
version: "0.
|
|
23
|
+
version: "0.8.0",
|
|
24
24
|
description: "Cross-repo spec engine \u2014 durable requirements with permanent IDs, bound to code via @spec tags, checked in CI. Ships the spec CLI, the local coverage webapp (spec serve), and offline docs (spec docs).",
|
|
25
25
|
license: "MIT",
|
|
26
26
|
type: "module",
|
|
@@ -59,11 +59,11 @@ var init_package = __esm(() => {
|
|
|
59
59
|
prepack: "bun run build:npm"
|
|
60
60
|
},
|
|
61
61
|
dependencies: {
|
|
62
|
-
"@modelcontextprotocol/sdk": "1.
|
|
62
|
+
"@modelcontextprotocol/sdk": "1.32.0",
|
|
63
63
|
"@spec-engine/platform-map": "0.5.0",
|
|
64
64
|
citty: "0.2.2",
|
|
65
65
|
hono: "4.13.11",
|
|
66
|
-
zod: "
|
|
66
|
+
zod: "4.4.3"
|
|
67
67
|
},
|
|
68
68
|
devDependencies: {
|
|
69
69
|
"@spec-engine/shared": "workspace:*",
|
|
@@ -91,6 +91,8 @@ var init_diagnostics = __esm(() => {
|
|
|
91
91
|
DRIFT: "DRIFT",
|
|
92
92
|
NO_SPEC_CONFIG: "NO_SPEC_CONFIG",
|
|
93
93
|
BROKEN_FILE_REF: "BROKEN_FILE_REF",
|
|
94
|
+
UNIMPLEMENTED_IN_FILE: "UNIMPLEMENTED_IN_FILE",
|
|
95
|
+
UNVERIFIED_IN_FILE: "UNVERIFIED_IN_FILE",
|
|
94
96
|
BROKEN_RELATES: "BROKEN_RELATES",
|
|
95
97
|
RELATES_SUPERSEDED: "RELATES_SUPERSEDED",
|
|
96
98
|
CYCLIC_SUPERSEDE: "CYCLIC_SUPERSEDE",
|
|
@@ -213,6 +215,7 @@ function orderDomain(d) {
|
|
|
213
215
|
supersededBy: r.supersededBy ?? null,
|
|
214
216
|
relates: r.relates,
|
|
215
217
|
livesIn: r.livesIn,
|
|
218
|
+
verifiedIn: r.verifiedIn,
|
|
216
219
|
issues: r.issues.map((iss) => ({ role: iss.role, id: iss.id })),
|
|
217
220
|
changedAtVersion: r.changedAtVersion,
|
|
218
221
|
supersededAtVersion: r.supersededAtVersion,
|
|
@@ -351,6 +354,7 @@ var init_domain = __esm(() => {
|
|
|
351
354
|
supersededBy: z.string().nullable().optional(),
|
|
352
355
|
relates: z.array(z.string()).optional().default([]),
|
|
353
356
|
livesIn: z.array(z.string()).optional().default([]),
|
|
357
|
+
verifiedIn: z.array(z.string()).optional().default([]),
|
|
354
358
|
issues: z.array(SpecIssueSchema).optional().default([]),
|
|
355
359
|
changedAtVersion: z.number().int().optional(),
|
|
356
360
|
supersededAtVersion: z.number().int().optional(),
|
|
@@ -1106,6 +1110,12 @@ function isPathIgnored(path, extra = []) {
|
|
|
1106
1110
|
const anchored = `/${path}`;
|
|
1107
1111
|
return IGNORE_SUBSTR.some((t) => anchored.includes(`/${t}`)) || extra.some((t) => path.startsWith(t.endsWith("/") ? t : `${t}/`));
|
|
1108
1112
|
}
|
|
1113
|
+
function isHiddenEntry(name) {
|
|
1114
|
+
return name.startsWith(".") && name !== ".github";
|
|
1115
|
+
}
|
|
1116
|
+
function isHiddenPath(rel) {
|
|
1117
|
+
return rel.split("/").some(isHiddenEntry);
|
|
1118
|
+
}
|
|
1109
1119
|
async function walkInto(rootDir, relDir, keep, extraIgnore, out) {
|
|
1110
1120
|
const absDir = relDir ? join2(rootDir, relDir) : rootDir;
|
|
1111
1121
|
let entries;
|
|
@@ -1115,7 +1125,7 @@ async function walkInto(rootDir, relDir, keep, extraIgnore, out) {
|
|
|
1115
1125
|
return;
|
|
1116
1126
|
}
|
|
1117
1127
|
for (const ent of entries) {
|
|
1118
|
-
if (ent.name
|
|
1128
|
+
if (isHiddenEntry(ent.name))
|
|
1119
1129
|
continue;
|
|
1120
1130
|
const rel = relDir ? `${relDir}/${ent.name}` : ent.name;
|
|
1121
1131
|
if (ent.isDirectory()) {
|
|
@@ -1147,8 +1157,11 @@ async function findDomainJsonFiles(canonicalDir) {
|
|
|
1147
1157
|
async function findCodeFiles(repoDir, exts = DEFAULT_EXTS, extraIgnore = []) {
|
|
1148
1158
|
return walkFiles(repoDir, (rel) => isTagSource(rel, exts), extraIgnore);
|
|
1149
1159
|
}
|
|
1160
|
+
function isDocSource(rel) {
|
|
1161
|
+
return rel.endsWith(".md") && rel !== "SPEC.md" && !rel.endsWith("/SPEC.md");
|
|
1162
|
+
}
|
|
1150
1163
|
async function findDocFiles(repoDir, extraIgnore = []) {
|
|
1151
|
-
return walkFiles(repoDir,
|
|
1164
|
+
return walkFiles(repoDir, isDocSource, extraIgnore);
|
|
1152
1165
|
}
|
|
1153
1166
|
var IGNORE_SUBSTR, DEFAULT_EXTS;
|
|
1154
1167
|
var init_fs = __esm(() => {
|
|
@@ -1179,7 +1192,10 @@ var init_fs = __esm(() => {
|
|
|
1179
1192
|
"tf",
|
|
1180
1193
|
"toml",
|
|
1181
1194
|
"sql",
|
|
1182
|
-
"css"
|
|
1195
|
+
"css",
|
|
1196
|
+
"yml",
|
|
1197
|
+
"yaml",
|
|
1198
|
+
"astro"
|
|
1183
1199
|
];
|
|
1184
1200
|
});
|
|
1185
1201
|
|
|
@@ -1405,8 +1421,9 @@ function toRepos(columns) {
|
|
|
1405
1421
|
return columns.map((c) => ({ ...c.repo, dependency_depth: depth.get(c.repo.name) ?? 0 })).sort(byName);
|
|
1406
1422
|
}
|
|
1407
1423
|
function selfMember(absPlatform, platformVersion) {
|
|
1424
|
+
const dir = basename(absPlatform);
|
|
1408
1425
|
return {
|
|
1409
|
-
name:
|
|
1426
|
+
name: dir === CANONICAL_SPECS_DIR ? "root" : dir,
|
|
1410
1427
|
path: absPlatform,
|
|
1411
1428
|
pinned_spec_version: platformVersion,
|
|
1412
1429
|
dependency_depth: 0,
|
|
@@ -1446,7 +1463,7 @@ function packageDirs(repo) {
|
|
|
1446
1463
|
function loneRootColumn(absPlatform, repo, platformVersion) {
|
|
1447
1464
|
const dirs = packageDirs(repo);
|
|
1448
1465
|
const { dependency_depth: _depth, ...self } = selfMember(absPlatform, platformVersion);
|
|
1449
|
-
if (dirs === null
|
|
1466
|
+
if (dirs === null)
|
|
1450
1467
|
return [];
|
|
1451
1468
|
return [{ repo: { ...self, ignore: dirs }, node: repo }];
|
|
1452
1469
|
}
|
|
@@ -1525,14 +1542,15 @@ async function membersOf(reading, absPlatform, platformVersion) {
|
|
|
1525
1542
|
const undeclared = mapped.diagnostics.filter((d) => d.code === "UNLISTED_REPO").map((d) => ({ name: d.subject, path: join3(absPlatform, d.subject) }));
|
|
1526
1543
|
return { mode: "multi-repo", members: toRepos(columns), unpinned, undeclared, diagnostics: diagnostics2 };
|
|
1527
1544
|
}
|
|
1528
|
-
async function discoverRepos(platformDir) {
|
|
1545
|
+
async function discoverRepos(platformDir, opts = {}) {
|
|
1529
1546
|
const absPlatform = resolve2(platformDir);
|
|
1530
1547
|
const canonicalPath = join3(absPlatform, CANONICAL_SPECS_DIR);
|
|
1531
1548
|
if (!isExistingDir(canonicalPath)) {
|
|
1532
1549
|
throw new NotASpecPlatformError(absPlatform);
|
|
1533
1550
|
}
|
|
1534
1551
|
const platformVersion = await derivePlatformVersion(absPlatform);
|
|
1535
|
-
|
|
1552
|
+
if (!opts.quiet)
|
|
1553
|
+
warnIfRetiredManifest(absPlatform, platformVersion);
|
|
1536
1554
|
const canonical = {
|
|
1537
1555
|
name: CANONICAL_SPECS_DIR,
|
|
1538
1556
|
path: canonicalPath,
|
|
@@ -1862,7 +1880,14 @@ ORDER BY tc.source_file, tc.line, tc.req_id
|
|
|
1862
1880
|
`;
|
|
1863
1881
|
|
|
1864
1882
|
// src/storage/sql/fts.ts
|
|
1865
|
-
|
|
1883
|
+
function quoteBareWords(text) {
|
|
1884
|
+
return text.split(/\s+/).map((token) => token === "" || FTS_OPERATORS.has(token) || FTS_SYNTAX.test(token) || /^[\p{L}\p{N}_]+$/u.test(token) ? token : `"${token}"`).join(" ");
|
|
1885
|
+
}
|
|
1886
|
+
var FTS_OPERATORS, FTS_SYNTAX, FTS_SEARCH_SQL = `SELECT r.id AS req_id, r.key AS key, r.text AS text, r.why AS why, r.source_file AS source_file, r.line AS line, bm25(requirements_fts, 1.0, 0.5) AS rank FROM requirements_fts JOIN requirements r ON r.rowid = requirements_fts.rowid WHERE requirements_fts MATCH $query AND r.status NOT IN ('Superseded', 'Deprecated') ORDER BY rank ASC LIMIT $limit`, RESOLVE_BY_FILES_SQL_PREFIX = "SELECT DISTINCT r.id, r.key, r.seq, r.status, r.superseded_by, r.text, r.why, r.source_file, r.line, r.spec_version, r.changed_at_version, r.superseded_at_version FROM tags t JOIN requirements r ON r.id = t.req_id WHERE t.file IN (", RESOLVE_BY_FILES_SQL_SUFFIX = ") ORDER BY r.key, r.seq";
|
|
1887
|
+
var init_fts = __esm(() => {
|
|
1888
|
+
FTS_OPERATORS = new Set(["AND", "OR", "NOT", "NEAR"]);
|
|
1889
|
+
FTS_SYNTAX = /["()*:^]/;
|
|
1890
|
+
});
|
|
1866
1891
|
|
|
1867
1892
|
// src/storage/sql/propagation.ts
|
|
1868
1893
|
var PROP_REPO_STATES_SQL = `
|
|
@@ -2139,7 +2164,7 @@ class SqliteStorage {
|
|
|
2139
2164
|
}
|
|
2140
2165
|
searchFts(text, limit = 10) {
|
|
2141
2166
|
try {
|
|
2142
|
-
return this.#db.query(FTS_SEARCH_SQL).all({ query: text, limit });
|
|
2167
|
+
return this.#db.query(FTS_SEARCH_SQL).all({ query: quoteBareWords(text), limit });
|
|
2143
2168
|
} catch (e) {
|
|
2144
2169
|
const msg = e instanceof Error ? e.message : String(e);
|
|
2145
2170
|
if (/fts5:|syntax error/i.test(msg)) {
|
|
@@ -2433,6 +2458,7 @@ var BUSY_TIMEOUT_MS = 2000;
|
|
|
2433
2458
|
var init_sqlite = __esm(() => {
|
|
2434
2459
|
init_src();
|
|
2435
2460
|
init_constants();
|
|
2461
|
+
init_fts();
|
|
2436
2462
|
init_reads();
|
|
2437
2463
|
});
|
|
2438
2464
|
|
|
@@ -2599,7 +2625,7 @@ var init_diagnostics2 = __esm(() => {
|
|
|
2599
2625
|
});
|
|
2600
2626
|
|
|
2601
2627
|
// src/indexer/pipeline.ts
|
|
2602
|
-
import { basename as basename2, dirname as dirname3, join as join4, resolve as resolve3 } from "path";
|
|
2628
|
+
import { basename as basename2, dirname as dirname3, isAbsolute, join as join4, relative, resolve as resolve3, sep as sep2 } from "path";
|
|
2603
2629
|
async function runIndex(opts) {
|
|
2604
2630
|
const platformDir = resolve3(opts.platformDir);
|
|
2605
2631
|
const {
|
|
@@ -2659,6 +2685,25 @@ function repoExtraIgnore(member) {
|
|
|
2659
2685
|
const repoIgnore = (member.ignore ?? []).map((e) => e.endsWith("/") ? e : `${e}/`);
|
|
2660
2686
|
return [...member.selfMember ? ["spec-engine/"] : [], ...repoIgnore];
|
|
2661
2687
|
}
|
|
2688
|
+
function scannedTagFile(members, platformDir, platformRel) {
|
|
2689
|
+
const abs = resolve3(platformDir, platformRel);
|
|
2690
|
+
let owner = null;
|
|
2691
|
+
for (const member2 of members) {
|
|
2692
|
+
const rel2 = relative(member2.path, abs).split(sep2).join("/");
|
|
2693
|
+
if (rel2 === "" || rel2 === ".." || rel2.startsWith("../") || isAbsolute(rel2))
|
|
2694
|
+
continue;
|
|
2695
|
+
if (owner === null || member2.path.length > owner.member.path.length)
|
|
2696
|
+
owner = { member: member2, rel: rel2 };
|
|
2697
|
+
}
|
|
2698
|
+
if (owner === null)
|
|
2699
|
+
return null;
|
|
2700
|
+
const { member, rel } = owner;
|
|
2701
|
+
if (isHiddenPath(rel) || isPathIgnored(rel, repoExtraIgnore(member)))
|
|
2702
|
+
return null;
|
|
2703
|
+
if (!isTagSource(rel) && !isDocSource(rel))
|
|
2704
|
+
return null;
|
|
2705
|
+
return `${member.name}/${rel}`;
|
|
2706
|
+
}
|
|
2662
2707
|
async function scanMemberCode(member, extraIgnore) {
|
|
2663
2708
|
const hitsOut = [];
|
|
2664
2709
|
const codePaths = await findCodeFiles(member.path, DEFAULT_EXTS, extraIgnore);
|
|
@@ -2892,7 +2937,7 @@ var init__result = __esm(() => {
|
|
|
2892
2937
|
|
|
2893
2938
|
// src/operations/init.ts
|
|
2894
2939
|
import { existsSync as existsSync3, realpathSync, statSync as statSync2 } from "fs";
|
|
2895
|
-
import { basename as basename3, join as join5, relative, resolve as resolve4, sep as
|
|
2940
|
+
import { basename as basename3, join as join5, relative as relative2, resolve as resolve4, sep as sep3 } from "path";
|
|
2896
2941
|
import {
|
|
2897
2942
|
applyInit,
|
|
2898
2943
|
applyLink,
|
|
@@ -2905,7 +2950,7 @@ function errMessage(err) {
|
|
|
2905
2950
|
return err instanceof Error ? err.message : String(err);
|
|
2906
2951
|
}
|
|
2907
2952
|
function hasSpecEngineSegment(path) {
|
|
2908
|
-
return path.split(
|
|
2953
|
+
return path.split(sep3).filter((s) => s.length > 0).includes(CANONICAL_SPECS_DIR);
|
|
2909
2954
|
}
|
|
2910
2955
|
function canonicalize(repoDir) {
|
|
2911
2956
|
let repoStat;
|
|
@@ -2926,7 +2971,7 @@ function canonicalize(repoDir) {
|
|
|
2926
2971
|
function judgeSpecTree(repoDir, canonical, root) {
|
|
2927
2972
|
if (!hasSpecEngineSegment(canonical))
|
|
2928
2973
|
return { ok: true };
|
|
2929
|
-
const judged = root !== null && isEnginePlatform(root, ENGINE_PACKAGE_NAME) ?
|
|
2974
|
+
const judged = root !== null && isEnginePlatform(root, ENGINE_PACKAGE_NAME) ? relative2(root, canonical) : canonical;
|
|
2930
2975
|
if (hasSpecEngineSegment(judged)) {
|
|
2931
2976
|
return fail("usage", `${repoDir} resolves to ${canonical}, which is inside a spec-engine/ tree \u2014 refusing to scaffold there.`);
|
|
2932
2977
|
}
|
|
@@ -3567,7 +3612,7 @@ function listFlag(rawArgs, name, parsed) {
|
|
|
3567
3612
|
}
|
|
3568
3613
|
return raw.flatMap((v) => v.split(",")).map((v) => v.trim()).filter((v) => v !== "");
|
|
3569
3614
|
}
|
|
3570
|
-
var platformDirArg, outArg, jsonArg, freshArg, noPromptArg, livesArg, relatesArg;
|
|
3615
|
+
var platformDirArg, outArg, jsonArg, freshArg, noPromptArg, livesArg, testsArg, relatesArg;
|
|
3571
3616
|
var init__args = __esm(() => {
|
|
3572
3617
|
init_constants();
|
|
3573
3618
|
platformDirArg = {
|
|
@@ -3592,6 +3637,10 @@ var init__args = __esm(() => {
|
|
|
3592
3637
|
type: "string",
|
|
3593
3638
|
description: "Lives in (livesIn) paths: repeat the flag or comma-separate; an empty value clears the list"
|
|
3594
3639
|
};
|
|
3640
|
+
testsArg = {
|
|
3641
|
+
type: "string",
|
|
3642
|
+
description: "Verified in (verifiedIn) test paths: repeat the flag or comma-separate; an empty value clears the list"
|
|
3643
|
+
};
|
|
3595
3644
|
relatesArg = {
|
|
3596
3645
|
type: "string",
|
|
3597
3646
|
description: "Related requirement ids (relates): repeat the flag or comma-separate; an empty value clears the list"
|
|
@@ -4049,67 +4098,117 @@ var init_filerefs = __esm(() => {
|
|
|
4049
4098
|
});
|
|
4050
4099
|
|
|
4051
4100
|
// src/check/filerefs.ts
|
|
4052
|
-
|
|
4053
|
-
const
|
|
4054
|
-
|
|
4055
|
-
try {
|
|
4056
|
-
raw = await Bun.file(abs).text();
|
|
4057
|
-
} catch {
|
|
4058
|
-
return null;
|
|
4059
|
-
}
|
|
4060
|
-
const parsed = parseDomainText(raw, rel);
|
|
4061
|
-
return parsed.ok ? { raw, requirements: parsed.data.requirements } : null;
|
|
4062
|
-
}
|
|
4063
|
-
function rowsForRequirement(platformDir, key, reqId, livesIn, line) {
|
|
4064
|
-
const rows = [];
|
|
4065
|
-
for (const entry of livesIn) {
|
|
4066
|
-
const ref = entry.startsWith("@") ? entry.slice(1) : entry;
|
|
4067
|
-
if (ref.length === 0 || resolveFileRef(platformDir, ref))
|
|
4068
|
-
continue;
|
|
4069
|
-
rows.push({
|
|
4070
|
-
code: DiagnosticCode.BROKEN_FILE_REF,
|
|
4071
|
-
severity: "error",
|
|
4072
|
-
repo: null,
|
|
4073
|
-
source_file: specPaths(platformDir, key).rel,
|
|
4074
|
-
line,
|
|
4075
|
-
req_id: reqId,
|
|
4076
|
-
detail: `livesIn entry ${entry} does not resolve to a file under the platform root`
|
|
4077
|
-
});
|
|
4078
|
-
}
|
|
4079
|
-
return rows;
|
|
4080
|
-
}
|
|
4081
|
-
function rowsForDomain(platformDir, key, domain2) {
|
|
4082
|
-
const rows = [];
|
|
4083
|
-
const rawLines = domain2.raw.split(`
|
|
4101
|
+
function locateInDomain(raw, sourceFile, requirements, statuses) {
|
|
4102
|
+
const out = [];
|
|
4103
|
+
const rawLines = raw.split(`
|
|
4084
4104
|
`);
|
|
4085
4105
|
let lineCursor = 0;
|
|
4086
|
-
for (const req of
|
|
4106
|
+
for (const req of requirements) {
|
|
4087
4107
|
const found = findRequirementIdLine(rawLines, req.id, lineCursor);
|
|
4088
4108
|
if (found >= 0)
|
|
4089
4109
|
lineCursor = found + 1;
|
|
4090
|
-
if (
|
|
4110
|
+
if (statuses.has(req.status.toLowerCase())) {
|
|
4111
|
+
out.push({ sourceFile, line: found >= 0 ? found + 1 : 0, req });
|
|
4112
|
+
}
|
|
4113
|
+
}
|
|
4114
|
+
return out;
|
|
4115
|
+
}
|
|
4116
|
+
async function locatedEntries(platformDir, statuses) {
|
|
4117
|
+
const out = [];
|
|
4118
|
+
for (const key of listDomainKeys(platformDir)) {
|
|
4119
|
+
const { abs, rel } = specPaths(platformDir, key);
|
|
4120
|
+
let raw;
|
|
4121
|
+
try {
|
|
4122
|
+
raw = await Bun.file(abs).text();
|
|
4123
|
+
} catch {
|
|
4091
4124
|
continue;
|
|
4092
|
-
|
|
4125
|
+
}
|
|
4126
|
+
const parsed = parseDomainText(raw, rel);
|
|
4127
|
+
if (parsed.ok)
|
|
4128
|
+
out.push(...locateInDomain(raw, rel, parsed.data.requirements, statuses));
|
|
4093
4129
|
}
|
|
4094
|
-
return
|
|
4130
|
+
return out;
|
|
4131
|
+
}
|
|
4132
|
+
function sortRows(rows) {
|
|
4133
|
+
return rows.sort((a, b) => (a.source_file ?? "").localeCompare(b.source_file ?? "") || (a.line ?? 0) - (b.line ?? 0) || (a.req_id ?? "").localeCompare(b.req_id ?? "") || a.code.localeCompare(b.code) || a.detail.localeCompare(b.detail));
|
|
4134
|
+
}
|
|
4135
|
+
function row(at, code, severity, detail) {
|
|
4136
|
+
return {
|
|
4137
|
+
code,
|
|
4138
|
+
severity,
|
|
4139
|
+
repo: null,
|
|
4140
|
+
source_file: at.sourceFile,
|
|
4141
|
+
line: at.line,
|
|
4142
|
+
req_id: at.req.id,
|
|
4143
|
+
detail
|
|
4144
|
+
};
|
|
4095
4145
|
}
|
|
4096
4146
|
async function brokenFileRefDiagnostics(platformDir) {
|
|
4097
4147
|
const rows = [];
|
|
4098
|
-
for (const
|
|
4099
|
-
const
|
|
4100
|
-
|
|
4101
|
-
|
|
4148
|
+
for (const at of await locatedEntries(platformDir, FILE_REF_STATUSES)) {
|
|
4149
|
+
for (const [field, entries] of [
|
|
4150
|
+
["livesIn", at.req.livesIn],
|
|
4151
|
+
["verifiedIn", at.req.verifiedIn]
|
|
4152
|
+
]) {
|
|
4153
|
+
for (const entry of entries) {
|
|
4154
|
+
const ref = refOf(entry);
|
|
4155
|
+
if (ref.length === 0 || resolveFileRef(platformDir, ref))
|
|
4156
|
+
continue;
|
|
4157
|
+
rows.push(row(at, DiagnosticCode.BROKEN_FILE_REF, "error", `${field} entry ${entry} does not resolve to a file under the platform root`));
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
}
|
|
4161
|
+
return sortRows(rows);
|
|
4162
|
+
}
|
|
4163
|
+
function tagKeys(tags, kinds) {
|
|
4164
|
+
return new Set(tags.filter((t) => kinds.includes(t.kind)).map((t) => `${t.req_id} ${t.file}`));
|
|
4165
|
+
}
|
|
4166
|
+
function inFileRows(platformDir, members, at, check) {
|
|
4167
|
+
const rows = [];
|
|
4168
|
+
for (const entry of at.req[check.field]) {
|
|
4169
|
+
const ref = refOf(entry);
|
|
4170
|
+
if (ref.length === 0 || !resolveFileRef(platformDir, ref))
|
|
4171
|
+
continue;
|
|
4172
|
+
const file = scannedTagFile(members, platformDir, ref);
|
|
4173
|
+
if (file === null || check.keys.has(`${at.req.id} ${file}`))
|
|
4174
|
+
continue;
|
|
4175
|
+
rows.push(row(at, check.code, "warning", `${check.field} entry ${entry} holds no ${check.missing} tag for ${at.req.id}`));
|
|
4102
4176
|
}
|
|
4103
|
-
rows.sort((a, b) => (a.source_file ?? "").localeCompare(b.source_file ?? "") || (a.line ?? 0) - (b.line ?? 0) || (a.req_id ?? "").localeCompare(b.req_id ?? "") || a.detail.localeCompare(b.detail));
|
|
4104
4177
|
return rows;
|
|
4105
4178
|
}
|
|
4106
|
-
|
|
4179
|
+
async function inFileTagDiagnostics(platformDir, tags) {
|
|
4180
|
+
const { members } = await discoverRepos(platformDir, { quiet: true });
|
|
4181
|
+
const checks = [
|
|
4182
|
+
{
|
|
4183
|
+
field: "livesIn",
|
|
4184
|
+
code: DiagnosticCode.UNIMPLEMENTED_IN_FILE,
|
|
4185
|
+
keys: tagKeys(tags, ["implements", "documents"]),
|
|
4186
|
+
missing: "implementing or documenting"
|
|
4187
|
+
},
|
|
4188
|
+
{
|
|
4189
|
+
field: "verifiedIn",
|
|
4190
|
+
code: DiagnosticCode.UNVERIFIED_IN_FILE,
|
|
4191
|
+
keys: tagKeys(tags, ["verifies"]),
|
|
4192
|
+
missing: "verifying"
|
|
4193
|
+
}
|
|
4194
|
+
];
|
|
4195
|
+
const rows = [];
|
|
4196
|
+
for (const at of await locatedEntries(platformDir, IN_FILE_STATUSES)) {
|
|
4197
|
+
for (const check of checks)
|
|
4198
|
+
rows.push(...inFileRows(platformDir, members, at, check));
|
|
4199
|
+
}
|
|
4200
|
+
return sortRows(rows);
|
|
4201
|
+
}
|
|
4202
|
+
var FILE_REF_STATUSES, IN_FILE_STATUSES, refOf = (entry) => entry.startsWith("@") ? entry.slice(1) : entry;
|
|
4107
4203
|
var init_filerefs2 = __esm(() => {
|
|
4108
4204
|
init_src();
|
|
4109
4205
|
init_domains();
|
|
4110
4206
|
init_filerefs();
|
|
4111
4207
|
init_constants();
|
|
4112
|
-
|
|
4208
|
+
init_discover();
|
|
4209
|
+
init_pipeline();
|
|
4210
|
+
FILE_REF_STATUSES = new Set(["active", "draft"]);
|
|
4211
|
+
IN_FILE_STATUSES = new Set(["active"]);
|
|
4113
4212
|
});
|
|
4114
4213
|
|
|
4115
4214
|
// src/check/grammar.ts
|
|
@@ -4130,7 +4229,7 @@ async function readDeclaredDomain(platformDir, key) {
|
|
|
4130
4229
|
requirements: parsed.data.requirements
|
|
4131
4230
|
};
|
|
4132
4231
|
}
|
|
4133
|
-
function
|
|
4232
|
+
function rowsForDomain(key, domain2) {
|
|
4134
4233
|
const rows = [];
|
|
4135
4234
|
const rawLines = domain2.raw.split(`
|
|
4136
4235
|
`);
|
|
@@ -4139,7 +4238,7 @@ function rowsForDomain2(key, domain2) {
|
|
|
4139
4238
|
const found = findRequirementIdLine(rawLines, req.id, lineCursor);
|
|
4140
4239
|
if (found >= 0)
|
|
4141
4240
|
lineCursor = found + 1;
|
|
4142
|
-
if (!
|
|
4241
|
+
if (!CHECKED_STATUSES.has(req.status.toLowerCase()))
|
|
4143
4242
|
continue;
|
|
4144
4243
|
const result = parseEarsStatement(req.statement);
|
|
4145
4244
|
if (result.ok)
|
|
@@ -4163,17 +4262,17 @@ async function statementGrammarDiagnostics(platformDir) {
|
|
|
4163
4262
|
continue;
|
|
4164
4263
|
const domain2 = await readDeclaredDomain(platformDir, key);
|
|
4165
4264
|
if (domain2 !== null)
|
|
4166
|
-
rows.push(...
|
|
4265
|
+
rows.push(...rowsForDomain(key, domain2));
|
|
4167
4266
|
}
|
|
4168
4267
|
rows.sort((a, b) => (a.source_file ?? "").localeCompare(b.source_file ?? "") || (a.line ?? 0) - (b.line ?? 0) || (a.req_id ?? "").localeCompare(b.req_id ?? ""));
|
|
4169
4268
|
return rows;
|
|
4170
4269
|
}
|
|
4171
|
-
var
|
|
4270
|
+
var CHECKED_STATUSES;
|
|
4172
4271
|
var init_grammar = __esm(() => {
|
|
4173
4272
|
init_src();
|
|
4174
4273
|
init_domains();
|
|
4175
4274
|
init_constants();
|
|
4176
|
-
|
|
4275
|
+
CHECKED_STATUSES = new Set(["active", "draft"]);
|
|
4177
4276
|
});
|
|
4178
4277
|
|
|
4179
4278
|
// src/check/propagation-teeth.ts
|
|
@@ -4250,12 +4349,12 @@ function readCheckConfig(platformDir) {
|
|
|
4250
4349
|
return { ok: true, config: { severity: [] } };
|
|
4251
4350
|
return parseCheckConfigText(readFileSync(path, "utf-8"), CHECK_CONFIG_REL);
|
|
4252
4351
|
}
|
|
4253
|
-
function specificity(rule,
|
|
4254
|
-
if (rule.code !==
|
|
4352
|
+
function specificity(rule, row2) {
|
|
4353
|
+
if (rule.code !== row2.code)
|
|
4255
4354
|
return Specificity.none;
|
|
4256
4355
|
if (rule.match === undefined)
|
|
4257
4356
|
return Specificity.code;
|
|
4258
|
-
const id =
|
|
4357
|
+
const id = row2.req_id;
|
|
4259
4358
|
if (id === null)
|
|
4260
4359
|
return Specificity.none;
|
|
4261
4360
|
if (rule.match.includes(id))
|
|
@@ -4265,20 +4364,20 @@ function specificity(rule, row) {
|
|
|
4265
4364
|
}
|
|
4266
4365
|
function applySeverity(rows, rules) {
|
|
4267
4366
|
const out = [];
|
|
4268
|
-
for (const
|
|
4367
|
+
for (const row2 of rows) {
|
|
4269
4368
|
let best;
|
|
4270
4369
|
let bestScore = Specificity.none;
|
|
4271
4370
|
for (const rule of rules) {
|
|
4272
|
-
const score = specificity(rule,
|
|
4371
|
+
const score = specificity(rule, row2);
|
|
4273
4372
|
if (score !== Specificity.none && score >= bestScore) {
|
|
4274
4373
|
best = rule;
|
|
4275
4374
|
bestScore = score;
|
|
4276
4375
|
}
|
|
4277
4376
|
}
|
|
4278
4377
|
if (best === undefined)
|
|
4279
|
-
out.push(
|
|
4378
|
+
out.push(row2);
|
|
4280
4379
|
else if (best.severity !== "off")
|
|
4281
|
-
out.push({ ...
|
|
4380
|
+
out.push({ ...row2, severity: best.severity });
|
|
4282
4381
|
}
|
|
4283
4382
|
return out;
|
|
4284
4383
|
}
|
|
@@ -4797,6 +4896,7 @@ function successorEntry(id, pred, fields) {
|
|
|
4797
4896
|
supersededBy: null,
|
|
4798
4897
|
relates: fields.relates,
|
|
4799
4898
|
livesIn: fields.livesIn,
|
|
4899
|
+
verifiedIn: fields.verifiedIn,
|
|
4800
4900
|
issues: fields.issue ? [{ role: "created", id: fields.issue }] : [],
|
|
4801
4901
|
aliases: [...pred.aliases],
|
|
4802
4902
|
cites: pred.cites.map((c) => ({ ...c }))
|
|
@@ -4884,7 +4984,8 @@ async function mint(input) {
|
|
|
4884
4984
|
const warnings = unresolvableRefWarnings(platformDir, [
|
|
4885
4985
|
input.statement,
|
|
4886
4986
|
input.why,
|
|
4887
|
-
...input.livesIn
|
|
4987
|
+
...input.livesIn,
|
|
4988
|
+
...input.verifiedIn ?? []
|
|
4888
4989
|
]);
|
|
4889
4990
|
const verdict = await judgeStatementGrammar(platformDir, key, input.statement);
|
|
4890
4991
|
if (!verdict.ok) {
|
|
@@ -4908,6 +5009,7 @@ async function mint(input) {
|
|
|
4908
5009
|
supersededBy: null,
|
|
4909
5010
|
relates: splitList(input.relates ?? []),
|
|
4910
5011
|
livesIn: input.livesIn,
|
|
5012
|
+
verifiedIn: input.verifiedIn ?? [],
|
|
4911
5013
|
issues: input.issue ? [{ role: "created", id: input.issue }] : [],
|
|
4912
5014
|
aliases: [],
|
|
4913
5015
|
cites: input.cites ?? []
|
|
@@ -4965,6 +5067,7 @@ async function mintTerm(input) {
|
|
|
4965
5067
|
supersededBy: null,
|
|
4966
5068
|
relates: [],
|
|
4967
5069
|
livesIn: [],
|
|
5070
|
+
verifiedIn: [],
|
|
4968
5071
|
issues: [],
|
|
4969
5072
|
aliases: input.aliases,
|
|
4970
5073
|
cites: [],
|
|
@@ -5151,6 +5254,7 @@ async function migrateGlossary(platformDir) {
|
|
|
5151
5254
|
supersededBy: null,
|
|
5152
5255
|
relates: [],
|
|
5153
5256
|
livesIn: [],
|
|
5257
|
+
verifiedIn: [],
|
|
5154
5258
|
issues: [],
|
|
5155
5259
|
aliases: [],
|
|
5156
5260
|
cites: [],
|
|
@@ -5508,6 +5612,7 @@ async function check(input, storage2) {
|
|
|
5508
5612
|
}
|
|
5509
5613
|
diagnostics2.push(...await statementGrammarDiagnostics(platformDir));
|
|
5510
5614
|
diagnostics2.push(...await brokenFileRefDiagnostics(platformDir));
|
|
5615
|
+
diagnostics2.push(...await inFileTagDiagnostics(platformDir, storage2.listTags()));
|
|
5511
5616
|
diagnostics2.push(...await supersedesPointerDiagnostics(platformDir));
|
|
5512
5617
|
const glossaryDrift = glossaryDriftDiagnostic(platformDir);
|
|
5513
5618
|
if (glossaryDrift !== null)
|
|
@@ -5935,12 +6040,12 @@ function sortMatrix(rows) {
|
|
|
5935
6040
|
return a.repo.localeCompare(b.repo);
|
|
5936
6041
|
});
|
|
5937
6042
|
}
|
|
5938
|
-
function cellStatus(
|
|
5939
|
-
if (
|
|
6043
|
+
function cellStatus(row2) {
|
|
6044
|
+
if (row2.implemented === 1 && row2.verified === 1)
|
|
5940
6045
|
return "src+test";
|
|
5941
|
-
if (
|
|
6046
|
+
if (row2.implemented === 1)
|
|
5942
6047
|
return "src";
|
|
5943
|
-
if (
|
|
6048
|
+
if (row2.verified === 1)
|
|
5944
6049
|
return "test";
|
|
5945
6050
|
return EMPTY_CELL;
|
|
5946
6051
|
}
|
|
@@ -5953,18 +6058,18 @@ function renderMatrix(rows, mode) {
|
|
|
5953
6058
|
return "";
|
|
5954
6059
|
const repos = Array.from(new Set(sorted.map((r) => r.repo))).sort((a, b) => a.localeCompare(b));
|
|
5955
6060
|
const byReq = new Map;
|
|
5956
|
-
for (const
|
|
5957
|
-
let entry = byReq.get(
|
|
6061
|
+
for (const row2 of sorted) {
|
|
6062
|
+
let entry = byReq.get(row2.req_id);
|
|
5958
6063
|
if (!entry) {
|
|
5959
6064
|
entry = {
|
|
5960
|
-
domain_key:
|
|
5961
|
-
req_id:
|
|
5962
|
-
req_status:
|
|
6065
|
+
domain_key: row2.domain_key,
|
|
6066
|
+
req_id: row2.req_id,
|
|
6067
|
+
req_status: row2.req_status,
|
|
5963
6068
|
cells: new Map
|
|
5964
6069
|
};
|
|
5965
|
-
byReq.set(
|
|
6070
|
+
byReq.set(row2.req_id, entry);
|
|
5966
6071
|
}
|
|
5967
|
-
entry.cells.set(
|
|
6072
|
+
entry.cells.set(row2.repo, cellStatus(row2));
|
|
5968
6073
|
}
|
|
5969
6074
|
const header = ["DOMAIN", "REQUIREMENT", "STATUS", ...repos];
|
|
5970
6075
|
const lines = [header];
|
|
@@ -5972,8 +6077,8 @@ function renderMatrix(rows, mode) {
|
|
|
5972
6077
|
const cells = repos.map((r) => entry.cells.get(r) ?? EMPTY_CELL);
|
|
5973
6078
|
lines.push([entry.domain_key, entry.req_id, entry.req_status, ...cells]);
|
|
5974
6079
|
}
|
|
5975
|
-
const widths = header.map((_, col) => Math.max(...lines.map((
|
|
5976
|
-
return lines.map((
|
|
6080
|
+
const widths = header.map((_, col) => Math.max(...lines.map((row2) => row2[col]?.length ?? 0)));
|
|
6081
|
+
return lines.map((row2) => row2.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ")).join(`
|
|
5977
6082
|
`);
|
|
5978
6083
|
}
|
|
5979
6084
|
var EMPTY_CELL = "\u2014";
|
|
@@ -6109,11 +6214,11 @@ var init_map = __esm(() => {
|
|
|
6109
6214
|
|
|
6110
6215
|
// src/operations/records.ts
|
|
6111
6216
|
function getRecord(storage2, id) {
|
|
6112
|
-
const
|
|
6217
|
+
const row2 = storage2.getRequirement(id);
|
|
6113
6218
|
return {
|
|
6114
6219
|
ok: true,
|
|
6115
|
-
row,
|
|
6116
|
-
platformEmpty:
|
|
6220
|
+
row: row2,
|
|
6221
|
+
platformEmpty: row2 === null && storage2.listRequirements().length === 0
|
|
6117
6222
|
};
|
|
6118
6223
|
}
|
|
6119
6224
|
function listRecords(storage2, filter = {}) {
|
|
@@ -6150,8 +6255,8 @@ function renderRecords(rows, mode) {
|
|
|
6150
6255
|
lines.push([r.id, r.status, r.key, String(r.seq), String(r.changed_at_version), r.text]);
|
|
6151
6256
|
}
|
|
6152
6257
|
const fixed = HEADER.length - 1;
|
|
6153
|
-
const widths = HEADER.slice(0, fixed).map((_, col) => Math.max(...lines.map((
|
|
6154
|
-
return lines.map((
|
|
6258
|
+
const widths = HEADER.slice(0, fixed).map((_, col) => Math.max(...lines.map((row2) => row2[col]?.length ?? 0)));
|
|
6259
|
+
return lines.map((row2) => row2.map((cell, i) => i < fixed ? cell.padEnd(widths[i] ?? 0) : cell).join(" ").trimEnd()).join(`
|
|
6155
6260
|
`);
|
|
6156
6261
|
}
|
|
6157
6262
|
var HEADER;
|
|
@@ -6303,11 +6408,11 @@ function renderPropagation(rows, mode) {
|
|
|
6303
6408
|
return "";
|
|
6304
6409
|
const header = ["REPO", "STATE", "VIA", "DRIFT?"];
|
|
6305
6410
|
const lines = [header];
|
|
6306
|
-
for (const
|
|
6307
|
-
lines.push([
|
|
6411
|
+
for (const row2 of rows) {
|
|
6412
|
+
lines.push([row2.repo, row2.state, row2.via_req_id ?? EMPTY_CELL2, row2.drifted ? "yes" : "no"]);
|
|
6308
6413
|
}
|
|
6309
|
-
const widths = header.map((_, col) => Math.max(...lines.map((
|
|
6310
|
-
return lines.map((
|
|
6414
|
+
const widths = header.map((_, col) => Math.max(...lines.map((row2) => row2[col]?.length ?? 0)));
|
|
6415
|
+
return lines.map((row2) => row2.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ")).join(`
|
|
6311
6416
|
`);
|
|
6312
6417
|
}
|
|
6313
6418
|
var EMPTY_CELL2 = "\u2014";
|
|
@@ -6381,8 +6486,8 @@ function excerptOf(raw) {
|
|
|
6381
6486
|
}
|
|
6382
6487
|
function renderTable(header, rows) {
|
|
6383
6488
|
const lines = [header, ...rows];
|
|
6384
|
-
const widths = header.map((_, col) => Math.max(...lines.map((
|
|
6385
|
-
return lines.map((
|
|
6489
|
+
const widths = header.map((_, col) => Math.max(...lines.map((row2) => row2[col]?.length ?? 0)));
|
|
6490
|
+
return lines.map((row2) => row2.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ")).join(`
|
|
6386
6491
|
`);
|
|
6387
6492
|
}
|
|
6388
6493
|
function renderQuery(hits, mode) {
|
|
@@ -6637,17 +6742,17 @@ function compareReqIds2(a, b) {
|
|
|
6637
6742
|
function sortProvenance(rows) {
|
|
6638
6743
|
return [...rows].sort((a, b) => compareReqIds2(a.req_id, b.req_id) || a.role.localeCompare(b.role) || a.issue_id.localeCompare(b.issue_id) || a.source_file.localeCompare(b.source_file) || a.line - b.line);
|
|
6639
6744
|
}
|
|
6640
|
-
function backingTests(
|
|
6745
|
+
function backingTests(row2) {
|
|
6641
6746
|
let base;
|
|
6642
|
-
if (
|
|
6747
|
+
if (row2.implemented === 1 && row2.verified === 1)
|
|
6643
6748
|
base = "src+test";
|
|
6644
|
-
else if (
|
|
6749
|
+
else if (row2.implemented === 1)
|
|
6645
6750
|
base = "src";
|
|
6646
|
-
else if (
|
|
6751
|
+
else if (row2.verified === 1)
|
|
6647
6752
|
base = "test";
|
|
6648
6753
|
else
|
|
6649
6754
|
base = "\u2014";
|
|
6650
|
-
return
|
|
6755
|
+
return row2.test_levels ? `${base} (${row2.test_levels})` : base;
|
|
6651
6756
|
}
|
|
6652
6757
|
function renderProvenance(rows, mode) {
|
|
6653
6758
|
const sorted = sortProvenance(rows);
|
|
@@ -6657,12 +6762,12 @@ function renderProvenance(rows, mode) {
|
|
|
6657
6762
|
if (sorted.length === 0)
|
|
6658
6763
|
return "";
|
|
6659
6764
|
const byReq = new Map;
|
|
6660
|
-
for (const
|
|
6661
|
-
const bucket = byReq.get(
|
|
6765
|
+
for (const row2 of sorted) {
|
|
6766
|
+
const bucket = byReq.get(row2.req_id);
|
|
6662
6767
|
if (bucket)
|
|
6663
|
-
bucket.push(
|
|
6768
|
+
bucket.push(row2);
|
|
6664
6769
|
else
|
|
6665
|
-
byReq.set(
|
|
6770
|
+
byReq.set(row2.req_id, [row2]);
|
|
6666
6771
|
}
|
|
6667
6772
|
const lines = [];
|
|
6668
6773
|
for (const [reqId, links] of byReq) {
|
|
@@ -6677,8 +6782,8 @@ function renderProvenance(rows, mode) {
|
|
|
6677
6782
|
return lines.join(`
|
|
6678
6783
|
`);
|
|
6679
6784
|
}
|
|
6680
|
-
function decorateRow(
|
|
6681
|
-
const prefix = ` ${
|
|
6785
|
+
function decorateRow(row2, resolved) {
|
|
6786
|
+
const prefix = ` ${row2.role} ${row2.issue_id} ${row2.source_file}:${row2.line}`;
|
|
6682
6787
|
if (resolved?.ok && resolved.title && resolved.url) {
|
|
6683
6788
|
const staleMark = resolved.stale ? " (cached \u2014 refresh failed)" : "";
|
|
6684
6789
|
return `${prefix} ${resolved.title} [${resolved.status ?? ""}] ${resolved.url}${staleMark}`;
|
|
@@ -6702,12 +6807,12 @@ function renderProvenanceDecorated(rows, resolved, mode) {
|
|
|
6702
6807
|
if (sorted.length === 0)
|
|
6703
6808
|
return "";
|
|
6704
6809
|
const byReq = new Map;
|
|
6705
|
-
for (const
|
|
6706
|
-
const bucket = byReq.get(
|
|
6810
|
+
for (const row2 of sorted) {
|
|
6811
|
+
const bucket = byReq.get(row2.req_id);
|
|
6707
6812
|
if (bucket)
|
|
6708
|
-
bucket.push(
|
|
6813
|
+
bucket.push(row2);
|
|
6709
6814
|
else
|
|
6710
|
-
byReq.set(
|
|
6815
|
+
byReq.set(row2.req_id, [row2]);
|
|
6711
6816
|
}
|
|
6712
6817
|
const lines = [];
|
|
6713
6818
|
for (const [reqId, links] of byReq) {
|
|
@@ -7074,6 +7179,7 @@ async function mintFromFlags(platformDir, key, textFlag, args, rawArgs) {
|
|
|
7074
7179
|
statement,
|
|
7075
7180
|
why: (args.why ?? "").trim(),
|
|
7076
7181
|
livesIn: listFlag(rawArgs, "lives", args.lives) ?? [],
|
|
7182
|
+
verifiedIn: listFlag(rawArgs, "tests", args.tests) ?? [],
|
|
7077
7183
|
relates: listFlag(rawArgs, "relates", args.relates) ?? [],
|
|
7078
7184
|
issue: (args.issue ?? "").trim() || undefined,
|
|
7079
7185
|
status: statusOf(args),
|
|
@@ -7153,6 +7259,7 @@ var init_req = __esm(() => {
|
|
|
7153
7259
|
},
|
|
7154
7260
|
why: { type: "string", description: "Why it matters field (only with --text)" },
|
|
7155
7261
|
lives: livesArg,
|
|
7262
|
+
tests: testsArg,
|
|
7156
7263
|
relates: relatesArg,
|
|
7157
7264
|
issue: {
|
|
7158
7265
|
type: "string",
|
|
@@ -7186,9 +7293,9 @@ var init_req = __esm(() => {
|
|
|
7186
7293
|
await mintFromFlags(platformDir, key, textFlag, args, rawArgs);
|
|
7187
7294
|
return;
|
|
7188
7295
|
}
|
|
7189
|
-
const hasFieldFlags = typeof args.why === "string" || typeof args.lives === "string" || typeof args.relates === "string" || typeof args.issue === "string";
|
|
7296
|
+
const hasFieldFlags = typeof args.why === "string" || typeof args.lives === "string" || typeof args.tests === "string" || typeof args.relates === "string" || typeof args.issue === "string";
|
|
7190
7297
|
if (hasFieldFlags) {
|
|
7191
|
-
console.error("spec req: --why/--lives/--relates/--issue require --text (the Requirement field is mandatory)");
|
|
7298
|
+
console.error("spec req: --why/--lives/--tests/--relates/--issue require --text (the Requirement field is mandatory)");
|
|
7192
7299
|
process.exit(EXIT.USAGE);
|
|
7193
7300
|
return;
|
|
7194
7301
|
}
|
|
@@ -7276,8 +7383,8 @@ async function listTermsCommand(platformDirRaw, json) {
|
|
|
7276
7383
|
console.log(JSON.stringify(result.rows));
|
|
7277
7384
|
return;
|
|
7278
7385
|
}
|
|
7279
|
-
for (const
|
|
7280
|
-
console.log(`${
|
|
7386
|
+
for (const row2 of result.rows) {
|
|
7387
|
+
console.log(`${row2.id} ${row2.term} ${row2.status}`);
|
|
7281
7388
|
}
|
|
7282
7389
|
}
|
|
7283
7390
|
async function reviseTermCommand(opts) {
|
|
@@ -7602,6 +7709,7 @@ function successorFields(input, req) {
|
|
|
7602
7709
|
return {
|
|
7603
7710
|
why: (input.why ?? req.why ?? "").trim(),
|
|
7604
7711
|
livesIn: input.livesIn ?? [...req.livesIn],
|
|
7712
|
+
verifiedIn: input.verifiedIn ?? [...req.verifiedIn],
|
|
7605
7713
|
relates: input.relates === undefined ? [...req.relates] : splitList(input.relates)
|
|
7606
7714
|
};
|
|
7607
7715
|
}
|
|
@@ -7646,14 +7754,15 @@ async function supersede(input, freshTags) {
|
|
|
7646
7754
|
}
|
|
7647
7755
|
warnings.push({ kind: "grammar", text: grammarWarningText("", verdict).slice(2) });
|
|
7648
7756
|
}
|
|
7649
|
-
const { why, livesIn, relates } = successorFields(input, req);
|
|
7650
|
-
warnings.push(...unresolvableRefWarnings(platformDir, [input.statement, why, ...livesIn]));
|
|
7757
|
+
const { why, livesIn, verifiedIn, relates } = successorFields(input, req);
|
|
7758
|
+
warnings.push(...unresolvableRefWarnings(platformDir, [input.statement, why, ...livesIn, ...verifiedIn]));
|
|
7651
7759
|
const newId = await nextRequirementId(platformDir, key);
|
|
7652
7760
|
const issue = input.issue?.trim() || undefined;
|
|
7653
7761
|
const successor = successorEntry(newId, req, {
|
|
7654
7762
|
statement: input.statement,
|
|
7655
7763
|
why,
|
|
7656
7764
|
livesIn,
|
|
7765
|
+
verifiedIn,
|
|
7657
7766
|
relates,
|
|
7658
7767
|
issue
|
|
7659
7768
|
});
|
|
@@ -7697,14 +7806,14 @@ function renderResolve(rows, mode) {
|
|
|
7697
7806
|
return "";
|
|
7698
7807
|
const header = ["REQ_ID", "STATUS", "TEXT"];
|
|
7699
7808
|
const lines = [header];
|
|
7700
|
-
for (const
|
|
7701
|
-
const raw =
|
|
7809
|
+
for (const row2 of sorted) {
|
|
7810
|
+
const raw = row2.text ?? "";
|
|
7702
7811
|
const truncated = raw.length > TEXT_MAX ? raw.slice(0, TEXT_MAX) + ELLIPSIS2 : raw;
|
|
7703
7812
|
const text = truncated.replace(/\s+/g, " ").trim();
|
|
7704
|
-
lines.push([
|
|
7813
|
+
lines.push([row2.id, row2.status, text || EMPTY_CELL4]);
|
|
7705
7814
|
}
|
|
7706
|
-
const widths = header.map((_, col) => Math.max(...lines.map((
|
|
7707
|
-
return lines.map((
|
|
7815
|
+
const widths = header.map((_, col) => Math.max(...lines.map((row2) => row2[col]?.length ?? 0)));
|
|
7816
|
+
return lines.map((row2) => row2.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ")).join(`
|
|
7708
7817
|
`);
|
|
7709
7818
|
}
|
|
7710
7819
|
function sortReqTags(rows) {
|
|
@@ -7719,8 +7828,8 @@ function renderReqTags(rows, mode) {
|
|
|
7719
7828
|
return "";
|
|
7720
7829
|
const header = ["REPO", "FILE", "LINE", "KIND", "LEVEL"];
|
|
7721
7830
|
const lines = [header];
|
|
7722
|
-
for (const
|
|
7723
|
-
lines.push([
|
|
7831
|
+
for (const row2 of sorted) {
|
|
7832
|
+
lines.push([row2.repo, row2.file, String(row2.line), row2.kind, row2.level ?? EMPTY_CELL4]);
|
|
7724
7833
|
}
|
|
7725
7834
|
const widths = header.map((_, col) => Math.max(...lines.map((l) => (l[col] ?? "").length)));
|
|
7726
7835
|
return lines.map((l) => l.map((cell, col) => cell.padEnd(widths[col])).join(" ").trimEnd()).join(`
|
|
@@ -7767,6 +7876,9 @@ function inputFromArgs(args, rawArgs, platformDir, id, statement) {
|
|
|
7767
7876
|
const livesIn = listFlag(rawArgs, "lives", args.lives);
|
|
7768
7877
|
if (livesIn !== undefined)
|
|
7769
7878
|
input.livesIn = livesIn;
|
|
7879
|
+
const verifiedIn = listFlag(rawArgs, "tests", args.tests);
|
|
7880
|
+
if (verifiedIn !== undefined)
|
|
7881
|
+
input.verifiedIn = verifiedIn;
|
|
7770
7882
|
const relates = listFlag(rawArgs, "relates", args.relates);
|
|
7771
7883
|
if (relates !== undefined)
|
|
7772
7884
|
input.relates = relates;
|
|
@@ -7807,6 +7919,10 @@ var init_supersede2 = __esm(() => {
|
|
|
7807
7919
|
type: "string",
|
|
7808
7920
|
description: "Successor's Lives in: repeat or comma-separate; empty clears (default: copied from the old entry)"
|
|
7809
7921
|
},
|
|
7922
|
+
tests: {
|
|
7923
|
+
type: "string",
|
|
7924
|
+
description: "Successor's Verified in (verifiedIn): repeat or comma-separate; empty clears (default: copied from the old entry)"
|
|
7925
|
+
},
|
|
7810
7926
|
relates: {
|
|
7811
7927
|
type: "string",
|
|
7812
7928
|
description: "Successor's related ids: repeat or comma-separate; empty clears (default: copied from the old entry)"
|
|
@@ -8000,6 +8116,7 @@ function successorFields2(input, req) {
|
|
|
8000
8116
|
statement: (input.statement ?? req.statement).trim(),
|
|
8001
8117
|
why: (input.why ?? req.why ?? "").trim(),
|
|
8002
8118
|
livesIn: input.livesIn ?? [...req.livesIn],
|
|
8119
|
+
verifiedIn: input.verifiedIn ?? [...req.verifiedIn],
|
|
8003
8120
|
relates: [...req.relates]
|
|
8004
8121
|
};
|
|
8005
8122
|
}
|
|
@@ -8044,7 +8161,12 @@ async function move(input, freshTags) {
|
|
|
8044
8161
|
warnings.push({ kind: "grammar", text: grammarWarningText("", verdict).slice(2) });
|
|
8045
8162
|
}
|
|
8046
8163
|
}
|
|
8047
|
-
warnings.push(...unresolvableRefWarnings(platformDir, [
|
|
8164
|
+
warnings.push(...unresolvableRefWarnings(platformDir, [
|
|
8165
|
+
fields.statement,
|
|
8166
|
+
fields.why,
|
|
8167
|
+
...fields.livesIn,
|
|
8168
|
+
...fields.verifiedIn
|
|
8169
|
+
]));
|
|
8048
8170
|
const newId = await nextRequirementId(platformDir, targetKey);
|
|
8049
8171
|
const sourceCurrent = source.domain.specVersion ?? 1;
|
|
8050
8172
|
req.status = "superseded";
|
|
@@ -8103,6 +8225,9 @@ function inputFromArgs2(args, rawArgs, platformDir, id, targetKey) {
|
|
|
8103
8225
|
const livesIn = listFlag(rawArgs, "lives", args.lives);
|
|
8104
8226
|
if (livesIn !== undefined)
|
|
8105
8227
|
input.livesIn = livesIn;
|
|
8228
|
+
const verifiedIn = listFlag(rawArgs, "tests", args.tests);
|
|
8229
|
+
if (verifiedIn !== undefined)
|
|
8230
|
+
input.verifiedIn = verifiedIn;
|
|
8106
8231
|
return input;
|
|
8107
8232
|
}
|
|
8108
8233
|
var moveArgs, moveCommand;
|
|
@@ -8138,6 +8263,10 @@ var init_move2 = __esm(() => {
|
|
|
8138
8263
|
type: "string",
|
|
8139
8264
|
description: "Rewrite the successor's Lives in: repeat or comma-separate; empty clears (default: copied from the source)"
|
|
8140
8265
|
},
|
|
8266
|
+
tests: {
|
|
8267
|
+
type: "string",
|
|
8268
|
+
description: "Rewrite the successor's Verified in (verifiedIn): repeat or comma-separate; empty clears (default: copied from the source)"
|
|
8269
|
+
},
|
|
8141
8270
|
noBump: {
|
|
8142
8271
|
type: "boolean",
|
|
8143
8272
|
description: "Do not bump either envelope's specVersion"
|
|
@@ -8228,7 +8357,7 @@ function gateRejection(platformDir, relFile, req, fields) {
|
|
|
8228
8357
|
const tag = releaseHolding(platformDir, relFile, id);
|
|
8229
8358
|
if (tag === null)
|
|
8230
8359
|
return null;
|
|
8231
|
-
return fail("conflict", `${id} is released in ${tag} \u2014 its promise is immutable. ` + `Run \`spec supersede ${id}\` to change the statement or why; ` + "pass only --lives or --relates to amend its metadata in place.");
|
|
8360
|
+
return fail("conflict", `${id} is released in ${tag} \u2014 its promise is immutable. ` + `Run \`spec supersede ${id}\` to change the statement or why; ` + "pass only --lives, --tests, or --relates to amend its metadata in place.");
|
|
8232
8361
|
}
|
|
8233
8362
|
function applyFields(req, fields) {
|
|
8234
8363
|
const fieldsChanged = [];
|
|
@@ -8248,6 +8377,11 @@ function applyFields(req, fields) {
|
|
|
8248
8377
|
fieldsChanged.push("lives");
|
|
8249
8378
|
refValues.push(...fields.livesIn);
|
|
8250
8379
|
}
|
|
8380
|
+
if (fields.verifiedIn !== undefined) {
|
|
8381
|
+
req.verifiedIn = fields.verifiedIn;
|
|
8382
|
+
fieldsChanged.push("tests");
|
|
8383
|
+
refValues.push(...fields.verifiedIn);
|
|
8384
|
+
}
|
|
8251
8385
|
if (fields.relates !== undefined) {
|
|
8252
8386
|
req.relates = splitList(fields.relates);
|
|
8253
8387
|
fieldsChanged.push("relates");
|
|
@@ -8317,9 +8451,9 @@ __export(exports_amend, {
|
|
|
8317
8451
|
import { defineCommand as defineCommand18 } from "citty";
|
|
8318
8452
|
function assertAmendFlags(args) {
|
|
8319
8453
|
const hasIssue = args.issue !== undefined && args.issue.trim() !== "";
|
|
8320
|
-
const hasField = args.text !== undefined || args.why !== undefined || args.lives !== undefined || args.relates !== undefined || args.term !== undefined || args.aliases !== undefined;
|
|
8454
|
+
const hasField = args.text !== undefined || args.why !== undefined || args.lives !== undefined || args.tests !== undefined || args.relates !== undefined || args.term !== undefined || args.aliases !== undefined;
|
|
8321
8455
|
if (!hasField && !hasIssue) {
|
|
8322
|
-
console.error("spec amend: nothing to amend \u2014 pass at least one of --text / --why / --lives / --relates / --term / --aliases");
|
|
8456
|
+
console.error("spec amend: nothing to amend \u2014 pass at least one of --text / --why / --lives / --tests / --relates / --term / --aliases");
|
|
8323
8457
|
process.exit(EXIT.USAGE);
|
|
8324
8458
|
}
|
|
8325
8459
|
if (args.text !== undefined && args.text.trim() === "") {
|
|
@@ -8346,6 +8480,9 @@ function fieldsFromArgs(args, rawArgs) {
|
|
|
8346
8480
|
const livesIn = listFlag(rawArgs, "lives", args.lives);
|
|
8347
8481
|
if (livesIn !== undefined)
|
|
8348
8482
|
fields.livesIn = livesIn;
|
|
8483
|
+
const verifiedIn = listFlag(rawArgs, "tests", args.tests);
|
|
8484
|
+
if (verifiedIn !== undefined)
|
|
8485
|
+
fields.verifiedIn = verifiedIn;
|
|
8349
8486
|
const relates = listFlag(rawArgs, "relates", args.relates);
|
|
8350
8487
|
if (relates !== undefined)
|
|
8351
8488
|
fields.relates = relates;
|
|
@@ -8375,6 +8512,7 @@ var init_amend2 = __esm(() => {
|
|
|
8375
8512
|
text: { type: "string", description: "New Requirement (statement) field value" },
|
|
8376
8513
|
why: { type: "string", description: "New Why it matters field value" },
|
|
8377
8514
|
lives: livesArg,
|
|
8515
|
+
tests: testsArg,
|
|
8378
8516
|
relates: relatesArg,
|
|
8379
8517
|
issue: {
|
|
8380
8518
|
type: "string",
|
|
@@ -8390,7 +8528,7 @@ var init_amend2 = __esm(() => {
|
|
|
8390
8528
|
amendCommand = defineCommand18({
|
|
8391
8529
|
meta: {
|
|
8392
8530
|
name: "amend",
|
|
8393
|
-
description: "Revise a requirement's fields in place (same id, no version bump). A released requirement's statement and why are superseded instead; --lives and --relates still amend."
|
|
8531
|
+
description: "Revise a requirement's fields in place (same id, no version bump). A released requirement's statement and why are superseded instead; --lives, --tests, and --relates still amend."
|
|
8394
8532
|
},
|
|
8395
8533
|
args: amendArgs,
|
|
8396
8534
|
async run({ args, rawArgs }) {
|
|
@@ -8544,6 +8682,8 @@ function amendFieldsOf(args) {
|
|
|
8544
8682
|
fields.why = args.why || null;
|
|
8545
8683
|
if (args.lives !== undefined)
|
|
8546
8684
|
fields.livesIn = args.lives;
|
|
8685
|
+
if (args.verified_in !== undefined)
|
|
8686
|
+
fields.verifiedIn = args.verified_in;
|
|
8547
8687
|
if (args.relates !== undefined)
|
|
8548
8688
|
fields.relates = args.relates;
|
|
8549
8689
|
if (args.issue)
|
|
@@ -8621,8 +8761,8 @@ function buildMcpServer(platformDir) {
|
|
|
8621
8761
|
return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
|
|
8622
8762
|
}
|
|
8623
8763
|
return fresh((s) => {
|
|
8624
|
-
const { row } = getRecord(s, req_id);
|
|
8625
|
-
return jsonResult(
|
|
8764
|
+
const { row: row2 } = getRecord(s, req_id);
|
|
8765
|
+
return jsonResult(row2 ?? []);
|
|
8626
8766
|
});
|
|
8627
8767
|
});
|
|
8628
8768
|
server.registerTool("spec_list", {
|
|
@@ -8653,14 +8793,24 @@ function buildMcpServer(platformDir) {
|
|
|
8653
8793
|
statement: z4.string().min(1).describe("The successor's statement"),
|
|
8654
8794
|
why: z4.string().optional().describe("Successor's why (default: copied from the predecessor)"),
|
|
8655
8795
|
lives_in: z4.array(z4.string()).optional().describe("Successor's livesIn paths (default: copied from the predecessor)"),
|
|
8796
|
+
verified_in: z4.array(z4.string()).optional().describe("Successor's verifiedIn paths (default: copied from the predecessor)"),
|
|
8656
8797
|
relates: z4.array(z4.string()).optional().describe("Successor's related ids, each comma-split; an empty list clears (default: copied from the predecessor)"),
|
|
8657
8798
|
issue: z4.string().optional().describe("Ticket recorded as supersedes-via on the predecessor and created on the successor")
|
|
8658
8799
|
}
|
|
8659
|
-
}, async ({ req_id, statement, why, lives_in, relates, issue }) => {
|
|
8800
|
+
}, async ({ req_id, statement, why, lives_in, verified_in, relates, issue }) => {
|
|
8660
8801
|
if (!ID_RE.test(req_id)) {
|
|
8661
8802
|
return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
|
|
8662
8803
|
}
|
|
8663
|
-
const r = await supersede({
|
|
8804
|
+
const r = await supersede({
|
|
8805
|
+
platformDir,
|
|
8806
|
+
id: req_id,
|
|
8807
|
+
statement,
|
|
8808
|
+
why,
|
|
8809
|
+
livesIn: lives_in,
|
|
8810
|
+
verifiedIn: verified_in,
|
|
8811
|
+
relates,
|
|
8812
|
+
issue
|
|
8813
|
+
}, coldFreshTags(platformDir));
|
|
8664
8814
|
if (!r.ok)
|
|
8665
8815
|
return errorResult(r.detail);
|
|
8666
8816
|
for (const w of r.warnings)
|
|
@@ -8711,11 +8861,12 @@ function buildMcpServer(platformDir) {
|
|
|
8711
8861
|
text: z4.string().trim().min(1).describe("The requirement statement"),
|
|
8712
8862
|
why: z4.string().trim().default("").describe("What breaks without it"),
|
|
8713
8863
|
lives: z4.array(z4.string()).default([]).describe("livesIn paths"),
|
|
8864
|
+
verified_in: z4.array(z4.string()).default([]).describe("verifiedIn test paths"),
|
|
8714
8865
|
relates: z4.array(z4.string()).default([]).describe("Related requirement ids, each comma-split"),
|
|
8715
8866
|
issue: z4.string().trim().optional().describe("Originating ticket, recorded as created"),
|
|
8716
8867
|
draft: z4.boolean().default(false).describe("Author as Draft instead of Active")
|
|
8717
8868
|
}
|
|
8718
|
-
}, async ({ domain: domain2, text, why, lives, relates, issue, draft }) => {
|
|
8869
|
+
}, async ({ domain: domain2, text, why, lives, verified_in, relates, issue, draft }) => {
|
|
8719
8870
|
const allocated = await nextId(platformDir, domain2);
|
|
8720
8871
|
if (!allocated.ok)
|
|
8721
8872
|
return errorResult(allocated.detail);
|
|
@@ -8725,6 +8876,7 @@ function buildMcpServer(platformDir) {
|
|
|
8725
8876
|
statement: text,
|
|
8726
8877
|
why,
|
|
8727
8878
|
livesIn: lives,
|
|
8879
|
+
verifiedIn: verified_in,
|
|
8728
8880
|
relates,
|
|
8729
8881
|
issue: issue || undefined,
|
|
8730
8882
|
status: draft ? "draft" : "active"
|
|
@@ -8743,15 +8895,16 @@ function buildMcpServer(platformDir) {
|
|
|
8743
8895
|
text: z4.string().trim().min(1).optional().describe("New statement"),
|
|
8744
8896
|
why: z4.string().trim().optional().describe("New why; empty clears it"),
|
|
8745
8897
|
lives: z4.array(z4.string()).optional().describe("Replacement livesIn list; empty clears"),
|
|
8898
|
+
verified_in: z4.array(z4.string()).optional().describe("Replacement verifiedIn list; empty clears"),
|
|
8746
8899
|
relates: z4.array(z4.string()).optional().describe("Replacement related ids, each comma-split; empty clears"),
|
|
8747
8900
|
issue: z4.string().trim().optional().describe("Ticket recorded as amends-via")
|
|
8748
8901
|
}
|
|
8749
|
-
}, async ({ id, text, why, lives, relates, issue }) => {
|
|
8902
|
+
}, async ({ id, text, why, lives, verified_in, relates, issue }) => {
|
|
8750
8903
|
if (!ID_RE.test(id))
|
|
8751
8904
|
return errorResult(`id must be a requirement id (KEY-NNN); got ${id}`);
|
|
8752
|
-
const fields = amendFieldsOf({ text, why, lives, relates, issue });
|
|
8905
|
+
const fields = amendFieldsOf({ text, why, lives, verified_in, relates, issue });
|
|
8753
8906
|
if (Object.keys(fields).length === 0) {
|
|
8754
|
-
return errorResult("nothing to amend \u2014 pass at least one of text, why, lives, relates");
|
|
8907
|
+
return errorResult("nothing to amend \u2014 pass at least one of text, why, lives, verified_in, relates");
|
|
8755
8908
|
}
|
|
8756
8909
|
const r = await amend({ platformDir, id, fields });
|
|
8757
8910
|
if (!r.ok)
|
|
@@ -8768,15 +8921,24 @@ function buildMcpServer(platformDir) {
|
|
|
8768
8921
|
newDomain: z4.string().min(1).describe("The existing target domain key"),
|
|
8769
8922
|
text: z4.string().optional().describe("Successor statement (default: copied)"),
|
|
8770
8923
|
why: z4.string().optional().describe("Successor why (default: copied)"),
|
|
8771
|
-
lives: z4.array(z4.string()).optional().describe("Successor livesIn list; empty clears (default: copied)")
|
|
8924
|
+
lives: z4.array(z4.string()).optional().describe("Successor livesIn list; empty clears (default: copied)"),
|
|
8925
|
+
verified_in: z4.array(z4.string()).optional().describe("Successor verifiedIn list; empty clears (default: copied)")
|
|
8772
8926
|
}
|
|
8773
|
-
}, async ({ id, newDomain: newDomain2, text, why, lives }) => {
|
|
8927
|
+
}, async ({ id, newDomain: newDomain2, text, why, lives, verified_in }) => {
|
|
8774
8928
|
if (!ID_RE.test(id))
|
|
8775
8929
|
return errorResult(`id must be a requirement id (KEY-NNN); got ${id}`);
|
|
8776
8930
|
const targetKey = normalizeDomainKey(newDomain2);
|
|
8777
8931
|
if (targetKey === "")
|
|
8778
8932
|
return errorResult(`newDomain must be a domain key; got ${newDomain2}`);
|
|
8779
|
-
const r = await move({
|
|
8933
|
+
const r = await move({
|
|
8934
|
+
platformDir,
|
|
8935
|
+
id,
|
|
8936
|
+
targetKey,
|
|
8937
|
+
statement: text,
|
|
8938
|
+
why,
|
|
8939
|
+
livesIn: lives,
|
|
8940
|
+
verifiedIn: verified_in
|
|
8941
|
+
}, coldFreshTags(platformDir));
|
|
8780
8942
|
if (!r.ok)
|
|
8781
8943
|
return errorResult(r.detail);
|
|
8782
8944
|
for (const w of r.warnings)
|
|
@@ -8880,7 +9042,7 @@ __export(exports_resolve, {
|
|
|
8880
9042
|
resolveCommand: () => resolveCommand
|
|
8881
9043
|
});
|
|
8882
9044
|
import { statSync as statSync5 } from "fs";
|
|
8883
|
-
import { join as join14, relative as
|
|
9045
|
+
import { join as join14, relative as relative3, resolve as resolve13, sep as sep4 } from "path";
|
|
8884
9046
|
import { defineCommand as defineCommand21 } from "citty";
|
|
8885
9047
|
function classifyArg(a) {
|
|
8886
9048
|
if (a.startsWith("--")) {
|
|
@@ -9016,8 +9178,8 @@ async function dispatchReverseQuery(reqArg, resolved, args) {
|
|
|
9016
9178
|
function normalizeFileInputs(files, platformDir) {
|
|
9017
9179
|
const normalized = [];
|
|
9018
9180
|
for (const input of files) {
|
|
9019
|
-
const rel =
|
|
9020
|
-
const segments = rel.split(
|
|
9181
|
+
const rel = relative3(platformDir, resolve13(platformDir, input));
|
|
9182
|
+
const segments = rel.split(sep4);
|
|
9021
9183
|
if (rel === "" || rel === "." || segments[0] === "..") {
|
|
9022
9184
|
console.error(`spec resolve: file path must be inside platformDir: ${input}`);
|
|
9023
9185
|
process.exit(EXIT.USAGE);
|
|
@@ -11058,9 +11220,9 @@ function requirementStatusBadge(status) {
|
|
|
11058
11220
|
const variant = status === "Active" ? "active" : status === "Superseded" ? "superseded" : status === "Draft" ? "draft" : "retired";
|
|
11059
11221
|
return badge(variant, STATUS_LABEL[status]);
|
|
11060
11222
|
}
|
|
11061
|
-
function propagationStateBadge(
|
|
11223
|
+
function propagationStateBadge(row2) {
|
|
11062
11224
|
let variant;
|
|
11063
|
-
switch (
|
|
11225
|
+
switch (row2.state) {
|
|
11064
11226
|
case "MIGRATED_VERIFIED":
|
|
11065
11227
|
variant = "verified";
|
|
11066
11228
|
break;
|
|
@@ -11073,7 +11235,7 @@ function propagationStateBadge(row) {
|
|
|
11073
11235
|
default:
|
|
11074
11236
|
variant = "unbound";
|
|
11075
11237
|
}
|
|
11076
|
-
return badge(variant,
|
|
11238
|
+
return badge(variant, row2.state);
|
|
11077
11239
|
}
|
|
11078
11240
|
function coverageGlyph(impl, verif) {
|
|
11079
11241
|
const [variant, char, title] = impl && verif ? ["both", "\u2713", "src + test"] : impl ? ["impl", "S", "src only"] : verif ? ["test", "T", "test only"] : ["none", "", "none"];
|
|
@@ -11207,18 +11369,18 @@ import { html as html4, raw as raw3 } from "hono/html";
|
|
|
11207
11369
|
function pivot(rows) {
|
|
11208
11370
|
const repos = [...new Set(rows.map((r) => r.repo))].sort();
|
|
11209
11371
|
const byDomain = new Map;
|
|
11210
|
-
for (const
|
|
11211
|
-
let dom = byDomain.get(
|
|
11372
|
+
for (const row2 of rows) {
|
|
11373
|
+
let dom = byDomain.get(row2.domain_key);
|
|
11212
11374
|
if (!dom) {
|
|
11213
11375
|
dom = new Map;
|
|
11214
|
-
byDomain.set(
|
|
11376
|
+
byDomain.set(row2.domain_key, dom);
|
|
11215
11377
|
}
|
|
11216
|
-
let req = dom.get(
|
|
11378
|
+
let req = dom.get(row2.req_id);
|
|
11217
11379
|
if (!req) {
|
|
11218
|
-
req = { id:
|
|
11219
|
-
dom.set(
|
|
11380
|
+
req = { id: row2.req_id, status: row2.req_status, cells: new Map };
|
|
11381
|
+
dom.set(row2.req_id, req);
|
|
11220
11382
|
}
|
|
11221
|
-
req.cells.set(
|
|
11383
|
+
req.cells.set(row2.repo, { impl: row2.implemented, verif: row2.verified });
|
|
11222
11384
|
}
|
|
11223
11385
|
const domains = [...byDomain.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([key, reqMap]) => ({
|
|
11224
11386
|
key,
|
|
@@ -11469,6 +11631,10 @@ function mountEditor(app, bindHost = DEFAULT_BIND_HOST) {
|
|
|
11469
11631
|
Lives in (optional, single path)
|
|
11470
11632
|
<input type="text" name="livesIn" placeholder="path/to/impl.ts" />
|
|
11471
11633
|
</label>
|
|
11634
|
+
<label>
|
|
11635
|
+
Verified in (optional, single path)
|
|
11636
|
+
<input type="text" name="verifiedIn" placeholder="path/to/impl.test.ts" />
|
|
11637
|
+
</label>
|
|
11472
11638
|
<label>
|
|
11473
11639
|
Relates to (optional, comma-separated ids)
|
|
11474
11640
|
<input type="text" name="relates" placeholder="BILLING-001, BILLING-002" />
|
|
@@ -11507,6 +11673,7 @@ function mountEditor(app, bindHost = DEFAULT_BIND_HOST) {
|
|
|
11507
11673
|
const statement = field(form, "statement");
|
|
11508
11674
|
const why = field(form, "why");
|
|
11509
11675
|
const livesIn = field(form, "livesIn");
|
|
11676
|
+
const verifiedIn = field(form, "verifiedIn");
|
|
11510
11677
|
const relates = field(form, "relates");
|
|
11511
11678
|
const payload = {
|
|
11512
11679
|
key: field(form, "key"),
|
|
@@ -11516,6 +11683,8 @@ function mountEditor(app, bindHost = DEFAULT_BIND_HOST) {
|
|
|
11516
11683
|
payload.why = why;
|
|
11517
11684
|
if (livesIn.length > 0)
|
|
11518
11685
|
payload.livesIn = livesIn;
|
|
11686
|
+
if (verifiedIn.length > 0)
|
|
11687
|
+
payload.verifiedIn = verifiedIn;
|
|
11519
11688
|
if (relates.length > 0)
|
|
11520
11689
|
payload.relates = relates;
|
|
11521
11690
|
const res = await app.request("/api/requirements", {
|
|
@@ -11832,15 +12001,15 @@ var init_relations2 = __esm(() => {
|
|
|
11832
12001
|
import { html as html10, raw as raw9 } from "hono/html";
|
|
11833
12002
|
function coverageRollups(coverage) {
|
|
11834
12003
|
const cov = new Map;
|
|
11835
|
-
for (const
|
|
11836
|
-
if (
|
|
12004
|
+
for (const row2 of coverage) {
|
|
12005
|
+
if (row2.req_status !== "Active")
|
|
11837
12006
|
continue;
|
|
11838
|
-
const c = cov.get(
|
|
11839
|
-
if (
|
|
12007
|
+
const c = cov.get(row2.req_id) ?? { impl: 0, verif: 0 };
|
|
12008
|
+
if (row2.implemented)
|
|
11840
12009
|
c.impl += 1;
|
|
11841
|
-
if (
|
|
12010
|
+
if (row2.verified)
|
|
11842
12011
|
c.verif += 1;
|
|
11843
|
-
cov.set(
|
|
12012
|
+
cov.set(row2.req_id, c);
|
|
11844
12013
|
}
|
|
11845
12014
|
return cov;
|
|
11846
12015
|
}
|
|
@@ -12187,15 +12356,15 @@ function mountSetup(app) {
|
|
|
12187
12356
|
const memberCount = members.length;
|
|
12188
12357
|
const note = readingNote(mode, memberCount);
|
|
12189
12358
|
const boundByRepo = new Map;
|
|
12190
|
-
for (const
|
|
12191
|
-
if (!
|
|
12359
|
+
for (const row2 of cov) {
|
|
12360
|
+
if (!row2.implemented)
|
|
12192
12361
|
continue;
|
|
12193
|
-
let set = boundByRepo.get(
|
|
12362
|
+
let set = boundByRepo.get(row2.repo);
|
|
12194
12363
|
if (!set) {
|
|
12195
12364
|
set = new Set;
|
|
12196
|
-
boundByRepo.set(
|
|
12365
|
+
boundByRepo.set(row2.repo, set);
|
|
12197
12366
|
}
|
|
12198
|
-
set.add(
|
|
12367
|
+
set.add(row2.req_id);
|
|
12199
12368
|
}
|
|
12200
12369
|
const body = html11`
|
|
12201
12370
|
${navBar("setup")}
|
|
@@ -12433,6 +12602,7 @@ function mintFieldsFromBody(body) {
|
|
|
12433
12602
|
statement: typeof body.statement === "string" ? body.statement : "",
|
|
12434
12603
|
why: typeof body.why === "string" ? body.why : "",
|
|
12435
12604
|
livesIn: toStringList(body.livesIn),
|
|
12605
|
+
verifiedIn: toStringList(body.verifiedIn),
|
|
12436
12606
|
relates: toStringList(body.relates),
|
|
12437
12607
|
issue: typeof body.issue === "string" && body.issue.trim() !== "" ? body.issue.trim() : undefined,
|
|
12438
12608
|
status: body.status === "draft" ? "draft" : "active"
|
|
@@ -12442,6 +12612,7 @@ function successorFieldsFromBody(body) {
|
|
|
12442
12612
|
return {
|
|
12443
12613
|
why: typeof body.why === "string" ? body.why : undefined,
|
|
12444
12614
|
livesIn: "livesIn" in body ? toStringList(body.livesIn) : undefined,
|
|
12615
|
+
verifiedIn: "verifiedIn" in body ? toStringList(body.verifiedIn) : undefined,
|
|
12445
12616
|
relates: "relates" in body ? toStringList(body.relates) : undefined,
|
|
12446
12617
|
issue: typeof body.issue === "string" && body.issue.trim() !== "" ? body.issue.trim() : undefined
|
|
12447
12618
|
};
|
|
@@ -12454,6 +12625,8 @@ function amendFieldsFromBody(body) {
|
|
|
12454
12625
|
fields.why = typeof body.why === "string" ? body.why : null;
|
|
12455
12626
|
if ("livesIn" in body)
|
|
12456
12627
|
fields.livesIn = toStringList(body.livesIn);
|
|
12628
|
+
if ("verifiedIn" in body)
|
|
12629
|
+
fields.verifiedIn = toStringList(body.verifiedIn);
|
|
12457
12630
|
if ("relates" in body)
|
|
12458
12631
|
fields.relates = toStringList(body.relates);
|
|
12459
12632
|
return Object.keys(fields).length === 0 ? null : fields;
|
|
@@ -12515,8 +12688,8 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
|
|
|
12515
12688
|
}));
|
|
12516
12689
|
app.get("/api/requirements/:id", guarded((c) => {
|
|
12517
12690
|
const id = c.req.param("id") ?? "";
|
|
12518
|
-
const { row } = getRecord(storage2, id);
|
|
12519
|
-
return
|
|
12691
|
+
const { row: row2 } = getRecord(storage2, id);
|
|
12692
|
+
return row2 ? c.json(row2) : c.json({ error: "not found" }, 404);
|
|
12520
12693
|
}));
|
|
12521
12694
|
app.post("/api/requirements", guarded(async function createRequirement(c) {
|
|
12522
12695
|
if (!featureEnabled("editor")) {
|
|
@@ -12562,7 +12735,7 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
|
|
|
12562
12735
|
return withWriteLock(async () => {
|
|
12563
12736
|
const fields = amendFieldsFromBody(body);
|
|
12564
12737
|
if (fields === null) {
|
|
12565
|
-
return c.json({ error: "nothing to amend \u2014 provide statement, why, livesIn, or relates" }, 400);
|
|
12738
|
+
return c.json({ error: "nothing to amend \u2014 provide statement, why, livesIn, verifiedIn, or relates" }, 400);
|
|
12566
12739
|
}
|
|
12567
12740
|
const r = await amend({ platformDir, id, fields });
|
|
12568
12741
|
if (!r.ok)
|
|
@@ -12635,7 +12808,8 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
|
|
|
12635
12808
|
targetKey,
|
|
12636
12809
|
statement: typeof body.text === "string" ? body.text : undefined,
|
|
12637
12810
|
why: typeof body.why === "string" ? body.why : undefined,
|
|
12638
|
-
livesIn: "lives" in body ? toStringList(body.lives) : undefined
|
|
12811
|
+
livesIn: "lives" in body ? toStringList(body.lives) : undefined,
|
|
12812
|
+
verifiedIn: "verifiedIn" in body ? toStringList(body.verifiedIn) : undefined
|
|
12639
12813
|
}, async (reqId) => {
|
|
12640
12814
|
await runIndex({ platformDir, storage: storage2 });
|
|
12641
12815
|
return storage2.listTags({ req_id: reqId });
|
|
@@ -13124,7 +13298,7 @@ __export(exports_docs, {
|
|
|
13124
13298
|
createDocsFetchHandler: () => createDocsFetchHandler
|
|
13125
13299
|
});
|
|
13126
13300
|
import { existsSync as existsSync15, statSync as statSync6 } from "fs";
|
|
13127
|
-
import { join as join16, normalize, sep as
|
|
13301
|
+
import { join as join16, normalize, sep as sep5 } from "path";
|
|
13128
13302
|
import { fileURLToPath } from "url";
|
|
13129
13303
|
import { defineCommand as defineCommand25 } from "citty";
|
|
13130
13304
|
function resolveDocsRootFrom(candidates) {
|
|
@@ -13142,7 +13316,7 @@ function resolveDocsRoot() {
|
|
|
13142
13316
|
}
|
|
13143
13317
|
function createDocsFetchHandler(root) {
|
|
13144
13318
|
const normalRoot = normalize(root);
|
|
13145
|
-
const rootPrefix = normalRoot.endsWith(
|
|
13319
|
+
const rootPrefix = normalRoot.endsWith(sep5) ? normalRoot : normalRoot + sep5;
|
|
13146
13320
|
return (req) => {
|
|
13147
13321
|
let pathname;
|
|
13148
13322
|
try {
|