@spec-engine/spec-engine 0.6.0 → 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 +2 -2
- package/dist/docs/checks/index.html +25 -5
- package/dist/docs/commands/index.html +6 -6
- 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 +1071 -557
- package/package.json +1 -1
- package/dist/docs/pagefind/fragment/en_01934b1.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_34cad94.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_4560516.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_94271d5.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_aa3ebfe.pf_fragment +0 -0
- package/dist/docs/pagefind/index/en_9af49f2.pf_index +0 -0
- package/dist/docs/pagefind/pagefind.en_beb6d752c1.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
|
|
@@ -373,10 +512,10 @@ var init_flags = __esm(() => {
|
|
|
373
512
|
FEATURE_KEYS = Object.keys(FEATURES);
|
|
374
513
|
});
|
|
375
514
|
// ../shared/src/net.ts
|
|
376
|
-
function
|
|
377
|
-
return LOOPBACK_HOSTNAMES.has(hostname);
|
|
515
|
+
function isServedHostname(hostname, bindHost = DEFAULT_BIND_HOST) {
|
|
516
|
+
return LOOPBACK_HOSTNAMES.has(hostname) || hostname === bindHost;
|
|
378
517
|
}
|
|
379
|
-
var LOOPBACK_HOSTNAMES;
|
|
518
|
+
var DEFAULT_BIND_HOST = "127.0.0.1", LOOPBACK_HOSTNAMES;
|
|
380
519
|
var init_net = __esm(() => {
|
|
381
520
|
LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
382
521
|
});
|
|
@@ -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
|
|
|
@@ -1120,6 +1267,9 @@ function readPlatformMap(dir) {
|
|
|
1120
1267
|
return null;
|
|
1121
1268
|
return { mapped: map(abs), located };
|
|
1122
1269
|
}
|
|
1270
|
+
function platformName(platformDir) {
|
|
1271
|
+
return readPlatformMap(platformDir)?.mapped.name ?? basename(resolve2(platformDir));
|
|
1272
|
+
}
|
|
1123
1273
|
function platformMode(platformDir) {
|
|
1124
1274
|
return readPlatformMap(platformDir)?.mapped.mode ?? "single-repo";
|
|
1125
1275
|
}
|
|
@@ -1545,9 +1695,13 @@ SELECT
|
|
|
1545
1695
|
t.file AS source_file,
|
|
1546
1696
|
t.line AS line,
|
|
1547
1697
|
t.req_id AS req_id,
|
|
1548
|
-
|
|
1549
|
-
'
|
|
1550
|
-
|
|
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
|
|
1551
1705
|
FROM tags t
|
|
1552
1706
|
JOIN requirements r ON r.id = t.req_id
|
|
1553
1707
|
WHERE r.status = 'Draft'
|
|
@@ -1577,11 +1731,10 @@ SELECT
|
|
|
1577
1731
|
'error' AS severity
|
|
1578
1732
|
FROM requirements r
|
|
1579
1733
|
WHERE r.status = 'Active'
|
|
1580
|
-
--
|
|
1581
|
-
-- term never fires ORPHAN_REQ. Literal 'TERM' only, never a real domain key;
|
|
1582
|
-
-- 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.
|
|
1583
1735
|
-- @spec SCHM-017
|
|
1584
1736
|
AND r.key != 'TERM'
|
|
1737
|
+
-- A doc paragraph citing a requirement is not an implementation.
|
|
1585
1738
|
AND NOT EXISTS (
|
|
1586
1739
|
SELECT 1 FROM tags t WHERE t.req_id = r.id AND t.kind != 'documents'
|
|
1587
1740
|
)
|
|
@@ -1595,11 +1748,10 @@ SELECT
|
|
|
1595
1748
|
r.id AS req_id,
|
|
1596
1749
|
('Requirement ' || r.id ||
|
|
1597
1750
|
' is implemented but has no verifies-kind tag') AS detail,
|
|
1598
|
-
'
|
|
1751
|
+
'warning' AS severity
|
|
1599
1752
|
FROM requirements r
|
|
1600
1753
|
WHERE r.status = 'Active'
|
|
1601
|
-
--
|
|
1602
|
-
-- Literal 'TERM' only; mirrors Q4 + the coverage VIEW exclusion.
|
|
1754
|
+
-- Must stay in step with Q4 and the coverage view's TERM exclusion.
|
|
1603
1755
|
AND r.key != 'TERM'
|
|
1604
1756
|
AND EXISTS (SELECT 1 FROM tags t WHERE t.req_id = r.id AND t.kind = 'implements')
|
|
1605
1757
|
AND NOT EXISTS (SELECT 1 FROM tags t WHERE t.req_id = r.id AND t.kind = 'verifies')
|
|
@@ -2442,7 +2594,7 @@ async function runIndex(opts) {
|
|
|
2442
2594
|
unpinned,
|
|
2443
2595
|
diagnostics: platformDiagnostics
|
|
2444
2596
|
} = await discoverRepos(platformDir);
|
|
2445
|
-
const
|
|
2597
|
+
const specs = await readSpecs(canonical);
|
|
2446
2598
|
const { tagHits, docMentionCandidates } = await scanMembers(members);
|
|
2447
2599
|
const rows = buildRows({
|
|
2448
2600
|
canonical,
|
|
@@ -2450,7 +2602,6 @@ async function runIndex(opts) {
|
|
|
2450
2602
|
members,
|
|
2451
2603
|
specs,
|
|
2452
2604
|
tagHits,
|
|
2453
|
-
structuralDiagnostics,
|
|
2454
2605
|
unpinned,
|
|
2455
2606
|
platformDiagnostics
|
|
2456
2607
|
});
|
|
@@ -2472,29 +2623,22 @@ async function runIndex(opts) {
|
|
|
2472
2623
|
async function readSpecs(canonical) {
|
|
2473
2624
|
const jsonPaths = await findDomainJsonFiles(canonical.path);
|
|
2474
2625
|
const specs = [];
|
|
2475
|
-
const
|
|
2626
|
+
const invalid = [];
|
|
2476
2627
|
for (const rel of jsonPaths) {
|
|
2477
2628
|
const absPath = join4(canonical.path, rel);
|
|
2478
2629
|
const text = await Bun.file(absPath).text();
|
|
2479
2630
|
const platformRelative = `${canonical.name}/${rel}`;
|
|
2480
2631
|
const fallbackKey = basename2(dirname3(rel));
|
|
2481
2632
|
const result = parseDomainJsonFile({ text, sourceFile: platformRelative, fallbackKey });
|
|
2482
|
-
if (
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
line: d.line ?? 0,
|
|
2488
|
-
req_id: d.req_id,
|
|
2489
|
-
detail: d.detail,
|
|
2490
|
-
severity: d.severity
|
|
2491
|
-
});
|
|
2492
|
-
}
|
|
2493
|
-
continue;
|
|
2494
|
-
}
|
|
2495
|
-
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 });
|
|
2496
2638
|
}
|
|
2497
|
-
|
|
2639
|
+
if (invalid.length > 0)
|
|
2640
|
+
throw new InvalidDomainFilesError(invalid);
|
|
2641
|
+
return specs;
|
|
2498
2642
|
}
|
|
2499
2643
|
function repoExtraIgnore(member) {
|
|
2500
2644
|
const repoIgnore = (member.ignore ?? []).map((e) => e.endsWith("/") ? e : `${e}/`);
|
|
@@ -2546,16 +2690,7 @@ async function scanMembers(members) {
|
|
|
2546
2690
|
return { tagHits, docMentionCandidates };
|
|
2547
2691
|
}
|
|
2548
2692
|
function buildRows(input) {
|
|
2549
|
-
const {
|
|
2550
|
-
canonical,
|
|
2551
|
-
platformVersion,
|
|
2552
|
-
members,
|
|
2553
|
-
specs,
|
|
2554
|
-
tagHits,
|
|
2555
|
-
structuralDiagnostics,
|
|
2556
|
-
unpinned,
|
|
2557
|
-
platformDiagnostics
|
|
2558
|
-
} = input;
|
|
2693
|
+
const { canonical, platformVersion, members, specs, tagHits, unpinned, platformDiagnostics } = input;
|
|
2559
2694
|
const sortedRepos = sortBy([
|
|
2560
2695
|
{
|
|
2561
2696
|
name: canonical.name,
|
|
@@ -2579,8 +2714,6 @@ function buildRows(input) {
|
|
|
2579
2714
|
const { sortedTermAliases, sortedTermCitations } = buildTermRows(specs, flatRequirements);
|
|
2580
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)}`);
|
|
2581
2716
|
const diagnostics2 = validateStructure(specs);
|
|
2582
|
-
for (const d of structuralDiagnostics)
|
|
2583
|
-
diagnostics2.push(d);
|
|
2584
2717
|
for (const s of unpinned) {
|
|
2585
2718
|
diagnostics2.push({
|
|
2586
2719
|
code: DiagnosticCode.NO_SPEC_CONFIG,
|
|
@@ -3014,6 +3147,49 @@ var init_init = __esm(() => {
|
|
|
3014
3147
|
KNOWN_KEYS = new Set(SpecConfigSchema.keyof().options);
|
|
3015
3148
|
});
|
|
3016
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
|
+
|
|
3017
3193
|
// src/operations/_index.ts
|
|
3018
3194
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync, statSync as statSync3 } from "fs";
|
|
3019
3195
|
import { dirname as dirname4, join as join6 } from "path";
|
|
@@ -3112,6 +3288,26 @@ function handleNotAPlatform(e) {
|
|
|
3112
3288
|
}
|
|
3113
3289
|
throw e;
|
|
3114
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
|
+
}
|
|
3115
3311
|
function handleStorageUnavailable(e, dbPath) {
|
|
3116
3312
|
const info = describeStorageError(e);
|
|
3117
3313
|
if (info === null)
|
|
@@ -3140,7 +3336,7 @@ function exitOnFailure(cmdName, failure) {
|
|
|
3140
3336
|
process.exit(EXIT.USAGE);
|
|
3141
3337
|
}
|
|
3142
3338
|
async function withReadStorage(opts, fn) {
|
|
3143
|
-
const { platformDir, dbPath, fresh } = opts;
|
|
3339
|
+
const { platformDir, dbPath, fresh, json = false } = opts;
|
|
3144
3340
|
try {
|
|
3145
3341
|
await withIndex({ platformDir, dbPath, build: fresh ? "fresh" : "missing" }, async (h) => {
|
|
3146
3342
|
for (const w of h.warnings)
|
|
@@ -3149,6 +3345,7 @@ async function withReadStorage(opts, fn) {
|
|
|
3149
3345
|
});
|
|
3150
3346
|
} catch (e) {
|
|
3151
3347
|
handleStorageUnavailable(e, dbPath);
|
|
3348
|
+
handleInvalidDomainFiles(e, json);
|
|
3152
3349
|
handleNotAPlatform(e);
|
|
3153
3350
|
}
|
|
3154
3351
|
}
|
|
@@ -3355,7 +3552,7 @@ function listFlag(rawArgs, name, parsed) {
|
|
|
3355
3552
|
}
|
|
3356
3553
|
return raw.flatMap((v) => v.split(",")).map((v) => v.trim()).filter((v) => v !== "");
|
|
3357
3554
|
}
|
|
3358
|
-
var platformDirArg, outArg, jsonArg, freshArg, noPromptArg, livesArg;
|
|
3555
|
+
var platformDirArg, outArg, jsonArg, freshArg, noPromptArg, livesArg, relatesArg;
|
|
3359
3556
|
var init__args = __esm(() => {
|
|
3360
3557
|
init_constants();
|
|
3361
3558
|
platformDirArg = {
|
|
@@ -3380,6 +3577,10 @@ var init__args = __esm(() => {
|
|
|
3380
3577
|
type: "string",
|
|
3381
3578
|
description: "Lives in (livesIn) paths: repeat the flag or comma-separate; an empty value clears the list"
|
|
3382
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
|
+
};
|
|
3383
3584
|
});
|
|
3384
3585
|
|
|
3385
3586
|
// src/commands/index.ts
|
|
@@ -3454,6 +3655,7 @@ var init_commands = __esm(() => {
|
|
|
3454
3655
|
console.error(formatNotASpecPlatform(err.platformDir));
|
|
3455
3656
|
process.exit(EXIT.USAGE);
|
|
3456
3657
|
}
|
|
3658
|
+
handleInvalidDomainFiles(err, !!args.json);
|
|
3457
3659
|
console.error("spec index FAILED:", err);
|
|
3458
3660
|
process.exit(EXIT.FAILURE);
|
|
3459
3661
|
} finally {
|
|
@@ -3463,49 +3665,6 @@ var init_commands = __esm(() => {
|
|
|
3463
3665
|
});
|
|
3464
3666
|
});
|
|
3465
3667
|
|
|
3466
|
-
// src/check/format.ts
|
|
3467
|
-
function compareRepoNullsLast(a, b) {
|
|
3468
|
-
if (a === b)
|
|
3469
|
-
return 0;
|
|
3470
|
-
if (a === null)
|
|
3471
|
-
return 1;
|
|
3472
|
-
if (b === null)
|
|
3473
|
-
return -1;
|
|
3474
|
-
return a.localeCompare(b);
|
|
3475
|
-
}
|
|
3476
|
-
function compareDiagnostics(a, b) {
|
|
3477
|
-
const codeCmp = a.code.localeCompare(b.code);
|
|
3478
|
-
if (codeCmp !== 0)
|
|
3479
|
-
return codeCmp;
|
|
3480
|
-
const repoCmp = compareRepoNullsLast(a.repo, b.repo);
|
|
3481
|
-
if (repoCmp !== 0)
|
|
3482
|
-
return repoCmp;
|
|
3483
|
-
const fileCmp = (a.source_file ?? "").localeCompare(b.source_file ?? "");
|
|
3484
|
-
if (fileCmp !== 0)
|
|
3485
|
-
return fileCmp;
|
|
3486
|
-
const lineCmp = (a.line ?? 0) - (b.line ?? 0);
|
|
3487
|
-
if (lineCmp !== 0)
|
|
3488
|
-
return lineCmp;
|
|
3489
|
-
return (a.req_id ?? "").localeCompare(b.req_id ?? "");
|
|
3490
|
-
}
|
|
3491
|
-
function sortDiagnostics(rows) {
|
|
3492
|
-
return [...rows].sort(compareDiagnostics);
|
|
3493
|
-
}
|
|
3494
|
-
function renderDiagnostics(rows, mode) {
|
|
3495
|
-
const sorted = sortDiagnostics(rows);
|
|
3496
|
-
if (mode === "json") {
|
|
3497
|
-
return JSON.stringify(sorted);
|
|
3498
|
-
}
|
|
3499
|
-
return sorted.map((d) => {
|
|
3500
|
-
const repo = d.repo ?? "";
|
|
3501
|
-
const file = d.source_file ?? "";
|
|
3502
|
-
const line = d.line ?? "";
|
|
3503
|
-
const reqId = d.req_id ?? "";
|
|
3504
|
-
return `${d.code} ${repo} ${file}:${line} ${reqId} ${d.detail}`;
|
|
3505
|
-
}).join(`
|
|
3506
|
-
`);
|
|
3507
|
-
}
|
|
3508
|
-
|
|
3509
3668
|
// src/check/proven.ts
|
|
3510
3669
|
function normalizeToSegments(path) {
|
|
3511
3670
|
const out = [];
|
|
@@ -3688,6 +3847,14 @@ function gitDiffNameStatus(platformDir, ref) {
|
|
|
3688
3847
|
}
|
|
3689
3848
|
return rows;
|
|
3690
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
|
+
}
|
|
3691
3858
|
|
|
3692
3859
|
// src/check/codeowners.ts
|
|
3693
3860
|
function parseCodeowners(text) {
|
|
@@ -4060,6 +4227,60 @@ var init_removed = __esm(() => {
|
|
|
4060
4227
|
init_src();
|
|
4061
4228
|
});
|
|
4062
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
|
+
|
|
4063
4284
|
// src/check/sqlDiagnostics.ts
|
|
4064
4285
|
function collectDiagnostics(storage2) {
|
|
4065
4286
|
const semantic = storage2.listSemanticDiagnostics();
|
|
@@ -4498,7 +4719,7 @@ function localToday() {
|
|
|
4498
4719
|
}
|
|
4499
4720
|
|
|
4500
4721
|
// src/operations/_envelope.ts
|
|
4501
|
-
import { existsSync as
|
|
4722
|
+
import { existsSync as existsSync8 } from "fs";
|
|
4502
4723
|
function domainKeyOf(id) {
|
|
4503
4724
|
return id.slice(0, id.indexOf("-"));
|
|
4504
4725
|
}
|
|
@@ -4507,7 +4728,7 @@ function displayStatus(raw) {
|
|
|
4507
4728
|
}
|
|
4508
4729
|
async function readEnvelope(platformDir, key, missingDetail) {
|
|
4509
4730
|
const { abs: specPath, rel: relFile } = specPaths(platformDir, key);
|
|
4510
|
-
if (!
|
|
4731
|
+
if (!existsSync8(specPath)) {
|
|
4511
4732
|
return fail("not_found", missingDetail ?? `no domain ${key} (expected ${relFile} under ${platformDir})`);
|
|
4512
4733
|
}
|
|
4513
4734
|
const parsed = parseDomainText(await Bun.file(specPath).text(), relFile);
|
|
@@ -4523,32 +4744,58 @@ async function readEnvelope(platformDir, key, missingDetail) {
|
|
|
4523
4744
|
const domain2 = parsed.data;
|
|
4524
4745
|
return { ok: true, key, specPath, relFile, domain: domain2, requirements: domain2.requirements };
|
|
4525
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
|
+
}
|
|
4526
4760
|
async function locateEntry(platformDir, id, missingDomainDetail) {
|
|
4527
|
-
const
|
|
4528
|
-
|
|
4761
|
+
const prefixKey = domainKeyOf(id);
|
|
4762
|
+
const file = await readEnvelope(platformDir, prefixKey, missingDomainDetail);
|
|
4763
|
+
if (!file.ok && file.reason !== "not_found")
|
|
4529
4764
|
return file;
|
|
4530
|
-
const req = file.requirements.find((r) => r.id === id);
|
|
4531
|
-
if (req
|
|
4532
|
-
return
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
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 = {
|
|
4537
4778
|
id,
|
|
4538
4779
|
status: "active",
|
|
4539
4780
|
statement: fields.statement,
|
|
4540
4781
|
why: fields.why === "" ? null : fields.why,
|
|
4541
4782
|
supersedes: null,
|
|
4542
4783
|
supersededBy: null,
|
|
4543
|
-
relates:
|
|
4784
|
+
relates: fields.relates,
|
|
4544
4785
|
livesIn: fields.livesIn,
|
|
4545
4786
|
issues: fields.issue ? [{ role: "created", id: fields.issue }] : [],
|
|
4546
|
-
aliases: [],
|
|
4547
|
-
cites:
|
|
4787
|
+
aliases: [...pred.aliases],
|
|
4788
|
+
cites: pred.cites.map((c) => ({ ...c }))
|
|
4548
4789
|
};
|
|
4790
|
+
if (pred.term !== undefined)
|
|
4791
|
+
entry.term = pred.term;
|
|
4792
|
+
if (pred.section !== undefined)
|
|
4793
|
+
entry.section = pred.section;
|
|
4794
|
+
return entry;
|
|
4549
4795
|
}
|
|
4550
4796
|
var init__envelope = __esm(() => {
|
|
4551
4797
|
init_src();
|
|
4798
|
+
init_domains();
|
|
4552
4799
|
init_constants();
|
|
4553
4800
|
init__result();
|
|
4554
4801
|
});
|
|
@@ -4603,7 +4850,7 @@ var init_grammar2 = __esm(() => {
|
|
|
4603
4850
|
});
|
|
4604
4851
|
|
|
4605
4852
|
// src/operations/mint.ts
|
|
4606
|
-
import { existsSync as
|
|
4853
|
+
import { existsSync as existsSync9 } from "fs";
|
|
4607
4854
|
function unresolvableRefWarnings(platformDir, fieldValues) {
|
|
4608
4855
|
const out = [];
|
|
4609
4856
|
for (const value of fieldValues) {
|
|
@@ -4618,7 +4865,7 @@ function unresolvableRefWarnings(platformDir, fieldValues) {
|
|
|
4618
4865
|
async function mint(input) {
|
|
4619
4866
|
const { platformDir, key } = input;
|
|
4620
4867
|
const { abs: specPath, rel: relFile } = specPaths(platformDir, key);
|
|
4621
|
-
if (!
|
|
4868
|
+
if (!existsSync9(specPath)) {
|
|
4622
4869
|
return fail("not_found", `no domain ${key} (expected ${relFile} under ${platformDir})`);
|
|
4623
4870
|
}
|
|
4624
4871
|
const warnings = unresolvableRefWarnings(platformDir, [
|
|
@@ -4646,7 +4893,7 @@ async function mint(input) {
|
|
|
4646
4893
|
why: input.why || null,
|
|
4647
4894
|
supersedes: null,
|
|
4648
4895
|
supersededBy: null,
|
|
4649
|
-
relates: input.relates ?? [],
|
|
4896
|
+
relates: splitList(input.relates ?? []),
|
|
4650
4897
|
livesIn: input.livesIn,
|
|
4651
4898
|
issues: input.issue ? [{ role: "created", id: input.issue }] : [],
|
|
4652
4899
|
aliases: [],
|
|
@@ -4805,8 +5052,8 @@ var init_term = __esm(() => {
|
|
|
4805
5052
|
});
|
|
4806
5053
|
|
|
4807
5054
|
// src/operations/glossary.ts
|
|
4808
|
-
import { existsSync as
|
|
4809
|
-
import { join as
|
|
5055
|
+
import { existsSync as existsSync10, readFileSync as readFileSync2 } from "fs";
|
|
5056
|
+
import { join as join9 } from "path";
|
|
4810
5057
|
function parseGlossary(md) {
|
|
4811
5058
|
const terms = [];
|
|
4812
5059
|
let section = null;
|
|
@@ -4858,15 +5105,15 @@ function generateGlossary(terms) {
|
|
|
4858
5105
|
}
|
|
4859
5106
|
function readStoreTerms(platformDir) {
|
|
4860
5107
|
const { abs: specPath, rel: relFile } = specPaths(platformDir, TERM_KEY);
|
|
4861
|
-
if (!
|
|
5108
|
+
if (!existsSync10(specPath))
|
|
4862
5109
|
return [];
|
|
4863
|
-
const parsed = parseDomainText(
|
|
5110
|
+
const parsed = parseDomainText(readFileSync2(specPath, "utf8"), relFile);
|
|
4864
5111
|
if (!parsed.ok)
|
|
4865
5112
|
return [];
|
|
4866
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 }));
|
|
4867
5114
|
}
|
|
4868
5115
|
function glossaryPath(platformDir) {
|
|
4869
|
-
return
|
|
5116
|
+
return join9(platformDir, "GLOSSARY.md");
|
|
4870
5117
|
}
|
|
4871
5118
|
async function migrateGlossary(platformDir) {
|
|
4872
5119
|
const store = await termStore(platformDir);
|
|
@@ -4877,10 +5124,10 @@ async function migrateGlossary(platformDir) {
|
|
|
4877
5124
|
return { ok: true, skipped: true, existing: domain2.requirements.length };
|
|
4878
5125
|
}
|
|
4879
5126
|
const gPath = glossaryPath(platformDir);
|
|
4880
|
-
if (!
|
|
5127
|
+
if (!existsSync10(gPath)) {
|
|
4881
5128
|
return fail("not_found", `no GLOSSARY.md to migrate (expected ${gPath})`);
|
|
4882
5129
|
}
|
|
4883
|
-
const parsed = parseGlossary(
|
|
5130
|
+
const parsed = parseGlossary(readFileSync2(gPath, "utf8"));
|
|
4884
5131
|
domain2.requirements = parsed.map((t, i) => ({
|
|
4885
5132
|
id: `${TERM_KEY}-${String(i + 1).padStart(3, "0")}`,
|
|
4886
5133
|
status: "active",
|
|
@@ -4917,10 +5164,10 @@ function glossaryDriftDiagnostic(platformDir) {
|
|
|
4917
5164
|
if (terms.length === 0)
|
|
4918
5165
|
return null;
|
|
4919
5166
|
const gPath = glossaryPath(platformDir);
|
|
4920
|
-
if (!
|
|
5167
|
+
if (!existsSync10(gPath))
|
|
4921
5168
|
return null;
|
|
4922
5169
|
const generated = generateGlossary(terms);
|
|
4923
|
-
const committed =
|
|
5170
|
+
const committed = readFileSync2(gPath, "utf8");
|
|
4924
5171
|
if (generated === committed)
|
|
4925
5172
|
return null;
|
|
4926
5173
|
return {
|
|
@@ -4936,7 +5183,7 @@ function glossaryDriftDiagnostic(platformDir) {
|
|
|
4936
5183
|
function checkGlossary(platformDir) {
|
|
4937
5184
|
const generated = generateGlossary(readStoreTerms(platformDir));
|
|
4938
5185
|
const gPath = glossaryPath(platformDir);
|
|
4939
|
-
const committed =
|
|
5186
|
+
const committed = existsSync10(gPath) ? readFileSync2(gPath, "utf8") : "";
|
|
4940
5187
|
return { ok: true, clean: generated === committed };
|
|
4941
5188
|
}
|
|
4942
5189
|
var GLOSSARY_INTRO = `Canonical names for Spec Engine concepts. When prose, code comments, diagnostics, or
|
|
@@ -4949,38 +5196,232 @@ var init_glossary = __esm(() => {
|
|
|
4949
5196
|
init_term();
|
|
4950
5197
|
});
|
|
4951
5198
|
|
|
4952
|
-
// src/
|
|
4953
|
-
import {
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
const path = join9(platformDir, ".github", "CODEOWNERS");
|
|
4957
|
-
if (!existsSync10(path))
|
|
4958
|
-
return null;
|
|
4959
|
-
return readFileSync2(path, "utf-8");
|
|
5199
|
+
// src/guard/collect.ts
|
|
5200
|
+
import { join as join10 } from "path";
|
|
5201
|
+
function isIgnored(path) {
|
|
5202
|
+
return isPathIgnored(path);
|
|
4960
5203
|
}
|
|
4961
|
-
function
|
|
4962
|
-
|
|
4963
|
-
if (!validated.ok)
|
|
4964
|
-
return { reqs: [], diagnostics: validated.diagnostics };
|
|
4965
|
-
return { reqs: validated.data.requirements, diagnostics: [] };
|
|
5204
|
+
function isSpecFile(path) {
|
|
5205
|
+
return path.startsWith(`${CANONICAL_SPECS_DIR}/`) && path.endsWith("/SPEC.json") && !isIgnored(path);
|
|
4966
5206
|
}
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
const parsed = domainReqsFromText(text, platformRel);
|
|
4974
|
-
for (const r of parsed.reqs) {
|
|
4975
|
-
reqs.push(r);
|
|
4976
|
-
relPathById.set(r.id, platformRel);
|
|
4977
|
-
}
|
|
4978
|
-
}
|
|
4979
|
-
return { reqs, relPathById };
|
|
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 : [];
|
|
4980
5213
|
}
|
|
4981
|
-
function collectBaseReqs(platformDir, ref,
|
|
5214
|
+
function collectBaseReqs(platformDir, ref, specPaths2) {
|
|
4982
5215
|
const reqs = [];
|
|
4983
|
-
const
|
|
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 };
|
|
5388
|
+
}
|
|
5389
|
+
var init_guard = __esm(() => {
|
|
5390
|
+
init_collect();
|
|
5391
|
+
});
|
|
5392
|
+
|
|
5393
|
+
// src/operations/check.ts
|
|
5394
|
+
import { existsSync as existsSync11, readFileSync as readFileSync3 } from "fs";
|
|
5395
|
+
import { join as join11 } from "path";
|
|
5396
|
+
function readCodeownersText(platformDir) {
|
|
5397
|
+
const path = join11(platformDir, ".github", "CODEOWNERS");
|
|
5398
|
+
if (!existsSync11(path))
|
|
5399
|
+
return null;
|
|
5400
|
+
return readFileSync3(path, "utf-8");
|
|
5401
|
+
}
|
|
5402
|
+
function domainReqsFromText(text, platformRel) {
|
|
5403
|
+
const validated = parseDomainText(text, platformRel);
|
|
5404
|
+
if (!validated.ok)
|
|
5405
|
+
return { reqs: [], diagnostics: validated.diagnostics };
|
|
5406
|
+
return { reqs: validated.data.requirements, diagnostics: [] };
|
|
5407
|
+
}
|
|
5408
|
+
async function collectChangeReqs(canonicalDir) {
|
|
5409
|
+
const reqs = [];
|
|
5410
|
+
const relPathById = new Map;
|
|
5411
|
+
for (const rel of await findDomainJsonFiles(canonicalDir)) {
|
|
5412
|
+
const platformRel = `${CANONICAL_SPECS_DIR}/${rel}`;
|
|
5413
|
+
const text = await Bun.file(join11(canonicalDir, rel)).text();
|
|
5414
|
+
const parsed = domainReqsFromText(text, platformRel);
|
|
5415
|
+
for (const r of parsed.reqs) {
|
|
5416
|
+
reqs.push(r);
|
|
5417
|
+
relPathById.set(r.id, platformRel);
|
|
5418
|
+
}
|
|
5419
|
+
}
|
|
5420
|
+
return { reqs, relPathById };
|
|
5421
|
+
}
|
|
5422
|
+
function collectBaseReqs2(platformDir, ref, diagnostics2) {
|
|
5423
|
+
const reqs = [];
|
|
5424
|
+
const relPathById = new Map;
|
|
4984
5425
|
for (const path of gitLsTree(platformDir, ref, CANONICAL_SPECS_DIR)) {
|
|
4985
5426
|
if (!path.endsWith(`/${SPEC_FILENAME}`))
|
|
4986
5427
|
continue;
|
|
@@ -5010,8 +5451,8 @@ async function ingestResults(storage2, resultsPath, diagnostics2) {
|
|
|
5010
5451
|
}
|
|
5011
5452
|
async function governanceGate(input, ref, diagnostics2, results) {
|
|
5012
5453
|
const { platformDir } = input;
|
|
5013
|
-
const change = await collectChangeReqs(
|
|
5014
|
-
const base =
|
|
5454
|
+
const change = await collectChangeReqs(join11(platformDir, CANONICAL_SPECS_DIR));
|
|
5455
|
+
const base = collectBaseReqs2(platformDir, ref, diagnostics2);
|
|
5015
5456
|
diagnostics2.push(...requirementRemoved(base.reqs, change.reqs, (id) => base.relPathById.get(id) ?? null));
|
|
5016
5457
|
const codeowners = parseCodeowners(readCodeownersText(platformDir) ?? "");
|
|
5017
5458
|
const approvedBy = (input.approvedBy ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -5020,12 +5461,33 @@ async function governanceGate(input, ref, diagnostics2, results) {
|
|
|
5020
5461
|
diagnostics2.push(...partialPropagation(changedRules(base.reqs, change.reqs), results.verifyingTags, results.resultsParsed, (id) => change.relPathById.get(id) ?? null));
|
|
5021
5462
|
}
|
|
5022
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
|
+
}
|
|
5023
5482
|
async function check(input, storage2) {
|
|
5024
5483
|
const { platformDir } = input;
|
|
5025
5484
|
if (input.base && !gitRefResolves(platformDir, input.base)) {
|
|
5026
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");
|
|
5027
5486
|
}
|
|
5028
5487
|
const result = await runIndex({ platformDir, storage: storage2 });
|
|
5488
|
+
const checkConfig2 = readCheckConfig(platformDir);
|
|
5489
|
+
if (!checkConfig2.ok)
|
|
5490
|
+
return fail("usage", checkConfig2.detail);
|
|
5029
5491
|
const diagnostics2 = collectDiagnostics(storage2);
|
|
5030
5492
|
if (input.unsourcedChange) {
|
|
5031
5493
|
const superseded = storage2.listRequirements({ status: "Superseded" });
|
|
@@ -5040,20 +5502,24 @@ async function check(input, storage2) {
|
|
|
5040
5502
|
const results = await ingestResults(storage2, input.resultsPath, diagnostics2);
|
|
5041
5503
|
if (input.base) {
|
|
5042
5504
|
await governanceGate(input, input.base, diagnostics2, results);
|
|
5505
|
+
diagnostics2.push(...await lossDiagnostics(storage2, platformDir, input.base));
|
|
5043
5506
|
} else if (gitRefResolves(platformDir, "HEAD")) {
|
|
5044
|
-
const change = await collectChangeReqs(
|
|
5507
|
+
const change = await collectChangeReqs(join11(platformDir, CANONICAL_SPECS_DIR));
|
|
5045
5508
|
const baseParseSink = [];
|
|
5046
|
-
const base =
|
|
5509
|
+
const base = collectBaseReqs2(platformDir, "HEAD", baseParseSink);
|
|
5047
5510
|
diagnostics2.push(...requirementRemoved(base.reqs, change.reqs, (id) => base.relPathById.get(id) ?? null));
|
|
5511
|
+
diagnostics2.push(...await lossDiagnostics(storage2, platformDir, "HEAD"));
|
|
5048
5512
|
}
|
|
5513
|
+
const graded = applySeverity(diagnostics2, checkConfig2.config.severity);
|
|
5049
5514
|
return {
|
|
5050
5515
|
ok: true,
|
|
5051
|
-
diagnostics:
|
|
5516
|
+
diagnostics: graded,
|
|
5052
5517
|
buildId: result.build_id,
|
|
5053
|
-
failing:
|
|
5518
|
+
failing: graded.some((d) => d.severity === "error"),
|
|
5054
5519
|
proofsUnconfirmed: !input.resultsPath
|
|
5055
5520
|
};
|
|
5056
5521
|
}
|
|
5522
|
+
var LOSS_SEVERITY;
|
|
5057
5523
|
var init_check = __esm(() => {
|
|
5058
5524
|
init_src();
|
|
5059
5525
|
init_filerefs2();
|
|
@@ -5061,6 +5527,7 @@ var init_check = __esm(() => {
|
|
|
5061
5527
|
init_propagation_teeth();
|
|
5062
5528
|
init_proven();
|
|
5063
5529
|
init_removed();
|
|
5530
|
+
init_severity();
|
|
5064
5531
|
init_sqlDiagnostics();
|
|
5065
5532
|
init_statusflip();
|
|
5066
5533
|
init_supersedes();
|
|
@@ -5071,6 +5538,12 @@ var init_check = __esm(() => {
|
|
|
5071
5538
|
init_fs();
|
|
5072
5539
|
init__result();
|
|
5073
5540
|
init_glossary();
|
|
5541
|
+
init_guard();
|
|
5542
|
+
LOSS_SEVERITY = {
|
|
5543
|
+
IMPL_LOST: "error",
|
|
5544
|
+
SPEC_FILE_DELETED: "error",
|
|
5545
|
+
VERIFY_LOST: "warning"
|
|
5546
|
+
};
|
|
5074
5547
|
});
|
|
5075
5548
|
|
|
5076
5549
|
// src/commands/check.ts
|
|
@@ -5120,17 +5593,17 @@ var init_check2 = __esm(() => {
|
|
|
5120
5593
|
out: outArg,
|
|
5121
5594
|
ci: {
|
|
5122
5595
|
type: "boolean",
|
|
5123
|
-
description: "Force cold rebuild: rm db+wal+shm BEFORE indexing
|
|
5596
|
+
description: "Force cold rebuild: rm db+wal+shm BEFORE indexing"
|
|
5124
5597
|
},
|
|
5125
5598
|
json: jsonArg,
|
|
5126
5599
|
noPrompt: noPromptArg,
|
|
5127
5600
|
unsourcedChange: {
|
|
5128
5601
|
type: "boolean",
|
|
5129
|
-
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)"
|
|
5130
5603
|
},
|
|
5131
5604
|
results: {
|
|
5132
5605
|
type: "string",
|
|
5133
|
-
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"
|
|
5134
5607
|
},
|
|
5135
5608
|
base: {
|
|
5136
5609
|
type: "string",
|
|
@@ -5181,6 +5654,7 @@ var init_check2 = __esm(() => {
|
|
|
5181
5654
|
return result;
|
|
5182
5655
|
});
|
|
5183
5656
|
} catch (e) {
|
|
5657
|
+
handleInvalidDomainFiles(e, jsonMode);
|
|
5184
5658
|
exitAfterCrash(e);
|
|
5185
5659
|
}
|
|
5186
5660
|
if (outcome === undefined) {
|
|
@@ -5204,8 +5678,8 @@ var init_check2 = __esm(() => {
|
|
|
5204
5678
|
});
|
|
5205
5679
|
|
|
5206
5680
|
// src/operations/domain.ts
|
|
5207
|
-
import { existsSync as
|
|
5208
|
-
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";
|
|
5209
5683
|
function definedFields(options) {
|
|
5210
5684
|
const out = {};
|
|
5211
5685
|
if (options.scope !== undefined)
|
|
@@ -5225,9 +5699,9 @@ async function newDomain(platformDir, key, options = {}) {
|
|
|
5225
5699
|
if (!(resolvedDest === resolvedRoot || resolvedDest.startsWith(`${resolvedRoot}/`))) {
|
|
5226
5700
|
return fail("usage", `refusing to write outside platformDir (${resolvedDest})`);
|
|
5227
5701
|
}
|
|
5228
|
-
if (
|
|
5702
|
+
if (existsSync12(dest))
|
|
5229
5703
|
return fail("conflict", `refusing to overwrite ${dest}`);
|
|
5230
|
-
if (
|
|
5704
|
+
if (existsSync12(join12(dirname6(dest), "SPEC.md"))) {
|
|
5231
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)");
|
|
5232
5706
|
}
|
|
5233
5707
|
mkdirSync4(dirname6(dest), { recursive: true });
|
|
@@ -5525,7 +5999,7 @@ var init_map = __esm(() => {
|
|
|
5525
5999
|
noPrompt: args.noPrompt
|
|
5526
6000
|
}
|
|
5527
6001
|
});
|
|
5528
|
-
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, (storage2) => {
|
|
6002
|
+
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, (storage2) => {
|
|
5529
6003
|
const { rows } = coverageMatrix(storage2);
|
|
5530
6004
|
if (!args.json && rows.length === 0) {
|
|
5531
6005
|
console.error(formatNoRequirementsIndexed(platformDir));
|
|
@@ -5638,7 +6112,7 @@ var init_get = __esm(() => {
|
|
|
5638
6112
|
platformDir,
|
|
5639
6113
|
args: { noPrompt: args.noPrompt }
|
|
5640
6114
|
});
|
|
5641
|
-
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, (storage2) => {
|
|
6115
|
+
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, (storage2) => {
|
|
5642
6116
|
const result = getRecord(storage2, reqId);
|
|
5643
6117
|
if (result.row === null) {
|
|
5644
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)`);
|
|
@@ -5712,7 +6186,7 @@ var init_list = __esm(() => {
|
|
|
5712
6186
|
platformDir,
|
|
5713
6187
|
args: { noPrompt: args.noPrompt }
|
|
5714
6188
|
});
|
|
5715
|
-
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, (storage2) => {
|
|
6189
|
+
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, (storage2) => {
|
|
5716
6190
|
const result = listRecords(storage2, filter);
|
|
5717
6191
|
if (!args.json && result.rows.length === 0) {
|
|
5718
6192
|
console.error(result.platformEmpty ? formatNoRequirementsIndexed(platformDir) : `spec list: no requirements match under ${platformDir}`);
|
|
@@ -5789,7 +6263,7 @@ var init_propagation2 = __esm(() => {
|
|
|
5789
6263
|
platformDir,
|
|
5790
6264
|
args: { noPrompt: args.noPrompt }
|
|
5791
6265
|
});
|
|
5792
|
-
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, (storage2) => {
|
|
6266
|
+
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, (storage2) => {
|
|
5793
6267
|
const result = propagation2(storage2, reqId);
|
|
5794
6268
|
if (!args.json && result.platformEmpty) {
|
|
5795
6269
|
console.error(formatNoRequirementsIndexed(platformDir));
|
|
@@ -5912,7 +6386,7 @@ var init_query = __esm(() => {
|
|
|
5912
6386
|
platformDir,
|
|
5913
6387
|
args: { noPrompt: args.noPrompt }
|
|
5914
6388
|
});
|
|
5915
|
-
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, (storage2) => {
|
|
6389
|
+
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, (storage2) => {
|
|
5916
6390
|
const result = query(storage2, text, limit);
|
|
5917
6391
|
if (!result.ok) {
|
|
5918
6392
|
console.error(`spec query: ${result.detail}`);
|
|
@@ -6034,7 +6508,7 @@ var init_relations = __esm(() => {
|
|
|
6034
6508
|
noPrompt: args.noPrompt
|
|
6035
6509
|
}
|
|
6036
6510
|
});
|
|
6037
|
-
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, (storage2) => {
|
|
6511
|
+
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, (storage2) => {
|
|
6038
6512
|
const { rows } = relations(storage2);
|
|
6039
6513
|
if (!args.json && rows.length === 0) {
|
|
6040
6514
|
console.error(formatNoRelations(platformDir));
|
|
@@ -6159,7 +6633,7 @@ var init_adapter = () => {};
|
|
|
6159
6633
|
|
|
6160
6634
|
// ../tracker/src/cache.ts
|
|
6161
6635
|
import { mkdirSync as mkdirSync5 } from "fs";
|
|
6162
|
-
import { dirname as dirname7, join as
|
|
6636
|
+
import { dirname as dirname7, join as join13 } from "path";
|
|
6163
6637
|
function isFresh(entry, now, ttlMs = CACHE_TTL_MS) {
|
|
6164
6638
|
const t = Date.parse(entry.fetched_at);
|
|
6165
6639
|
if (Number.isNaN(t))
|
|
@@ -6173,7 +6647,7 @@ function isValidEntry(e) {
|
|
|
6173
6647
|
return typeof r.title === "string" && typeof r.status === "string" && typeof r.url === "string" && typeof r.fetched_at === "string";
|
|
6174
6648
|
}
|
|
6175
6649
|
function sidecarPath(platformDir) {
|
|
6176
|
-
return
|
|
6650
|
+
return join13(platformDir, ".spec-engine", SIDECAR_FILE);
|
|
6177
6651
|
}
|
|
6178
6652
|
async function readCache(platformDir) {
|
|
6179
6653
|
try {
|
|
@@ -6440,7 +6914,7 @@ var init_provenance = __esm(() => {
|
|
|
6440
6914
|
noPrompt: args.noPrompt
|
|
6441
6915
|
}
|
|
6442
6916
|
});
|
|
6443
|
-
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, async (storage2) => {
|
|
6917
|
+
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, async (storage2) => {
|
|
6444
6918
|
const { rows } = provenance(storage2, issueId || undefined);
|
|
6445
6919
|
if (!args.json && rows.length === 0) {
|
|
6446
6920
|
console.error(issueId ? formatNoProvenanceForIssue(issueId, platformDir) : formatNoProvenance(platformDir));
|
|
@@ -6504,6 +6978,7 @@ async function mintFromFlags(platformDir, key, textFlag, args, rawArgs) {
|
|
|
6504
6978
|
statement,
|
|
6505
6979
|
why: (args.why ?? "").trim(),
|
|
6506
6980
|
livesIn: listFlag(rawArgs, "lives", args.lives) ?? [],
|
|
6981
|
+
relates: listFlag(rawArgs, "relates", args.relates) ?? [],
|
|
6507
6982
|
issue: (args.issue ?? "").trim() || undefined,
|
|
6508
6983
|
status: statusOf(args),
|
|
6509
6984
|
json: Boolean(args.json)
|
|
@@ -6582,6 +7057,7 @@ var init_req = __esm(() => {
|
|
|
6582
7057
|
},
|
|
6583
7058
|
why: { type: "string", description: "Why it matters field (only with --text)" },
|
|
6584
7059
|
lives: livesArg,
|
|
7060
|
+
relates: relatesArg,
|
|
6585
7061
|
issue: {
|
|
6586
7062
|
type: "string",
|
|
6587
7063
|
description: "Originating ticket id, recorded as created-provenance on the entry (only with --text; opaque \u2014 never a requirement id)"
|
|
@@ -6614,9 +7090,9 @@ var init_req = __esm(() => {
|
|
|
6614
7090
|
await mintFromFlags(platformDir, key, textFlag, args, rawArgs);
|
|
6615
7091
|
return;
|
|
6616
7092
|
}
|
|
6617
|
-
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";
|
|
6618
7094
|
if (hasFieldFlags) {
|
|
6619
|
-
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)");
|
|
6620
7096
|
process.exit(EXIT.USAGE);
|
|
6621
7097
|
return;
|
|
6622
7098
|
}
|
|
@@ -6752,6 +7228,9 @@ async function confirmTermCommand(opts) {
|
|
|
6752
7228
|
exitOnFailure("spec term confirm", result);
|
|
6753
7229
|
await withIndex({ platformDir, build: "fresh" }, () => {
|
|
6754
7230
|
return;
|
|
7231
|
+
}).catch((e) => {
|
|
7232
|
+
handleInvalidDomainFiles(e, false, EXIT.USAGE);
|
|
7233
|
+
throw e;
|
|
6755
7234
|
});
|
|
6756
7235
|
const { pinned, file } = result;
|
|
6757
7236
|
if (opts.json) {
|
|
@@ -7024,14 +7503,18 @@ async function supersedeTarget(platformDir, id) {
|
|
|
7024
7503
|
return located;
|
|
7025
7504
|
}
|
|
7026
7505
|
function successorFields(input, req) {
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
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
|
+
};
|
|
7031
7511
|
}
|
|
7032
|
-
function
|
|
7033
|
-
const term =
|
|
7034
|
-
|
|
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;
|
|
7035
7518
|
}
|
|
7036
7519
|
function flipPredecessor(req, newId, issue) {
|
|
7037
7520
|
req.status = "superseded";
|
|
@@ -7067,18 +7550,19 @@ async function supersede(input, freshTags) {
|
|
|
7067
7550
|
}
|
|
7068
7551
|
warnings.push({ kind: "grammar", text: grammarWarningText("", verdict).slice(2) });
|
|
7069
7552
|
}
|
|
7070
|
-
const { why, livesIn } = successorFields(input, req);
|
|
7553
|
+
const { why, livesIn, relates } = successorFields(input, req);
|
|
7071
7554
|
warnings.push(...unresolvableRefWarnings(platformDir, [input.statement, why, ...livesIn]));
|
|
7072
7555
|
const newId = await nextRequirementId(platformDir, key);
|
|
7073
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);
|
|
7074
7565
|
flipPredecessor(req, newId, issue);
|
|
7075
|
-
const successor = successorEntry(newId, { statement: input.statement, why, livesIn, issue });
|
|
7076
|
-
if (key === "TERM") {
|
|
7077
|
-
const termFields = successorTermFields(input, req);
|
|
7078
|
-
if (termFields.term !== undefined)
|
|
7079
|
-
successor.term = termFields.term;
|
|
7080
|
-
successor.aliases = termFields.aliases;
|
|
7081
|
-
}
|
|
7082
7566
|
requirements.push(successor);
|
|
7083
7567
|
const specVersion = applyVersionStage(domain2, req, successor, Boolean(input.noBump));
|
|
7084
7568
|
domain2.updated = localToday();
|
|
@@ -7187,6 +7671,9 @@ function inputFromArgs(args, rawArgs, platformDir, id, statement) {
|
|
|
7187
7671
|
const livesIn = listFlag(rawArgs, "lives", args.lives);
|
|
7188
7672
|
if (livesIn !== undefined)
|
|
7189
7673
|
input.livesIn = livesIn;
|
|
7674
|
+
const relates = listFlag(rawArgs, "relates", args.relates);
|
|
7675
|
+
if (relates !== undefined)
|
|
7676
|
+
input.relates = relates;
|
|
7190
7677
|
if (typeof args.term === "string")
|
|
7191
7678
|
input.term = args.term;
|
|
7192
7679
|
if (typeof args.aliases === "string")
|
|
@@ -7200,7 +7687,6 @@ var supersedeArgs, supersedeCommand;
|
|
|
7200
7687
|
var init_supersede2 = __esm(() => {
|
|
7201
7688
|
init_constants();
|
|
7202
7689
|
init_discover();
|
|
7203
|
-
init__index();
|
|
7204
7690
|
init_supersede();
|
|
7205
7691
|
init_grammar3();
|
|
7206
7692
|
init__args();
|
|
@@ -7225,6 +7711,10 @@ var init_supersede2 = __esm(() => {
|
|
|
7225
7711
|
type: "string",
|
|
7226
7712
|
description: "Successor's Lives in: repeat or comma-separate; empty clears (default: copied from the old entry)"
|
|
7227
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
|
+
},
|
|
7228
7718
|
term: {
|
|
7229
7719
|
type: "string",
|
|
7230
7720
|
description: "TERM successor's headword (default: copied from the old entry; TERM ids only)"
|
|
@@ -7265,7 +7755,7 @@ var init_supersede2 = __esm(() => {
|
|
|
7265
7755
|
handleNotAPlatform(e);
|
|
7266
7756
|
}
|
|
7267
7757
|
const statement = await resolveSuccessorText(args, platformDir, id);
|
|
7268
|
-
const result = await supersede(inputFromArgs(args, rawArgs, platformDir, id, statement),
|
|
7758
|
+
const result = await supersede(inputFromArgs(args, rawArgs, platformDir, id, statement), cliFreshTags(platformDir));
|
|
7269
7759
|
if (!result.ok)
|
|
7270
7760
|
exitOnFailure("spec supersede", result);
|
|
7271
7761
|
printWarnings("spec supersede", result.warnings);
|
|
@@ -7332,7 +7822,6 @@ var deprecateCommand;
|
|
|
7332
7822
|
var init_deprecate2 = __esm(() => {
|
|
7333
7823
|
init_constants();
|
|
7334
7824
|
init_discover();
|
|
7335
|
-
init__index();
|
|
7336
7825
|
init_deprecate();
|
|
7337
7826
|
init_grammar3();
|
|
7338
7827
|
init__args();
|
|
@@ -7375,7 +7864,7 @@ var init_deprecate2 = __esm(() => {
|
|
|
7375
7864
|
} catch (e) {
|
|
7376
7865
|
handleNotAPlatform(e);
|
|
7377
7866
|
}
|
|
7378
|
-
const result = await deprecate({ platformDir, id, reason },
|
|
7867
|
+
const result = await deprecate({ platformDir, id, reason }, cliFreshTags(platformDir));
|
|
7379
7868
|
if (!result.ok)
|
|
7380
7869
|
exitOnFailure("spec deprecate", result);
|
|
7381
7870
|
const { file, sites } = result;
|
|
@@ -7394,30 +7883,28 @@ var init_deprecate2 = __esm(() => {
|
|
|
7394
7883
|
|
|
7395
7884
|
// src/operations/move.ts
|
|
7396
7885
|
async function locateMove(platformDir, id, targetKey) {
|
|
7397
|
-
const
|
|
7398
|
-
|
|
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) {
|
|
7399
7892
|
return fail("usage", `${id} is already in ${targetKey} \u2014 use spec supersede for an in-domain revision`);
|
|
7400
7893
|
}
|
|
7401
|
-
const source = await readEnvelope(platformDir, sourceKey, `no domain ${sourceKey} (expected spec-engine/${sourceKey}/SPEC.json)`);
|
|
7402
|
-
if (!source.ok)
|
|
7403
|
-
return source;
|
|
7404
7894
|
const target = await readEnvelope(platformDir, targetKey, `no target domain ${targetKey} \u2014 run \`spec domain new ${targetKey}\` first`);
|
|
7405
7895
|
if (!target.ok)
|
|
7406
7896
|
return target;
|
|
7407
|
-
const req = source.requirements.find((r) => r.id === id);
|
|
7408
|
-
if (req === undefined)
|
|
7409
|
-
return fail("not_found", `no entry ${id} in ${source.relFile}`);
|
|
7410
7897
|
if (req.status.toLowerCase() !== "active") {
|
|
7411
7898
|
return fail("conflict", `${id} is ${req.status} \u2014 only Active requirements move (superseded/retired entries stay as history)`);
|
|
7412
7899
|
}
|
|
7413
7900
|
return { ok: true, source, target, req };
|
|
7414
7901
|
}
|
|
7415
7902
|
function successorFields2(input, req) {
|
|
7416
|
-
const srcLives = (req.livesIn[0] ?? "").trim();
|
|
7417
7903
|
return {
|
|
7418
7904
|
statement: (input.statement ?? req.statement).trim(),
|
|
7419
7905
|
why: (input.why ?? req.why ?? "").trim(),
|
|
7420
|
-
livesIn: input.livesIn ??
|
|
7906
|
+
livesIn: input.livesIn ?? [...req.livesIn],
|
|
7907
|
+
relates: [...req.relates]
|
|
7421
7908
|
};
|
|
7422
7909
|
}
|
|
7423
7910
|
function versionSide(domain2, key, noBump) {
|
|
@@ -7466,7 +7953,7 @@ async function move(input, freshTags) {
|
|
|
7466
7953
|
const sourceCurrent = source.domain.specVersion ?? 1;
|
|
7467
7954
|
req.status = "superseded";
|
|
7468
7955
|
req.supersededBy = newId;
|
|
7469
|
-
target.requirements.push(successorEntry(newId, fields));
|
|
7956
|
+
target.requirements.push(successorEntry(newId, req, fields));
|
|
7470
7957
|
const noBump = Boolean(input.noBump);
|
|
7471
7958
|
const sourceSpecVersion = versionSide(source.domain, source.key, noBump);
|
|
7472
7959
|
const targetSpecVersion = versionSide(target.domain, target.key, noBump);
|
|
@@ -7527,7 +8014,6 @@ var init_move2 = __esm(() => {
|
|
|
7527
8014
|
init_domains();
|
|
7528
8015
|
init_constants();
|
|
7529
8016
|
init_discover();
|
|
7530
|
-
init__index();
|
|
7531
8017
|
init_move();
|
|
7532
8018
|
init_grammar3();
|
|
7533
8019
|
init__args();
|
|
@@ -7589,7 +8075,7 @@ var init_move2 = __esm(() => {
|
|
|
7589
8075
|
} catch (e) {
|
|
7590
8076
|
handleNotAPlatform(e);
|
|
7591
8077
|
}
|
|
7592
|
-
const result = await move(inputFromArgs2(args, rawArgs, platformDir, id, targetKey),
|
|
8078
|
+
const result = await move(inputFromArgs2(args, rawArgs, platformDir, id, targetKey), cliFreshTags(platformDir));
|
|
7593
8079
|
if (!result.ok)
|
|
7594
8080
|
exitOnFailure("spec move", result);
|
|
7595
8081
|
printWarnings("spec move", result.warnings);
|
|
@@ -7623,18 +8109,30 @@ var init_move2 = __esm(() => {
|
|
|
7623
8109
|
});
|
|
7624
8110
|
|
|
7625
8111
|
// src/operations/amend.ts
|
|
7626
|
-
|
|
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;
|
|
7627
8124
|
const statusLc = req.status.toLowerCase();
|
|
7628
8125
|
if (statusLc !== "active" && statusLc !== "draft") {
|
|
7629
8126
|
return fail("conflict", `${id} is ${displayStatus(req.status)} \u2014 only Active/Draft entries amend (a superseded entry is history; supersede its successor instead)`);
|
|
7630
8127
|
}
|
|
7631
8128
|
if (statusLc !== "active")
|
|
7632
8129
|
return null;
|
|
7633
|
-
|
|
7634
|
-
const site = bound[0];
|
|
7635
|
-
if (site === undefined)
|
|
8130
|
+
if (!PROMISE_FIELDS.some((f) => fields[f] !== undefined))
|
|
7636
8131
|
return null;
|
|
7637
|
-
|
|
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.");
|
|
7638
8136
|
}
|
|
7639
8137
|
function applyFields(req, fields) {
|
|
7640
8138
|
const fieldsChanged = [];
|
|
@@ -7654,6 +8152,10 @@ function applyFields(req, fields) {
|
|
|
7654
8152
|
fieldsChanged.push("lives");
|
|
7655
8153
|
refValues.push(...fields.livesIn);
|
|
7656
8154
|
}
|
|
8155
|
+
if (fields.relates !== undefined) {
|
|
8156
|
+
req.relates = splitList(fields.relates);
|
|
8157
|
+
fieldsChanged.push("relates");
|
|
8158
|
+
}
|
|
7657
8159
|
if (fields.term !== undefined) {
|
|
7658
8160
|
req.term = fields.term;
|
|
7659
8161
|
fieldsChanged.push("term");
|
|
@@ -7669,18 +8171,17 @@ function applyFields(req, fields) {
|
|
|
7669
8171
|
}
|
|
7670
8172
|
return { fieldsChanged, refValues };
|
|
7671
8173
|
}
|
|
7672
|
-
async function amend(input
|
|
8174
|
+
async function amend(input) {
|
|
7673
8175
|
const { platformDir, id, fields } = input;
|
|
7674
8176
|
const located = await locateEntry(platformDir, id);
|
|
7675
8177
|
if (!located.ok)
|
|
7676
8178
|
return located;
|
|
7677
|
-
const { specPath, relFile, domain: domain2, req } = located;
|
|
7678
|
-
const rejection =
|
|
8179
|
+
const { key, specPath, relFile, domain: domain2, req } = located;
|
|
8180
|
+
const rejection = gateRejection(platformDir, relFile, req, fields);
|
|
7679
8181
|
if (rejection !== null)
|
|
7680
8182
|
return rejection;
|
|
7681
8183
|
const warnings = [];
|
|
7682
8184
|
if (fields.statement !== undefined) {
|
|
7683
|
-
const key = domainKeyOf(id);
|
|
7684
8185
|
const verdict = await judgeStatementGrammar(platformDir, key, fields.statement);
|
|
7685
8186
|
if (!verdict.ok) {
|
|
7686
8187
|
if (verdict.severity === "error") {
|
|
@@ -7702,12 +8203,14 @@ async function amend(input, freshTags) {
|
|
|
7702
8203
|
}
|
|
7703
8204
|
return { ok: true, id, file: relFile, fieldsChanged: [...fieldsChanged].sort(), warnings };
|
|
7704
8205
|
}
|
|
8206
|
+
var PROMISE_FIELDS;
|
|
7705
8207
|
var init_amend = __esm(() => {
|
|
7706
8208
|
init_src();
|
|
7707
8209
|
init_grammar2();
|
|
7708
8210
|
init__envelope();
|
|
7709
8211
|
init__result();
|
|
7710
8212
|
init_mint();
|
|
8213
|
+
PROMISE_FIELDS = ["statement", "why", "term", "aliases"];
|
|
7711
8214
|
});
|
|
7712
8215
|
|
|
7713
8216
|
// src/commands/amend.ts
|
|
@@ -7718,9 +8221,9 @@ __export(exports_amend, {
|
|
|
7718
8221
|
import { defineCommand as defineCommand18 } from "citty";
|
|
7719
8222
|
function assertAmendFlags(args) {
|
|
7720
8223
|
const hasIssue = args.issue !== undefined && args.issue.trim() !== "";
|
|
7721
|
-
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;
|
|
7722
8225
|
if (!hasField && !hasIssue) {
|
|
7723
|
-
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");
|
|
7724
8227
|
process.exit(EXIT.USAGE);
|
|
7725
8228
|
}
|
|
7726
8229
|
if (args.text !== undefined && args.text.trim() === "") {
|
|
@@ -7747,6 +8250,9 @@ function fieldsFromArgs(args, rawArgs) {
|
|
|
7747
8250
|
const livesIn = listFlag(rawArgs, "lives", args.lives);
|
|
7748
8251
|
if (livesIn !== undefined)
|
|
7749
8252
|
fields.livesIn = livesIn;
|
|
8253
|
+
const relates = listFlag(rawArgs, "relates", args.relates);
|
|
8254
|
+
if (relates !== undefined)
|
|
8255
|
+
fields.relates = relates;
|
|
7750
8256
|
if (args.term !== undefined)
|
|
7751
8257
|
fields.term = args.term.trim();
|
|
7752
8258
|
if (args.issue !== undefined && args.issue.trim() !== "")
|
|
@@ -7759,7 +8265,6 @@ var amendArgs, amendCommand;
|
|
|
7759
8265
|
var init_amend2 = __esm(() => {
|
|
7760
8266
|
init_constants();
|
|
7761
8267
|
init_discover();
|
|
7762
|
-
init__index();
|
|
7763
8268
|
init_amend();
|
|
7764
8269
|
init_grammar3();
|
|
7765
8270
|
init__args();
|
|
@@ -7774,6 +8279,7 @@ var init_amend2 = __esm(() => {
|
|
|
7774
8279
|
text: { type: "string", description: "New Requirement (statement) field value" },
|
|
7775
8280
|
why: { type: "string", description: "New Why it matters field value" },
|
|
7776
8281
|
lives: livesArg,
|
|
8282
|
+
relates: relatesArg,
|
|
7777
8283
|
issue: {
|
|
7778
8284
|
type: "string",
|
|
7779
8285
|
description: "Ticket that motivated this amendment \u2014 recorded as amends-via provenance (opaque, never a requirement id)"
|
|
@@ -7788,7 +8294,7 @@ var init_amend2 = __esm(() => {
|
|
|
7788
8294
|
amendCommand = defineCommand18({
|
|
7789
8295
|
meta: {
|
|
7790
8296
|
name: "amend",
|
|
7791
|
-
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."
|
|
7792
8298
|
},
|
|
7793
8299
|
args: amendArgs,
|
|
7794
8300
|
async run({ args, rawArgs }) {
|
|
@@ -7805,7 +8311,7 @@ var init_amend2 = __esm(() => {
|
|
|
7805
8311
|
handleNotAPlatform(e);
|
|
7806
8312
|
}
|
|
7807
8313
|
const fields = fieldsFromArgs(args, rawArgs);
|
|
7808
|
-
const result = await amend({ platformDir, id, fields }
|
|
8314
|
+
const result = await amend({ platformDir, id, fields });
|
|
7809
8315
|
if (!result.ok)
|
|
7810
8316
|
exitOnFailure("spec amend", result);
|
|
7811
8317
|
printWarnings("spec amend", result.warnings);
|
|
@@ -7927,7 +8433,7 @@ Target domain: ${opts.domain} (no charter/scope recorded \u2014 check placement
|
|
|
7927
8433
|
|
|
7928
8434
|
// src/server/mcp.ts
|
|
7929
8435
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7930
|
-
import { z as
|
|
8436
|
+
import { z as z4 } from "zod";
|
|
7931
8437
|
function jsonResult(value) {
|
|
7932
8438
|
return { content: [{ type: "text", text: JSON.stringify(value) }] };
|
|
7933
8439
|
}
|
|
@@ -7935,14 +8441,14 @@ function errorResult(message) {
|
|
|
7935
8441
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
7936
8442
|
}
|
|
7937
8443
|
function buildMcpServer(platformDir) {
|
|
7938
|
-
const server = new McpServer({ name: "spec", version:
|
|
8444
|
+
const server = new McpServer({ name: "spec", version: package_default.version });
|
|
7939
8445
|
const fresh = (fn) => withIndex({ platformDir, build: "fresh" }, (h) => fn(h.storage));
|
|
7940
8446
|
server.registerTool("spec_query", {
|
|
7941
8447
|
title: "Full-text requirement retrieval",
|
|
7942
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.",
|
|
7943
8449
|
inputSchema: {
|
|
7944
|
-
text:
|
|
7945
|
-
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})`)
|
|
7946
8452
|
}
|
|
7947
8453
|
}, async ({ text, limit }) => fresh((s) => {
|
|
7948
8454
|
const r = query(s, text, limit ?? DEFAULT_QUERY_LIMIT);
|
|
@@ -7952,13 +8458,13 @@ function buildMcpServer(platformDir) {
|
|
|
7952
8458
|
title: "Files \u2192 requirements",
|
|
7953
8459
|
description: "The requirements tagged in the given files (platform-relative paths, e.g. api/src/renew.ts). Load these before changing the files.",
|
|
7954
8460
|
inputSchema: {
|
|
7955
|
-
files:
|
|
8461
|
+
files: z4.array(z4.string().min(1)).min(1).max(100).describe("Platform-relative file paths")
|
|
7956
8462
|
}
|
|
7957
8463
|
}, async ({ files }) => fresh((s) => jsonResult(resolveFiles(s, files).rows)));
|
|
7958
8464
|
server.registerTool("spec_req_tags", {
|
|
7959
8465
|
title: "Requirement \u2192 tag sites (reverse query)",
|
|
7960
8466
|
description: "Every code/test/doc site tagging the given requirement, across all repos \u2014 impact analysis and retag worklists.",
|
|
7961
|
-
inputSchema: { req_id:
|
|
8467
|
+
inputSchema: { req_id: z4.string().describe("Requirement id (KEY-NNN)") }
|
|
7962
8468
|
}, async ({ req_id }) => {
|
|
7963
8469
|
if (!ID_RE.test(req_id)) {
|
|
7964
8470
|
return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
|
|
@@ -7981,7 +8487,7 @@ function buildMcpServer(platformDir) {
|
|
|
7981
8487
|
server.registerTool("spec_propagation", {
|
|
7982
8488
|
title: "Cross-repo propagation for a superseded requirement",
|
|
7983
8489
|
description: "Per member repo: migrated to the successor (verified or not), still on the predecessor, or unrelated \u2014 plus the drift flag.",
|
|
7984
|
-
inputSchema: { req_id:
|
|
8490
|
+
inputSchema: { req_id: z4.string().describe("Requirement id (KEY-NNN)") }
|
|
7985
8491
|
}, async ({ req_id }) => {
|
|
7986
8492
|
if (!ID_RE.test(req_id)) {
|
|
7987
8493
|
return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
|
|
@@ -7991,7 +8497,7 @@ function buildMcpServer(platformDir) {
|
|
|
7991
8497
|
server.registerTool("spec_next_id", {
|
|
7992
8498
|
title: "Next unused requirement id",
|
|
7993
8499
|
description: "Allocate-preview the next unused id in a domain (filesystem-derived; nothing is written). Use before authoring a new requirement.",
|
|
7994
|
-
inputSchema: { domain:
|
|
8500
|
+
inputSchema: { domain: z4.string().min(1).describe("Domain key (e.g. BILLING)") }
|
|
7995
8501
|
}, async ({ domain: domain2 }) => {
|
|
7996
8502
|
const r = await nextId(platformDir, domain2);
|
|
7997
8503
|
return r.ok ? jsonResult({ domain: r.key, next_id: r.nextId }) : errorResult(r.detail);
|
|
@@ -7999,7 +8505,7 @@ function buildMcpServer(platformDir) {
|
|
|
7999
8505
|
server.registerTool("spec_get", {
|
|
8000
8506
|
title: "One requirement's full record",
|
|
8001
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).",
|
|
8002
|
-
inputSchema: { req_id:
|
|
8508
|
+
inputSchema: { req_id: z4.string().describe("Requirement id (KEY-NNN)") }
|
|
8003
8509
|
}, async ({ req_id }) => {
|
|
8004
8510
|
if (!ID_RE.test(req_id)) {
|
|
8005
8511
|
return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
|
|
@@ -8013,8 +8519,8 @@ function buildMcpServer(platformDir) {
|
|
|
8013
8519
|
title: "Every requirement record",
|
|
8014
8520
|
description: "Every requirement record in (key, seq) order, superseded and deprecated history included. Optional domain and status filters. The `spec list --json` shape.",
|
|
8015
8521
|
inputSchema: {
|
|
8016
|
-
domain:
|
|
8017
|
-
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(" | ")}`)
|
|
8018
8524
|
}
|
|
8019
8525
|
}, async ({ domain: domain2, status }) => {
|
|
8020
8526
|
const filter = {};
|
|
@@ -8033,17 +8539,18 @@ function buildMcpServer(platformDir) {
|
|
|
8033
8539
|
title: "Supersede a shipped requirement",
|
|
8034
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`.",
|
|
8035
8541
|
inputSchema: {
|
|
8036
|
-
req_id:
|
|
8037
|
-
statement:
|
|
8038
|
-
why:
|
|
8039
|
-
lives_in:
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
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 }) => {
|
|
8043
8550
|
if (!ID_RE.test(req_id)) {
|
|
8044
8551
|
return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
|
|
8045
8552
|
}
|
|
8046
|
-
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));
|
|
8047
8554
|
if (!r.ok)
|
|
8048
8555
|
return errorResult(r.detail);
|
|
8049
8556
|
for (const w of r.warnings)
|
|
@@ -8060,8 +8567,8 @@ function buildMcpServer(platformDir) {
|
|
|
8060
8567
|
title: "Deprecate a requirement",
|
|
8061
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`.",
|
|
8062
8569
|
inputSchema: {
|
|
8063
|
-
req_id:
|
|
8064
|
-
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")
|
|
8065
8572
|
}
|
|
8066
8573
|
}, async ({ req_id, reason }) => {
|
|
8067
8574
|
if (!ID_RE.test(req_id)) {
|
|
@@ -8077,7 +8584,7 @@ function buildMcpServer(platformDir) {
|
|
|
8077
8584
|
title: "Accept a draft requirement",
|
|
8078
8585
|
description: "Promote a Draft requirement to Active, leaving every other field untouched. The same JSON as `spec accept --json`.",
|
|
8079
8586
|
inputSchema: {
|
|
8080
|
-
req_id:
|
|
8587
|
+
req_id: z4.string().describe("The Draft requirement id to accept (KEY-NNN)")
|
|
8081
8588
|
}
|
|
8082
8589
|
}, async ({ req_id }) => {
|
|
8083
8590
|
if (!ID_RE.test(req_id)) {
|
|
@@ -8090,8 +8597,8 @@ function buildMcpServer(platformDir) {
|
|
|
8090
8597
|
title: "Author requirements from a brief",
|
|
8091
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.",
|
|
8092
8599
|
argsSchema: {
|
|
8093
|
-
brief:
|
|
8094
|
-
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")
|
|
8095
8602
|
}
|
|
8096
8603
|
}, async ({ brief, domain: domain2 }) => {
|
|
8097
8604
|
let resolvedDomain;
|
|
@@ -8118,6 +8625,7 @@ function buildMcpServer(platformDir) {
|
|
|
8118
8625
|
}
|
|
8119
8626
|
var init_mcp = __esm(() => {
|
|
8120
8627
|
init_src();
|
|
8628
|
+
init_package();
|
|
8121
8629
|
init_domains();
|
|
8122
8630
|
init__index();
|
|
8123
8631
|
init_accept();
|
|
@@ -8159,7 +8667,7 @@ var init_mcp2 = __esm(() => {
|
|
|
8159
8667
|
handleNotAPlatform(e);
|
|
8160
8668
|
}
|
|
8161
8669
|
const server = buildMcpServer(platformDir);
|
|
8162
|
-
console.error(`spec mcp: serving ${platformDir} over stdio`);
|
|
8670
|
+
console.error(`spec mcp: serving ${platformName(platformDir)} over stdio`);
|
|
8163
8671
|
await server.connect(new StdioServerTransport);
|
|
8164
8672
|
}
|
|
8165
8673
|
});
|
|
@@ -8171,7 +8679,7 @@ __export(exports_resolve, {
|
|
|
8171
8679
|
resolveCommand: () => resolveCommand
|
|
8172
8680
|
});
|
|
8173
8681
|
import { statSync as statSync5 } from "fs";
|
|
8174
|
-
import { join as
|
|
8682
|
+
import { join as join14, relative as relative2, resolve as resolve13, sep as sep3 } from "path";
|
|
8175
8683
|
import { defineCommand as defineCommand21 } from "citty";
|
|
8176
8684
|
function classifyArg(a) {
|
|
8177
8685
|
if (a.startsWith("--")) {
|
|
@@ -8210,7 +8718,7 @@ function splitFilesAndPlatformDir(positionals, fallbackPlatformDir) {
|
|
|
8210
8718
|
const resolved = resolve13(last);
|
|
8211
8719
|
if (statSync5(resolved).isDirectory()) {
|
|
8212
8720
|
try {
|
|
8213
|
-
if (statSync5(
|
|
8721
|
+
if (statSync5(join14(resolved, "spec-engine")).isDirectory()) {
|
|
8214
8722
|
lastIsPlatformDir = true;
|
|
8215
8723
|
} else {
|
|
8216
8724
|
lastIsDirWithoutSpecEngine = true;
|
|
@@ -8250,7 +8758,7 @@ async function runReverseQuery(opts) {
|
|
|
8250
8758
|
if (outArg2)
|
|
8251
8759
|
assertContainedPath(dbPath, platformDir, "spec resolve: --out");
|
|
8252
8760
|
await maybePromptForOnboarding({ platformDir, args: { noPrompt } });
|
|
8253
|
-
await withReadStorage({ platformDir, dbPath, fresh }, (storage2) => {
|
|
8761
|
+
await withReadStorage({ platformDir, dbPath, fresh, json }, (storage2) => {
|
|
8254
8762
|
const { rows, known } = reqTags(storage2, reqId);
|
|
8255
8763
|
if (rows.length === 0) {
|
|
8256
8764
|
if (!known) {
|
|
@@ -8339,7 +8847,7 @@ var init_resolve2 = __esm(() => {
|
|
|
8339
8847
|
},
|
|
8340
8848
|
req: {
|
|
8341
8849
|
type: "string",
|
|
8342
|
-
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."
|
|
8343
8851
|
},
|
|
8344
8852
|
out: {
|
|
8345
8853
|
type: "string",
|
|
@@ -8361,7 +8869,7 @@ var init_resolve2 = __esm(() => {
|
|
|
8361
8869
|
resolveCommand = defineCommand21({
|
|
8362
8870
|
meta: {
|
|
8363
8871
|
name: "resolve",
|
|
8364
|
-
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."
|
|
8365
8873
|
},
|
|
8366
8874
|
args: resolveArgs,
|
|
8367
8875
|
async run({ args, rawArgs }) {
|
|
@@ -8394,7 +8902,7 @@ var init_resolve2 = __esm(() => {
|
|
|
8394
8902
|
noPrompt: args.noPrompt
|
|
8395
8903
|
}
|
|
8396
8904
|
});
|
|
8397
|
-
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, (storage2) => {
|
|
8905
|
+
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, (storage2) => {
|
|
8398
8906
|
const rows = storage2.resolveByFiles(normalized);
|
|
8399
8907
|
if (!args.json && rows.length === 0 && storage2.listRequirements().length === 0) {
|
|
8400
8908
|
console.error(formatNoRequirementsIndexed(platformDir));
|
|
@@ -8521,8 +9029,8 @@ var exports_gate = {};
|
|
|
8521
9029
|
__export(exports_gate, {
|
|
8522
9030
|
gateCommand: () => gateCommand
|
|
8523
9031
|
});
|
|
8524
|
-
import { existsSync as
|
|
8525
|
-
import { join as
|
|
9032
|
+
import { existsSync as existsSync13 } from "fs";
|
|
9033
|
+
import { join as join15 } from "path";
|
|
8526
9034
|
import { defineCommand as defineCommand22 } from "citty";
|
|
8527
9035
|
function isEpipe(e) {
|
|
8528
9036
|
if (e === null || typeof e !== "object" || !("code" in e))
|
|
@@ -8546,7 +9054,7 @@ function resolveGateArgs(args) {
|
|
|
8546
9054
|
process.exit(EXIT.USAGE);
|
|
8547
9055
|
}
|
|
8548
9056
|
const platformDir = resolvePlatformDir(args);
|
|
8549
|
-
if (!
|
|
9057
|
+
if (!existsSync13(join15(platformDir, "spec-engine"))) {
|
|
8550
9058
|
console.error(formatNotASpecPlatform(platformDir));
|
|
8551
9059
|
process.exit(EXIT.USAGE);
|
|
8552
9060
|
}
|
|
@@ -8619,6 +9127,7 @@ var init_gate2 = __esm(() => {
|
|
|
8619
9127
|
writeStdoutTolerant(`build_id: ${gated.buildId}`);
|
|
8620
9128
|
}
|
|
8621
9129
|
} catch (e) {
|
|
9130
|
+
handleInvalidDomainFiles(e, Boolean(args.json));
|
|
8622
9131
|
const msg = e instanceof Error ? e.message : String(e);
|
|
8623
9132
|
console.error(`spec gate: crashed: ${msg}`);
|
|
8624
9133
|
process.exit(EXIT.USAGE);
|
|
@@ -8704,205 +9213,6 @@ function renderGuard(losses, mode, ref) {
|
|
|
8704
9213
|
}
|
|
8705
9214
|
var STOP = "\uD83D\uDED1";
|
|
8706
9215
|
|
|
8707
|
-
// src/guard/collect.ts
|
|
8708
|
-
import { join as join14 } from "path";
|
|
8709
|
-
function isIgnored(path) {
|
|
8710
|
-
return isPathIgnored(path);
|
|
8711
|
-
}
|
|
8712
|
-
function isSpecFile(path) {
|
|
8713
|
-
return path.startsWith(`${CANONICAL_SPECS_DIR}/`) && path.endsWith("/SPEC.json") && !isIgnored(path);
|
|
8714
|
-
}
|
|
8715
|
-
function isCodeFile(path) {
|
|
8716
|
-
if (isSpecFile(path) || isIgnored(path))
|
|
8717
|
-
return false;
|
|
8718
|
-
const ext = path.includes(".") ? path.split(".").pop() ?? "" : "";
|
|
8719
|
-
return CODE_EXTS.has(ext);
|
|
8720
|
-
}
|
|
8721
|
-
function parseReqs(text) {
|
|
8722
|
-
const v = parseDomainText(text, "");
|
|
8723
|
-
return v.ok ? v.data.requirements : [];
|
|
8724
|
-
}
|
|
8725
|
-
function collectBaseReqs2(platformDir, ref, specPaths2) {
|
|
8726
|
-
const reqs = [];
|
|
8727
|
-
const path = new Map;
|
|
8728
|
-
for (const p of specPaths2) {
|
|
8729
|
-
const bytes = gitShow(platformDir, ref, p);
|
|
8730
|
-
if (bytes === null)
|
|
8731
|
-
continue;
|
|
8732
|
-
for (const r of parseReqs(bytes)) {
|
|
8733
|
-
reqs.push(r);
|
|
8734
|
-
path.set(r.id, p);
|
|
8735
|
-
}
|
|
8736
|
-
}
|
|
8737
|
-
return { reqs, path };
|
|
8738
|
-
}
|
|
8739
|
-
function collectBaseTags(platformDir, ref, codePaths) {
|
|
8740
|
-
const implSite = new Map;
|
|
8741
|
-
const verifySite = new Map;
|
|
8742
|
-
for (const p of codePaths) {
|
|
8743
|
-
const bytes = gitShow(platformDir, ref, p);
|
|
8744
|
-
if (bytes === null)
|
|
8745
|
-
continue;
|
|
8746
|
-
for (const tag of scanTagsInFile("", p, bytes)) {
|
|
8747
|
-
const target = tag.kind === "verifies" ? verifySite : implSite;
|
|
8748
|
-
if (!target.has(tag.req_id))
|
|
8749
|
-
target.set(tag.req_id, { file: tag.file, line: tag.line });
|
|
8750
|
-
}
|
|
8751
|
-
}
|
|
8752
|
-
return { implSite, verifySite };
|
|
8753
|
-
}
|
|
8754
|
-
async function collectWorktreeReqs(canonicalDir) {
|
|
8755
|
-
const ids = new Set;
|
|
8756
|
-
const activeIds = new Set;
|
|
8757
|
-
const supersedesTargets = new Set;
|
|
8758
|
-
for (const rel of await findDomainJsonFiles(canonicalDir)) {
|
|
8759
|
-
const text = await Bun.file(join14(canonicalDir, rel)).text();
|
|
8760
|
-
for (const r of parseReqs(text)) {
|
|
8761
|
-
ids.add(r.id);
|
|
8762
|
-
if (r.status.toLowerCase() === "active")
|
|
8763
|
-
activeIds.add(r.id);
|
|
8764
|
-
if (r.supersedes != null)
|
|
8765
|
-
supersedesTargets.add(r.supersedes);
|
|
8766
|
-
}
|
|
8767
|
-
}
|
|
8768
|
-
return { ids, activeIds, supersedesTargets };
|
|
8769
|
-
}
|
|
8770
|
-
function tallyWorktreeTags(storage2) {
|
|
8771
|
-
const implCount = new Map;
|
|
8772
|
-
const verifyCount = new Map;
|
|
8773
|
-
for (const t of storage2.listTags()) {
|
|
8774
|
-
const bucket = t.kind === "implements" ? implCount : t.kind === "verifies" ? verifyCount : null;
|
|
8775
|
-
if (bucket !== null)
|
|
8776
|
-
bucket.set(t.req_id, (bucket.get(t.req_id) ?? 0) + 1);
|
|
8777
|
-
}
|
|
8778
|
-
return { implCount, verifyCount };
|
|
8779
|
-
}
|
|
8780
|
-
async function collectFacts(platformDir, ref, storage2) {
|
|
8781
|
-
const changed = gitDiffNameStatus(platformDir, ref);
|
|
8782
|
-
const specPaths2 = changed.filter((e) => isSpecFile(e.path)).map((e) => e.path);
|
|
8783
|
-
const codePaths = changed.filter((e) => isCodeFile(e.path)).map((e) => e.path);
|
|
8784
|
-
const deletedSpecFiles = changed.filter((e) => e.status === "D" && isSpecFile(e.path)).map((e) => e.path);
|
|
8785
|
-
const base = collectBaseReqs2(platformDir, ref, specPaths2);
|
|
8786
|
-
const baseTags = collectBaseTags(platformDir, ref, codePaths);
|
|
8787
|
-
const worktree = await collectWorktreeReqs(join14(platformDir, CANONICAL_SPECS_DIR));
|
|
8788
|
-
const counts = tallyWorktreeTags(storage2);
|
|
8789
|
-
return {
|
|
8790
|
-
baseReqs: base.reqs,
|
|
8791
|
-
baseReqPath: base.path,
|
|
8792
|
-
baseImplSite: baseTags.implSite,
|
|
8793
|
-
baseVerifySite: baseTags.verifySite,
|
|
8794
|
-
worktreeReqIds: worktree.ids,
|
|
8795
|
-
worktreeActiveIds: worktree.activeIds,
|
|
8796
|
-
worktreeSupersedesTargets: worktree.supersedesTargets,
|
|
8797
|
-
worktreeImplCount: counts.implCount,
|
|
8798
|
-
worktreeVerifyCount: counts.verifyCount,
|
|
8799
|
-
deletedSpecFiles
|
|
8800
|
-
};
|
|
8801
|
-
}
|
|
8802
|
-
var CODE_EXTS;
|
|
8803
|
-
var init_collect = __esm(() => {
|
|
8804
|
-
init_src();
|
|
8805
|
-
init_constants();
|
|
8806
|
-
init_fs();
|
|
8807
|
-
init_tags();
|
|
8808
|
-
CODE_EXTS = new Set(DEFAULT_EXTS);
|
|
8809
|
-
});
|
|
8810
|
-
|
|
8811
|
-
// src/guard/losses.ts
|
|
8812
|
-
function isActive(status) {
|
|
8813
|
-
return status.toLowerCase() === "active";
|
|
8814
|
-
}
|
|
8815
|
-
function isSuperseded(base, f) {
|
|
8816
|
-
const forward = base.supersededBy != null && f.worktreeReqIds.has(base.supersededBy);
|
|
8817
|
-
return forward || f.worktreeSupersedesTargets.has(base.id);
|
|
8818
|
-
}
|
|
8819
|
-
function reqDeletedLoss(base, f) {
|
|
8820
|
-
return {
|
|
8821
|
-
kind: "REQUIREMENT_REMOVED",
|
|
8822
|
-
req_id: base.id,
|
|
8823
|
-
file: f.baseReqPath.get(base.id) ?? "",
|
|
8824
|
-
line: 0,
|
|
8825
|
-
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`
|
|
8826
|
-
};
|
|
8827
|
-
}
|
|
8828
|
-
function historyRemovedLoss(base, f) {
|
|
8829
|
-
const display = base.status.charAt(0).toUpperCase() + base.status.slice(1).toLowerCase();
|
|
8830
|
-
return {
|
|
8831
|
-
kind: "REQUIREMENT_REMOVED",
|
|
8832
|
-
req_id: base.id,
|
|
8833
|
-
file: f.baseReqPath.get(base.id) ?? "",
|
|
8834
|
-
line: 0,
|
|
8835
|
-
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`
|
|
8836
|
-
};
|
|
8837
|
-
}
|
|
8838
|
-
function implLostLoss(id, site) {
|
|
8839
|
-
return {
|
|
8840
|
-
kind: "IMPL_LOST",
|
|
8841
|
-
req_id: id,
|
|
8842
|
-
file: site.file,
|
|
8843
|
-
line: site.line,
|
|
8844
|
-
detail: `${id} lost its last implementing @spec tag (was ${site.file}:${site.line})`
|
|
8845
|
-
};
|
|
8846
|
-
}
|
|
8847
|
-
function verifyLostLoss(id, site) {
|
|
8848
|
-
return {
|
|
8849
|
-
kind: "VERIFY_LOST",
|
|
8850
|
-
req_id: id,
|
|
8851
|
-
file: site.file,
|
|
8852
|
-
line: site.line,
|
|
8853
|
-
detail: `${id} lost its last verifying @spec tag (was ${site.file}:${site.line})`
|
|
8854
|
-
};
|
|
8855
|
-
}
|
|
8856
|
-
function specFileDeletedLoss(path) {
|
|
8857
|
-
return {
|
|
8858
|
-
kind: "SPEC_FILE_DELETED",
|
|
8859
|
-
req_id: null,
|
|
8860
|
-
file: path,
|
|
8861
|
-
line: 0,
|
|
8862
|
-
detail: `canonical spec file ${path} present at the ref is absent from the working tree`
|
|
8863
|
-
};
|
|
8864
|
-
}
|
|
8865
|
-
function reqDeletedFor(base, f) {
|
|
8866
|
-
if (f.worktreeReqIds.has(base.id))
|
|
8867
|
-
return null;
|
|
8868
|
-
if (!isActive(base.status))
|
|
8869
|
-
return historyRemovedLoss(base, f);
|
|
8870
|
-
if (isSuperseded(base, f))
|
|
8871
|
-
return null;
|
|
8872
|
-
return reqDeletedLoss(base, f);
|
|
8873
|
-
}
|
|
8874
|
-
function lastTagGone(id, count, f) {
|
|
8875
|
-
return f.worktreeActiveIds.has(id) && count === 0;
|
|
8876
|
-
}
|
|
8877
|
-
function classifyLosses(f) {
|
|
8878
|
-
const out = [];
|
|
8879
|
-
for (const base of f.baseReqs) {
|
|
8880
|
-
const loss = reqDeletedFor(base, f);
|
|
8881
|
-
if (loss !== null)
|
|
8882
|
-
out.push(loss);
|
|
8883
|
-
}
|
|
8884
|
-
for (const [id, site] of f.baseImplSite) {
|
|
8885
|
-
if (lastTagGone(id, f.worktreeImplCount.get(id) ?? 0, f))
|
|
8886
|
-
out.push(implLostLoss(id, site));
|
|
8887
|
-
}
|
|
8888
|
-
for (const [id, site] of f.baseVerifySite) {
|
|
8889
|
-
if (lastTagGone(id, f.worktreeVerifyCount.get(id) ?? 0, f))
|
|
8890
|
-
out.push(verifyLostLoss(id, site));
|
|
8891
|
-
}
|
|
8892
|
-
for (const path of f.deletedSpecFiles)
|
|
8893
|
-
out.push(specFileDeletedLoss(path));
|
|
8894
|
-
return out;
|
|
8895
|
-
}
|
|
8896
|
-
|
|
8897
|
-
// src/operations/guard.ts
|
|
8898
|
-
async function guard(storage2, platformDir, ref) {
|
|
8899
|
-
const losses = classifyLosses(await collectFacts(platformDir, ref, storage2));
|
|
8900
|
-
return { ok: true, ref, losses };
|
|
8901
|
-
}
|
|
8902
|
-
var init_guard = __esm(() => {
|
|
8903
|
-
init_collect();
|
|
8904
|
-
});
|
|
8905
|
-
|
|
8906
9216
|
// src/commands/guard.ts
|
|
8907
9217
|
var exports_guard = {};
|
|
8908
9218
|
__export(exports_guard, {
|
|
@@ -8941,7 +9251,7 @@ var init_guard2 = __esm(() => {
|
|
|
8941
9251
|
process.exit(EXIT.OK);
|
|
8942
9252
|
}
|
|
8943
9253
|
let losses = [];
|
|
8944
|
-
await withReadStorage({ platformDir, dbPath: defaultIndexPath(platformDir), fresh: true }, async (storage2) => {
|
|
9254
|
+
await withReadStorage({ platformDir, dbPath: defaultIndexPath(platformDir), fresh: true, json: jsonMode }, async (storage2) => {
|
|
8945
9255
|
losses = (await guard(storage2, platformDir, ref)).losses;
|
|
8946
9256
|
});
|
|
8947
9257
|
console.log(renderGuard(losses, jsonMode ? "json" : "text", ref));
|
|
@@ -9248,6 +9558,18 @@ code {
|
|
|
9248
9558
|
display: block;
|
|
9249
9559
|
}
|
|
9250
9560
|
|
|
9561
|
+
.sidebar-platform {
|
|
9562
|
+
margin-left: auto;
|
|
9563
|
+
min-width: 0;
|
|
9564
|
+
overflow: hidden;
|
|
9565
|
+
text-overflow: ellipsis;
|
|
9566
|
+
white-space: nowrap;
|
|
9567
|
+
font-family: var(--font-mono);
|
|
9568
|
+
font-weight: 500;
|
|
9569
|
+
font-size: 0.75rem;
|
|
9570
|
+
color: var(--fg-dim);
|
|
9571
|
+
}
|
|
9572
|
+
|
|
9251
9573
|
.sidebar-scroll {
|
|
9252
9574
|
flex: 1;
|
|
9253
9575
|
overflow-y: auto;
|
|
@@ -10854,9 +11176,9 @@ var init_coverage = __esm(() => {
|
|
|
10854
11176
|
// ../webapp/src/pages/editor.ts
|
|
10855
11177
|
import { bodyLimit } from "hono/body-limit";
|
|
10856
11178
|
import { html as html5, raw as raw4 } from "hono/html";
|
|
10857
|
-
function crossOriginRejected(c) {
|
|
11179
|
+
function crossOriginRejected(c, bindHost) {
|
|
10858
11180
|
const selfUrl = new URL(c.req.url);
|
|
10859
|
-
if (!
|
|
11181
|
+
if (!isServedHostname(selfUrl.hostname, bindHost))
|
|
10860
11182
|
return true;
|
|
10861
11183
|
const origin = c.req.header("origin");
|
|
10862
11184
|
if (origin === undefined)
|
|
@@ -10914,7 +11236,7 @@ async function renderResult(res) {
|
|
|
10914
11236
|
<p><a href="/editor">Back to editor</a></p>
|
|
10915
11237
|
`);
|
|
10916
11238
|
}
|
|
10917
|
-
function mountEditor(app) {
|
|
11239
|
+
function mountEditor(app, bindHost = DEFAULT_BIND_HOST) {
|
|
10918
11240
|
app.get("/editor", (c) => {
|
|
10919
11241
|
if (!featureEnabled("editor"))
|
|
10920
11242
|
return c.html(comingSoonDoc("editor", "Editor"));
|
|
@@ -10946,6 +11268,10 @@ function mountEditor(app) {
|
|
|
10946
11268
|
Lives in (optional, single path)
|
|
10947
11269
|
<input type="text" name="livesIn" placeholder="path/to/impl.ts" />
|
|
10948
11270
|
</label>
|
|
11271
|
+
<label>
|
|
11272
|
+
Relates to (optional, comma-separated ids)
|
|
11273
|
+
<input type="text" name="relates" placeholder="BILLING-001, BILLING-002" />
|
|
11274
|
+
</label>
|
|
10949
11275
|
<button type="submit">Create</button>
|
|
10950
11276
|
</form>
|
|
10951
11277
|
</section>
|
|
@@ -10974,12 +11300,13 @@ function mountEditor(app) {
|
|
|
10974
11300
|
app.post("/editor/create", editorBodyLimit, async (c) => {
|
|
10975
11301
|
if (!featureEnabled("editor"))
|
|
10976
11302
|
return c.html(rejectPage(featureDisabledMessage("editor")), 404);
|
|
10977
|
-
if (crossOriginRejected(c))
|
|
11303
|
+
if (crossOriginRejected(c, bindHost))
|
|
10978
11304
|
return c.html(rejectPage("cross-origin request rejected"), 403);
|
|
10979
11305
|
const form = await c.req.parseBody();
|
|
10980
11306
|
const statement = field(form, "statement");
|
|
10981
11307
|
const why = field(form, "why");
|
|
10982
11308
|
const livesIn = field(form, "livesIn");
|
|
11309
|
+
const relates = field(form, "relates");
|
|
10983
11310
|
const payload = {
|
|
10984
11311
|
key: field(form, "key"),
|
|
10985
11312
|
statement
|
|
@@ -10988,6 +11315,8 @@ function mountEditor(app) {
|
|
|
10988
11315
|
payload.why = why;
|
|
10989
11316
|
if (livesIn.length > 0)
|
|
10990
11317
|
payload.livesIn = livesIn;
|
|
11318
|
+
if (relates.length > 0)
|
|
11319
|
+
payload.relates = relates;
|
|
10991
11320
|
const res = await app.request("/api/requirements", {
|
|
10992
11321
|
method: "POST",
|
|
10993
11322
|
headers: { "content-type": "application/json" },
|
|
@@ -10998,7 +11327,7 @@ function mountEditor(app) {
|
|
|
10998
11327
|
app.post("/editor/amend", editorBodyLimit, async (c) => {
|
|
10999
11328
|
if (!featureEnabled("editor"))
|
|
11000
11329
|
return c.html(rejectPage(featureDisabledMessage("editor")), 404);
|
|
11001
|
-
if (crossOriginRejected(c))
|
|
11330
|
+
if (crossOriginRejected(c, bindHost))
|
|
11002
11331
|
return c.html(rejectPage("cross-origin request rejected"), 403);
|
|
11003
11332
|
const form = await c.req.parseBody();
|
|
11004
11333
|
const id = field(form, "id");
|
|
@@ -11752,12 +12081,27 @@ var init_setup = __esm(() => {
|
|
|
11752
12081
|
|
|
11753
12082
|
// ../webapp/src/server.ts
|
|
11754
12083
|
import { Hono } from "hono";
|
|
12084
|
+
import { html as htmlTemplate } from "hono/html";
|
|
11755
12085
|
function createApp() {
|
|
11756
12086
|
const app = new Hono;
|
|
11757
12087
|
app.get("/", (c) => c.html(index_html_default));
|
|
11758
12088
|
return app;
|
|
11759
12089
|
}
|
|
11760
|
-
function
|
|
12090
|
+
function labelPlatform(app, platformName2) {
|
|
12091
|
+
const name = htmlTemplate`${platformName2}`.toString();
|
|
12092
|
+
app.use("*", async (c, next) => {
|
|
12093
|
+
await next();
|
|
12094
|
+
if (!c.res.headers.get("content-type")?.startsWith("text/html"))
|
|
12095
|
+
return;
|
|
12096
|
+
const body = (await c.res.text()).replace("<title>Spec Engine", `<title>${name} \xB7 Spec Engine`).replace("<span>Spec Engine</span></a>", `<span>Spec Engine</span><span class="sidebar-platform">${name}</span></a>`);
|
|
12097
|
+
const headers = new Headers(c.res.headers);
|
|
12098
|
+
headers.delete("content-length");
|
|
12099
|
+
c.res = new Response(body, { status: c.res.status, headers });
|
|
12100
|
+
});
|
|
12101
|
+
}
|
|
12102
|
+
function mountWebapp(app, bindHost = DEFAULT_BIND_HOST, platformName2) {
|
|
12103
|
+
if (platformName2 !== undefined)
|
|
12104
|
+
labelPlatform(app, platformName2);
|
|
11761
12105
|
registerErrorBoundary(app);
|
|
11762
12106
|
mountCoverage(app);
|
|
11763
12107
|
mountRequirements(app);
|
|
@@ -11765,13 +12109,14 @@ function mountWebapp(app) {
|
|
|
11765
12109
|
mountQuery(app);
|
|
11766
12110
|
mountRelations(app);
|
|
11767
12111
|
mountProvenance(app);
|
|
11768
|
-
mountEditor(app);
|
|
12112
|
+
mountEditor(app, bindHost);
|
|
11769
12113
|
mountSetup(app);
|
|
11770
12114
|
mountGlossary(app);
|
|
11771
12115
|
mountLogs(app);
|
|
11772
12116
|
return app;
|
|
11773
12117
|
}
|
|
11774
12118
|
var init_server = __esm(() => {
|
|
12119
|
+
init_src();
|
|
11775
12120
|
init_index_html();
|
|
11776
12121
|
init_coverage();
|
|
11777
12122
|
init_data();
|
|
@@ -11821,9 +12166,9 @@ function withWriteLock(fn) {
|
|
|
11821
12166
|
writeChain = run.catch(() => {});
|
|
11822
12167
|
return run;
|
|
11823
12168
|
}
|
|
11824
|
-
function rejectCrossOrigin(c) {
|
|
12169
|
+
function rejectCrossOrigin(c, bindHost) {
|
|
11825
12170
|
const selfUrl = new URL(c.req.url);
|
|
11826
|
-
if (!
|
|
12171
|
+
if (!isServedHostname(selfUrl.hostname, bindHost)) {
|
|
11827
12172
|
return c.json({ error: "cross-origin request rejected" }, 403);
|
|
11828
12173
|
}
|
|
11829
12174
|
const origin = c.req.header("origin");
|
|
@@ -11872,7 +12217,7 @@ async function readJsonBody(c) {
|
|
|
11872
12217
|
}
|
|
11873
12218
|
return { ok: true, body: parsed };
|
|
11874
12219
|
}
|
|
11875
|
-
function
|
|
12220
|
+
function toStringList(v) {
|
|
11876
12221
|
if (Array.isArray(v))
|
|
11877
12222
|
return v.filter((x) => typeof x === "string");
|
|
11878
12223
|
if (typeof v === "string" && v.length > 0)
|
|
@@ -11883,11 +12228,20 @@ function mintFieldsFromBody(body) {
|
|
|
11883
12228
|
return {
|
|
11884
12229
|
statement: typeof body.statement === "string" ? body.statement : "",
|
|
11885
12230
|
why: typeof body.why === "string" ? body.why : "",
|
|
11886
|
-
livesIn:
|
|
12231
|
+
livesIn: toStringList(body.livesIn),
|
|
12232
|
+
relates: toStringList(body.relates),
|
|
11887
12233
|
issue: typeof body.issue === "string" && body.issue.trim() !== "" ? body.issue.trim() : undefined,
|
|
11888
12234
|
status: body.status === "draft" ? "draft" : "active"
|
|
11889
12235
|
};
|
|
11890
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
|
+
}
|
|
11891
12245
|
function amendFieldsFromBody(body) {
|
|
11892
12246
|
const fields = {};
|
|
11893
12247
|
if (typeof body.statement === "string")
|
|
@@ -11895,7 +12249,9 @@ function amendFieldsFromBody(body) {
|
|
|
11895
12249
|
if ("why" in body)
|
|
11896
12250
|
fields.why = typeof body.why === "string" ? body.why : null;
|
|
11897
12251
|
if ("livesIn" in body)
|
|
11898
|
-
fields.livesIn =
|
|
12252
|
+
fields.livesIn = toStringList(body.livesIn);
|
|
12253
|
+
if ("relates" in body)
|
|
12254
|
+
fields.relates = toStringList(body.relates);
|
|
11899
12255
|
return Object.keys(fields).length === 0 ? null : fields;
|
|
11900
12256
|
}
|
|
11901
12257
|
function failureResponse(c, failure) {
|
|
@@ -11926,7 +12282,7 @@ function parseQueryLimit2(c, rawLimit) {
|
|
|
11926
12282
|
}
|
|
11927
12283
|
return { ok: true, limit };
|
|
11928
12284
|
}
|
|
11929
|
-
function mountApi(app, storage2, platformDir = process.cwd()) {
|
|
12285
|
+
function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT_BIND_HOST) {
|
|
11930
12286
|
app.use("/api/*", async (c, next) => {
|
|
11931
12287
|
c.header("Cache-Control", "no-store");
|
|
11932
12288
|
await next();
|
|
@@ -11935,7 +12291,12 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
|
|
|
11935
12291
|
app.get("/api/repos", guarded((c) => c.json(storage2.listRepos())));
|
|
11936
12292
|
app.get("/api/platform", async (c) => {
|
|
11937
12293
|
const version = await derivePlatformVersion(platformDir);
|
|
11938
|
-
const info = {
|
|
12294
|
+
const info = {
|
|
12295
|
+
version,
|
|
12296
|
+
name: platformName(platformDir),
|
|
12297
|
+
source: "derived",
|
|
12298
|
+
mode: platformMode(platformDir)
|
|
12299
|
+
};
|
|
11939
12300
|
return c.json(info);
|
|
11940
12301
|
});
|
|
11941
12302
|
app.get("/api/report", guarded((c) => c.json(coverageReport(storage2).rows)));
|
|
@@ -11957,7 +12318,7 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
|
|
|
11957
12318
|
if (!featureEnabled("editor")) {
|
|
11958
12319
|
return c.json({ error: featureDisabledMessage("editor") }, 404);
|
|
11959
12320
|
}
|
|
11960
|
-
const originReject = rejectCrossOrigin(c);
|
|
12321
|
+
const originReject = rejectCrossOrigin(c, bindHost);
|
|
11961
12322
|
if (originReject)
|
|
11962
12323
|
return originReject;
|
|
11963
12324
|
const headerReject = rejectBadWriteHeaders(c);
|
|
@@ -11983,7 +12344,7 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
|
|
|
11983
12344
|
if (!featureEnabled("editor")) {
|
|
11984
12345
|
return c.json({ error: featureDisabledMessage("editor") }, 404);
|
|
11985
12346
|
}
|
|
11986
|
-
const originReject = rejectCrossOrigin(c);
|
|
12347
|
+
const originReject = rejectCrossOrigin(c, bindHost);
|
|
11987
12348
|
if (originReject)
|
|
11988
12349
|
return originReject;
|
|
11989
12350
|
const headerReject = rejectBadWriteHeaders(c);
|
|
@@ -11997,12 +12358,9 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
|
|
|
11997
12358
|
return withWriteLock(async () => {
|
|
11998
12359
|
const fields = amendFieldsFromBody(body);
|
|
11999
12360
|
if (fields === null) {
|
|
12000
|
-
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);
|
|
12001
12362
|
}
|
|
12002
|
-
const r = await amend({ platformDir, id, fields }
|
|
12003
|
-
await runIndex({ platformDir, storage: storage2 });
|
|
12004
|
-
return storage2.listTags({ req_id: reqId });
|
|
12005
|
-
});
|
|
12363
|
+
const r = await amend({ platformDir, id, fields });
|
|
12006
12364
|
if (!r.ok)
|
|
12007
12365
|
return failureResponse(c, r);
|
|
12008
12366
|
await runIndex({ platformDir, storage: storage2 });
|
|
@@ -12013,7 +12371,7 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
|
|
|
12013
12371
|
if (!featureEnabled("editor")) {
|
|
12014
12372
|
return c.json({ error: featureDisabledMessage("editor") }, 404);
|
|
12015
12373
|
}
|
|
12016
|
-
const originReject = rejectCrossOrigin(c);
|
|
12374
|
+
const originReject = rejectCrossOrigin(c, bindHost);
|
|
12017
12375
|
if (originReject)
|
|
12018
12376
|
return originReject;
|
|
12019
12377
|
const headerReject = rejectBadWriteHeaders(c);
|
|
@@ -12030,14 +12388,7 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
|
|
|
12030
12388
|
if (statement === "")
|
|
12031
12389
|
return c.json({ error: "statement is required (non-empty)" }, 400);
|
|
12032
12390
|
return withWriteLock(async () => {
|
|
12033
|
-
const r = await supersede({
|
|
12034
|
-
platformDir,
|
|
12035
|
-
id,
|
|
12036
|
-
statement,
|
|
12037
|
-
why: typeof body.why === "string" ? body.why : undefined,
|
|
12038
|
-
livesIn: "livesIn" in body ? toLivesIn(body.livesIn) : undefined,
|
|
12039
|
-
issue: typeof body.issue === "string" && body.issue.trim() !== "" ? body.issue.trim() : undefined
|
|
12040
|
-
}, async (reqId) => {
|
|
12391
|
+
const r = await supersede({ platformDir, id, statement, ...successorFieldsFromBody(body) }, async (reqId) => {
|
|
12041
12392
|
await runIndex({ platformDir, storage: storage2 });
|
|
12042
12393
|
return storage2.listTags({ req_id: reqId });
|
|
12043
12394
|
});
|
|
@@ -12057,7 +12408,7 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
|
|
|
12057
12408
|
if (!featureEnabled("editor")) {
|
|
12058
12409
|
return c.json({ error: featureDisabledMessage("editor") }, 404);
|
|
12059
12410
|
}
|
|
12060
|
-
const originReject = rejectCrossOrigin(c);
|
|
12411
|
+
const originReject = rejectCrossOrigin(c, bindHost);
|
|
12061
12412
|
if (originReject)
|
|
12062
12413
|
return originReject;
|
|
12063
12414
|
const headerReject = rejectBadWriteHeaders(c);
|
|
@@ -12087,7 +12438,7 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
|
|
|
12087
12438
|
if (!featureEnabled("editor")) {
|
|
12088
12439
|
return c.json({ error: featureDisabledMessage("editor") }, 404);
|
|
12089
12440
|
}
|
|
12090
|
-
const originReject = rejectCrossOrigin(c);
|
|
12441
|
+
const originReject = rejectCrossOrigin(c, bindHost);
|
|
12091
12442
|
if (originReject)
|
|
12092
12443
|
return originReject;
|
|
12093
12444
|
const headerReject = rejectBadWriteHeaders(c);
|
|
@@ -12201,22 +12552,162 @@ var init_api = __esm(() => {
|
|
|
12201
12552
|
writeChain = Promise.resolve();
|
|
12202
12553
|
});
|
|
12203
12554
|
|
|
12555
|
+
// src/server/lan.ts
|
|
12556
|
+
import { createHash as createHash2, randomBytes, timingSafeEqual } from "crypto";
|
|
12557
|
+
import { isIP } from "net";
|
|
12558
|
+
function ipv4Prefix(ip) {
|
|
12559
|
+
const parts = ip.split(".");
|
|
12560
|
+
if (parts.length !== 4 || parts.some((p) => String(Number(p)) !== p))
|
|
12561
|
+
return null;
|
|
12562
|
+
return [Number(parts[0]), Number(parts[1])];
|
|
12563
|
+
}
|
|
12564
|
+
function isLoopbackAddress(ip) {
|
|
12565
|
+
if (ip === "::1")
|
|
12566
|
+
return true;
|
|
12567
|
+
return ipv4Prefix(ip)?.[0] === 127;
|
|
12568
|
+
}
|
|
12569
|
+
function isPrivateIPv4([a, b]) {
|
|
12570
|
+
if (a === 127 || a === 10)
|
|
12571
|
+
return true;
|
|
12572
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
12573
|
+
return true;
|
|
12574
|
+
if (a === 192 && b === 168)
|
|
12575
|
+
return true;
|
|
12576
|
+
return a === 100 && b >= 64 && b <= 127;
|
|
12577
|
+
}
|
|
12578
|
+
function bindHostRefusal(raw11) {
|
|
12579
|
+
const kind = isIP(raw11);
|
|
12580
|
+
if (kind === 0)
|
|
12581
|
+
return `--host must be a literal IP address, not "${raw11}"`;
|
|
12582
|
+
if (kind === 6) {
|
|
12583
|
+
return raw11 === "::1" ? null : `--host ${raw11}: the only IPv6 address accepted is ::1`;
|
|
12584
|
+
}
|
|
12585
|
+
const prefix = ipv4Prefix(raw11);
|
|
12586
|
+
if (prefix === null || !isPrivateIPv4(prefix)) {
|
|
12587
|
+
return `--host ${raw11} is not a loopback, RFC 1918, or 100.64.0.0/10 address`;
|
|
12588
|
+
}
|
|
12589
|
+
return null;
|
|
12590
|
+
}
|
|
12591
|
+
function urlHost(ip, port) {
|
|
12592
|
+
return `${isIP(ip) === 6 ? `[${ip}]` : ip}:${port}`;
|
|
12593
|
+
}
|
|
12594
|
+
function generateToken() {
|
|
12595
|
+
return randomBytes(32).toString("hex");
|
|
12596
|
+
}
|
|
12597
|
+
function pinnedTokenRefusal(token) {
|
|
12598
|
+
if (!/^[A-Za-z0-9_-]+$/.test(token))
|
|
12599
|
+
return "the access token may only contain A-Z a-z 0-9 _ -";
|
|
12600
|
+
if (token.length < MIN_PINNED_TOKEN_LENGTH) {
|
|
12601
|
+
return `the access token must be at least ${MIN_PINNED_TOKEN_LENGTH} characters`;
|
|
12602
|
+
}
|
|
12603
|
+
return null;
|
|
12604
|
+
}
|
|
12605
|
+
function digest(value) {
|
|
12606
|
+
return createHash2("sha256").update(value).digest();
|
|
12607
|
+
}
|
|
12608
|
+
function tokenMatches(candidate, token) {
|
|
12609
|
+
if (!candidate)
|
|
12610
|
+
return false;
|
|
12611
|
+
return timingSafeEqual(digest(candidate), digest(token));
|
|
12612
|
+
}
|
|
12613
|
+
function cookieValue(header, name) {
|
|
12614
|
+
for (const pair of (header ?? "").split(";")) {
|
|
12615
|
+
const eq = pair.indexOf("=");
|
|
12616
|
+
if (eq !== -1 && pair.slice(0, eq).trim() === name)
|
|
12617
|
+
return pair.slice(eq + 1).trim();
|
|
12618
|
+
}
|
|
12619
|
+
return null;
|
|
12620
|
+
}
|
|
12621
|
+
function isWrite(req, url) {
|
|
12622
|
+
if (req.method !== "GET" && req.method !== "HEAD")
|
|
12623
|
+
return true;
|
|
12624
|
+
if (url.pathname === "/provenance")
|
|
12625
|
+
return true;
|
|
12626
|
+
return url.pathname === "/api/provenance" && url.searchParams.has("resolve");
|
|
12627
|
+
}
|
|
12628
|
+
function plain(status, body, headers = {}) {
|
|
12629
|
+
return new Response(`${body}
|
|
12630
|
+
`, {
|
|
12631
|
+
status,
|
|
12632
|
+
headers: {
|
|
12633
|
+
"content-type": "text/plain; charset=utf-8",
|
|
12634
|
+
"cache-control": "no-store",
|
|
12635
|
+
...headers
|
|
12636
|
+
}
|
|
12637
|
+
});
|
|
12638
|
+
}
|
|
12639
|
+
function lanGate(fetch2, { bindHost, token, allowWrites }) {
|
|
12640
|
+
return (req) => {
|
|
12641
|
+
const url = new URL(req.url);
|
|
12642
|
+
if (!isServedHostname(url.hostname, bindHost))
|
|
12643
|
+
return plain(403, "host rejected");
|
|
12644
|
+
const queryToken = url.searchParams.get("token");
|
|
12645
|
+
if (queryToken !== null) {
|
|
12646
|
+
if (!tokenMatches(queryToken, token))
|
|
12647
|
+
return plain(401, "invalid access token");
|
|
12648
|
+
url.searchParams.delete("token");
|
|
12649
|
+
return plain(302, "authenticated", {
|
|
12650
|
+
location: `${url.pathname}${url.search}`,
|
|
12651
|
+
"set-cookie": `${TOKEN_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Strict`
|
|
12652
|
+
});
|
|
12653
|
+
}
|
|
12654
|
+
if (!tokenMatches(cookieValue(req.headers.get("cookie"), TOKEN_COOKIE), token)) {
|
|
12655
|
+
return plain(401, "access token required: open the tokenized URL spec serve printed");
|
|
12656
|
+
}
|
|
12657
|
+
if (!allowWrites && isWrite(req, url)) {
|
|
12658
|
+
return plain(403, "read-only: restart spec serve with --allow-writes to permit changes");
|
|
12659
|
+
}
|
|
12660
|
+
return fetch2(req);
|
|
12661
|
+
};
|
|
12662
|
+
}
|
|
12663
|
+
var TOKEN_COOKIE = "spec_token", MIN_PINNED_TOKEN_LENGTH = 16;
|
|
12664
|
+
var init_lan = __esm(() => {
|
|
12665
|
+
init_src();
|
|
12666
|
+
});
|
|
12667
|
+
|
|
12204
12668
|
// src/commands/serve.ts
|
|
12205
12669
|
var exports_serve = {};
|
|
12206
12670
|
__export(exports_serve, {
|
|
12207
12671
|
serveCommand: () => serveCommand,
|
|
12672
|
+
resolveBind: () => resolveBind,
|
|
12208
12673
|
composeServeApp: () => composeServeApp
|
|
12209
12674
|
});
|
|
12210
|
-
import { existsSync as
|
|
12675
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync6 } from "fs";
|
|
12211
12676
|
import { dirname as dirname8 } from "path";
|
|
12212
12677
|
import { defineCommand as defineCommand24 } from "citty";
|
|
12213
12678
|
import { Hono as Hono2 } from "hono";
|
|
12214
|
-
function composeServeApp(storage2, platformDir = process.cwd()) {
|
|
12679
|
+
function composeServeApp(storage2, platformDir = process.cwd(), bindHost = DEFAULT_BIND_HOST) {
|
|
12215
12680
|
const app = new Hono2;
|
|
12216
|
-
mountApi(app, storage2, platformDir);
|
|
12217
|
-
mountWebapp(app);
|
|
12681
|
+
mountApi(app, storage2, platformDir, bindHost);
|
|
12682
|
+
mountWebapp(app, bindHost, platformName(platformDir));
|
|
12218
12683
|
return app;
|
|
12219
12684
|
}
|
|
12685
|
+
function usageError(message) {
|
|
12686
|
+
console.error(`spec serve: ${message}`);
|
|
12687
|
+
process.exit(EXIT.USAGE);
|
|
12688
|
+
}
|
|
12689
|
+
function resolveBind(args) {
|
|
12690
|
+
const host = args.host ?? DEFAULT_BIND_HOST;
|
|
12691
|
+
const hostRefusal = bindHostRefusal(host);
|
|
12692
|
+
if (hostRefusal !== null)
|
|
12693
|
+
usageError(hostRefusal);
|
|
12694
|
+
if (isLoopbackAddress(host))
|
|
12695
|
+
return { host, lan: null };
|
|
12696
|
+
const pinned = args.token ?? process.env.SPEC_SERVE_TOKEN;
|
|
12697
|
+
if (pinned !== undefined) {
|
|
12698
|
+
const tokenRefusal = pinnedTokenRefusal(pinned);
|
|
12699
|
+
if (tokenRefusal !== null)
|
|
12700
|
+
usageError(tokenRefusal);
|
|
12701
|
+
}
|
|
12702
|
+
return {
|
|
12703
|
+
host,
|
|
12704
|
+
lan: {
|
|
12705
|
+
bindHost: host,
|
|
12706
|
+
token: pinned ?? generateToken(),
|
|
12707
|
+
allowWrites: args.allowWrites === true
|
|
12708
|
+
}
|
|
12709
|
+
};
|
|
12710
|
+
}
|
|
12220
12711
|
async function runProbe() {
|
|
12221
12712
|
const app = createApp();
|
|
12222
12713
|
const server = Bun.serve({
|
|
@@ -12234,7 +12725,7 @@ async function runProbe() {
|
|
|
12234
12725
|
}
|
|
12235
12726
|
const body = await res.text();
|
|
12236
12727
|
if (!body.includes("Spec Engine \u2014 coming online")) {
|
|
12237
|
-
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`);
|
|
12238
12729
|
server.stop();
|
|
12239
12730
|
process.exit(EXIT.FAILURE);
|
|
12240
12731
|
}
|
|
@@ -12249,26 +12740,41 @@ async function runProbe() {
|
|
|
12249
12740
|
}
|
|
12250
12741
|
var serveCommand;
|
|
12251
12742
|
var init_serve = __esm(() => {
|
|
12743
|
+
init_src();
|
|
12252
12744
|
init_server();
|
|
12253
12745
|
init_constants();
|
|
12254
12746
|
init_discover();
|
|
12255
12747
|
init_pipeline();
|
|
12256
12748
|
init_prompt();
|
|
12257
12749
|
init_api();
|
|
12750
|
+
init_lan();
|
|
12258
12751
|
init_sqlite();
|
|
12259
12752
|
init__args();
|
|
12260
12753
|
init__shared();
|
|
12261
12754
|
serveCommand = defineCommand24({
|
|
12262
12755
|
meta: {
|
|
12263
12756
|
name: "serve",
|
|
12264
|
-
description: "Run the local webapp. Binds 127.0.0.1 on --port
|
|
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."
|
|
12265
12758
|
},
|
|
12266
12759
|
args: {
|
|
12267
12760
|
platformDir: platformDirArg,
|
|
12268
12761
|
port: {
|
|
12269
12762
|
type: "string",
|
|
12270
12763
|
default: "4000",
|
|
12271
|
-
description: "Port (default 4000; 0 = ephemeral).
|
|
12764
|
+
description: "Port (default 4000; 0 = ephemeral)."
|
|
12765
|
+
},
|
|
12766
|
+
host: {
|
|
12767
|
+
type: "string",
|
|
12768
|
+
default: DEFAULT_BIND_HOST,
|
|
12769
|
+
description: "Address to bind (default 127.0.0.1). Only a literal loopback, RFC 1918, or 100.64.0.0/10 IP; a non-loopback address requires the printed access token and is read-only."
|
|
12770
|
+
},
|
|
12771
|
+
token: {
|
|
12772
|
+
type: "string",
|
|
12773
|
+
description: "Pin the access token for a non-loopback --host (else SPEC_SERVE_TOKEN, else random). At least 16 of A-Z a-z 0-9 _ -."
|
|
12774
|
+
},
|
|
12775
|
+
allowWrites: {
|
|
12776
|
+
type: "boolean",
|
|
12777
|
+
description: "On a non-loopback --host, permit editor writes and tracker resolution."
|
|
12272
12778
|
},
|
|
12273
12779
|
out: {
|
|
12274
12780
|
type: "string",
|
|
@@ -12276,7 +12782,7 @@ var init_serve = __esm(() => {
|
|
|
12276
12782
|
},
|
|
12277
12783
|
probe: {
|
|
12278
12784
|
type: "boolean",
|
|
12279
|
-
description: "Smoke test: bind ephemeral port on 127.0.0.1, GET /, assert body contains placeholder, exit."
|
|
12785
|
+
description: "Smoke test: bind ephemeral port on 127.0.0.1 (ignores --host), GET /, assert body contains placeholder, exit."
|
|
12280
12786
|
},
|
|
12281
12787
|
noPrompt: {
|
|
12282
12788
|
type: "boolean",
|
|
@@ -12300,6 +12806,7 @@ var init_serve = __esm(() => {
|
|
|
12300
12806
|
process.exit(EXIT.USAGE);
|
|
12301
12807
|
return;
|
|
12302
12808
|
}
|
|
12809
|
+
const { host, lan } = resolveBind(args);
|
|
12303
12810
|
const platformDir = resolvePlatformDir(args);
|
|
12304
12811
|
const outArg2 = args.out;
|
|
12305
12812
|
const dbPath = resolveDbPath(platformDir, outArg2);
|
|
@@ -12317,7 +12824,7 @@ var init_serve = __esm(() => {
|
|
|
12317
12824
|
}
|
|
12318
12825
|
});
|
|
12319
12826
|
mkdirSync6(dirname8(dbPath), { recursive: true });
|
|
12320
|
-
const needsIndex = !
|
|
12827
|
+
const needsIndex = !existsSync14(dbPath);
|
|
12321
12828
|
let storage2;
|
|
12322
12829
|
try {
|
|
12323
12830
|
storage2 = openStorage(dbPath);
|
|
@@ -12330,20 +12837,27 @@ var init_serve = __esm(() => {
|
|
|
12330
12837
|
if (needsIndex || storage2.listRepos().length === 0) {
|
|
12331
12838
|
await runIndex({ platformDir, storage: storage2 });
|
|
12332
12839
|
}
|
|
12333
|
-
const app = composeServeApp(storage2, platformDir);
|
|
12840
|
+
const app = composeServeApp(storage2, platformDir, host);
|
|
12334
12841
|
server = Bun.serve({
|
|
12335
12842
|
port,
|
|
12336
|
-
hostname:
|
|
12337
|
-
fetch: app.fetch
|
|
12843
|
+
hostname: host,
|
|
12844
|
+
fetch: lan === null ? app.fetch : lanGate(app.fetch, lan)
|
|
12338
12845
|
});
|
|
12339
12846
|
} catch (err) {
|
|
12340
12847
|
storage2.close();
|
|
12848
|
+
handleInvalidDomainFiles(err, false);
|
|
12341
12849
|
handleStorageUnavailable(err, dbPath);
|
|
12342
|
-
console.error(`spec serve: failed to start on
|
|
12850
|
+
console.error(`spec serve: failed to start on ${urlHost(host, port)}:`, err);
|
|
12343
12851
|
process.exit(EXIT.FAILURE);
|
|
12344
12852
|
return;
|
|
12345
12853
|
}
|
|
12346
|
-
|
|
12854
|
+
const serving = `spec: serving ${platformName(platformDir)} on ${server.url.origin}`;
|
|
12855
|
+
if (lan === null) {
|
|
12856
|
+
console.log(serving);
|
|
12857
|
+
return;
|
|
12858
|
+
}
|
|
12859
|
+
console.log(`${serving}/?token=${lan.token}`);
|
|
12860
|
+
console.log(lan.allowWrites ? "spec: writes ALLOWED (--allow-writes)" : "spec: read-only");
|
|
12347
12861
|
}
|
|
12348
12862
|
});
|
|
12349
12863
|
});
|
|
@@ -12358,13 +12872,13 @@ __export(exports_docs, {
|
|
|
12358
12872
|
docsCommand: () => docsCommand,
|
|
12359
12873
|
createDocsFetchHandler: () => createDocsFetchHandler
|
|
12360
12874
|
});
|
|
12361
|
-
import { existsSync as
|
|
12362
|
-
import { join as
|
|
12875
|
+
import { existsSync as existsSync15, statSync as statSync6 } from "fs";
|
|
12876
|
+
import { join as join16, normalize, sep as sep4 } from "path";
|
|
12363
12877
|
import { fileURLToPath } from "url";
|
|
12364
12878
|
import { defineCommand as defineCommand25 } from "citty";
|
|
12365
12879
|
function resolveDocsRootFrom(candidates) {
|
|
12366
12880
|
for (const candidate of candidates) {
|
|
12367
|
-
if (
|
|
12881
|
+
if (existsSync15(join16(candidate, "index.html")))
|
|
12368
12882
|
return candidate;
|
|
12369
12883
|
}
|
|
12370
12884
|
return null;
|
|
@@ -12388,17 +12902,17 @@ function createDocsFetchHandler(root) {
|
|
|
12388
12902
|
if (pathname.includes("\x00")) {
|
|
12389
12903
|
return new Response("Bad request", { status: 400 });
|
|
12390
12904
|
}
|
|
12391
|
-
const target = normalize(
|
|
12905
|
+
const target = normalize(join16(normalRoot, pathname));
|
|
12392
12906
|
if (target !== normalRoot && !target.startsWith(rootPrefix)) {
|
|
12393
12907
|
return new Response("Not found", { status: 404 });
|
|
12394
12908
|
}
|
|
12395
|
-
for (const candidate of [target,
|
|
12396
|
-
if (
|
|
12909
|
+
for (const candidate of [target, join16(target, "index.html"), `${target}.html`]) {
|
|
12910
|
+
if (existsSync15(candidate) && statSync6(candidate).isFile()) {
|
|
12397
12911
|
return new Response(Bun.file(candidate));
|
|
12398
12912
|
}
|
|
12399
12913
|
}
|
|
12400
|
-
const notFoundPage =
|
|
12401
|
-
if (
|
|
12914
|
+
const notFoundPage = join16(normalRoot, "404.html");
|
|
12915
|
+
if (existsSync15(notFoundPage)) {
|
|
12402
12916
|
return new Response(Bun.file(notFoundPage), { status: 404 });
|
|
12403
12917
|
}
|
|
12404
12918
|
return new Response("Not found", { status: 404 });
|
|
@@ -12448,7 +12962,7 @@ var init_docs2 = __esm(() => {
|
|
|
12448
12962
|
port: {
|
|
12449
12963
|
type: "string",
|
|
12450
12964
|
default: "4100",
|
|
12451
|
-
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."
|
|
12452
12966
|
},
|
|
12453
12967
|
probe: {
|
|
12454
12968
|
type: "boolean",
|