@spec-engine/spec-engine 0.6.1 → 0.7.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/architecture/index.html +1 -1
- package/dist/docs/checks/index.html +25 -5
- package/dist/docs/commands/index.html +3 -3
- package/dist/docs/pagefind/fragment/en_6ce6fae.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_775cc8c.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_a3cd8bf.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_b44542a.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_d652b3c.pf_fragment +0 -0
- package/dist/docs/pagefind/index/en_6b9dc89.pf_index +0 -0
- package/dist/docs/pagefind/index/en_c5bf3e1.pf_index +0 -0
- package/dist/docs/pagefind/pagefind-entry.json +1 -1
- package/dist/docs/pagefind/pagefind.en_c6c8e941d5.pf_meta +0 -0
- package/dist/docs/platform-mapping/index.html +1 -1
- package/dist/docs/tags/index.html +1 -1
- package/dist/impl.js +832 -517
- package/package.json +1 -1
- package/dist/docs/pagefind/fragment/en_01934b1.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_4560516.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_aa3ebfe.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_dfaa776.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_edd950c.pf_fragment +0 -0
- package/dist/docs/pagefind/index/en_a63e8e2.pf_index +0 -0
- package/dist/docs/pagefind/pagefind.en_2ed26d23fb.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.7.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",
|
|
@@ -73,18 +73,8 @@ var init_package = __esm(() => {
|
|
|
73
73
|
};
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
// ../shared/src/config.ts
|
|
77
|
-
import { z } from "zod";
|
|
78
|
-
var SpecConfigSchema;
|
|
79
|
-
var init_config = __esm(() => {
|
|
80
|
-
SpecConfigSchema = z.object({
|
|
81
|
-
specs: z.string().regex(/^spec-engine@\d+$/, "must be of the form spec-engine@N where N is an integer"),
|
|
82
|
-
ignore: z.array(z.string().min(1, "ignore entries must be non-empty strings")).optional()
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
76
|
// ../shared/src/diagnostics.ts
|
|
87
|
-
var DiagnosticCode, NotASpecPlatformError;
|
|
77
|
+
var DiagnosticCode, NotASpecPlatformError, InvalidDomainFilesError;
|
|
88
78
|
var init_diagnostics = __esm(() => {
|
|
89
79
|
DiagnosticCode = {
|
|
90
80
|
DUP_ID: "DUP_ID",
|
|
@@ -111,6 +101,9 @@ var init_diagnostics = __esm(() => {
|
|
|
111
101
|
UNPROVEN_REQ: "UNPROVEN_REQ",
|
|
112
102
|
PROOFS_UNCONFIRMED: "PROOFS_UNCONFIRMED",
|
|
113
103
|
REQUIREMENT_REMOVED: "REQUIREMENT_REMOVED",
|
|
104
|
+
IMPL_LOST: "IMPL_LOST",
|
|
105
|
+
VERIFY_LOST: "VERIFY_LOST",
|
|
106
|
+
SPEC_FILE_DELETED: "SPEC_FILE_DELETED",
|
|
114
107
|
UNAPPROVED_STATUS_FLIP: "UNAPPROVED_STATUS_FLIP",
|
|
115
108
|
PARTIAL_PROPAGATION: "PARTIAL_PROPAGATION",
|
|
116
109
|
MALFORMED_FILE: "MALFORMED_FILE",
|
|
@@ -135,10 +128,20 @@ var init_diagnostics = __esm(() => {
|
|
|
135
128
|
Object.setPrototypeOf(this, NotASpecPlatformError.prototype);
|
|
136
129
|
}
|
|
137
130
|
};
|
|
131
|
+
InvalidDomainFilesError = class InvalidDomainFilesError extends Error {
|
|
132
|
+
diagnostics;
|
|
133
|
+
constructor(diagnostics) {
|
|
134
|
+
super(diagnostics.map((d) => `${d.source_file ?? ""}: ${d.detail}`).join(`
|
|
135
|
+
`));
|
|
136
|
+
this.name = "InvalidDomainFilesError";
|
|
137
|
+
this.diagnostics = diagnostics;
|
|
138
|
+
Object.setPrototypeOf(this, InvalidDomainFilesError.prototype);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
138
141
|
});
|
|
139
142
|
|
|
140
143
|
// ../shared/src/domain.ts
|
|
141
|
-
import { z
|
|
144
|
+
import { z } from "zod";
|
|
142
145
|
function reqIdForIssuePath(input, path) {
|
|
143
146
|
if (path.length < 2 || path[0] !== "requirements" || typeof path[1] !== "number") {
|
|
144
147
|
return null;
|
|
@@ -221,57 +224,193 @@ function orderDomain(d) {
|
|
|
221
224
|
}))
|
|
222
225
|
};
|
|
223
226
|
}
|
|
227
|
+
function isDefaultValue(v) {
|
|
228
|
+
return v === undefined || v === null || Array.isArray(v) && v.length === 0;
|
|
229
|
+
}
|
|
230
|
+
function patchFields(raw, before, next) {
|
|
231
|
+
for (const key of new Set([...Object.keys(before), ...Object.keys(next)])) {
|
|
232
|
+
if (key === "requirements")
|
|
233
|
+
continue;
|
|
234
|
+
const v = next[key];
|
|
235
|
+
if (sameValue(before[key], v))
|
|
236
|
+
continue;
|
|
237
|
+
if (v === undefined)
|
|
238
|
+
delete raw[key];
|
|
239
|
+
else if (!isDefaultValue(v) || key in raw)
|
|
240
|
+
raw[key] = v;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function shapeLike(template, next) {
|
|
244
|
+
if (template === undefined)
|
|
245
|
+
return next;
|
|
246
|
+
const out = {};
|
|
247
|
+
for (const key of Object.keys(template)) {
|
|
248
|
+
if (next[key] !== undefined)
|
|
249
|
+
out[key] = next[key];
|
|
250
|
+
}
|
|
251
|
+
for (const [key, v] of Object.entries(next)) {
|
|
252
|
+
if (!(key in out) && !isDefaultValue(v))
|
|
253
|
+
out[key] = v;
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
function preserveShape(raw, rawDomain, domain) {
|
|
258
|
+
const before = orderDomain(rawDomain);
|
|
259
|
+
const next = orderDomain(domain);
|
|
260
|
+
patchFields(raw, before, next);
|
|
261
|
+
const rawEntries = raw.requirements;
|
|
262
|
+
const byId = new Map;
|
|
263
|
+
rawDomain.requirements.forEach((r, i) => {
|
|
264
|
+
byId.set(r.id, [...byId.get(r.id) ?? [], i]);
|
|
265
|
+
});
|
|
266
|
+
const template = rawEntries[rawEntries.length - 1];
|
|
267
|
+
raw.requirements = next.requirements.map((entry, i) => {
|
|
268
|
+
const idx = byId.get(domain.requirements[i]?.id ?? "")?.shift();
|
|
269
|
+
if (idx === undefined)
|
|
270
|
+
return shapeLike(template, entry);
|
|
271
|
+
const rawEntry = rawEntries[idx];
|
|
272
|
+
patchFields(rawEntry, before.requirements[idx], entry);
|
|
273
|
+
return rawEntry;
|
|
274
|
+
});
|
|
275
|
+
return raw;
|
|
276
|
+
}
|
|
277
|
+
function verifyRoundTrip(serialized, domain, sourceFile) {
|
|
278
|
+
const reparsed = parseDomainText(serialized, sourceFile);
|
|
279
|
+
if (!reparsed.ok)
|
|
280
|
+
return { ok: false, diagnostics: reparsed.diagnostics };
|
|
281
|
+
if (sameValue(orderDomain(reparsed.data), orderDomain(domain)))
|
|
282
|
+
return { ok: true };
|
|
283
|
+
return {
|
|
284
|
+
ok: false,
|
|
285
|
+
diagnostics: [
|
|
286
|
+
{
|
|
287
|
+
code: DiagnosticCode.INVALID_DOMAIN_FILE,
|
|
288
|
+
source_file: sourceFile,
|
|
289
|
+
line: 0,
|
|
290
|
+
repo: null,
|
|
291
|
+
req_id: null,
|
|
292
|
+
detail: "the shape-preserving rewrite does not re-parse to the validated domain",
|
|
293
|
+
severity: "error"
|
|
294
|
+
}
|
|
295
|
+
]
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
async function readExisting(path, sourceFile) {
|
|
299
|
+
const file = Bun.file(path);
|
|
300
|
+
if (!await file.exists())
|
|
301
|
+
return null;
|
|
302
|
+
let raw;
|
|
303
|
+
try {
|
|
304
|
+
raw = JSON.parse(await file.text());
|
|
305
|
+
} catch {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
const validated = validateDomainFile(raw, sourceFile);
|
|
309
|
+
return validated.ok ? { raw, data: validated.data } : null;
|
|
310
|
+
}
|
|
224
311
|
async function validateAndWrite(path, domain, sourceFile) {
|
|
225
|
-
const
|
|
312
|
+
const src = sourceFile ?? path;
|
|
313
|
+
const validated = validateDomainFile(domain, src);
|
|
226
314
|
if (!validated.ok) {
|
|
227
315
|
return { ok: false, diagnostics: validated.diagnostics };
|
|
228
316
|
}
|
|
229
|
-
const
|
|
317
|
+
const existing = await readExisting(path, src);
|
|
318
|
+
if (existing === null) {
|
|
319
|
+
await Bun.write(path, `${JSON.stringify(orderDomain(validated.data), null, 2)}
|
|
320
|
+
`);
|
|
321
|
+
return { ok: true };
|
|
322
|
+
}
|
|
323
|
+
const shaped = preserveShape(existing.raw, existing.data, validated.data);
|
|
324
|
+
const serialized = `${JSON.stringify(shaped, null, 2)}
|
|
230
325
|
`;
|
|
326
|
+
const verdict = verifyRoundTrip(serialized, validated.data, src);
|
|
327
|
+
if (!verdict.ok)
|
|
328
|
+
return verdict;
|
|
231
329
|
await Bun.write(path, serialized);
|
|
232
330
|
return { ok: true };
|
|
233
331
|
}
|
|
234
|
-
var ID_RE, KEY_RE, SpecIssueSchema, SpecCiteSchema, SpecRequirementSchema, SpecDomainSchema;
|
|
332
|
+
var ID_RE, KEY_RE, SpecIssueSchema, SpecCiteSchema, SpecRequirementSchema, SpecDomainSchema, sameValue = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
235
333
|
var init_domain = __esm(() => {
|
|
236
334
|
init_diagnostics();
|
|
237
335
|
ID_RE = /^[A-Z][A-Z0-9]*-\d+$/;
|
|
238
336
|
KEY_RE = /^[A-Z][A-Z0-9]*$/;
|
|
239
|
-
SpecIssueSchema =
|
|
240
|
-
role:
|
|
241
|
-
id:
|
|
337
|
+
SpecIssueSchema = z.object({
|
|
338
|
+
role: z.string().min(1),
|
|
339
|
+
id: z.string().min(1)
|
|
340
|
+
}).strict();
|
|
341
|
+
SpecCiteSchema = z.object({
|
|
342
|
+
term: z.string(),
|
|
343
|
+
pinned: z.number().int()
|
|
242
344
|
}).strict();
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
345
|
+
SpecRequirementSchema = z.object({
|
|
346
|
+
id: z.string().regex(ID_RE),
|
|
347
|
+
status: z.string().min(1),
|
|
348
|
+
statement: z.string().min(1),
|
|
349
|
+
why: z.string().nullable().optional(),
|
|
350
|
+
supersedes: z.string().nullable().optional(),
|
|
351
|
+
supersededBy: z.string().nullable().optional(),
|
|
352
|
+
relates: z.array(z.string()).optional().default([]),
|
|
353
|
+
livesIn: z.array(z.string()).optional().default([]),
|
|
354
|
+
issues: z.array(SpecIssueSchema).optional().default([]),
|
|
355
|
+
changedAtVersion: z.number().int().optional(),
|
|
356
|
+
supersededAtVersion: z.number().int().optional(),
|
|
357
|
+
deprecatedReason: z.string().optional(),
|
|
358
|
+
term: z.string().optional(),
|
|
359
|
+
aliases: z.array(z.string()).optional().default([]),
|
|
360
|
+
cites: z.array(SpecCiteSchema).optional().default([]),
|
|
361
|
+
section: z.string().nullable().optional()
|
|
246
362
|
}).strict();
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
issues: z2.array(SpecIssueSchema).optional().default([]),
|
|
257
|
-
changedAtVersion: z2.number().int().optional(),
|
|
258
|
-
supersededAtVersion: z2.number().int().optional(),
|
|
259
|
-
deprecatedReason: z2.string().optional(),
|
|
260
|
-
term: z2.string().optional(),
|
|
261
|
-
aliases: z2.array(z2.string()).optional().default([]),
|
|
262
|
-
cites: z2.array(SpecCiteSchema).optional().default([]),
|
|
263
|
-
section: z2.string().nullable().optional()
|
|
363
|
+
SpecDomainSchema = z.object({
|
|
364
|
+
key: z.string().regex(KEY_RE),
|
|
365
|
+
owner: z.string().nullable().optional(),
|
|
366
|
+
specVersion: z.number().int().positive().optional(),
|
|
367
|
+
updated: z.string(),
|
|
368
|
+
scope: z.string().nullable().optional(),
|
|
369
|
+
grammar: z.enum(["ears", "freeform"]).optional(),
|
|
370
|
+
grammarSeverity: z.enum(["warning", "error"]).optional(),
|
|
371
|
+
requirements: z.array(SpecRequirementSchema)
|
|
264
372
|
}).strict();
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// ../shared/src/checkConfig.ts
|
|
376
|
+
import { z as z2 } from "zod";
|
|
377
|
+
function parseCheckConfigText(text, sourceFile) {
|
|
378
|
+
let parsed;
|
|
379
|
+
try {
|
|
380
|
+
parsed = JSON.parse(text);
|
|
381
|
+
} catch (err) {
|
|
382
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
383
|
+
return { ok: false, detail: `${sourceFile}: not valid JSON: ${msg}` };
|
|
384
|
+
}
|
|
385
|
+
const result = CheckConfigSchema.safeParse(parsed);
|
|
386
|
+
if (result.success)
|
|
387
|
+
return { ok: true, config: result.data };
|
|
388
|
+
const issue = result.error.issues[0];
|
|
389
|
+
const path = issue && issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
390
|
+
return { ok: false, detail: `${sourceFile}: ${path}: ${issue?.message ?? "invalid"}` };
|
|
391
|
+
}
|
|
392
|
+
var CODES, matchEntry, SeverityRuleSchema, CheckConfigSchema;
|
|
393
|
+
var init_checkConfig = __esm(() => {
|
|
394
|
+
init_diagnostics();
|
|
395
|
+
init_domain();
|
|
396
|
+
CODES = Object.values(DiagnosticCode);
|
|
397
|
+
matchEntry = z2.string().refine((s) => KEY_RE.test(s) || ID_RE.test(s), "must be a domain key or a requirement id");
|
|
398
|
+
SeverityRuleSchema = z2.object({
|
|
399
|
+
code: z2.enum(CODES),
|
|
400
|
+
severity: z2.enum(["error", "warning", "off"]),
|
|
401
|
+
match: z2.array(matchEntry).optional()
|
|
274
402
|
}).strict();
|
|
403
|
+
CheckConfigSchema = z2.object({ severity: z2.array(SeverityRuleSchema) }).strict();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// ../shared/src/config.ts
|
|
407
|
+
import { z as z3 } from "zod";
|
|
408
|
+
var SpecConfigSchema;
|
|
409
|
+
var init_config = __esm(() => {
|
|
410
|
+
SpecConfigSchema = z3.object({
|
|
411
|
+
specs: z3.string().regex(/^spec-engine@\d+$/, "must be of the form spec-engine@N where N is an integer"),
|
|
412
|
+
ignore: z3.array(z3.string().min(1, "ignore entries must be non-empty strings")).optional()
|
|
413
|
+
});
|
|
275
414
|
});
|
|
276
415
|
|
|
277
416
|
// ../shared/src/ears.ts
|
|
@@ -677,6 +816,7 @@ var init_storage = __esm(() => {
|
|
|
677
816
|
|
|
678
817
|
// ../shared/src/index.ts
|
|
679
818
|
var init_src = __esm(() => {
|
|
819
|
+
init_checkConfig();
|
|
680
820
|
init_config();
|
|
681
821
|
init_diagnostics();
|
|
682
822
|
init_domain();
|
|
@@ -949,8 +1089,7 @@ import { readdir } from "fs/promises";
|
|
|
949
1089
|
import { join as join2 } from "path";
|
|
950
1090
|
function isPathIgnored(path, extra = []) {
|
|
951
1091
|
const anchored = `/${path}`;
|
|
952
|
-
|
|
953
|
-
return tokens.some((t) => anchored.includes(`/${t.endsWith("/") ? t : `${t}/`}`));
|
|
1092
|
+
return IGNORE_SUBSTR.some((t) => anchored.includes(`/${t}`)) || extra.some((t) => path.startsWith(t.endsWith("/") ? t : `${t}/`));
|
|
954
1093
|
}
|
|
955
1094
|
async function walkInto(rootDir, relDir, keep, extraIgnore, out) {
|
|
956
1095
|
const absDir = relDir ? join2(rootDir, relDir) : rootDir;
|
|
@@ -979,15 +1118,19 @@ async function walkFiles(rootDir, keep, extraIgnore) {
|
|
|
979
1118
|
out.sort();
|
|
980
1119
|
return out;
|
|
981
1120
|
}
|
|
1121
|
+
function isDockerfile(name) {
|
|
1122
|
+
return name === "Dockerfile" || name.startsWith("Dockerfile.") && name.length > 11;
|
|
1123
|
+
}
|
|
1124
|
+
function isTagSource(rel, exts = DEFAULT_EXTS) {
|
|
1125
|
+
const name = rel.slice(rel.lastIndexOf("/") + 1);
|
|
1126
|
+
const dot = name.lastIndexOf(".");
|
|
1127
|
+
return isDockerfile(name) || dot > 0 && exts.includes(name.slice(dot + 1));
|
|
1128
|
+
}
|
|
982
1129
|
async function findDomainJsonFiles(canonicalDir) {
|
|
983
1130
|
return walkFiles(canonicalDir, (rel) => rel === "SPEC.json" || rel.endsWith("/SPEC.json"), []);
|
|
984
1131
|
}
|
|
985
1132
|
async function findCodeFiles(repoDir, exts = DEFAULT_EXTS, extraIgnore = []) {
|
|
986
|
-
|
|
987
|
-
return walkFiles(repoDir, (rel) => {
|
|
988
|
-
const dot = rel.lastIndexOf(".");
|
|
989
|
-
return dot > 0 && extSet.has(rel.slice(dot + 1));
|
|
990
|
-
}, extraIgnore);
|
|
1133
|
+
return walkFiles(repoDir, (rel) => isTagSource(rel, exts), extraIgnore);
|
|
991
1134
|
}
|
|
992
1135
|
async function findDocFiles(repoDir, extraIgnore = []) {
|
|
993
1136
|
return walkFiles(repoDir, (rel) => rel.endsWith(".md") && rel !== "SPEC.md" && !rel.endsWith("/SPEC.md"), extraIgnore);
|
|
@@ -1017,7 +1160,11 @@ var init_fs = __esm(() => {
|
|
|
1017
1160
|
"lua",
|
|
1018
1161
|
"luau",
|
|
1019
1162
|
"go",
|
|
1020
|
-
"rs"
|
|
1163
|
+
"rs",
|
|
1164
|
+
"tf",
|
|
1165
|
+
"toml",
|
|
1166
|
+
"sql",
|
|
1167
|
+
"css"
|
|
1021
1168
|
];
|
|
1022
1169
|
});
|
|
1023
1170
|
|
|
@@ -1548,9 +1695,13 @@ SELECT
|
|
|
1548
1695
|
t.file AS source_file,
|
|
1549
1696
|
t.line AS line,
|
|
1550
1697
|
t.req_id AS req_id,
|
|
1551
|
-
|
|
1552
|
-
'
|
|
1553
|
-
|
|
1698
|
+
CASE WHEN t.kind = 'verifies'
|
|
1699
|
+
THEN ('Test verifies Draft requirement ' || t.req_id ||
|
|
1700
|
+
' \u2014 it asserts a promise nobody accepted yet')
|
|
1701
|
+
ELSE ('Tag references Draft requirement ' || t.req_id ||
|
|
1702
|
+
' \u2014 code is bound to a promise nobody accepted yet')
|
|
1703
|
+
END AS detail,
|
|
1704
|
+
CASE WHEN t.kind = 'verifies' THEN 'error' ELSE 'warning' END AS severity
|
|
1554
1705
|
FROM tags t
|
|
1555
1706
|
JOIN requirements r ON r.id = t.req_id
|
|
1556
1707
|
WHERE r.status = 'Draft'
|
|
@@ -1580,11 +1731,10 @@ SELECT
|
|
|
1580
1731
|
'error' AS severity
|
|
1581
1732
|
FROM requirements r
|
|
1582
1733
|
WHERE r.status = 'Active'
|
|
1583
|
-
--
|
|
1584
|
-
-- term never fires ORPHAN_REQ. Literal 'TERM' only, never a real domain key;
|
|
1585
|
-
-- mirrors the coverage VIEW exclusion (WHERE r.key != 'TERM') in schema.ts.
|
|
1734
|
+
-- Must stay in step with the coverage view's TERM exclusion in schema.ts.
|
|
1586
1735
|
-- @spec SCHM-017
|
|
1587
1736
|
AND r.key != 'TERM'
|
|
1737
|
+
-- A doc paragraph citing a requirement is not an implementation.
|
|
1588
1738
|
AND NOT EXISTS (
|
|
1589
1739
|
SELECT 1 FROM tags t WHERE t.req_id = r.id AND t.kind != 'documents'
|
|
1590
1740
|
)
|
|
@@ -1598,11 +1748,10 @@ SELECT
|
|
|
1598
1748
|
r.id AS req_id,
|
|
1599
1749
|
('Requirement ' || r.id ||
|
|
1600
1750
|
' is implemented but has no verifies-kind tag') AS detail,
|
|
1601
|
-
'
|
|
1751
|
+
'warning' AS severity
|
|
1602
1752
|
FROM requirements r
|
|
1603
1753
|
WHERE r.status = 'Active'
|
|
1604
|
-
--
|
|
1605
|
-
-- Literal 'TERM' only; mirrors Q4 + the coverage VIEW exclusion.
|
|
1754
|
+
-- Must stay in step with Q4 and the coverage view's TERM exclusion.
|
|
1606
1755
|
AND r.key != 'TERM'
|
|
1607
1756
|
AND EXISTS (SELECT 1 FROM tags t WHERE t.req_id = r.id AND t.kind = 'implements')
|
|
1608
1757
|
AND NOT EXISTS (SELECT 1 FROM tags t WHERE t.req_id = r.id AND t.kind = 'verifies')
|
|
@@ -2445,7 +2594,7 @@ async function runIndex(opts) {
|
|
|
2445
2594
|
unpinned,
|
|
2446
2595
|
diagnostics: platformDiagnostics
|
|
2447
2596
|
} = await discoverRepos(platformDir);
|
|
2448
|
-
const
|
|
2597
|
+
const specs = await readSpecs(canonical);
|
|
2449
2598
|
const { tagHits, docMentionCandidates } = await scanMembers(members);
|
|
2450
2599
|
const rows = buildRows({
|
|
2451
2600
|
canonical,
|
|
@@ -2453,7 +2602,6 @@ async function runIndex(opts) {
|
|
|
2453
2602
|
members,
|
|
2454
2603
|
specs,
|
|
2455
2604
|
tagHits,
|
|
2456
|
-
structuralDiagnostics,
|
|
2457
2605
|
unpinned,
|
|
2458
2606
|
platformDiagnostics
|
|
2459
2607
|
});
|
|
@@ -2475,29 +2623,22 @@ async function runIndex(opts) {
|
|
|
2475
2623
|
async function readSpecs(canonical) {
|
|
2476
2624
|
const jsonPaths = await findDomainJsonFiles(canonical.path);
|
|
2477
2625
|
const specs = [];
|
|
2478
|
-
const
|
|
2626
|
+
const invalid = [];
|
|
2479
2627
|
for (const rel of jsonPaths) {
|
|
2480
2628
|
const absPath = join4(canonical.path, rel);
|
|
2481
2629
|
const text = await Bun.file(absPath).text();
|
|
2482
2630
|
const platformRelative = `${canonical.name}/${rel}`;
|
|
2483
2631
|
const fallbackKey = basename2(dirname3(rel));
|
|
2484
2632
|
const result = parseDomainJsonFile({ text, sourceFile: platformRelative, fallbackKey });
|
|
2485
|
-
if (
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
line: d.line ?? 0,
|
|
2491
|
-
req_id: d.req_id,
|
|
2492
|
-
detail: d.detail,
|
|
2493
|
-
severity: d.severity
|
|
2494
|
-
});
|
|
2495
|
-
}
|
|
2496
|
-
continue;
|
|
2497
|
-
}
|
|
2498
|
-
specs.push(result.spec);
|
|
2633
|
+
if (result.ok)
|
|
2634
|
+
specs.push(result.spec);
|
|
2635
|
+
else
|
|
2636
|
+
for (const d of result.diagnostics)
|
|
2637
|
+
invalid.push({ ...d, source_file: d.source_file ?? platformRelative });
|
|
2499
2638
|
}
|
|
2500
|
-
|
|
2639
|
+
if (invalid.length > 0)
|
|
2640
|
+
throw new InvalidDomainFilesError(invalid);
|
|
2641
|
+
return specs;
|
|
2501
2642
|
}
|
|
2502
2643
|
function repoExtraIgnore(member) {
|
|
2503
2644
|
const repoIgnore = (member.ignore ?? []).map((e) => e.endsWith("/") ? e : `${e}/`);
|
|
@@ -2549,16 +2690,7 @@ async function scanMembers(members) {
|
|
|
2549
2690
|
return { tagHits, docMentionCandidates };
|
|
2550
2691
|
}
|
|
2551
2692
|
function buildRows(input) {
|
|
2552
|
-
const {
|
|
2553
|
-
canonical,
|
|
2554
|
-
platformVersion,
|
|
2555
|
-
members,
|
|
2556
|
-
specs,
|
|
2557
|
-
tagHits,
|
|
2558
|
-
structuralDiagnostics,
|
|
2559
|
-
unpinned,
|
|
2560
|
-
platformDiagnostics
|
|
2561
|
-
} = input;
|
|
2693
|
+
const { canonical, platformVersion, members, specs, tagHits, unpinned, platformDiagnostics } = input;
|
|
2562
2694
|
const sortedRepos = sortBy([
|
|
2563
2695
|
{
|
|
2564
2696
|
name: canonical.name,
|
|
@@ -2582,8 +2714,6 @@ function buildRows(input) {
|
|
|
2582
2714
|
const { sortedTermAliases, sortedTermCitations } = buildTermRows(specs, flatRequirements);
|
|
2583
2715
|
const sortedProvenance = sortBy(specs.flatMap((s) => s.provenance), (p) => `${p.req_id}\x00${p.role}\x00${p.issue_id}\x00${p.source_file}\x00${pad(p.line)}`);
|
|
2584
2716
|
const diagnostics2 = validateStructure(specs);
|
|
2585
|
-
for (const d of structuralDiagnostics)
|
|
2586
|
-
diagnostics2.push(d);
|
|
2587
2717
|
for (const s of unpinned) {
|
|
2588
2718
|
diagnostics2.push({
|
|
2589
2719
|
code: DiagnosticCode.NO_SPEC_CONFIG,
|
|
@@ -3017,6 +3147,49 @@ var init_init = __esm(() => {
|
|
|
3017
3147
|
KNOWN_KEYS = new Set(SpecConfigSchema.keyof().options);
|
|
3018
3148
|
});
|
|
3019
3149
|
|
|
3150
|
+
// src/check/format.ts
|
|
3151
|
+
function compareRepoNullsLast(a, b) {
|
|
3152
|
+
if (a === b)
|
|
3153
|
+
return 0;
|
|
3154
|
+
if (a === null)
|
|
3155
|
+
return 1;
|
|
3156
|
+
if (b === null)
|
|
3157
|
+
return -1;
|
|
3158
|
+
return a.localeCompare(b);
|
|
3159
|
+
}
|
|
3160
|
+
function compareDiagnostics(a, b) {
|
|
3161
|
+
const codeCmp = a.code.localeCompare(b.code);
|
|
3162
|
+
if (codeCmp !== 0)
|
|
3163
|
+
return codeCmp;
|
|
3164
|
+
const repoCmp = compareRepoNullsLast(a.repo, b.repo);
|
|
3165
|
+
if (repoCmp !== 0)
|
|
3166
|
+
return repoCmp;
|
|
3167
|
+
const fileCmp = (a.source_file ?? "").localeCompare(b.source_file ?? "");
|
|
3168
|
+
if (fileCmp !== 0)
|
|
3169
|
+
return fileCmp;
|
|
3170
|
+
const lineCmp = (a.line ?? 0) - (b.line ?? 0);
|
|
3171
|
+
if (lineCmp !== 0)
|
|
3172
|
+
return lineCmp;
|
|
3173
|
+
return (a.req_id ?? "").localeCompare(b.req_id ?? "");
|
|
3174
|
+
}
|
|
3175
|
+
function sortDiagnostics(rows) {
|
|
3176
|
+
return [...rows].sort(compareDiagnostics);
|
|
3177
|
+
}
|
|
3178
|
+
function renderDiagnostics(rows, mode) {
|
|
3179
|
+
const sorted = sortDiagnostics(rows);
|
|
3180
|
+
if (mode === "json") {
|
|
3181
|
+
return JSON.stringify(sorted);
|
|
3182
|
+
}
|
|
3183
|
+
return sorted.map((d) => {
|
|
3184
|
+
const repo = d.repo ?? "";
|
|
3185
|
+
const file = d.source_file ?? "";
|
|
3186
|
+
const line = d.line ?? "";
|
|
3187
|
+
const reqId = d.req_id ?? "";
|
|
3188
|
+
return `${d.code} ${repo} ${file}:${line} ${reqId} ${d.detail}`;
|
|
3189
|
+
}).join(`
|
|
3190
|
+
`);
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3020
3193
|
// src/operations/_index.ts
|
|
3021
3194
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync, statSync as statSync3 } from "fs";
|
|
3022
3195
|
import { dirname as dirname4, join as join6 } from "path";
|
|
@@ -3115,6 +3288,26 @@ function handleNotAPlatform(e) {
|
|
|
3115
3288
|
}
|
|
3116
3289
|
throw e;
|
|
3117
3290
|
}
|
|
3291
|
+
function handleInvalidDomainFiles(e, json, code = EXIT.FAILURE) {
|
|
3292
|
+
if (!(e instanceof InvalidDomainFilesError))
|
|
3293
|
+
return;
|
|
3294
|
+
if (json)
|
|
3295
|
+
console.log(renderDiagnostics(e.diagnostics, "json"));
|
|
3296
|
+
else
|
|
3297
|
+
console.error(renderDiagnostics(e.diagnostics, "text"));
|
|
3298
|
+
process.exit(code);
|
|
3299
|
+
}
|
|
3300
|
+
function cliFreshTags(platformDir) {
|
|
3301
|
+
const fresh = coldFreshTags(platformDir);
|
|
3302
|
+
return async (reqId) => {
|
|
3303
|
+
try {
|
|
3304
|
+
return await fresh(reqId);
|
|
3305
|
+
} catch (e) {
|
|
3306
|
+
handleInvalidDomainFiles(e, false, EXIT.USAGE);
|
|
3307
|
+
throw e;
|
|
3308
|
+
}
|
|
3309
|
+
};
|
|
3310
|
+
}
|
|
3118
3311
|
function handleStorageUnavailable(e, dbPath) {
|
|
3119
3312
|
const info = describeStorageError(e);
|
|
3120
3313
|
if (info === null)
|
|
@@ -3143,7 +3336,7 @@ function exitOnFailure(cmdName, failure) {
|
|
|
3143
3336
|
process.exit(EXIT.USAGE);
|
|
3144
3337
|
}
|
|
3145
3338
|
async function withReadStorage(opts, fn) {
|
|
3146
|
-
const { platformDir, dbPath, fresh } = opts;
|
|
3339
|
+
const { platformDir, dbPath, fresh, json = false } = opts;
|
|
3147
3340
|
try {
|
|
3148
3341
|
await withIndex({ platformDir, dbPath, build: fresh ? "fresh" : "missing" }, async (h) => {
|
|
3149
3342
|
for (const w of h.warnings)
|
|
@@ -3152,6 +3345,7 @@ async function withReadStorage(opts, fn) {
|
|
|
3152
3345
|
});
|
|
3153
3346
|
} catch (e) {
|
|
3154
3347
|
handleStorageUnavailable(e, dbPath);
|
|
3348
|
+
handleInvalidDomainFiles(e, json);
|
|
3155
3349
|
handleNotAPlatform(e);
|
|
3156
3350
|
}
|
|
3157
3351
|
}
|
|
@@ -3358,7 +3552,7 @@ function listFlag(rawArgs, name, parsed) {
|
|
|
3358
3552
|
}
|
|
3359
3553
|
return raw.flatMap((v) => v.split(",")).map((v) => v.trim()).filter((v) => v !== "");
|
|
3360
3554
|
}
|
|
3361
|
-
var platformDirArg, outArg, jsonArg, freshArg, noPromptArg, livesArg;
|
|
3555
|
+
var platformDirArg, outArg, jsonArg, freshArg, noPromptArg, livesArg, relatesArg;
|
|
3362
3556
|
var init__args = __esm(() => {
|
|
3363
3557
|
init_constants();
|
|
3364
3558
|
platformDirArg = {
|
|
@@ -3383,6 +3577,10 @@ var init__args = __esm(() => {
|
|
|
3383
3577
|
type: "string",
|
|
3384
3578
|
description: "Lives in (livesIn) paths: repeat the flag or comma-separate; an empty value clears the list"
|
|
3385
3579
|
};
|
|
3580
|
+
relatesArg = {
|
|
3581
|
+
type: "string",
|
|
3582
|
+
description: "Related requirement ids (relates): repeat the flag or comma-separate; an empty value clears the list"
|
|
3583
|
+
};
|
|
3386
3584
|
});
|
|
3387
3585
|
|
|
3388
3586
|
// src/commands/index.ts
|
|
@@ -3457,6 +3655,7 @@ var init_commands = __esm(() => {
|
|
|
3457
3655
|
console.error(formatNotASpecPlatform(err.platformDir));
|
|
3458
3656
|
process.exit(EXIT.USAGE);
|
|
3459
3657
|
}
|
|
3658
|
+
handleInvalidDomainFiles(err, !!args.json);
|
|
3460
3659
|
console.error("spec index FAILED:", err);
|
|
3461
3660
|
process.exit(EXIT.FAILURE);
|
|
3462
3661
|
} finally {
|
|
@@ -3466,49 +3665,6 @@ var init_commands = __esm(() => {
|
|
|
3466
3665
|
});
|
|
3467
3666
|
});
|
|
3468
3667
|
|
|
3469
|
-
// src/check/format.ts
|
|
3470
|
-
function compareRepoNullsLast(a, b) {
|
|
3471
|
-
if (a === b)
|
|
3472
|
-
return 0;
|
|
3473
|
-
if (a === null)
|
|
3474
|
-
return 1;
|
|
3475
|
-
if (b === null)
|
|
3476
|
-
return -1;
|
|
3477
|
-
return a.localeCompare(b);
|
|
3478
|
-
}
|
|
3479
|
-
function compareDiagnostics(a, b) {
|
|
3480
|
-
const codeCmp = a.code.localeCompare(b.code);
|
|
3481
|
-
if (codeCmp !== 0)
|
|
3482
|
-
return codeCmp;
|
|
3483
|
-
const repoCmp = compareRepoNullsLast(a.repo, b.repo);
|
|
3484
|
-
if (repoCmp !== 0)
|
|
3485
|
-
return repoCmp;
|
|
3486
|
-
const fileCmp = (a.source_file ?? "").localeCompare(b.source_file ?? "");
|
|
3487
|
-
if (fileCmp !== 0)
|
|
3488
|
-
return fileCmp;
|
|
3489
|
-
const lineCmp = (a.line ?? 0) - (b.line ?? 0);
|
|
3490
|
-
if (lineCmp !== 0)
|
|
3491
|
-
return lineCmp;
|
|
3492
|
-
return (a.req_id ?? "").localeCompare(b.req_id ?? "");
|
|
3493
|
-
}
|
|
3494
|
-
function sortDiagnostics(rows) {
|
|
3495
|
-
return [...rows].sort(compareDiagnostics);
|
|
3496
|
-
}
|
|
3497
|
-
function renderDiagnostics(rows, mode) {
|
|
3498
|
-
const sorted = sortDiagnostics(rows);
|
|
3499
|
-
if (mode === "json") {
|
|
3500
|
-
return JSON.stringify(sorted);
|
|
3501
|
-
}
|
|
3502
|
-
return sorted.map((d) => {
|
|
3503
|
-
const repo = d.repo ?? "";
|
|
3504
|
-
const file = d.source_file ?? "";
|
|
3505
|
-
const line = d.line ?? "";
|
|
3506
|
-
const reqId = d.req_id ?? "";
|
|
3507
|
-
return `${d.code} ${repo} ${file}:${line} ${reqId} ${d.detail}`;
|
|
3508
|
-
}).join(`
|
|
3509
|
-
`);
|
|
3510
|
-
}
|
|
3511
|
-
|
|
3512
3668
|
// src/check/proven.ts
|
|
3513
3669
|
function normalizeToSegments(path) {
|
|
3514
3670
|
const out = [];
|
|
@@ -3691,6 +3847,14 @@ function gitDiffNameStatus(platformDir, ref) {
|
|
|
3691
3847
|
}
|
|
3692
3848
|
return rows;
|
|
3693
3849
|
}
|
|
3850
|
+
function newestReleaseTag(platformDir) {
|
|
3851
|
+
const proc = Bun.spawnSync(["git", "tag", "--list", "v*", "--merged", "HEAD", "--sort=-v:refname"], { cwd: platformDir });
|
|
3852
|
+
if (proc.exitCode !== 0)
|
|
3853
|
+
return null;
|
|
3854
|
+
const tag = proc.stdout.toString().split(`
|
|
3855
|
+
`)[0]?.trim() ?? "";
|
|
3856
|
+
return tag !== "" && isSafeRef(tag) ? tag : null;
|
|
3857
|
+
}
|
|
3694
3858
|
|
|
3695
3859
|
// src/check/codeowners.ts
|
|
3696
3860
|
function parseCodeowners(text) {
|
|
@@ -4063,6 +4227,60 @@ var init_removed = __esm(() => {
|
|
|
4063
4227
|
init_src();
|
|
4064
4228
|
});
|
|
4065
4229
|
|
|
4230
|
+
// src/check/severity.ts
|
|
4231
|
+
import { existsSync as existsSync7, readFileSync } from "fs";
|
|
4232
|
+
import { join as join8 } from "path";
|
|
4233
|
+
function readCheckConfig(platformDir) {
|
|
4234
|
+
const path = join8(platformDir, CHECK_CONFIG_REL);
|
|
4235
|
+
if (!existsSync7(path))
|
|
4236
|
+
return { ok: true, config: { severity: [] } };
|
|
4237
|
+
return parseCheckConfigText(readFileSync(path, "utf-8"), CHECK_CONFIG_REL);
|
|
4238
|
+
}
|
|
4239
|
+
function specificity(rule, row) {
|
|
4240
|
+
if (rule.code !== row.code)
|
|
4241
|
+
return Specificity.none;
|
|
4242
|
+
if (rule.match === undefined)
|
|
4243
|
+
return Specificity.code;
|
|
4244
|
+
const id = row.req_id;
|
|
4245
|
+
if (id === null)
|
|
4246
|
+
return Specificity.none;
|
|
4247
|
+
if (rule.match.includes(id))
|
|
4248
|
+
return Specificity.id;
|
|
4249
|
+
const key = id.slice(0, id.lastIndexOf("-"));
|
|
4250
|
+
return rule.match.includes(key) ? Specificity.domain : Specificity.none;
|
|
4251
|
+
}
|
|
4252
|
+
function applySeverity(rows, rules) {
|
|
4253
|
+
const out = [];
|
|
4254
|
+
for (const row of rows) {
|
|
4255
|
+
let best;
|
|
4256
|
+
let bestScore = Specificity.none;
|
|
4257
|
+
for (const rule of rules) {
|
|
4258
|
+
const score = specificity(rule, row);
|
|
4259
|
+
if (score !== Specificity.none && score >= bestScore) {
|
|
4260
|
+
best = rule;
|
|
4261
|
+
bestScore = score;
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4264
|
+
if (best === undefined)
|
|
4265
|
+
out.push(row);
|
|
4266
|
+
else if (best.severity !== "off")
|
|
4267
|
+
out.push({ ...row, severity: best.severity });
|
|
4268
|
+
}
|
|
4269
|
+
return out;
|
|
4270
|
+
}
|
|
4271
|
+
var CHECK_CONFIG_REL, Specificity;
|
|
4272
|
+
var init_severity = __esm(() => {
|
|
4273
|
+
init_src();
|
|
4274
|
+
init_constants();
|
|
4275
|
+
CHECK_CONFIG_REL = `${CANONICAL_SPECS_DIR}/check.json`;
|
|
4276
|
+
Specificity = {
|
|
4277
|
+
none: -1,
|
|
4278
|
+
code: 0,
|
|
4279
|
+
domain: 1,
|
|
4280
|
+
id: 2
|
|
4281
|
+
};
|
|
4282
|
+
});
|
|
4283
|
+
|
|
4066
4284
|
// src/check/sqlDiagnostics.ts
|
|
4067
4285
|
function collectDiagnostics(storage2) {
|
|
4068
4286
|
const semantic = storage2.listSemanticDiagnostics();
|
|
@@ -4501,7 +4719,7 @@ function localToday() {
|
|
|
4501
4719
|
}
|
|
4502
4720
|
|
|
4503
4721
|
// src/operations/_envelope.ts
|
|
4504
|
-
import { existsSync as
|
|
4722
|
+
import { existsSync as existsSync8 } from "fs";
|
|
4505
4723
|
function domainKeyOf(id) {
|
|
4506
4724
|
return id.slice(0, id.indexOf("-"));
|
|
4507
4725
|
}
|
|
@@ -4510,7 +4728,7 @@ function displayStatus(raw) {
|
|
|
4510
4728
|
}
|
|
4511
4729
|
async function readEnvelope(platformDir, key, missingDetail) {
|
|
4512
4730
|
const { abs: specPath, rel: relFile } = specPaths(platformDir, key);
|
|
4513
|
-
if (!
|
|
4731
|
+
if (!existsSync8(specPath)) {
|
|
4514
4732
|
return fail("not_found", missingDetail ?? `no domain ${key} (expected ${relFile} under ${platformDir})`);
|
|
4515
4733
|
}
|
|
4516
4734
|
const parsed = parseDomainText(await Bun.file(specPath).text(), relFile);
|
|
@@ -4526,32 +4744,58 @@ async function readEnvelope(platformDir, key, missingDetail) {
|
|
|
4526
4744
|
const domain2 = parsed.data;
|
|
4527
4745
|
return { ok: true, key, specPath, relFile, domain: domain2, requirements: domain2.requirements };
|
|
4528
4746
|
}
|
|
4747
|
+
async function scanForEntry(platformDir, id, skipKey) {
|
|
4748
|
+
for (const key of listDomainKeys(platformDir)) {
|
|
4749
|
+
if (key === skipKey)
|
|
4750
|
+
continue;
|
|
4751
|
+
const file = await readEnvelope(platformDir, key);
|
|
4752
|
+
if (!file.ok)
|
|
4753
|
+
continue;
|
|
4754
|
+
const req = file.requirements.find((r) => r.id === id);
|
|
4755
|
+
if (req !== undefined)
|
|
4756
|
+
return { ...file, req };
|
|
4757
|
+
}
|
|
4758
|
+
return null;
|
|
4759
|
+
}
|
|
4529
4760
|
async function locateEntry(platformDir, id, missingDomainDetail) {
|
|
4530
|
-
const
|
|
4531
|
-
|
|
4761
|
+
const prefixKey = domainKeyOf(id);
|
|
4762
|
+
const file = await readEnvelope(platformDir, prefixKey, missingDomainDetail);
|
|
4763
|
+
if (!file.ok && file.reason !== "not_found")
|
|
4532
4764
|
return file;
|
|
4533
|
-
const req = file.requirements.find((r) => r.id === id);
|
|
4534
|
-
if (req
|
|
4535
|
-
return
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
return {
|
|
4765
|
+
const req = file.ok ? file.requirements.find((r) => r.id === id) : undefined;
|
|
4766
|
+
if (file.ok && req !== undefined)
|
|
4767
|
+
return { ...file, req };
|
|
4768
|
+
const scanned = await scanForEntry(platformDir, id, prefixKey);
|
|
4769
|
+
if (scanned !== null)
|
|
4770
|
+
return scanned;
|
|
4771
|
+
return file.ok ? fail("not_found", `no entry ${id} in ${file.relFile}`) : file;
|
|
4772
|
+
}
|
|
4773
|
+
function splitList(values) {
|
|
4774
|
+
return values.flatMap((v) => v.split(",")).map((v) => v.trim()).filter((v) => v !== "");
|
|
4775
|
+
}
|
|
4776
|
+
function successorEntry(id, pred, fields) {
|
|
4777
|
+
const entry = {
|
|
4540
4778
|
id,
|
|
4541
4779
|
status: "active",
|
|
4542
4780
|
statement: fields.statement,
|
|
4543
4781
|
why: fields.why === "" ? null : fields.why,
|
|
4544
4782
|
supersedes: null,
|
|
4545
4783
|
supersededBy: null,
|
|
4546
|
-
relates:
|
|
4784
|
+
relates: fields.relates,
|
|
4547
4785
|
livesIn: fields.livesIn,
|
|
4548
4786
|
issues: fields.issue ? [{ role: "created", id: fields.issue }] : [],
|
|
4549
|
-
aliases: [],
|
|
4550
|
-
cites:
|
|
4787
|
+
aliases: [...pred.aliases],
|
|
4788
|
+
cites: pred.cites.map((c) => ({ ...c }))
|
|
4551
4789
|
};
|
|
4790
|
+
if (pred.term !== undefined)
|
|
4791
|
+
entry.term = pred.term;
|
|
4792
|
+
if (pred.section !== undefined)
|
|
4793
|
+
entry.section = pred.section;
|
|
4794
|
+
return entry;
|
|
4552
4795
|
}
|
|
4553
4796
|
var init__envelope = __esm(() => {
|
|
4554
4797
|
init_src();
|
|
4798
|
+
init_domains();
|
|
4555
4799
|
init_constants();
|
|
4556
4800
|
init__result();
|
|
4557
4801
|
});
|
|
@@ -4606,7 +4850,7 @@ var init_grammar2 = __esm(() => {
|
|
|
4606
4850
|
});
|
|
4607
4851
|
|
|
4608
4852
|
// src/operations/mint.ts
|
|
4609
|
-
import { existsSync as
|
|
4853
|
+
import { existsSync as existsSync9 } from "fs";
|
|
4610
4854
|
function unresolvableRefWarnings(platformDir, fieldValues) {
|
|
4611
4855
|
const out = [];
|
|
4612
4856
|
for (const value of fieldValues) {
|
|
@@ -4621,7 +4865,7 @@ function unresolvableRefWarnings(platformDir, fieldValues) {
|
|
|
4621
4865
|
async function mint(input) {
|
|
4622
4866
|
const { platformDir, key } = input;
|
|
4623
4867
|
const { abs: specPath, rel: relFile } = specPaths(platformDir, key);
|
|
4624
|
-
if (!
|
|
4868
|
+
if (!existsSync9(specPath)) {
|
|
4625
4869
|
return fail("not_found", `no domain ${key} (expected ${relFile} under ${platformDir})`);
|
|
4626
4870
|
}
|
|
4627
4871
|
const warnings = unresolvableRefWarnings(platformDir, [
|
|
@@ -4649,7 +4893,7 @@ async function mint(input) {
|
|
|
4649
4893
|
why: input.why || null,
|
|
4650
4894
|
supersedes: null,
|
|
4651
4895
|
supersededBy: null,
|
|
4652
|
-
relates: input.relates ?? [],
|
|
4896
|
+
relates: splitList(input.relates ?? []),
|
|
4653
4897
|
livesIn: input.livesIn,
|
|
4654
4898
|
issues: input.issue ? [{ role: "created", id: input.issue }] : [],
|
|
4655
4899
|
aliases: [],
|
|
@@ -4808,8 +5052,8 @@ var init_term = __esm(() => {
|
|
|
4808
5052
|
});
|
|
4809
5053
|
|
|
4810
5054
|
// src/operations/glossary.ts
|
|
4811
|
-
import { existsSync as
|
|
4812
|
-
import { join as
|
|
5055
|
+
import { existsSync as existsSync10, readFileSync as readFileSync2 } from "fs";
|
|
5056
|
+
import { join as join9 } from "path";
|
|
4813
5057
|
function parseGlossary(md) {
|
|
4814
5058
|
const terms = [];
|
|
4815
5059
|
let section = null;
|
|
@@ -4861,15 +5105,15 @@ function generateGlossary(terms) {
|
|
|
4861
5105
|
}
|
|
4862
5106
|
function readStoreTerms(platformDir) {
|
|
4863
5107
|
const { abs: specPath, rel: relFile } = specPaths(platformDir, TERM_KEY);
|
|
4864
|
-
if (!
|
|
5108
|
+
if (!existsSync10(specPath))
|
|
4865
5109
|
return [];
|
|
4866
|
-
const parsed = parseDomainText(
|
|
5110
|
+
const parsed = parseDomainText(readFileSync2(specPath, "utf8"), relFile);
|
|
4867
5111
|
if (!parsed.ok)
|
|
4868
5112
|
return [];
|
|
4869
5113
|
return parsed.data.requirements.filter((r) => r.status.toLowerCase() === "active").sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0).map((r) => ({ term: r.term ?? "", statement: r.statement, section: r.section ?? null }));
|
|
4870
5114
|
}
|
|
4871
5115
|
function glossaryPath(platformDir) {
|
|
4872
|
-
return
|
|
5116
|
+
return join9(platformDir, "GLOSSARY.md");
|
|
4873
5117
|
}
|
|
4874
5118
|
async function migrateGlossary(platformDir) {
|
|
4875
5119
|
const store = await termStore(platformDir);
|
|
@@ -4880,10 +5124,10 @@ async function migrateGlossary(platformDir) {
|
|
|
4880
5124
|
return { ok: true, skipped: true, existing: domain2.requirements.length };
|
|
4881
5125
|
}
|
|
4882
5126
|
const gPath = glossaryPath(platformDir);
|
|
4883
|
-
if (!
|
|
5127
|
+
if (!existsSync10(gPath)) {
|
|
4884
5128
|
return fail("not_found", `no GLOSSARY.md to migrate (expected ${gPath})`);
|
|
4885
5129
|
}
|
|
4886
|
-
const parsed = parseGlossary(
|
|
5130
|
+
const parsed = parseGlossary(readFileSync2(gPath, "utf8"));
|
|
4887
5131
|
domain2.requirements = parsed.map((t, i) => ({
|
|
4888
5132
|
id: `${TERM_KEY}-${String(i + 1).padStart(3, "0")}`,
|
|
4889
5133
|
status: "active",
|
|
@@ -4920,10 +5164,10 @@ function glossaryDriftDiagnostic(platformDir) {
|
|
|
4920
5164
|
if (terms.length === 0)
|
|
4921
5165
|
return null;
|
|
4922
5166
|
const gPath = glossaryPath(platformDir);
|
|
4923
|
-
if (!
|
|
5167
|
+
if (!existsSync10(gPath))
|
|
4924
5168
|
return null;
|
|
4925
5169
|
const generated = generateGlossary(terms);
|
|
4926
|
-
const committed =
|
|
5170
|
+
const committed = readFileSync2(gPath, "utf8");
|
|
4927
5171
|
if (generated === committed)
|
|
4928
5172
|
return null;
|
|
4929
5173
|
return {
|
|
@@ -4936,30 +5180,224 @@ function glossaryDriftDiagnostic(platformDir) {
|
|
|
4936
5180
|
detail: "GLOSSARY.md does not match the TERM store \u2014 run `spec glossary .` to regenerate the human view"
|
|
4937
5181
|
};
|
|
4938
5182
|
}
|
|
4939
|
-
function checkGlossary(platformDir) {
|
|
4940
|
-
const generated = generateGlossary(readStoreTerms(platformDir));
|
|
4941
|
-
const gPath = glossaryPath(platformDir);
|
|
4942
|
-
const committed =
|
|
4943
|
-
return { ok: true, clean: generated === committed };
|
|
5183
|
+
function checkGlossary(platformDir) {
|
|
5184
|
+
const generated = generateGlossary(readStoreTerms(platformDir));
|
|
5185
|
+
const gPath = glossaryPath(platformDir);
|
|
5186
|
+
const committed = existsSync10(gPath) ? readFileSync2(gPath, "utf8") : "";
|
|
5187
|
+
return { ok: true, clean: generated === committed };
|
|
5188
|
+
}
|
|
5189
|
+
var GLOSSARY_INTRO = `Canonical names for Spec Engine concepts. When prose, code comments, diagnostics, or
|
|
5190
|
+
docs need one of these ideas, use the term below \u2014 not a synonym. Terms are ordered
|
|
5191
|
+
by the data model, outside-in.`;
|
|
5192
|
+
var init_glossary = __esm(() => {
|
|
5193
|
+
init_src();
|
|
5194
|
+
init_constants();
|
|
5195
|
+
init__result();
|
|
5196
|
+
init_term();
|
|
5197
|
+
});
|
|
5198
|
+
|
|
5199
|
+
// src/guard/collect.ts
|
|
5200
|
+
import { join as join10 } from "path";
|
|
5201
|
+
function isIgnored(path) {
|
|
5202
|
+
return isPathIgnored(path);
|
|
5203
|
+
}
|
|
5204
|
+
function isSpecFile(path) {
|
|
5205
|
+
return path.startsWith(`${CANONICAL_SPECS_DIR}/`) && path.endsWith("/SPEC.json") && !isIgnored(path);
|
|
5206
|
+
}
|
|
5207
|
+
function isCodeFile(path) {
|
|
5208
|
+
return !isSpecFile(path) && !isIgnored(path) && isTagSource(path);
|
|
5209
|
+
}
|
|
5210
|
+
function parseReqs(text) {
|
|
5211
|
+
const v = parseDomainText(text, "");
|
|
5212
|
+
return v.ok ? v.data.requirements : [];
|
|
5213
|
+
}
|
|
5214
|
+
function collectBaseReqs(platformDir, ref, specPaths2) {
|
|
5215
|
+
const reqs = [];
|
|
5216
|
+
const path = new Map;
|
|
5217
|
+
for (const p of specPaths2) {
|
|
5218
|
+
const bytes = gitShow(platformDir, ref, p);
|
|
5219
|
+
if (bytes === null)
|
|
5220
|
+
continue;
|
|
5221
|
+
for (const r of parseReqs(bytes)) {
|
|
5222
|
+
reqs.push(r);
|
|
5223
|
+
path.set(r.id, p);
|
|
5224
|
+
}
|
|
5225
|
+
}
|
|
5226
|
+
return { reqs, path };
|
|
5227
|
+
}
|
|
5228
|
+
function collectBaseTags(platformDir, ref, codePaths) {
|
|
5229
|
+
const implSite = new Map;
|
|
5230
|
+
const verifySite = new Map;
|
|
5231
|
+
for (const p of codePaths) {
|
|
5232
|
+
const bytes = gitShow(platformDir, ref, p);
|
|
5233
|
+
if (bytes === null)
|
|
5234
|
+
continue;
|
|
5235
|
+
for (const tag of scanTagsInFile("", p, bytes)) {
|
|
5236
|
+
const target = tag.kind === "verifies" ? verifySite : implSite;
|
|
5237
|
+
if (!target.has(tag.req_id))
|
|
5238
|
+
target.set(tag.req_id, { file: tag.file, line: tag.line });
|
|
5239
|
+
}
|
|
5240
|
+
}
|
|
5241
|
+
return { implSite, verifySite };
|
|
5242
|
+
}
|
|
5243
|
+
async function collectWorktreeReqs(canonicalDir) {
|
|
5244
|
+
const ids = new Set;
|
|
5245
|
+
const activeIds = new Set;
|
|
5246
|
+
const supersedesTargets = new Set;
|
|
5247
|
+
for (const rel of await findDomainJsonFiles(canonicalDir)) {
|
|
5248
|
+
const text = await Bun.file(join10(canonicalDir, rel)).text();
|
|
5249
|
+
for (const r of parseReqs(text)) {
|
|
5250
|
+
ids.add(r.id);
|
|
5251
|
+
if (r.status.toLowerCase() === "active")
|
|
5252
|
+
activeIds.add(r.id);
|
|
5253
|
+
if (r.supersedes != null)
|
|
5254
|
+
supersedesTargets.add(r.supersedes);
|
|
5255
|
+
}
|
|
5256
|
+
}
|
|
5257
|
+
return { ids, activeIds, supersedesTargets };
|
|
5258
|
+
}
|
|
5259
|
+
function tallyWorktreeTags(storage2) {
|
|
5260
|
+
const implCount = new Map;
|
|
5261
|
+
const verifyCount = new Map;
|
|
5262
|
+
for (const t of storage2.listTags()) {
|
|
5263
|
+
const bucket = t.kind === "implements" ? implCount : t.kind === "verifies" ? verifyCount : null;
|
|
5264
|
+
if (bucket !== null)
|
|
5265
|
+
bucket.set(t.req_id, (bucket.get(t.req_id) ?? 0) + 1);
|
|
5266
|
+
}
|
|
5267
|
+
return { implCount, verifyCount };
|
|
5268
|
+
}
|
|
5269
|
+
async function collectFacts(platformDir, ref, storage2) {
|
|
5270
|
+
const changed = gitDiffNameStatus(platformDir, ref);
|
|
5271
|
+
const specPaths2 = changed.filter((e) => isSpecFile(e.path)).map((e) => e.path);
|
|
5272
|
+
const codePaths = changed.filter((e) => isCodeFile(e.path)).map((e) => e.path);
|
|
5273
|
+
const deletedSpecFiles = changed.filter((e) => e.status === "D" && isSpecFile(e.path)).map((e) => e.path);
|
|
5274
|
+
const base = collectBaseReqs(platformDir, ref, specPaths2);
|
|
5275
|
+
const baseTags = collectBaseTags(platformDir, ref, codePaths);
|
|
5276
|
+
const worktree = await collectWorktreeReqs(join10(platformDir, CANONICAL_SPECS_DIR));
|
|
5277
|
+
const counts = tallyWorktreeTags(storage2);
|
|
5278
|
+
return {
|
|
5279
|
+
baseReqs: base.reqs,
|
|
5280
|
+
baseReqPath: base.path,
|
|
5281
|
+
baseImplSite: baseTags.implSite,
|
|
5282
|
+
baseVerifySite: baseTags.verifySite,
|
|
5283
|
+
worktreeReqIds: worktree.ids,
|
|
5284
|
+
worktreeActiveIds: worktree.activeIds,
|
|
5285
|
+
worktreeSupersedesTargets: worktree.supersedesTargets,
|
|
5286
|
+
worktreeImplCount: counts.implCount,
|
|
5287
|
+
worktreeVerifyCount: counts.verifyCount,
|
|
5288
|
+
deletedSpecFiles
|
|
5289
|
+
};
|
|
5290
|
+
}
|
|
5291
|
+
var init_collect = __esm(() => {
|
|
5292
|
+
init_src();
|
|
5293
|
+
init_constants();
|
|
5294
|
+
init_fs();
|
|
5295
|
+
init_tags();
|
|
5296
|
+
});
|
|
5297
|
+
|
|
5298
|
+
// src/guard/losses.ts
|
|
5299
|
+
function isActive(status) {
|
|
5300
|
+
return status.toLowerCase() === "active";
|
|
5301
|
+
}
|
|
5302
|
+
function isSuperseded(base, f) {
|
|
5303
|
+
const forward = base.supersededBy != null && f.worktreeReqIds.has(base.supersededBy);
|
|
5304
|
+
return forward || f.worktreeSupersedesTargets.has(base.id);
|
|
5305
|
+
}
|
|
5306
|
+
function reqDeletedLoss(base, f) {
|
|
5307
|
+
return {
|
|
5308
|
+
kind: "REQUIREMENT_REMOVED",
|
|
5309
|
+
req_id: base.id,
|
|
5310
|
+
file: f.baseReqPath.get(base.id) ?? "",
|
|
5311
|
+
line: 0,
|
|
5312
|
+
detail: `${base.id} was Active at the ref but is absent from the working-tree spec and was not superseded \u2014 a requirement is never deleted`
|
|
5313
|
+
};
|
|
5314
|
+
}
|
|
5315
|
+
function historyRemovedLoss(base, f) {
|
|
5316
|
+
const display = base.status.charAt(0).toUpperCase() + base.status.slice(1).toLowerCase();
|
|
5317
|
+
return {
|
|
5318
|
+
kind: "REQUIREMENT_REMOVED",
|
|
5319
|
+
req_id: base.id,
|
|
5320
|
+
file: f.baseReqPath.get(base.id) ?? "",
|
|
5321
|
+
line: 0,
|
|
5322
|
+
detail: `${base.id} was ${display} at the ref but is absent from the working-tree spec \u2014 a requirement is never deleted, and its history is the record`
|
|
5323
|
+
};
|
|
5324
|
+
}
|
|
5325
|
+
function implLostLoss(id, site) {
|
|
5326
|
+
return {
|
|
5327
|
+
kind: "IMPL_LOST",
|
|
5328
|
+
req_id: id,
|
|
5329
|
+
file: site.file,
|
|
5330
|
+
line: site.line,
|
|
5331
|
+
detail: `${id} lost its last implementing @spec tag (was ${site.file}:${site.line})`
|
|
5332
|
+
};
|
|
5333
|
+
}
|
|
5334
|
+
function verifyLostLoss(id, site) {
|
|
5335
|
+
return {
|
|
5336
|
+
kind: "VERIFY_LOST",
|
|
5337
|
+
req_id: id,
|
|
5338
|
+
file: site.file,
|
|
5339
|
+
line: site.line,
|
|
5340
|
+
detail: `${id} lost its last verifying @spec tag (was ${site.file}:${site.line})`
|
|
5341
|
+
};
|
|
5342
|
+
}
|
|
5343
|
+
function specFileDeletedLoss(path) {
|
|
5344
|
+
return {
|
|
5345
|
+
kind: "SPEC_FILE_DELETED",
|
|
5346
|
+
req_id: null,
|
|
5347
|
+
file: path,
|
|
5348
|
+
line: 0,
|
|
5349
|
+
detail: `canonical spec file ${path} present at the ref is absent from the working tree`
|
|
5350
|
+
};
|
|
5351
|
+
}
|
|
5352
|
+
function reqDeletedFor(base, f) {
|
|
5353
|
+
if (f.worktreeReqIds.has(base.id))
|
|
5354
|
+
return null;
|
|
5355
|
+
if (!isActive(base.status))
|
|
5356
|
+
return historyRemovedLoss(base, f);
|
|
5357
|
+
if (isSuperseded(base, f))
|
|
5358
|
+
return null;
|
|
5359
|
+
return reqDeletedLoss(base, f);
|
|
5360
|
+
}
|
|
5361
|
+
function lastTagGone(id, count, f) {
|
|
5362
|
+
return f.worktreeActiveIds.has(id) && count === 0;
|
|
5363
|
+
}
|
|
5364
|
+
function classifyLosses(f) {
|
|
5365
|
+
const out = [];
|
|
5366
|
+
for (const base of f.baseReqs) {
|
|
5367
|
+
const loss = reqDeletedFor(base, f);
|
|
5368
|
+
if (loss !== null)
|
|
5369
|
+
out.push(loss);
|
|
5370
|
+
}
|
|
5371
|
+
for (const [id, site] of f.baseImplSite) {
|
|
5372
|
+
if (lastTagGone(id, f.worktreeImplCount.get(id) ?? 0, f))
|
|
5373
|
+
out.push(implLostLoss(id, site));
|
|
5374
|
+
}
|
|
5375
|
+
for (const [id, site] of f.baseVerifySite) {
|
|
5376
|
+
if (lastTagGone(id, f.worktreeVerifyCount.get(id) ?? 0, f))
|
|
5377
|
+
out.push(verifyLostLoss(id, site));
|
|
5378
|
+
}
|
|
5379
|
+
for (const path of f.deletedSpecFiles)
|
|
5380
|
+
out.push(specFileDeletedLoss(path));
|
|
5381
|
+
return out;
|
|
5382
|
+
}
|
|
5383
|
+
|
|
5384
|
+
// src/operations/guard.ts
|
|
5385
|
+
async function guard(storage2, platformDir, ref) {
|
|
5386
|
+
const losses = classifyLosses(await collectFacts(platformDir, ref, storage2));
|
|
5387
|
+
return { ok: true, ref, losses };
|
|
4944
5388
|
}
|
|
4945
|
-
var
|
|
4946
|
-
|
|
4947
|
-
by the data model, outside-in.`;
|
|
4948
|
-
var init_glossary = __esm(() => {
|
|
4949
|
-
init_src();
|
|
4950
|
-
init_constants();
|
|
4951
|
-
init__result();
|
|
4952
|
-
init_term();
|
|
5389
|
+
var init_guard = __esm(() => {
|
|
5390
|
+
init_collect();
|
|
4953
5391
|
});
|
|
4954
5392
|
|
|
4955
5393
|
// src/operations/check.ts
|
|
4956
|
-
import { existsSync as
|
|
4957
|
-
import { join as
|
|
5394
|
+
import { existsSync as existsSync11, readFileSync as readFileSync3 } from "fs";
|
|
5395
|
+
import { join as join11 } from "path";
|
|
4958
5396
|
function readCodeownersText(platformDir) {
|
|
4959
|
-
const path =
|
|
4960
|
-
if (!
|
|
5397
|
+
const path = join11(platformDir, ".github", "CODEOWNERS");
|
|
5398
|
+
if (!existsSync11(path))
|
|
4961
5399
|
return null;
|
|
4962
|
-
return
|
|
5400
|
+
return readFileSync3(path, "utf-8");
|
|
4963
5401
|
}
|
|
4964
5402
|
function domainReqsFromText(text, platformRel) {
|
|
4965
5403
|
const validated = parseDomainText(text, platformRel);
|
|
@@ -4972,7 +5410,7 @@ async function collectChangeReqs(canonicalDir) {
|
|
|
4972
5410
|
const relPathById = new Map;
|
|
4973
5411
|
for (const rel of await findDomainJsonFiles(canonicalDir)) {
|
|
4974
5412
|
const platformRel = `${CANONICAL_SPECS_DIR}/${rel}`;
|
|
4975
|
-
const text = await Bun.file(
|
|
5413
|
+
const text = await Bun.file(join11(canonicalDir, rel)).text();
|
|
4976
5414
|
const parsed = domainReqsFromText(text, platformRel);
|
|
4977
5415
|
for (const r of parsed.reqs) {
|
|
4978
5416
|
reqs.push(r);
|
|
@@ -4981,7 +5419,7 @@ async function collectChangeReqs(canonicalDir) {
|
|
|
4981
5419
|
}
|
|
4982
5420
|
return { reqs, relPathById };
|
|
4983
5421
|
}
|
|
4984
|
-
function
|
|
5422
|
+
function collectBaseReqs2(platformDir, ref, diagnostics2) {
|
|
4985
5423
|
const reqs = [];
|
|
4986
5424
|
const relPathById = new Map;
|
|
4987
5425
|
for (const path of gitLsTree(platformDir, ref, CANONICAL_SPECS_DIR)) {
|
|
@@ -5013,8 +5451,8 @@ async function ingestResults(storage2, resultsPath, diagnostics2) {
|
|
|
5013
5451
|
}
|
|
5014
5452
|
async function governanceGate(input, ref, diagnostics2, results) {
|
|
5015
5453
|
const { platformDir } = input;
|
|
5016
|
-
const change = await collectChangeReqs(
|
|
5017
|
-
const base =
|
|
5454
|
+
const change = await collectChangeReqs(join11(platformDir, CANONICAL_SPECS_DIR));
|
|
5455
|
+
const base = collectBaseReqs2(platformDir, ref, diagnostics2);
|
|
5018
5456
|
diagnostics2.push(...requirementRemoved(base.reqs, change.reqs, (id) => base.relPathById.get(id) ?? null));
|
|
5019
5457
|
const codeowners = parseCodeowners(readCodeownersText(platformDir) ?? "");
|
|
5020
5458
|
const approvedBy = (input.approvedBy ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -5023,12 +5461,33 @@ async function governanceGate(input, ref, diagnostics2, results) {
|
|
|
5023
5461
|
diagnostics2.push(...partialPropagation(changedRules(base.reqs, change.reqs), results.verifyingTags, results.resultsParsed, (id) => change.relPathById.get(id) ?? null));
|
|
5024
5462
|
}
|
|
5025
5463
|
}
|
|
5464
|
+
async function lossDiagnostics(storage2, platformDir, ref) {
|
|
5465
|
+
const { losses } = await guard(storage2, platformDir, ref);
|
|
5466
|
+
const rows = [];
|
|
5467
|
+
for (const loss of losses) {
|
|
5468
|
+
if (loss.kind === "REQUIREMENT_REMOVED")
|
|
5469
|
+
continue;
|
|
5470
|
+
rows.push({
|
|
5471
|
+
code: loss.kind,
|
|
5472
|
+
severity: LOSS_SEVERITY[loss.kind],
|
|
5473
|
+
repo: null,
|
|
5474
|
+
source_file: loss.file === "" ? null : loss.file,
|
|
5475
|
+
line: loss.line,
|
|
5476
|
+
req_id: loss.req_id,
|
|
5477
|
+
detail: loss.detail
|
|
5478
|
+
});
|
|
5479
|
+
}
|
|
5480
|
+
return rows;
|
|
5481
|
+
}
|
|
5026
5482
|
async function check(input, storage2) {
|
|
5027
5483
|
const { platformDir } = input;
|
|
5028
5484
|
if (input.base && !gitRefResolves(platformDir, input.base)) {
|
|
5029
5485
|
return fail("usage", `--base ref '${input.base}' does not resolve in ${platformDir} ` + "(not a git repo, or the ref is unfetched/misspelled) \u2014 refusing to run the governance gate fail-open");
|
|
5030
5486
|
}
|
|
5031
5487
|
const result = await runIndex({ platformDir, storage: storage2 });
|
|
5488
|
+
const checkConfig2 = readCheckConfig(platformDir);
|
|
5489
|
+
if (!checkConfig2.ok)
|
|
5490
|
+
return fail("usage", checkConfig2.detail);
|
|
5032
5491
|
const diagnostics2 = collectDiagnostics(storage2);
|
|
5033
5492
|
if (input.unsourcedChange) {
|
|
5034
5493
|
const superseded = storage2.listRequirements({ status: "Superseded" });
|
|
@@ -5043,20 +5502,24 @@ async function check(input, storage2) {
|
|
|
5043
5502
|
const results = await ingestResults(storage2, input.resultsPath, diagnostics2);
|
|
5044
5503
|
if (input.base) {
|
|
5045
5504
|
await governanceGate(input, input.base, diagnostics2, results);
|
|
5505
|
+
diagnostics2.push(...await lossDiagnostics(storage2, platformDir, input.base));
|
|
5046
5506
|
} else if (gitRefResolves(platformDir, "HEAD")) {
|
|
5047
|
-
const change = await collectChangeReqs(
|
|
5507
|
+
const change = await collectChangeReqs(join11(platformDir, CANONICAL_SPECS_DIR));
|
|
5048
5508
|
const baseParseSink = [];
|
|
5049
|
-
const base =
|
|
5509
|
+
const base = collectBaseReqs2(platformDir, "HEAD", baseParseSink);
|
|
5050
5510
|
diagnostics2.push(...requirementRemoved(base.reqs, change.reqs, (id) => base.relPathById.get(id) ?? null));
|
|
5511
|
+
diagnostics2.push(...await lossDiagnostics(storage2, platformDir, "HEAD"));
|
|
5051
5512
|
}
|
|
5513
|
+
const graded = applySeverity(diagnostics2, checkConfig2.config.severity);
|
|
5052
5514
|
return {
|
|
5053
5515
|
ok: true,
|
|
5054
|
-
diagnostics:
|
|
5516
|
+
diagnostics: graded,
|
|
5055
5517
|
buildId: result.build_id,
|
|
5056
|
-
failing:
|
|
5518
|
+
failing: graded.some((d) => d.severity === "error"),
|
|
5057
5519
|
proofsUnconfirmed: !input.resultsPath
|
|
5058
5520
|
};
|
|
5059
5521
|
}
|
|
5522
|
+
var LOSS_SEVERITY;
|
|
5060
5523
|
var init_check = __esm(() => {
|
|
5061
5524
|
init_src();
|
|
5062
5525
|
init_filerefs2();
|
|
@@ -5064,6 +5527,7 @@ var init_check = __esm(() => {
|
|
|
5064
5527
|
init_propagation_teeth();
|
|
5065
5528
|
init_proven();
|
|
5066
5529
|
init_removed();
|
|
5530
|
+
init_severity();
|
|
5067
5531
|
init_sqlDiagnostics();
|
|
5068
5532
|
init_statusflip();
|
|
5069
5533
|
init_supersedes();
|
|
@@ -5074,6 +5538,12 @@ var init_check = __esm(() => {
|
|
|
5074
5538
|
init_fs();
|
|
5075
5539
|
init__result();
|
|
5076
5540
|
init_glossary();
|
|
5541
|
+
init_guard();
|
|
5542
|
+
LOSS_SEVERITY = {
|
|
5543
|
+
IMPL_LOST: "error",
|
|
5544
|
+
SPEC_FILE_DELETED: "error",
|
|
5545
|
+
VERIFY_LOST: "warning"
|
|
5546
|
+
};
|
|
5077
5547
|
});
|
|
5078
5548
|
|
|
5079
5549
|
// src/commands/check.ts
|
|
@@ -5123,17 +5593,17 @@ var init_check2 = __esm(() => {
|
|
|
5123
5593
|
out: outArg,
|
|
5124
5594
|
ci: {
|
|
5125
5595
|
type: "boolean",
|
|
5126
|
-
description: "Force cold rebuild: rm db+wal+shm BEFORE indexing
|
|
5596
|
+
description: "Force cold rebuild: rm db+wal+shm BEFORE indexing"
|
|
5127
5597
|
},
|
|
5128
5598
|
json: jsonArg,
|
|
5129
5599
|
noPrompt: noPromptArg,
|
|
5130
5600
|
unsourcedChange: {
|
|
5131
5601
|
type: "boolean",
|
|
5132
|
-
description: "Opt-in: emit warning-severity UNSOURCED_CHANGE for Superseded requirements lacking a supersedes-via issue (OFF by default
|
|
5602
|
+
description: "Opt-in: emit warning-severity UNSOURCED_CHANGE for Superseded requirements lacking a supersedes-via issue (OFF by default)"
|
|
5133
5603
|
},
|
|
5134
5604
|
results: {
|
|
5135
5605
|
type: "string",
|
|
5136
|
-
description: "Ingest a JUnit XML results file; enforce the trusted-red PROVEN gate
|
|
5606
|
+
description: "Ingest a JUnit XML results file; enforce the trusted-red PROVEN gate"
|
|
5137
5607
|
},
|
|
5138
5608
|
base: {
|
|
5139
5609
|
type: "string",
|
|
@@ -5184,6 +5654,7 @@ var init_check2 = __esm(() => {
|
|
|
5184
5654
|
return result;
|
|
5185
5655
|
});
|
|
5186
5656
|
} catch (e) {
|
|
5657
|
+
handleInvalidDomainFiles(e, jsonMode);
|
|
5187
5658
|
exitAfterCrash(e);
|
|
5188
5659
|
}
|
|
5189
5660
|
if (outcome === undefined) {
|
|
@@ -5207,8 +5678,8 @@ var init_check2 = __esm(() => {
|
|
|
5207
5678
|
});
|
|
5208
5679
|
|
|
5209
5680
|
// src/operations/domain.ts
|
|
5210
|
-
import { existsSync as
|
|
5211
|
-
import { dirname as dirname6, join as
|
|
5681
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync4 } from "fs";
|
|
5682
|
+
import { dirname as dirname6, join as join12, resolve as resolve9 } from "path";
|
|
5212
5683
|
function definedFields(options) {
|
|
5213
5684
|
const out = {};
|
|
5214
5685
|
if (options.scope !== undefined)
|
|
@@ -5228,9 +5699,9 @@ async function newDomain(platformDir, key, options = {}) {
|
|
|
5228
5699
|
if (!(resolvedDest === resolvedRoot || resolvedDest.startsWith(`${resolvedRoot}/`))) {
|
|
5229
5700
|
return fail("usage", `refusing to write outside platformDir (${resolvedDest})`);
|
|
5230
5701
|
}
|
|
5231
|
-
if (
|
|
5702
|
+
if (existsSync12(dest))
|
|
5232
5703
|
return fail("conflict", `refusing to overwrite ${dest}`);
|
|
5233
|
-
if (
|
|
5704
|
+
if (existsSync12(join12(dirname6(dest), "SPEC.md"))) {
|
|
5234
5705
|
return fail("conflict", `refusing to create ${dest}: domain ${key} already exists as spec-engine/${key}/SPEC.md ` + "\u2014 migrate the SPEC.md first (a fresh empty SPEC.json would shadow it on the next index)");
|
|
5235
5706
|
}
|
|
5236
5707
|
mkdirSync4(dirname6(dest), { recursive: true });
|
|
@@ -5528,7 +5999,7 @@ var init_map = __esm(() => {
|
|
|
5528
5999
|
noPrompt: args.noPrompt
|
|
5529
6000
|
}
|
|
5530
6001
|
});
|
|
5531
|
-
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, (storage2) => {
|
|
6002
|
+
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, (storage2) => {
|
|
5532
6003
|
const { rows } = coverageMatrix(storage2);
|
|
5533
6004
|
if (!args.json && rows.length === 0) {
|
|
5534
6005
|
console.error(formatNoRequirementsIndexed(platformDir));
|
|
@@ -5641,7 +6112,7 @@ var init_get = __esm(() => {
|
|
|
5641
6112
|
platformDir,
|
|
5642
6113
|
args: { noPrompt: args.noPrompt }
|
|
5643
6114
|
});
|
|
5644
|
-
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, (storage2) => {
|
|
6115
|
+
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, (storage2) => {
|
|
5645
6116
|
const result = getRecord(storage2, reqId);
|
|
5646
6117
|
if (result.row === null) {
|
|
5647
6118
|
console.error(result.platformEmpty ? formatNoRequirementsIndexed(platformDir) : `spec get: no requirement ${reqId} in the index under ${platformDir} (check the id, or re-run with --fresh after editing specs)`);
|
|
@@ -5715,7 +6186,7 @@ var init_list = __esm(() => {
|
|
|
5715
6186
|
platformDir,
|
|
5716
6187
|
args: { noPrompt: args.noPrompt }
|
|
5717
6188
|
});
|
|
5718
|
-
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, (storage2) => {
|
|
6189
|
+
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, (storage2) => {
|
|
5719
6190
|
const result = listRecords(storage2, filter);
|
|
5720
6191
|
if (!args.json && result.rows.length === 0) {
|
|
5721
6192
|
console.error(result.platformEmpty ? formatNoRequirementsIndexed(platformDir) : `spec list: no requirements match under ${platformDir}`);
|
|
@@ -5792,7 +6263,7 @@ var init_propagation2 = __esm(() => {
|
|
|
5792
6263
|
platformDir,
|
|
5793
6264
|
args: { noPrompt: args.noPrompt }
|
|
5794
6265
|
});
|
|
5795
|
-
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, (storage2) => {
|
|
6266
|
+
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, (storage2) => {
|
|
5796
6267
|
const result = propagation2(storage2, reqId);
|
|
5797
6268
|
if (!args.json && result.platformEmpty) {
|
|
5798
6269
|
console.error(formatNoRequirementsIndexed(platformDir));
|
|
@@ -5915,7 +6386,7 @@ var init_query = __esm(() => {
|
|
|
5915
6386
|
platformDir,
|
|
5916
6387
|
args: { noPrompt: args.noPrompt }
|
|
5917
6388
|
});
|
|
5918
|
-
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, (storage2) => {
|
|
6389
|
+
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, (storage2) => {
|
|
5919
6390
|
const result = query(storage2, text, limit);
|
|
5920
6391
|
if (!result.ok) {
|
|
5921
6392
|
console.error(`spec query: ${result.detail}`);
|
|
@@ -6037,7 +6508,7 @@ var init_relations = __esm(() => {
|
|
|
6037
6508
|
noPrompt: args.noPrompt
|
|
6038
6509
|
}
|
|
6039
6510
|
});
|
|
6040
|
-
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, (storage2) => {
|
|
6511
|
+
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, (storage2) => {
|
|
6041
6512
|
const { rows } = relations(storage2);
|
|
6042
6513
|
if (!args.json && rows.length === 0) {
|
|
6043
6514
|
console.error(formatNoRelations(platformDir));
|
|
@@ -6162,7 +6633,7 @@ var init_adapter = () => {};
|
|
|
6162
6633
|
|
|
6163
6634
|
// ../tracker/src/cache.ts
|
|
6164
6635
|
import { mkdirSync as mkdirSync5 } from "fs";
|
|
6165
|
-
import { dirname as dirname7, join as
|
|
6636
|
+
import { dirname as dirname7, join as join13 } from "path";
|
|
6166
6637
|
function isFresh(entry, now, ttlMs = CACHE_TTL_MS) {
|
|
6167
6638
|
const t = Date.parse(entry.fetched_at);
|
|
6168
6639
|
if (Number.isNaN(t))
|
|
@@ -6176,7 +6647,7 @@ function isValidEntry(e) {
|
|
|
6176
6647
|
return typeof r.title === "string" && typeof r.status === "string" && typeof r.url === "string" && typeof r.fetched_at === "string";
|
|
6177
6648
|
}
|
|
6178
6649
|
function sidecarPath(platformDir) {
|
|
6179
|
-
return
|
|
6650
|
+
return join13(platformDir, ".spec-engine", SIDECAR_FILE);
|
|
6180
6651
|
}
|
|
6181
6652
|
async function readCache(platformDir) {
|
|
6182
6653
|
try {
|
|
@@ -6443,7 +6914,7 @@ var init_provenance = __esm(() => {
|
|
|
6443
6914
|
noPrompt: args.noPrompt
|
|
6444
6915
|
}
|
|
6445
6916
|
});
|
|
6446
|
-
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, async (storage2) => {
|
|
6917
|
+
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, async (storage2) => {
|
|
6447
6918
|
const { rows } = provenance(storage2, issueId || undefined);
|
|
6448
6919
|
if (!args.json && rows.length === 0) {
|
|
6449
6920
|
console.error(issueId ? formatNoProvenanceForIssue(issueId, platformDir) : formatNoProvenance(platformDir));
|
|
@@ -6507,6 +6978,7 @@ async function mintFromFlags(platformDir, key, textFlag, args, rawArgs) {
|
|
|
6507
6978
|
statement,
|
|
6508
6979
|
why: (args.why ?? "").trim(),
|
|
6509
6980
|
livesIn: listFlag(rawArgs, "lives", args.lives) ?? [],
|
|
6981
|
+
relates: listFlag(rawArgs, "relates", args.relates) ?? [],
|
|
6510
6982
|
issue: (args.issue ?? "").trim() || undefined,
|
|
6511
6983
|
status: statusOf(args),
|
|
6512
6984
|
json: Boolean(args.json)
|
|
@@ -6585,6 +7057,7 @@ var init_req = __esm(() => {
|
|
|
6585
7057
|
},
|
|
6586
7058
|
why: { type: "string", description: "Why it matters field (only with --text)" },
|
|
6587
7059
|
lives: livesArg,
|
|
7060
|
+
relates: relatesArg,
|
|
6588
7061
|
issue: {
|
|
6589
7062
|
type: "string",
|
|
6590
7063
|
description: "Originating ticket id, recorded as created-provenance on the entry (only with --text; opaque \u2014 never a requirement id)"
|
|
@@ -6617,9 +7090,9 @@ var init_req = __esm(() => {
|
|
|
6617
7090
|
await mintFromFlags(platformDir, key, textFlag, args, rawArgs);
|
|
6618
7091
|
return;
|
|
6619
7092
|
}
|
|
6620
|
-
const hasFieldFlags = typeof args.why === "string" || typeof args.lives === "string" || typeof args.issue === "string";
|
|
7093
|
+
const hasFieldFlags = typeof args.why === "string" || typeof args.lives === "string" || typeof args.relates === "string" || typeof args.issue === "string";
|
|
6621
7094
|
if (hasFieldFlags) {
|
|
6622
|
-
console.error("spec req: --why/--lives/--issue require --text (the Requirement field is mandatory)");
|
|
7095
|
+
console.error("spec req: --why/--lives/--relates/--issue require --text (the Requirement field is mandatory)");
|
|
6623
7096
|
process.exit(EXIT.USAGE);
|
|
6624
7097
|
return;
|
|
6625
7098
|
}
|
|
@@ -6755,6 +7228,9 @@ async function confirmTermCommand(opts) {
|
|
|
6755
7228
|
exitOnFailure("spec term confirm", result);
|
|
6756
7229
|
await withIndex({ platformDir, build: "fresh" }, () => {
|
|
6757
7230
|
return;
|
|
7231
|
+
}).catch((e) => {
|
|
7232
|
+
handleInvalidDomainFiles(e, false, EXIT.USAGE);
|
|
7233
|
+
throw e;
|
|
6758
7234
|
});
|
|
6759
7235
|
const { pinned, file } = result;
|
|
6760
7236
|
if (opts.json) {
|
|
@@ -7027,14 +7503,18 @@ async function supersedeTarget(platformDir, id) {
|
|
|
7027
7503
|
return located;
|
|
7028
7504
|
}
|
|
7029
7505
|
function successorFields(input, req) {
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7506
|
+
return {
|
|
7507
|
+
why: (input.why ?? req.why ?? "").trim(),
|
|
7508
|
+
livesIn: input.livesIn ?? [...req.livesIn],
|
|
7509
|
+
relates: input.relates === undefined ? [...req.relates] : splitList(input.relates)
|
|
7510
|
+
};
|
|
7034
7511
|
}
|
|
7035
|
-
function
|
|
7036
|
-
const term =
|
|
7037
|
-
|
|
7512
|
+
function applyTermOverrides(successor, input) {
|
|
7513
|
+
const term = input.term?.trim();
|
|
7514
|
+
if (term)
|
|
7515
|
+
successor.term = term;
|
|
7516
|
+
if (input.aliases !== undefined)
|
|
7517
|
+
successor.aliases = input.aliases;
|
|
7038
7518
|
}
|
|
7039
7519
|
function flipPredecessor(req, newId, issue) {
|
|
7040
7520
|
req.status = "superseded";
|
|
@@ -7070,18 +7550,19 @@ async function supersede(input, freshTags) {
|
|
|
7070
7550
|
}
|
|
7071
7551
|
warnings.push({ kind: "grammar", text: grammarWarningText("", verdict).slice(2) });
|
|
7072
7552
|
}
|
|
7073
|
-
const { why, livesIn } = successorFields(input, req);
|
|
7553
|
+
const { why, livesIn, relates } = successorFields(input, req);
|
|
7074
7554
|
warnings.push(...unresolvableRefWarnings(platformDir, [input.statement, why, ...livesIn]));
|
|
7075
7555
|
const newId = await nextRequirementId(platformDir, key);
|
|
7076
7556
|
const issue = input.issue?.trim() || undefined;
|
|
7557
|
+
const successor = successorEntry(newId, req, {
|
|
7558
|
+
statement: input.statement,
|
|
7559
|
+
why,
|
|
7560
|
+
livesIn,
|
|
7561
|
+
relates,
|
|
7562
|
+
issue
|
|
7563
|
+
});
|
|
7564
|
+
applyTermOverrides(successor, input);
|
|
7077
7565
|
flipPredecessor(req, newId, issue);
|
|
7078
|
-
const successor = successorEntry(newId, { statement: input.statement, why, livesIn, issue });
|
|
7079
|
-
if (key === "TERM") {
|
|
7080
|
-
const termFields = successorTermFields(input, req);
|
|
7081
|
-
if (termFields.term !== undefined)
|
|
7082
|
-
successor.term = termFields.term;
|
|
7083
|
-
successor.aliases = termFields.aliases;
|
|
7084
|
-
}
|
|
7085
7566
|
requirements.push(successor);
|
|
7086
7567
|
const specVersion = applyVersionStage(domain2, req, successor, Boolean(input.noBump));
|
|
7087
7568
|
domain2.updated = localToday();
|
|
@@ -7190,6 +7671,9 @@ function inputFromArgs(args, rawArgs, platformDir, id, statement) {
|
|
|
7190
7671
|
const livesIn = listFlag(rawArgs, "lives", args.lives);
|
|
7191
7672
|
if (livesIn !== undefined)
|
|
7192
7673
|
input.livesIn = livesIn;
|
|
7674
|
+
const relates = listFlag(rawArgs, "relates", args.relates);
|
|
7675
|
+
if (relates !== undefined)
|
|
7676
|
+
input.relates = relates;
|
|
7193
7677
|
if (typeof args.term === "string")
|
|
7194
7678
|
input.term = args.term;
|
|
7195
7679
|
if (typeof args.aliases === "string")
|
|
@@ -7203,7 +7687,6 @@ var supersedeArgs, supersedeCommand;
|
|
|
7203
7687
|
var init_supersede2 = __esm(() => {
|
|
7204
7688
|
init_constants();
|
|
7205
7689
|
init_discover();
|
|
7206
|
-
init__index();
|
|
7207
7690
|
init_supersede();
|
|
7208
7691
|
init_grammar3();
|
|
7209
7692
|
init__args();
|
|
@@ -7228,6 +7711,10 @@ var init_supersede2 = __esm(() => {
|
|
|
7228
7711
|
type: "string",
|
|
7229
7712
|
description: "Successor's Lives in: repeat or comma-separate; empty clears (default: copied from the old entry)"
|
|
7230
7713
|
},
|
|
7714
|
+
relates: {
|
|
7715
|
+
type: "string",
|
|
7716
|
+
description: "Successor's related ids: repeat or comma-separate; empty clears (default: copied from the old entry)"
|
|
7717
|
+
},
|
|
7231
7718
|
term: {
|
|
7232
7719
|
type: "string",
|
|
7233
7720
|
description: "TERM successor's headword (default: copied from the old entry; TERM ids only)"
|
|
@@ -7268,7 +7755,7 @@ var init_supersede2 = __esm(() => {
|
|
|
7268
7755
|
handleNotAPlatform(e);
|
|
7269
7756
|
}
|
|
7270
7757
|
const statement = await resolveSuccessorText(args, platformDir, id);
|
|
7271
|
-
const result = await supersede(inputFromArgs(args, rawArgs, platformDir, id, statement),
|
|
7758
|
+
const result = await supersede(inputFromArgs(args, rawArgs, platformDir, id, statement), cliFreshTags(platformDir));
|
|
7272
7759
|
if (!result.ok)
|
|
7273
7760
|
exitOnFailure("spec supersede", result);
|
|
7274
7761
|
printWarnings("spec supersede", result.warnings);
|
|
@@ -7335,7 +7822,6 @@ var deprecateCommand;
|
|
|
7335
7822
|
var init_deprecate2 = __esm(() => {
|
|
7336
7823
|
init_constants();
|
|
7337
7824
|
init_discover();
|
|
7338
|
-
init__index();
|
|
7339
7825
|
init_deprecate();
|
|
7340
7826
|
init_grammar3();
|
|
7341
7827
|
init__args();
|
|
@@ -7378,7 +7864,7 @@ var init_deprecate2 = __esm(() => {
|
|
|
7378
7864
|
} catch (e) {
|
|
7379
7865
|
handleNotAPlatform(e);
|
|
7380
7866
|
}
|
|
7381
|
-
const result = await deprecate({ platformDir, id, reason },
|
|
7867
|
+
const result = await deprecate({ platformDir, id, reason }, cliFreshTags(platformDir));
|
|
7382
7868
|
if (!result.ok)
|
|
7383
7869
|
exitOnFailure("spec deprecate", result);
|
|
7384
7870
|
const { file, sites } = result;
|
|
@@ -7397,30 +7883,28 @@ var init_deprecate2 = __esm(() => {
|
|
|
7397
7883
|
|
|
7398
7884
|
// src/operations/move.ts
|
|
7399
7885
|
async function locateMove(platformDir, id, targetKey) {
|
|
7400
|
-
const
|
|
7401
|
-
|
|
7886
|
+
const prefixKey = domainKeyOf(id);
|
|
7887
|
+
const located = await locateEntry(platformDir, id, `no domain ${prefixKey} (expected spec-engine/${prefixKey}/SPEC.json)`);
|
|
7888
|
+
if (!located.ok)
|
|
7889
|
+
return located;
|
|
7890
|
+
const { req, ...source } = located;
|
|
7891
|
+
if (source.key === targetKey) {
|
|
7402
7892
|
return fail("usage", `${id} is already in ${targetKey} \u2014 use spec supersede for an in-domain revision`);
|
|
7403
7893
|
}
|
|
7404
|
-
const source = await readEnvelope(platformDir, sourceKey, `no domain ${sourceKey} (expected spec-engine/${sourceKey}/SPEC.json)`);
|
|
7405
|
-
if (!source.ok)
|
|
7406
|
-
return source;
|
|
7407
7894
|
const target = await readEnvelope(platformDir, targetKey, `no target domain ${targetKey} \u2014 run \`spec domain new ${targetKey}\` first`);
|
|
7408
7895
|
if (!target.ok)
|
|
7409
7896
|
return target;
|
|
7410
|
-
const req = source.requirements.find((r) => r.id === id);
|
|
7411
|
-
if (req === undefined)
|
|
7412
|
-
return fail("not_found", `no entry ${id} in ${source.relFile}`);
|
|
7413
7897
|
if (req.status.toLowerCase() !== "active") {
|
|
7414
7898
|
return fail("conflict", `${id} is ${req.status} \u2014 only Active requirements move (superseded/retired entries stay as history)`);
|
|
7415
7899
|
}
|
|
7416
7900
|
return { ok: true, source, target, req };
|
|
7417
7901
|
}
|
|
7418
7902
|
function successorFields2(input, req) {
|
|
7419
|
-
const srcLives = (req.livesIn[0] ?? "").trim();
|
|
7420
7903
|
return {
|
|
7421
7904
|
statement: (input.statement ?? req.statement).trim(),
|
|
7422
7905
|
why: (input.why ?? req.why ?? "").trim(),
|
|
7423
|
-
livesIn: input.livesIn ??
|
|
7906
|
+
livesIn: input.livesIn ?? [...req.livesIn],
|
|
7907
|
+
relates: [...req.relates]
|
|
7424
7908
|
};
|
|
7425
7909
|
}
|
|
7426
7910
|
function versionSide(domain2, key, noBump) {
|
|
@@ -7469,7 +7953,7 @@ async function move(input, freshTags) {
|
|
|
7469
7953
|
const sourceCurrent = source.domain.specVersion ?? 1;
|
|
7470
7954
|
req.status = "superseded";
|
|
7471
7955
|
req.supersededBy = newId;
|
|
7472
|
-
target.requirements.push(successorEntry(newId, fields));
|
|
7956
|
+
target.requirements.push(successorEntry(newId, req, fields));
|
|
7473
7957
|
const noBump = Boolean(input.noBump);
|
|
7474
7958
|
const sourceSpecVersion = versionSide(source.domain, source.key, noBump);
|
|
7475
7959
|
const targetSpecVersion = versionSide(target.domain, target.key, noBump);
|
|
@@ -7530,7 +8014,6 @@ var init_move2 = __esm(() => {
|
|
|
7530
8014
|
init_domains();
|
|
7531
8015
|
init_constants();
|
|
7532
8016
|
init_discover();
|
|
7533
|
-
init__index();
|
|
7534
8017
|
init_move();
|
|
7535
8018
|
init_grammar3();
|
|
7536
8019
|
init__args();
|
|
@@ -7592,7 +8075,7 @@ var init_move2 = __esm(() => {
|
|
|
7592
8075
|
} catch (e) {
|
|
7593
8076
|
handleNotAPlatform(e);
|
|
7594
8077
|
}
|
|
7595
|
-
const result = await move(inputFromArgs2(args, rawArgs, platformDir, id, targetKey),
|
|
8078
|
+
const result = await move(inputFromArgs2(args, rawArgs, platformDir, id, targetKey), cliFreshTags(platformDir));
|
|
7596
8079
|
if (!result.ok)
|
|
7597
8080
|
exitOnFailure("spec move", result);
|
|
7598
8081
|
printWarnings("spec move", result.warnings);
|
|
@@ -7626,18 +8109,30 @@ var init_move2 = __esm(() => {
|
|
|
7626
8109
|
});
|
|
7627
8110
|
|
|
7628
8111
|
// src/operations/amend.ts
|
|
7629
|
-
|
|
8112
|
+
function releaseHolding(platformDir, relFile, id) {
|
|
8113
|
+
const tag = newestReleaseTag(platformDir);
|
|
8114
|
+
if (tag === null)
|
|
8115
|
+
return null;
|
|
8116
|
+
const text = gitShow(platformDir, tag, relFile);
|
|
8117
|
+
if (text === null)
|
|
8118
|
+
return null;
|
|
8119
|
+
const parsed = parseDomainText(text, relFile);
|
|
8120
|
+
return parsed.ok && parsed.data.requirements.some((r) => r.id === id) ? tag : null;
|
|
8121
|
+
}
|
|
8122
|
+
function gateRejection(platformDir, relFile, req, fields) {
|
|
8123
|
+
const { id } = req;
|
|
7630
8124
|
const statusLc = req.status.toLowerCase();
|
|
7631
8125
|
if (statusLc !== "active" && statusLc !== "draft") {
|
|
7632
8126
|
return fail("conflict", `${id} is ${displayStatus(req.status)} \u2014 only Active/Draft entries amend (a superseded entry is history; supersede its successor instead)`);
|
|
7633
8127
|
}
|
|
7634
8128
|
if (statusLc !== "active")
|
|
7635
8129
|
return null;
|
|
7636
|
-
|
|
7637
|
-
const site = bound[0];
|
|
7638
|
-
if (site === undefined)
|
|
8130
|
+
if (!PROMISE_FIELDS.some((f) => fields[f] !== undefined))
|
|
7639
8131
|
return null;
|
|
7640
|
-
|
|
8132
|
+
const tag = releaseHolding(platformDir, relFile, id);
|
|
8133
|
+
if (tag === null)
|
|
8134
|
+
return null;
|
|
8135
|
+
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.");
|
|
7641
8136
|
}
|
|
7642
8137
|
function applyFields(req, fields) {
|
|
7643
8138
|
const fieldsChanged = [];
|
|
@@ -7657,6 +8152,10 @@ function applyFields(req, fields) {
|
|
|
7657
8152
|
fieldsChanged.push("lives");
|
|
7658
8153
|
refValues.push(...fields.livesIn);
|
|
7659
8154
|
}
|
|
8155
|
+
if (fields.relates !== undefined) {
|
|
8156
|
+
req.relates = splitList(fields.relates);
|
|
8157
|
+
fieldsChanged.push("relates");
|
|
8158
|
+
}
|
|
7660
8159
|
if (fields.term !== undefined) {
|
|
7661
8160
|
req.term = fields.term;
|
|
7662
8161
|
fieldsChanged.push("term");
|
|
@@ -7672,18 +8171,17 @@ function applyFields(req, fields) {
|
|
|
7672
8171
|
}
|
|
7673
8172
|
return { fieldsChanged, refValues };
|
|
7674
8173
|
}
|
|
7675
|
-
async function amend(input
|
|
8174
|
+
async function amend(input) {
|
|
7676
8175
|
const { platformDir, id, fields } = input;
|
|
7677
8176
|
const located = await locateEntry(platformDir, id);
|
|
7678
8177
|
if (!located.ok)
|
|
7679
8178
|
return located;
|
|
7680
|
-
const { specPath, relFile, domain: domain2, req } = located;
|
|
7681
|
-
const rejection =
|
|
8179
|
+
const { key, specPath, relFile, domain: domain2, req } = located;
|
|
8180
|
+
const rejection = gateRejection(platformDir, relFile, req, fields);
|
|
7682
8181
|
if (rejection !== null)
|
|
7683
8182
|
return rejection;
|
|
7684
8183
|
const warnings = [];
|
|
7685
8184
|
if (fields.statement !== undefined) {
|
|
7686
|
-
const key = domainKeyOf(id);
|
|
7687
8185
|
const verdict = await judgeStatementGrammar(platformDir, key, fields.statement);
|
|
7688
8186
|
if (!verdict.ok) {
|
|
7689
8187
|
if (verdict.severity === "error") {
|
|
@@ -7705,12 +8203,14 @@ async function amend(input, freshTags) {
|
|
|
7705
8203
|
}
|
|
7706
8204
|
return { ok: true, id, file: relFile, fieldsChanged: [...fieldsChanged].sort(), warnings };
|
|
7707
8205
|
}
|
|
8206
|
+
var PROMISE_FIELDS;
|
|
7708
8207
|
var init_amend = __esm(() => {
|
|
7709
8208
|
init_src();
|
|
7710
8209
|
init_grammar2();
|
|
7711
8210
|
init__envelope();
|
|
7712
8211
|
init__result();
|
|
7713
8212
|
init_mint();
|
|
8213
|
+
PROMISE_FIELDS = ["statement", "why", "term", "aliases"];
|
|
7714
8214
|
});
|
|
7715
8215
|
|
|
7716
8216
|
// src/commands/amend.ts
|
|
@@ -7721,9 +8221,9 @@ __export(exports_amend, {
|
|
|
7721
8221
|
import { defineCommand as defineCommand18 } from "citty";
|
|
7722
8222
|
function assertAmendFlags(args) {
|
|
7723
8223
|
const hasIssue = args.issue !== undefined && args.issue.trim() !== "";
|
|
7724
|
-
const hasField = args.text !== undefined || args.why !== undefined || args.lives !== undefined || args.term !== undefined || args.aliases !== undefined;
|
|
8224
|
+
const hasField = args.text !== undefined || args.why !== undefined || args.lives !== undefined || args.relates !== undefined || args.term !== undefined || args.aliases !== undefined;
|
|
7725
8225
|
if (!hasField && !hasIssue) {
|
|
7726
|
-
console.error("spec amend: nothing to amend \u2014 pass at least one of --text / --why / --lives / --term / --aliases");
|
|
8226
|
+
console.error("spec amend: nothing to amend \u2014 pass at least one of --text / --why / --lives / --relates / --term / --aliases");
|
|
7727
8227
|
process.exit(EXIT.USAGE);
|
|
7728
8228
|
}
|
|
7729
8229
|
if (args.text !== undefined && args.text.trim() === "") {
|
|
@@ -7750,6 +8250,9 @@ function fieldsFromArgs(args, rawArgs) {
|
|
|
7750
8250
|
const livesIn = listFlag(rawArgs, "lives", args.lives);
|
|
7751
8251
|
if (livesIn !== undefined)
|
|
7752
8252
|
fields.livesIn = livesIn;
|
|
8253
|
+
const relates = listFlag(rawArgs, "relates", args.relates);
|
|
8254
|
+
if (relates !== undefined)
|
|
8255
|
+
fields.relates = relates;
|
|
7753
8256
|
if (args.term !== undefined)
|
|
7754
8257
|
fields.term = args.term.trim();
|
|
7755
8258
|
if (args.issue !== undefined && args.issue.trim() !== "")
|
|
@@ -7762,7 +8265,6 @@ var amendArgs, amendCommand;
|
|
|
7762
8265
|
var init_amend2 = __esm(() => {
|
|
7763
8266
|
init_constants();
|
|
7764
8267
|
init_discover();
|
|
7765
|
-
init__index();
|
|
7766
8268
|
init_amend();
|
|
7767
8269
|
init_grammar3();
|
|
7768
8270
|
init__args();
|
|
@@ -7777,6 +8279,7 @@ var init_amend2 = __esm(() => {
|
|
|
7777
8279
|
text: { type: "string", description: "New Requirement (statement) field value" },
|
|
7778
8280
|
why: { type: "string", description: "New Why it matters field value" },
|
|
7779
8281
|
lives: livesArg,
|
|
8282
|
+
relates: relatesArg,
|
|
7780
8283
|
issue: {
|
|
7781
8284
|
type: "string",
|
|
7782
8285
|
description: "Ticket that motivated this amendment \u2014 recorded as amends-via provenance (opaque, never a requirement id)"
|
|
@@ -7791,7 +8294,7 @@ var init_amend2 = __esm(() => {
|
|
|
7791
8294
|
amendCommand = defineCommand18({
|
|
7792
8295
|
meta: {
|
|
7793
8296
|
name: "amend",
|
|
7794
|
-
description: "Revise
|
|
8297
|
+
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."
|
|
7795
8298
|
},
|
|
7796
8299
|
args: amendArgs,
|
|
7797
8300
|
async run({ args, rawArgs }) {
|
|
@@ -7808,7 +8311,7 @@ var init_amend2 = __esm(() => {
|
|
|
7808
8311
|
handleNotAPlatform(e);
|
|
7809
8312
|
}
|
|
7810
8313
|
const fields = fieldsFromArgs(args, rawArgs);
|
|
7811
|
-
const result = await amend({ platformDir, id, fields }
|
|
8314
|
+
const result = await amend({ platformDir, id, fields });
|
|
7812
8315
|
if (!result.ok)
|
|
7813
8316
|
exitOnFailure("spec amend", result);
|
|
7814
8317
|
printWarnings("spec amend", result.warnings);
|
|
@@ -7930,7 +8433,7 @@ Target domain: ${opts.domain} (no charter/scope recorded \u2014 check placement
|
|
|
7930
8433
|
|
|
7931
8434
|
// src/server/mcp.ts
|
|
7932
8435
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7933
|
-
import { z as
|
|
8436
|
+
import { z as z4 } from "zod";
|
|
7934
8437
|
function jsonResult(value) {
|
|
7935
8438
|
return { content: [{ type: "text", text: JSON.stringify(value) }] };
|
|
7936
8439
|
}
|
|
@@ -7944,8 +8447,8 @@ function buildMcpServer(platformDir) {
|
|
|
7944
8447
|
title: "Full-text requirement retrieval",
|
|
7945
8448
|
description: "Search requirements by full text (SQLite FTS5 MATCH syntax; wrap phrases in double quotes). Returns ranked hits \u2014 use this to load the requirements a task touches instead of re-asking the user.",
|
|
7946
8449
|
inputSchema: {
|
|
7947
|
-
text:
|
|
7948
|
-
limit:
|
|
8450
|
+
text: z4.string().min(1).describe("FTS5 query text"),
|
|
8451
|
+
limit: z4.number().int().min(1).max(LIMIT_MAX).optional().describe(`Max hits (default ${DEFAULT_QUERY_LIMIT})`)
|
|
7949
8452
|
}
|
|
7950
8453
|
}, async ({ text, limit }) => fresh((s) => {
|
|
7951
8454
|
const r = query(s, text, limit ?? DEFAULT_QUERY_LIMIT);
|
|
@@ -7955,13 +8458,13 @@ function buildMcpServer(platformDir) {
|
|
|
7955
8458
|
title: "Files \u2192 requirements",
|
|
7956
8459
|
description: "The requirements tagged in the given files (platform-relative paths, e.g. api/src/renew.ts). Load these before changing the files.",
|
|
7957
8460
|
inputSchema: {
|
|
7958
|
-
files:
|
|
8461
|
+
files: z4.array(z4.string().min(1)).min(1).max(100).describe("Platform-relative file paths")
|
|
7959
8462
|
}
|
|
7960
8463
|
}, async ({ files }) => fresh((s) => jsonResult(resolveFiles(s, files).rows)));
|
|
7961
8464
|
server.registerTool("spec_req_tags", {
|
|
7962
8465
|
title: "Requirement \u2192 tag sites (reverse query)",
|
|
7963
8466
|
description: "Every code/test/doc site tagging the given requirement, across all repos \u2014 impact analysis and retag worklists.",
|
|
7964
|
-
inputSchema: { req_id:
|
|
8467
|
+
inputSchema: { req_id: z4.string().describe("Requirement id (KEY-NNN)") }
|
|
7965
8468
|
}, async ({ req_id }) => {
|
|
7966
8469
|
if (!ID_RE.test(req_id)) {
|
|
7967
8470
|
return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
|
|
@@ -7984,7 +8487,7 @@ function buildMcpServer(platformDir) {
|
|
|
7984
8487
|
server.registerTool("spec_propagation", {
|
|
7985
8488
|
title: "Cross-repo propagation for a superseded requirement",
|
|
7986
8489
|
description: "Per member repo: migrated to the successor (verified or not), still on the predecessor, or unrelated \u2014 plus the drift flag.",
|
|
7987
|
-
inputSchema: { req_id:
|
|
8490
|
+
inputSchema: { req_id: z4.string().describe("Requirement id (KEY-NNN)") }
|
|
7988
8491
|
}, async ({ req_id }) => {
|
|
7989
8492
|
if (!ID_RE.test(req_id)) {
|
|
7990
8493
|
return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
|
|
@@ -7994,7 +8497,7 @@ function buildMcpServer(platformDir) {
|
|
|
7994
8497
|
server.registerTool("spec_next_id", {
|
|
7995
8498
|
title: "Next unused requirement id",
|
|
7996
8499
|
description: "Allocate-preview the next unused id in a domain (filesystem-derived; nothing is written). Use before authoring a new requirement.",
|
|
7997
|
-
inputSchema: { domain:
|
|
8500
|
+
inputSchema: { domain: z4.string().min(1).describe("Domain key (e.g. BILLING)") }
|
|
7998
8501
|
}, async ({ domain: domain2 }) => {
|
|
7999
8502
|
const r = await nextId(platformDir, domain2);
|
|
8000
8503
|
return r.ok ? jsonResult({ domain: r.key, next_id: r.nextId }) : errorResult(r.detail);
|
|
@@ -8002,7 +8505,7 @@ function buildMcpServer(platformDir) {
|
|
|
8002
8505
|
server.registerTool("spec_get", {
|
|
8003
8506
|
title: "One requirement's full record",
|
|
8004
8507
|
description: "The full record for one requirement id: status, key, seq, statement, why, versions, and source location. An unknown id returns [] (the `spec get --json` shape).",
|
|
8005
|
-
inputSchema: { req_id:
|
|
8508
|
+
inputSchema: { req_id: z4.string().describe("Requirement id (KEY-NNN)") }
|
|
8006
8509
|
}, async ({ req_id }) => {
|
|
8007
8510
|
if (!ID_RE.test(req_id)) {
|
|
8008
8511
|
return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
|
|
@@ -8016,8 +8519,8 @@ function buildMcpServer(platformDir) {
|
|
|
8016
8519
|
title: "Every requirement record",
|
|
8017
8520
|
description: "Every requirement record in (key, seq) order, superseded and deprecated history included. Optional domain and status filters. The `spec list --json` shape.",
|
|
8018
8521
|
inputSchema: {
|
|
8019
|
-
domain:
|
|
8020
|
-
status:
|
|
8522
|
+
domain: z4.string().optional().describe("Only this domain key (e.g. BILLING)"),
|
|
8523
|
+
status: z4.string().optional().describe(`Only this status: ${STATUS_WORDS.join(" | ")}`)
|
|
8021
8524
|
}
|
|
8022
8525
|
}, async ({ domain: domain2, status }) => {
|
|
8023
8526
|
const filter = {};
|
|
@@ -8036,17 +8539,18 @@ function buildMcpServer(platformDir) {
|
|
|
8036
8539
|
title: "Supersede a shipped requirement",
|
|
8037
8540
|
description: "Flip an Active requirement to superseded, mint its successor in the same domain, and return the retag worklist (the tag sites spec check reports as SUPERSEDED_REFERENCED until retagged). The same JSON as `spec supersede --json`.",
|
|
8038
8541
|
inputSchema: {
|
|
8039
|
-
req_id:
|
|
8040
|
-
statement:
|
|
8041
|
-
why:
|
|
8042
|
-
lives_in:
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8542
|
+
req_id: z4.string().describe("The Active requirement id to supersede (KEY-NNN)"),
|
|
8543
|
+
statement: z4.string().min(1).describe("The successor's statement"),
|
|
8544
|
+
why: z4.string().optional().describe("Successor's why (default: copied from the predecessor)"),
|
|
8545
|
+
lives_in: z4.array(z4.string()).optional().describe("Successor's livesIn paths (default: copied from the predecessor)"),
|
|
8546
|
+
relates: z4.array(z4.string()).optional().describe("Successor's related ids, each comma-split; an empty list clears (default: copied from the predecessor)"),
|
|
8547
|
+
issue: z4.string().optional().describe("Ticket recorded as supersedes-via on the predecessor and created on the successor")
|
|
8548
|
+
}
|
|
8549
|
+
}, async ({ req_id, statement, why, lives_in, relates, issue }) => {
|
|
8046
8550
|
if (!ID_RE.test(req_id)) {
|
|
8047
8551
|
return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
|
|
8048
8552
|
}
|
|
8049
|
-
const r = await supersede({ platformDir, id: req_id, statement, why, livesIn: lives_in, issue }, coldFreshTags(platformDir));
|
|
8553
|
+
const r = await supersede({ platformDir, id: req_id, statement, why, livesIn: lives_in, relates, issue }, coldFreshTags(platformDir));
|
|
8050
8554
|
if (!r.ok)
|
|
8051
8555
|
return errorResult(r.detail);
|
|
8052
8556
|
for (const w of r.warnings)
|
|
@@ -8063,8 +8567,8 @@ function buildMcpServer(platformDir) {
|
|
|
8063
8567
|
title: "Deprecate a requirement",
|
|
8064
8568
|
description: "Mark an Active or Draft requirement deprecated with a recorded reason and return the code tags still bound to it (each a DEPRECATED_REFERENCED error in spec check until removed). The same JSON as `spec deprecate --json`.",
|
|
8065
8569
|
inputSchema: {
|
|
8066
|
-
req_id:
|
|
8067
|
-
reason:
|
|
8570
|
+
req_id: z4.string().describe("The requirement id to deprecate (KEY-NNN)"),
|
|
8571
|
+
reason: z4.string().min(1).describe("Why the requirement ended; the durable record")
|
|
8068
8572
|
}
|
|
8069
8573
|
}, async ({ req_id, reason }) => {
|
|
8070
8574
|
if (!ID_RE.test(req_id)) {
|
|
@@ -8080,7 +8584,7 @@ function buildMcpServer(platformDir) {
|
|
|
8080
8584
|
title: "Accept a draft requirement",
|
|
8081
8585
|
description: "Promote a Draft requirement to Active, leaving every other field untouched. The same JSON as `spec accept --json`.",
|
|
8082
8586
|
inputSchema: {
|
|
8083
|
-
req_id:
|
|
8587
|
+
req_id: z4.string().describe("The Draft requirement id to accept (KEY-NNN)")
|
|
8084
8588
|
}
|
|
8085
8589
|
}, async ({ req_id }) => {
|
|
8086
8590
|
if (!ID_RE.test(req_id)) {
|
|
@@ -8093,8 +8597,8 @@ function buildMcpServer(platformDir) {
|
|
|
8093
8597
|
title: "Author requirements from a brief",
|
|
8094
8598
|
description: "Turn a vague brief/ticket into well-formed Spec Engine requirements: one requirement per testable promise, placed by domain charter, deduped via spec query, drafted to the GUARD template + cold-read rubric, minted with spec req. The engine returns a static playbook template \u2014 your model runs it.",
|
|
8095
8599
|
argsSchema: {
|
|
8096
|
-
brief:
|
|
8097
|
-
domain:
|
|
8600
|
+
brief: z4.string().min(1).describe("The vague brief / ticket text to author from"),
|
|
8601
|
+
domain: z4.string().optional().describe("Optional target domain KEY \u2014 its charter/scope is injected when given")
|
|
8098
8602
|
}
|
|
8099
8603
|
}, async ({ brief, domain: domain2 }) => {
|
|
8100
8604
|
let resolvedDomain;
|
|
@@ -8175,7 +8679,7 @@ __export(exports_resolve, {
|
|
|
8175
8679
|
resolveCommand: () => resolveCommand
|
|
8176
8680
|
});
|
|
8177
8681
|
import { statSync as statSync5 } from "fs";
|
|
8178
|
-
import { join as
|
|
8682
|
+
import { join as join14, relative as relative2, resolve as resolve13, sep as sep3 } from "path";
|
|
8179
8683
|
import { defineCommand as defineCommand21 } from "citty";
|
|
8180
8684
|
function classifyArg(a) {
|
|
8181
8685
|
if (a.startsWith("--")) {
|
|
@@ -8214,7 +8718,7 @@ function splitFilesAndPlatformDir(positionals, fallbackPlatformDir) {
|
|
|
8214
8718
|
const resolved = resolve13(last);
|
|
8215
8719
|
if (statSync5(resolved).isDirectory()) {
|
|
8216
8720
|
try {
|
|
8217
|
-
if (statSync5(
|
|
8721
|
+
if (statSync5(join14(resolved, "spec-engine")).isDirectory()) {
|
|
8218
8722
|
lastIsPlatformDir = true;
|
|
8219
8723
|
} else {
|
|
8220
8724
|
lastIsDirWithoutSpecEngine = true;
|
|
@@ -8254,7 +8758,7 @@ async function runReverseQuery(opts) {
|
|
|
8254
8758
|
if (outArg2)
|
|
8255
8759
|
assertContainedPath(dbPath, platformDir, "spec resolve: --out");
|
|
8256
8760
|
await maybePromptForOnboarding({ platformDir, args: { noPrompt } });
|
|
8257
|
-
await withReadStorage({ platformDir, dbPath, fresh }, (storage2) => {
|
|
8761
|
+
await withReadStorage({ platformDir, dbPath, fresh, json }, (storage2) => {
|
|
8258
8762
|
const { rows, known } = reqTags(storage2, reqId);
|
|
8259
8763
|
if (rows.length === 0) {
|
|
8260
8764
|
if (!known) {
|
|
@@ -8343,7 +8847,7 @@ var init_resolve2 = __esm(() => {
|
|
|
8343
8847
|
},
|
|
8344
8848
|
req: {
|
|
8345
8849
|
type: "string",
|
|
8346
|
-
description: "Reverse query
|
|
8850
|
+
description: "Reverse query: list every tag site (repo, file, line, kind, level) for the given requirement id instead of resolving files. Takes no file positionals."
|
|
8347
8851
|
},
|
|
8348
8852
|
out: {
|
|
8349
8853
|
type: "string",
|
|
@@ -8365,7 +8869,7 @@ var init_resolve2 = __esm(() => {
|
|
|
8365
8869
|
resolveCommand = defineCommand21({
|
|
8366
8870
|
meta: {
|
|
8367
8871
|
name: "resolve",
|
|
8368
|
-
description: "Return the requirements tagged in the given files
|
|
8872
|
+
description: "Return the requirements tagged in the given files. Accepts multiple positional file paths (e.g. `spec resolve api/src/renew.ts api/src/charge.ts`) and a comma-split fallback inside a single positional (`spec resolve a.ts,b.ts`). Paths may be platform-relative or absolute under the platform tree."
|
|
8369
8873
|
},
|
|
8370
8874
|
args: resolveArgs,
|
|
8371
8875
|
async run({ args, rawArgs }) {
|
|
@@ -8398,7 +8902,7 @@ var init_resolve2 = __esm(() => {
|
|
|
8398
8902
|
noPrompt: args.noPrompt
|
|
8399
8903
|
}
|
|
8400
8904
|
});
|
|
8401
|
-
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, (storage2) => {
|
|
8905
|
+
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, (storage2) => {
|
|
8402
8906
|
const rows = storage2.resolveByFiles(normalized);
|
|
8403
8907
|
if (!args.json && rows.length === 0 && storage2.listRequirements().length === 0) {
|
|
8404
8908
|
console.error(formatNoRequirementsIndexed(platformDir));
|
|
@@ -8525,8 +9029,8 @@ var exports_gate = {};
|
|
|
8525
9029
|
__export(exports_gate, {
|
|
8526
9030
|
gateCommand: () => gateCommand
|
|
8527
9031
|
});
|
|
8528
|
-
import { existsSync as
|
|
8529
|
-
import { join as
|
|
9032
|
+
import { existsSync as existsSync13 } from "fs";
|
|
9033
|
+
import { join as join15 } from "path";
|
|
8530
9034
|
import { defineCommand as defineCommand22 } from "citty";
|
|
8531
9035
|
function isEpipe(e) {
|
|
8532
9036
|
if (e === null || typeof e !== "object" || !("code" in e))
|
|
@@ -8550,7 +9054,7 @@ function resolveGateArgs(args) {
|
|
|
8550
9054
|
process.exit(EXIT.USAGE);
|
|
8551
9055
|
}
|
|
8552
9056
|
const platformDir = resolvePlatformDir(args);
|
|
8553
|
-
if (!
|
|
9057
|
+
if (!existsSync13(join15(platformDir, "spec-engine"))) {
|
|
8554
9058
|
console.error(formatNotASpecPlatform(platformDir));
|
|
8555
9059
|
process.exit(EXIT.USAGE);
|
|
8556
9060
|
}
|
|
@@ -8623,6 +9127,7 @@ var init_gate2 = __esm(() => {
|
|
|
8623
9127
|
writeStdoutTolerant(`build_id: ${gated.buildId}`);
|
|
8624
9128
|
}
|
|
8625
9129
|
} catch (e) {
|
|
9130
|
+
handleInvalidDomainFiles(e, Boolean(args.json));
|
|
8626
9131
|
const msg = e instanceof Error ? e.message : String(e);
|
|
8627
9132
|
console.error(`spec gate: crashed: ${msg}`);
|
|
8628
9133
|
process.exit(EXIT.USAGE);
|
|
@@ -8708,205 +9213,6 @@ function renderGuard(losses, mode, ref) {
|
|
|
8708
9213
|
}
|
|
8709
9214
|
var STOP = "\uD83D\uDED1";
|
|
8710
9215
|
|
|
8711
|
-
// src/guard/collect.ts
|
|
8712
|
-
import { join as join14 } from "path";
|
|
8713
|
-
function isIgnored(path) {
|
|
8714
|
-
return isPathIgnored(path);
|
|
8715
|
-
}
|
|
8716
|
-
function isSpecFile(path) {
|
|
8717
|
-
return path.startsWith(`${CANONICAL_SPECS_DIR}/`) && path.endsWith("/SPEC.json") && !isIgnored(path);
|
|
8718
|
-
}
|
|
8719
|
-
function isCodeFile(path) {
|
|
8720
|
-
if (isSpecFile(path) || isIgnored(path))
|
|
8721
|
-
return false;
|
|
8722
|
-
const ext = path.includes(".") ? path.split(".").pop() ?? "" : "";
|
|
8723
|
-
return CODE_EXTS.has(ext);
|
|
8724
|
-
}
|
|
8725
|
-
function parseReqs(text) {
|
|
8726
|
-
const v = parseDomainText(text, "");
|
|
8727
|
-
return v.ok ? v.data.requirements : [];
|
|
8728
|
-
}
|
|
8729
|
-
function collectBaseReqs2(platformDir, ref, specPaths2) {
|
|
8730
|
-
const reqs = [];
|
|
8731
|
-
const path = new Map;
|
|
8732
|
-
for (const p of specPaths2) {
|
|
8733
|
-
const bytes = gitShow(platformDir, ref, p);
|
|
8734
|
-
if (bytes === null)
|
|
8735
|
-
continue;
|
|
8736
|
-
for (const r of parseReqs(bytes)) {
|
|
8737
|
-
reqs.push(r);
|
|
8738
|
-
path.set(r.id, p);
|
|
8739
|
-
}
|
|
8740
|
-
}
|
|
8741
|
-
return { reqs, path };
|
|
8742
|
-
}
|
|
8743
|
-
function collectBaseTags(platformDir, ref, codePaths) {
|
|
8744
|
-
const implSite = new Map;
|
|
8745
|
-
const verifySite = new Map;
|
|
8746
|
-
for (const p of codePaths) {
|
|
8747
|
-
const bytes = gitShow(platformDir, ref, p);
|
|
8748
|
-
if (bytes === null)
|
|
8749
|
-
continue;
|
|
8750
|
-
for (const tag of scanTagsInFile("", p, bytes)) {
|
|
8751
|
-
const target = tag.kind === "verifies" ? verifySite : implSite;
|
|
8752
|
-
if (!target.has(tag.req_id))
|
|
8753
|
-
target.set(tag.req_id, { file: tag.file, line: tag.line });
|
|
8754
|
-
}
|
|
8755
|
-
}
|
|
8756
|
-
return { implSite, verifySite };
|
|
8757
|
-
}
|
|
8758
|
-
async function collectWorktreeReqs(canonicalDir) {
|
|
8759
|
-
const ids = new Set;
|
|
8760
|
-
const activeIds = new Set;
|
|
8761
|
-
const supersedesTargets = new Set;
|
|
8762
|
-
for (const rel of await findDomainJsonFiles(canonicalDir)) {
|
|
8763
|
-
const text = await Bun.file(join14(canonicalDir, rel)).text();
|
|
8764
|
-
for (const r of parseReqs(text)) {
|
|
8765
|
-
ids.add(r.id);
|
|
8766
|
-
if (r.status.toLowerCase() === "active")
|
|
8767
|
-
activeIds.add(r.id);
|
|
8768
|
-
if (r.supersedes != null)
|
|
8769
|
-
supersedesTargets.add(r.supersedes);
|
|
8770
|
-
}
|
|
8771
|
-
}
|
|
8772
|
-
return { ids, activeIds, supersedesTargets };
|
|
8773
|
-
}
|
|
8774
|
-
function tallyWorktreeTags(storage2) {
|
|
8775
|
-
const implCount = new Map;
|
|
8776
|
-
const verifyCount = new Map;
|
|
8777
|
-
for (const t of storage2.listTags()) {
|
|
8778
|
-
const bucket = t.kind === "implements" ? implCount : t.kind === "verifies" ? verifyCount : null;
|
|
8779
|
-
if (bucket !== null)
|
|
8780
|
-
bucket.set(t.req_id, (bucket.get(t.req_id) ?? 0) + 1);
|
|
8781
|
-
}
|
|
8782
|
-
return { implCount, verifyCount };
|
|
8783
|
-
}
|
|
8784
|
-
async function collectFacts(platformDir, ref, storage2) {
|
|
8785
|
-
const changed = gitDiffNameStatus(platformDir, ref);
|
|
8786
|
-
const specPaths2 = changed.filter((e) => isSpecFile(e.path)).map((e) => e.path);
|
|
8787
|
-
const codePaths = changed.filter((e) => isCodeFile(e.path)).map((e) => e.path);
|
|
8788
|
-
const deletedSpecFiles = changed.filter((e) => e.status === "D" && isSpecFile(e.path)).map((e) => e.path);
|
|
8789
|
-
const base = collectBaseReqs2(platformDir, ref, specPaths2);
|
|
8790
|
-
const baseTags = collectBaseTags(platformDir, ref, codePaths);
|
|
8791
|
-
const worktree = await collectWorktreeReqs(join14(platformDir, CANONICAL_SPECS_DIR));
|
|
8792
|
-
const counts = tallyWorktreeTags(storage2);
|
|
8793
|
-
return {
|
|
8794
|
-
baseReqs: base.reqs,
|
|
8795
|
-
baseReqPath: base.path,
|
|
8796
|
-
baseImplSite: baseTags.implSite,
|
|
8797
|
-
baseVerifySite: baseTags.verifySite,
|
|
8798
|
-
worktreeReqIds: worktree.ids,
|
|
8799
|
-
worktreeActiveIds: worktree.activeIds,
|
|
8800
|
-
worktreeSupersedesTargets: worktree.supersedesTargets,
|
|
8801
|
-
worktreeImplCount: counts.implCount,
|
|
8802
|
-
worktreeVerifyCount: counts.verifyCount,
|
|
8803
|
-
deletedSpecFiles
|
|
8804
|
-
};
|
|
8805
|
-
}
|
|
8806
|
-
var CODE_EXTS;
|
|
8807
|
-
var init_collect = __esm(() => {
|
|
8808
|
-
init_src();
|
|
8809
|
-
init_constants();
|
|
8810
|
-
init_fs();
|
|
8811
|
-
init_tags();
|
|
8812
|
-
CODE_EXTS = new Set(DEFAULT_EXTS);
|
|
8813
|
-
});
|
|
8814
|
-
|
|
8815
|
-
// src/guard/losses.ts
|
|
8816
|
-
function isActive(status) {
|
|
8817
|
-
return status.toLowerCase() === "active";
|
|
8818
|
-
}
|
|
8819
|
-
function isSuperseded(base, f) {
|
|
8820
|
-
const forward = base.supersededBy != null && f.worktreeReqIds.has(base.supersededBy);
|
|
8821
|
-
return forward || f.worktreeSupersedesTargets.has(base.id);
|
|
8822
|
-
}
|
|
8823
|
-
function reqDeletedLoss(base, f) {
|
|
8824
|
-
return {
|
|
8825
|
-
kind: "REQUIREMENT_REMOVED",
|
|
8826
|
-
req_id: base.id,
|
|
8827
|
-
file: f.baseReqPath.get(base.id) ?? "",
|
|
8828
|
-
line: 0,
|
|
8829
|
-
detail: `${base.id} was Active at the ref but is absent from the working-tree spec and was not superseded \u2014 a requirement is never deleted`
|
|
8830
|
-
};
|
|
8831
|
-
}
|
|
8832
|
-
function historyRemovedLoss(base, f) {
|
|
8833
|
-
const display = base.status.charAt(0).toUpperCase() + base.status.slice(1).toLowerCase();
|
|
8834
|
-
return {
|
|
8835
|
-
kind: "REQUIREMENT_REMOVED",
|
|
8836
|
-
req_id: base.id,
|
|
8837
|
-
file: f.baseReqPath.get(base.id) ?? "",
|
|
8838
|
-
line: 0,
|
|
8839
|
-
detail: `${base.id} was ${display} at the ref but is absent from the working-tree spec \u2014 a requirement is never deleted, and its history is the record`
|
|
8840
|
-
};
|
|
8841
|
-
}
|
|
8842
|
-
function implLostLoss(id, site) {
|
|
8843
|
-
return {
|
|
8844
|
-
kind: "IMPL_LOST",
|
|
8845
|
-
req_id: id,
|
|
8846
|
-
file: site.file,
|
|
8847
|
-
line: site.line,
|
|
8848
|
-
detail: `${id} lost its last implementing @spec tag (was ${site.file}:${site.line})`
|
|
8849
|
-
};
|
|
8850
|
-
}
|
|
8851
|
-
function verifyLostLoss(id, site) {
|
|
8852
|
-
return {
|
|
8853
|
-
kind: "VERIFY_LOST",
|
|
8854
|
-
req_id: id,
|
|
8855
|
-
file: site.file,
|
|
8856
|
-
line: site.line,
|
|
8857
|
-
detail: `${id} lost its last verifying @spec tag (was ${site.file}:${site.line})`
|
|
8858
|
-
};
|
|
8859
|
-
}
|
|
8860
|
-
function specFileDeletedLoss(path) {
|
|
8861
|
-
return {
|
|
8862
|
-
kind: "SPEC_FILE_DELETED",
|
|
8863
|
-
req_id: null,
|
|
8864
|
-
file: path,
|
|
8865
|
-
line: 0,
|
|
8866
|
-
detail: `canonical spec file ${path} present at the ref is absent from the working tree`
|
|
8867
|
-
};
|
|
8868
|
-
}
|
|
8869
|
-
function reqDeletedFor(base, f) {
|
|
8870
|
-
if (f.worktreeReqIds.has(base.id))
|
|
8871
|
-
return null;
|
|
8872
|
-
if (!isActive(base.status))
|
|
8873
|
-
return historyRemovedLoss(base, f);
|
|
8874
|
-
if (isSuperseded(base, f))
|
|
8875
|
-
return null;
|
|
8876
|
-
return reqDeletedLoss(base, f);
|
|
8877
|
-
}
|
|
8878
|
-
function lastTagGone(id, count, f) {
|
|
8879
|
-
return f.worktreeActiveIds.has(id) && count === 0;
|
|
8880
|
-
}
|
|
8881
|
-
function classifyLosses(f) {
|
|
8882
|
-
const out = [];
|
|
8883
|
-
for (const base of f.baseReqs) {
|
|
8884
|
-
const loss = reqDeletedFor(base, f);
|
|
8885
|
-
if (loss !== null)
|
|
8886
|
-
out.push(loss);
|
|
8887
|
-
}
|
|
8888
|
-
for (const [id, site] of f.baseImplSite) {
|
|
8889
|
-
if (lastTagGone(id, f.worktreeImplCount.get(id) ?? 0, f))
|
|
8890
|
-
out.push(implLostLoss(id, site));
|
|
8891
|
-
}
|
|
8892
|
-
for (const [id, site] of f.baseVerifySite) {
|
|
8893
|
-
if (lastTagGone(id, f.worktreeVerifyCount.get(id) ?? 0, f))
|
|
8894
|
-
out.push(verifyLostLoss(id, site));
|
|
8895
|
-
}
|
|
8896
|
-
for (const path of f.deletedSpecFiles)
|
|
8897
|
-
out.push(specFileDeletedLoss(path));
|
|
8898
|
-
return out;
|
|
8899
|
-
}
|
|
8900
|
-
|
|
8901
|
-
// src/operations/guard.ts
|
|
8902
|
-
async function guard(storage2, platformDir, ref) {
|
|
8903
|
-
const losses = classifyLosses(await collectFacts(platformDir, ref, storage2));
|
|
8904
|
-
return { ok: true, ref, losses };
|
|
8905
|
-
}
|
|
8906
|
-
var init_guard = __esm(() => {
|
|
8907
|
-
init_collect();
|
|
8908
|
-
});
|
|
8909
|
-
|
|
8910
9216
|
// src/commands/guard.ts
|
|
8911
9217
|
var exports_guard = {};
|
|
8912
9218
|
__export(exports_guard, {
|
|
@@ -8945,7 +9251,7 @@ var init_guard2 = __esm(() => {
|
|
|
8945
9251
|
process.exit(EXIT.OK);
|
|
8946
9252
|
}
|
|
8947
9253
|
let losses = [];
|
|
8948
|
-
await withReadStorage({ platformDir, dbPath: defaultIndexPath(platformDir), fresh: true }, async (storage2) => {
|
|
9254
|
+
await withReadStorage({ platformDir, dbPath: defaultIndexPath(platformDir), fresh: true, json: jsonMode }, async (storage2) => {
|
|
8949
9255
|
losses = (await guard(storage2, platformDir, ref)).losses;
|
|
8950
9256
|
});
|
|
8951
9257
|
console.log(renderGuard(losses, jsonMode ? "json" : "text", ref));
|
|
@@ -10962,6 +11268,10 @@ function mountEditor(app, bindHost = DEFAULT_BIND_HOST) {
|
|
|
10962
11268
|
Lives in (optional, single path)
|
|
10963
11269
|
<input type="text" name="livesIn" placeholder="path/to/impl.ts" />
|
|
10964
11270
|
</label>
|
|
11271
|
+
<label>
|
|
11272
|
+
Relates to (optional, comma-separated ids)
|
|
11273
|
+
<input type="text" name="relates" placeholder="BILLING-001, BILLING-002" />
|
|
11274
|
+
</label>
|
|
10965
11275
|
<button type="submit">Create</button>
|
|
10966
11276
|
</form>
|
|
10967
11277
|
</section>
|
|
@@ -10996,6 +11306,7 @@ function mountEditor(app, bindHost = DEFAULT_BIND_HOST) {
|
|
|
10996
11306
|
const statement = field(form, "statement");
|
|
10997
11307
|
const why = field(form, "why");
|
|
10998
11308
|
const livesIn = field(form, "livesIn");
|
|
11309
|
+
const relates = field(form, "relates");
|
|
10999
11310
|
const payload = {
|
|
11000
11311
|
key: field(form, "key"),
|
|
11001
11312
|
statement
|
|
@@ -11004,6 +11315,8 @@ function mountEditor(app, bindHost = DEFAULT_BIND_HOST) {
|
|
|
11004
11315
|
payload.why = why;
|
|
11005
11316
|
if (livesIn.length > 0)
|
|
11006
11317
|
payload.livesIn = livesIn;
|
|
11318
|
+
if (relates.length > 0)
|
|
11319
|
+
payload.relates = relates;
|
|
11007
11320
|
const res = await app.request("/api/requirements", {
|
|
11008
11321
|
method: "POST",
|
|
11009
11322
|
headers: { "content-type": "application/json" },
|
|
@@ -11904,7 +12217,7 @@ async function readJsonBody(c) {
|
|
|
11904
12217
|
}
|
|
11905
12218
|
return { ok: true, body: parsed };
|
|
11906
12219
|
}
|
|
11907
|
-
function
|
|
12220
|
+
function toStringList(v) {
|
|
11908
12221
|
if (Array.isArray(v))
|
|
11909
12222
|
return v.filter((x) => typeof x === "string");
|
|
11910
12223
|
if (typeof v === "string" && v.length > 0)
|
|
@@ -11915,11 +12228,20 @@ function mintFieldsFromBody(body) {
|
|
|
11915
12228
|
return {
|
|
11916
12229
|
statement: typeof body.statement === "string" ? body.statement : "",
|
|
11917
12230
|
why: typeof body.why === "string" ? body.why : "",
|
|
11918
|
-
livesIn:
|
|
12231
|
+
livesIn: toStringList(body.livesIn),
|
|
12232
|
+
relates: toStringList(body.relates),
|
|
11919
12233
|
issue: typeof body.issue === "string" && body.issue.trim() !== "" ? body.issue.trim() : undefined,
|
|
11920
12234
|
status: body.status === "draft" ? "draft" : "active"
|
|
11921
12235
|
};
|
|
11922
12236
|
}
|
|
12237
|
+
function successorFieldsFromBody(body) {
|
|
12238
|
+
return {
|
|
12239
|
+
why: typeof body.why === "string" ? body.why : undefined,
|
|
12240
|
+
livesIn: "livesIn" in body ? toStringList(body.livesIn) : undefined,
|
|
12241
|
+
relates: "relates" in body ? toStringList(body.relates) : undefined,
|
|
12242
|
+
issue: typeof body.issue === "string" && body.issue.trim() !== "" ? body.issue.trim() : undefined
|
|
12243
|
+
};
|
|
12244
|
+
}
|
|
11923
12245
|
function amendFieldsFromBody(body) {
|
|
11924
12246
|
const fields = {};
|
|
11925
12247
|
if (typeof body.statement === "string")
|
|
@@ -11927,7 +12249,9 @@ function amendFieldsFromBody(body) {
|
|
|
11927
12249
|
if ("why" in body)
|
|
11928
12250
|
fields.why = typeof body.why === "string" ? body.why : null;
|
|
11929
12251
|
if ("livesIn" in body)
|
|
11930
|
-
fields.livesIn =
|
|
12252
|
+
fields.livesIn = toStringList(body.livesIn);
|
|
12253
|
+
if ("relates" in body)
|
|
12254
|
+
fields.relates = toStringList(body.relates);
|
|
11931
12255
|
return Object.keys(fields).length === 0 ? null : fields;
|
|
11932
12256
|
}
|
|
11933
12257
|
function failureResponse(c, failure) {
|
|
@@ -12034,12 +12358,9 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
|
|
|
12034
12358
|
return withWriteLock(async () => {
|
|
12035
12359
|
const fields = amendFieldsFromBody(body);
|
|
12036
12360
|
if (fields === null) {
|
|
12037
|
-
return c.json({ error: "nothing to amend \u2014 provide statement, why, or
|
|
12361
|
+
return c.json({ error: "nothing to amend \u2014 provide statement, why, livesIn, or relates" }, 400);
|
|
12038
12362
|
}
|
|
12039
|
-
const r = await amend({ platformDir, id, fields }
|
|
12040
|
-
await runIndex({ platformDir, storage: storage2 });
|
|
12041
|
-
return storage2.listTags({ req_id: reqId });
|
|
12042
|
-
});
|
|
12363
|
+
const r = await amend({ platformDir, id, fields });
|
|
12043
12364
|
if (!r.ok)
|
|
12044
12365
|
return failureResponse(c, r);
|
|
12045
12366
|
await runIndex({ platformDir, storage: storage2 });
|
|
@@ -12067,14 +12388,7 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
|
|
|
12067
12388
|
if (statement === "")
|
|
12068
12389
|
return c.json({ error: "statement is required (non-empty)" }, 400);
|
|
12069
12390
|
return withWriteLock(async () => {
|
|
12070
|
-
const r = await supersede({
|
|
12071
|
-
platformDir,
|
|
12072
|
-
id,
|
|
12073
|
-
statement,
|
|
12074
|
-
why: typeof body.why === "string" ? body.why : undefined,
|
|
12075
|
-
livesIn: "livesIn" in body ? toLivesIn(body.livesIn) : undefined,
|
|
12076
|
-
issue: typeof body.issue === "string" && body.issue.trim() !== "" ? body.issue.trim() : undefined
|
|
12077
|
-
}, async (reqId) => {
|
|
12391
|
+
const r = await supersede({ platformDir, id, statement, ...successorFieldsFromBody(body) }, async (reqId) => {
|
|
12078
12392
|
await runIndex({ platformDir, storage: storage2 });
|
|
12079
12393
|
return storage2.listTags({ req_id: reqId });
|
|
12080
12394
|
});
|
|
@@ -12358,7 +12672,7 @@ __export(exports_serve, {
|
|
|
12358
12672
|
resolveBind: () => resolveBind,
|
|
12359
12673
|
composeServeApp: () => composeServeApp
|
|
12360
12674
|
});
|
|
12361
|
-
import { existsSync as
|
|
12675
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync6 } from "fs";
|
|
12362
12676
|
import { dirname as dirname8 } from "path";
|
|
12363
12677
|
import { defineCommand as defineCommand24 } from "citty";
|
|
12364
12678
|
import { Hono as Hono2 } from "hono";
|
|
@@ -12411,7 +12725,7 @@ async function runProbe() {
|
|
|
12411
12725
|
}
|
|
12412
12726
|
const body = await res.text();
|
|
12413
12727
|
if (!body.includes("Spec Engine \u2014 coming online")) {
|
|
12414
|
-
console.error(`serve --probe FAILED: body did not contain the placeholder string
|
|
12728
|
+
console.error(`serve --probe FAILED: body did not contain the placeholder string`);
|
|
12415
12729
|
server.stop();
|
|
12416
12730
|
process.exit(EXIT.FAILURE);
|
|
12417
12731
|
}
|
|
@@ -12440,7 +12754,7 @@ var init_serve = __esm(() => {
|
|
|
12440
12754
|
serveCommand = defineCommand24({
|
|
12441
12755
|
meta: {
|
|
12442
12756
|
name: "serve",
|
|
12443
|
-
description: "Run the local webapp. Binds 127.0.0.1 on --port, or one private --host address behind an access token. --probe is the
|
|
12757
|
+
description: "Run the local webapp. Binds 127.0.0.1 on --port, or one private --host address behind an access token. --probe is the asset-embedding smoke."
|
|
12444
12758
|
},
|
|
12445
12759
|
args: {
|
|
12446
12760
|
platformDir: platformDirArg,
|
|
@@ -12510,7 +12824,7 @@ var init_serve = __esm(() => {
|
|
|
12510
12824
|
}
|
|
12511
12825
|
});
|
|
12512
12826
|
mkdirSync6(dirname8(dbPath), { recursive: true });
|
|
12513
|
-
const needsIndex = !
|
|
12827
|
+
const needsIndex = !existsSync14(dbPath);
|
|
12514
12828
|
let storage2;
|
|
12515
12829
|
try {
|
|
12516
12830
|
storage2 = openStorage(dbPath);
|
|
@@ -12531,6 +12845,7 @@ var init_serve = __esm(() => {
|
|
|
12531
12845
|
});
|
|
12532
12846
|
} catch (err) {
|
|
12533
12847
|
storage2.close();
|
|
12848
|
+
handleInvalidDomainFiles(err, false);
|
|
12534
12849
|
handleStorageUnavailable(err, dbPath);
|
|
12535
12850
|
console.error(`spec serve: failed to start on ${urlHost(host, port)}:`, err);
|
|
12536
12851
|
process.exit(EXIT.FAILURE);
|
|
@@ -12557,13 +12872,13 @@ __export(exports_docs, {
|
|
|
12557
12872
|
docsCommand: () => docsCommand,
|
|
12558
12873
|
createDocsFetchHandler: () => createDocsFetchHandler
|
|
12559
12874
|
});
|
|
12560
|
-
import { existsSync as
|
|
12561
|
-
import { join as
|
|
12875
|
+
import { existsSync as existsSync15, statSync as statSync6 } from "fs";
|
|
12876
|
+
import { join as join16, normalize, sep as sep4 } from "path";
|
|
12562
12877
|
import { fileURLToPath } from "url";
|
|
12563
12878
|
import { defineCommand as defineCommand25 } from "citty";
|
|
12564
12879
|
function resolveDocsRootFrom(candidates) {
|
|
12565
12880
|
for (const candidate of candidates) {
|
|
12566
|
-
if (
|
|
12881
|
+
if (existsSync15(join16(candidate, "index.html")))
|
|
12567
12882
|
return candidate;
|
|
12568
12883
|
}
|
|
12569
12884
|
return null;
|
|
@@ -12587,17 +12902,17 @@ function createDocsFetchHandler(root) {
|
|
|
12587
12902
|
if (pathname.includes("\x00")) {
|
|
12588
12903
|
return new Response("Bad request", { status: 400 });
|
|
12589
12904
|
}
|
|
12590
|
-
const target = normalize(
|
|
12905
|
+
const target = normalize(join16(normalRoot, pathname));
|
|
12591
12906
|
if (target !== normalRoot && !target.startsWith(rootPrefix)) {
|
|
12592
12907
|
return new Response("Not found", { status: 404 });
|
|
12593
12908
|
}
|
|
12594
|
-
for (const candidate of [target,
|
|
12595
|
-
if (
|
|
12909
|
+
for (const candidate of [target, join16(target, "index.html"), `${target}.html`]) {
|
|
12910
|
+
if (existsSync15(candidate) && statSync6(candidate).isFile()) {
|
|
12596
12911
|
return new Response(Bun.file(candidate));
|
|
12597
12912
|
}
|
|
12598
12913
|
}
|
|
12599
|
-
const notFoundPage =
|
|
12600
|
-
if (
|
|
12914
|
+
const notFoundPage = join16(normalRoot, "404.html");
|
|
12915
|
+
if (existsSync15(notFoundPage)) {
|
|
12601
12916
|
return new Response(Bun.file(notFoundPage), { status: 404 });
|
|
12602
12917
|
}
|
|
12603
12918
|
return new Response("Not found", { status: 404 });
|
|
@@ -12647,7 +12962,7 @@ var init_docs2 = __esm(() => {
|
|
|
12647
12962
|
port: {
|
|
12648
12963
|
type: "string",
|
|
12649
12964
|
default: "4100",
|
|
12650
|
-
description: "Port (default 4100; 0 = ephemeral). Hostname is hardcoded 127.0.0.1
|
|
12965
|
+
description: "Port (default 4100; 0 = ephemeral). Hostname is hardcoded 127.0.0.1; no --host accepted."
|
|
12651
12966
|
},
|
|
12652
12967
|
probe: {
|
|
12653
12968
|
type: "boolean",
|