@spec-engine/spec-engine 0.6.1 → 0.7.1
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 +3 -3
- package/dist/docs/checks/index.html +25 -5
- package/dist/docs/commands/index.html +8 -5
- package/dist/docs/pagefind/fragment/en_a3cd8bf.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_aaced0a.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/fragment/en_eb51244.pf_fragment +0 -0
- package/dist/docs/pagefind/index/en_91ab2a1.pf_index +0 -0
- package/dist/docs/pagefind/index/en_b75c632.pf_index +0 -0
- package/dist/docs/pagefind/pagefind-entry.json +1 -1
- package/dist/docs/pagefind/pagefind.en_dac6a68e28.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 +1112 -546
- package/package.json +1 -1
- package/dist/docs/pagefind/fragment/en_01934b1.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_4560516.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_aa3ebfe.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_dfaa776.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_edd950c.pf_fragment +0 -0
- package/dist/docs/pagefind/index/en_a63e8e2.pf_index +0 -0
- package/dist/docs/pagefind/pagefind.en_2ed26d23fb.pf_meta +0 -0
package/dist/impl.js
CHANGED
|
@@ -20,7 +20,7 @@ var package_default;
|
|
|
20
20
|
var init_package = __esm(() => {
|
|
21
21
|
package_default = {
|
|
22
22
|
name: "@spec-engine/spec-engine",
|
|
23
|
-
version: "0.
|
|
23
|
+
version: "0.7.1",
|
|
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,62 +224,215 @@ 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
|
|
278
|
-
function reject(problem) {
|
|
279
|
-
return { ok: false, problem, expected
|
|
417
|
+
function reject(problem, expected = SHAPE_EXPECTED) {
|
|
418
|
+
return { ok: false, problem, expected };
|
|
419
|
+
}
|
|
420
|
+
function onePromiseProblem(statement, response) {
|
|
421
|
+
if (/;\s/.test(response)) {
|
|
422
|
+
return 'joins a second clause with "; " after "shall" \u2014 split it into its own requirement';
|
|
423
|
+
}
|
|
424
|
+
if (/,\s+and\s+(?!shall\b)\S[^,;]*?\s+shall\b/i.test(response)) {
|
|
425
|
+
return 'joins a second clause with its own subject (", and <subject> shall") \u2014 split it into its own requirement';
|
|
426
|
+
}
|
|
427
|
+
const rationale = statement.match(RATIONALE_RE);
|
|
428
|
+
if (rationale) {
|
|
429
|
+
return `carries a rationale ("${rationale[1]}") \u2014 the reason belongs in the why field`;
|
|
430
|
+
}
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
function lint(clauses, statement) {
|
|
434
|
+
const problem = onePromiseProblem(statement, clauses.response);
|
|
435
|
+
return problem === null ? { ok: true, clauses } : reject(problem, ONE_PROMISE_EXPECTED);
|
|
280
436
|
}
|
|
281
437
|
function parseEarsStatement(raw) {
|
|
282
438
|
const statement = raw.trim();
|
|
@@ -291,15 +447,12 @@ function parseEarsStatement(raw) {
|
|
|
291
447
|
const m2 = statement.match(re);
|
|
292
448
|
if (m2) {
|
|
293
449
|
const [, condition = "", system = "", response = ""] = m2;
|
|
294
|
-
return {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
response: response.trim()
|
|
301
|
-
}
|
|
302
|
-
};
|
|
450
|
+
return lint({
|
|
451
|
+
pattern,
|
|
452
|
+
condition: condition.trim(),
|
|
453
|
+
system: system.trim().replace(/^the\s+/i, ""),
|
|
454
|
+
response: response.trim()
|
|
455
|
+
}, statement);
|
|
303
456
|
}
|
|
304
457
|
if (keyword === "If" && !/,\s*then\s/.test(statement)) {
|
|
305
458
|
return reject('starts with "If" but has no ", then" \u2014 the failure clause never hands over to the system');
|
|
@@ -312,19 +465,16 @@ function parseEarsStatement(raw) {
|
|
|
312
465
|
if (/,/.test(system)) {
|
|
313
466
|
return reject('the clause before "shall" contains a comma but starts with no condition keyword (When/While/If/Where) \u2014 either drop the comma or lead with the condition');
|
|
314
467
|
}
|
|
315
|
-
return {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
response: response.trim()
|
|
322
|
-
}
|
|
323
|
-
};
|
|
468
|
+
return lint({
|
|
469
|
+
pattern: "always",
|
|
470
|
+
condition: null,
|
|
471
|
+
system: system.trim().replace(/^the\s+/i, ""),
|
|
472
|
+
response: response.trim()
|
|
473
|
+
}, statement);
|
|
324
474
|
}
|
|
325
475
|
return reject('nothing follows "shall" \u2014 the observable result is missing');
|
|
326
476
|
}
|
|
327
|
-
var EARS_EXPECTED, CONDITIONAL, ALWAYS_RE;
|
|
477
|
+
var EARS_EXPECTED, CONDITIONAL, ALWAYS_RE, SHAPE_EXPECTED, ONE_PROMISE_EXPECTED = "Expected one promise per statement: a second behavior is its own requirement, and the reason goes in the why.", RATIONALE_RE;
|
|
328
478
|
var init_ears = __esm(() => {
|
|
329
479
|
EARS_EXPECTED = [
|
|
330
480
|
'When <trigger>, the <system> shall <result> \u2014 e.g. "When an unknown id is passed, spec resolve shall print [] and exit 0."',
|
|
@@ -341,6 +491,9 @@ var init_ears = __esm(() => {
|
|
|
341
491
|
{ keyword: "Where", pattern: "where", re: /^Where\s+(.+?),\s*(.+?)\s+shall\s+(.+)$/s }
|
|
342
492
|
];
|
|
343
493
|
ALWAYS_RE = /^(.+?)\s+shall\s+(.+)$/s;
|
|
494
|
+
SHAPE_EXPECTED = `Expected one of:
|
|
495
|
+
${EARS_EXPECTED}`;
|
|
496
|
+
RATIONALE_RE = /\b(because|so that|in order to)\b/i;
|
|
344
497
|
});
|
|
345
498
|
|
|
346
499
|
// ../shared/src/flags.ts
|
|
@@ -531,7 +684,8 @@ CREATE INDEX IF NOT EXISTS idx_term_citations_term ON term_citations(term_id);
|
|
|
531
684
|
-- supersedes-via | amends-via), validated at parse time, never as a DB
|
|
532
685
|
-- constraint. Exactly one index, on req_id; never on issue_id, which would
|
|
533
686
|
-- treat the opaque payload as a lookup key.
|
|
534
|
-
-- @spec PROV-
|
|
687
|
+
-- @spec PROV-005
|
|
688
|
+
-- @spec PROV-006
|
|
535
689
|
CREATE TABLE IF NOT EXISTS provenance (
|
|
536
690
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
537
691
|
req_id TEXT NOT NULL,
|
|
@@ -677,6 +831,7 @@ var init_storage = __esm(() => {
|
|
|
677
831
|
|
|
678
832
|
// ../shared/src/index.ts
|
|
679
833
|
var init_src = __esm(() => {
|
|
834
|
+
init_checkConfig();
|
|
680
835
|
init_config();
|
|
681
836
|
init_diagnostics();
|
|
682
837
|
init_domain();
|
|
@@ -949,8 +1104,7 @@ import { readdir } from "fs/promises";
|
|
|
949
1104
|
import { join as join2 } from "path";
|
|
950
1105
|
function isPathIgnored(path, extra = []) {
|
|
951
1106
|
const anchored = `/${path}`;
|
|
952
|
-
|
|
953
|
-
return tokens.some((t) => anchored.includes(`/${t.endsWith("/") ? t : `${t}/`}`));
|
|
1107
|
+
return IGNORE_SUBSTR.some((t) => anchored.includes(`/${t}`)) || extra.some((t) => path.startsWith(t.endsWith("/") ? t : `${t}/`));
|
|
954
1108
|
}
|
|
955
1109
|
async function walkInto(rootDir, relDir, keep, extraIgnore, out) {
|
|
956
1110
|
const absDir = relDir ? join2(rootDir, relDir) : rootDir;
|
|
@@ -979,15 +1133,19 @@ async function walkFiles(rootDir, keep, extraIgnore) {
|
|
|
979
1133
|
out.sort();
|
|
980
1134
|
return out;
|
|
981
1135
|
}
|
|
1136
|
+
function isDockerfile(name) {
|
|
1137
|
+
return name === "Dockerfile" || name.startsWith("Dockerfile.") && name.length > 11;
|
|
1138
|
+
}
|
|
1139
|
+
function isTagSource(rel, exts = DEFAULT_EXTS) {
|
|
1140
|
+
const name = rel.slice(rel.lastIndexOf("/") + 1);
|
|
1141
|
+
const dot = name.lastIndexOf(".");
|
|
1142
|
+
return isDockerfile(name) || dot > 0 && exts.includes(name.slice(dot + 1));
|
|
1143
|
+
}
|
|
982
1144
|
async function findDomainJsonFiles(canonicalDir) {
|
|
983
1145
|
return walkFiles(canonicalDir, (rel) => rel === "SPEC.json" || rel.endsWith("/SPEC.json"), []);
|
|
984
1146
|
}
|
|
985
1147
|
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);
|
|
1148
|
+
return walkFiles(repoDir, (rel) => isTagSource(rel, exts), extraIgnore);
|
|
991
1149
|
}
|
|
992
1150
|
async function findDocFiles(repoDir, extraIgnore = []) {
|
|
993
1151
|
return walkFiles(repoDir, (rel) => rel.endsWith(".md") && rel !== "SPEC.md" && !rel.endsWith("/SPEC.md"), extraIgnore);
|
|
@@ -1017,7 +1175,11 @@ var init_fs = __esm(() => {
|
|
|
1017
1175
|
"lua",
|
|
1018
1176
|
"luau",
|
|
1019
1177
|
"go",
|
|
1020
|
-
"rs"
|
|
1178
|
+
"rs",
|
|
1179
|
+
"tf",
|
|
1180
|
+
"toml",
|
|
1181
|
+
"sql",
|
|
1182
|
+
"css"
|
|
1021
1183
|
];
|
|
1022
1184
|
});
|
|
1023
1185
|
|
|
@@ -1548,9 +1710,13 @@ SELECT
|
|
|
1548
1710
|
t.file AS source_file,
|
|
1549
1711
|
t.line AS line,
|
|
1550
1712
|
t.req_id AS req_id,
|
|
1551
|
-
|
|
1552
|
-
'
|
|
1553
|
-
|
|
1713
|
+
CASE WHEN t.kind = 'verifies'
|
|
1714
|
+
THEN ('Test verifies Draft requirement ' || t.req_id ||
|
|
1715
|
+
' \u2014 it asserts a promise nobody accepted yet')
|
|
1716
|
+
ELSE ('Tag references Draft requirement ' || t.req_id ||
|
|
1717
|
+
' \u2014 code is bound to a promise nobody accepted yet')
|
|
1718
|
+
END AS detail,
|
|
1719
|
+
CASE WHEN t.kind = 'verifies' THEN 'error' ELSE 'warning' END AS severity
|
|
1554
1720
|
FROM tags t
|
|
1555
1721
|
JOIN requirements r ON r.id = t.req_id
|
|
1556
1722
|
WHERE r.status = 'Draft'
|
|
@@ -1580,11 +1746,10 @@ SELECT
|
|
|
1580
1746
|
'error' AS severity
|
|
1581
1747
|
FROM requirements r
|
|
1582
1748
|
WHERE r.status = 'Active'
|
|
1583
|
-
--
|
|
1584
|
-
-- term never fires ORPHAN_REQ. Literal 'TERM' only, never a real domain key;
|
|
1585
|
-
-- mirrors the coverage VIEW exclusion (WHERE r.key != 'TERM') in schema.ts.
|
|
1749
|
+
-- Must stay in step with the coverage view's TERM exclusion in schema.ts.
|
|
1586
1750
|
-- @spec SCHM-017
|
|
1587
1751
|
AND r.key != 'TERM'
|
|
1752
|
+
-- A doc paragraph citing a requirement is not an implementation.
|
|
1588
1753
|
AND NOT EXISTS (
|
|
1589
1754
|
SELECT 1 FROM tags t WHERE t.req_id = r.id AND t.kind != 'documents'
|
|
1590
1755
|
)
|
|
@@ -1598,11 +1763,10 @@ SELECT
|
|
|
1598
1763
|
r.id AS req_id,
|
|
1599
1764
|
('Requirement ' || r.id ||
|
|
1600
1765
|
' is implemented but has no verifies-kind tag') AS detail,
|
|
1601
|
-
'
|
|
1766
|
+
'warning' AS severity
|
|
1602
1767
|
FROM requirements r
|
|
1603
1768
|
WHERE r.status = 'Active'
|
|
1604
|
-
--
|
|
1605
|
-
-- Literal 'TERM' only; mirrors Q4 + the coverage VIEW exclusion.
|
|
1769
|
+
-- Must stay in step with Q4 and the coverage view's TERM exclusion.
|
|
1606
1770
|
AND r.key != 'TERM'
|
|
1607
1771
|
AND EXISTS (SELECT 1 FROM tags t WHERE t.req_id = r.id AND t.kind = 'implements')
|
|
1608
1772
|
AND NOT EXISTS (SELECT 1 FROM tags t WHERE t.req_id = r.id AND t.kind = 'verifies')
|
|
@@ -2445,7 +2609,7 @@ async function runIndex(opts) {
|
|
|
2445
2609
|
unpinned,
|
|
2446
2610
|
diagnostics: platformDiagnostics
|
|
2447
2611
|
} = await discoverRepos(platformDir);
|
|
2448
|
-
const
|
|
2612
|
+
const specs = await readSpecs(canonical);
|
|
2449
2613
|
const { tagHits, docMentionCandidates } = await scanMembers(members);
|
|
2450
2614
|
const rows = buildRows({
|
|
2451
2615
|
canonical,
|
|
@@ -2453,7 +2617,6 @@ async function runIndex(opts) {
|
|
|
2453
2617
|
members,
|
|
2454
2618
|
specs,
|
|
2455
2619
|
tagHits,
|
|
2456
|
-
structuralDiagnostics,
|
|
2457
2620
|
unpinned,
|
|
2458
2621
|
platformDiagnostics
|
|
2459
2622
|
});
|
|
@@ -2475,29 +2638,22 @@ async function runIndex(opts) {
|
|
|
2475
2638
|
async function readSpecs(canonical) {
|
|
2476
2639
|
const jsonPaths = await findDomainJsonFiles(canonical.path);
|
|
2477
2640
|
const specs = [];
|
|
2478
|
-
const
|
|
2641
|
+
const invalid = [];
|
|
2479
2642
|
for (const rel of jsonPaths) {
|
|
2480
2643
|
const absPath = join4(canonical.path, rel);
|
|
2481
2644
|
const text = await Bun.file(absPath).text();
|
|
2482
2645
|
const platformRelative = `${canonical.name}/${rel}`;
|
|
2483
2646
|
const fallbackKey = basename2(dirname3(rel));
|
|
2484
2647
|
const result = parseDomainJsonFile({ text, sourceFile: platformRelative, fallbackKey });
|
|
2485
|
-
if (
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
line: d.line ?? 0,
|
|
2491
|
-
req_id: d.req_id,
|
|
2492
|
-
detail: d.detail,
|
|
2493
|
-
severity: d.severity
|
|
2494
|
-
});
|
|
2495
|
-
}
|
|
2496
|
-
continue;
|
|
2497
|
-
}
|
|
2498
|
-
specs.push(result.spec);
|
|
2648
|
+
if (result.ok)
|
|
2649
|
+
specs.push(result.spec);
|
|
2650
|
+
else
|
|
2651
|
+
for (const d of result.diagnostics)
|
|
2652
|
+
invalid.push({ ...d, source_file: d.source_file ?? platformRelative });
|
|
2499
2653
|
}
|
|
2500
|
-
|
|
2654
|
+
if (invalid.length > 0)
|
|
2655
|
+
throw new InvalidDomainFilesError(invalid);
|
|
2656
|
+
return specs;
|
|
2501
2657
|
}
|
|
2502
2658
|
function repoExtraIgnore(member) {
|
|
2503
2659
|
const repoIgnore = (member.ignore ?? []).map((e) => e.endsWith("/") ? e : `${e}/`);
|
|
@@ -2549,16 +2705,7 @@ async function scanMembers(members) {
|
|
|
2549
2705
|
return { tagHits, docMentionCandidates };
|
|
2550
2706
|
}
|
|
2551
2707
|
function buildRows(input) {
|
|
2552
|
-
const {
|
|
2553
|
-
canonical,
|
|
2554
|
-
platformVersion,
|
|
2555
|
-
members,
|
|
2556
|
-
specs,
|
|
2557
|
-
tagHits,
|
|
2558
|
-
structuralDiagnostics,
|
|
2559
|
-
unpinned,
|
|
2560
|
-
platformDiagnostics
|
|
2561
|
-
} = input;
|
|
2708
|
+
const { canonical, platformVersion, members, specs, tagHits, unpinned, platformDiagnostics } = input;
|
|
2562
2709
|
const sortedRepos = sortBy([
|
|
2563
2710
|
{
|
|
2564
2711
|
name: canonical.name,
|
|
@@ -2582,8 +2729,6 @@ function buildRows(input) {
|
|
|
2582
2729
|
const { sortedTermAliases, sortedTermCitations } = buildTermRows(specs, flatRequirements);
|
|
2583
2730
|
const sortedProvenance = sortBy(specs.flatMap((s) => s.provenance), (p) => `${p.req_id}\x00${p.role}\x00${p.issue_id}\x00${p.source_file}\x00${pad(p.line)}`);
|
|
2584
2731
|
const diagnostics2 = validateStructure(specs);
|
|
2585
|
-
for (const d of structuralDiagnostics)
|
|
2586
|
-
diagnostics2.push(d);
|
|
2587
2732
|
for (const s of unpinned) {
|
|
2588
2733
|
diagnostics2.push({
|
|
2589
2734
|
code: DiagnosticCode.NO_SPEC_CONFIG,
|
|
@@ -3017,6 +3162,49 @@ var init_init = __esm(() => {
|
|
|
3017
3162
|
KNOWN_KEYS = new Set(SpecConfigSchema.keyof().options);
|
|
3018
3163
|
});
|
|
3019
3164
|
|
|
3165
|
+
// src/check/format.ts
|
|
3166
|
+
function compareRepoNullsLast(a, b) {
|
|
3167
|
+
if (a === b)
|
|
3168
|
+
return 0;
|
|
3169
|
+
if (a === null)
|
|
3170
|
+
return 1;
|
|
3171
|
+
if (b === null)
|
|
3172
|
+
return -1;
|
|
3173
|
+
return a.localeCompare(b);
|
|
3174
|
+
}
|
|
3175
|
+
function compareDiagnostics(a, b) {
|
|
3176
|
+
const codeCmp = a.code.localeCompare(b.code);
|
|
3177
|
+
if (codeCmp !== 0)
|
|
3178
|
+
return codeCmp;
|
|
3179
|
+
const repoCmp = compareRepoNullsLast(a.repo, b.repo);
|
|
3180
|
+
if (repoCmp !== 0)
|
|
3181
|
+
return repoCmp;
|
|
3182
|
+
const fileCmp = (a.source_file ?? "").localeCompare(b.source_file ?? "");
|
|
3183
|
+
if (fileCmp !== 0)
|
|
3184
|
+
return fileCmp;
|
|
3185
|
+
const lineCmp = (a.line ?? 0) - (b.line ?? 0);
|
|
3186
|
+
if (lineCmp !== 0)
|
|
3187
|
+
return lineCmp;
|
|
3188
|
+
return (a.req_id ?? "").localeCompare(b.req_id ?? "");
|
|
3189
|
+
}
|
|
3190
|
+
function sortDiagnostics(rows) {
|
|
3191
|
+
return [...rows].sort(compareDiagnostics);
|
|
3192
|
+
}
|
|
3193
|
+
function renderDiagnostics(rows, mode) {
|
|
3194
|
+
const sorted = sortDiagnostics(rows);
|
|
3195
|
+
if (mode === "json") {
|
|
3196
|
+
return JSON.stringify(sorted);
|
|
3197
|
+
}
|
|
3198
|
+
return sorted.map((d) => {
|
|
3199
|
+
const repo = d.repo ?? "";
|
|
3200
|
+
const file = d.source_file ?? "";
|
|
3201
|
+
const line = d.line ?? "";
|
|
3202
|
+
const reqId = d.req_id ?? "";
|
|
3203
|
+
return `${d.code} ${repo} ${file}:${line} ${reqId} ${d.detail}`;
|
|
3204
|
+
}).join(`
|
|
3205
|
+
`);
|
|
3206
|
+
}
|
|
3207
|
+
|
|
3020
3208
|
// src/operations/_index.ts
|
|
3021
3209
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync, statSync as statSync3 } from "fs";
|
|
3022
3210
|
import { dirname as dirname4, join as join6 } from "path";
|
|
@@ -3115,6 +3303,26 @@ function handleNotAPlatform(e) {
|
|
|
3115
3303
|
}
|
|
3116
3304
|
throw e;
|
|
3117
3305
|
}
|
|
3306
|
+
function handleInvalidDomainFiles(e, json, code = EXIT.FAILURE) {
|
|
3307
|
+
if (!(e instanceof InvalidDomainFilesError))
|
|
3308
|
+
return;
|
|
3309
|
+
if (json)
|
|
3310
|
+
console.log(renderDiagnostics(e.diagnostics, "json"));
|
|
3311
|
+
else
|
|
3312
|
+
console.error(renderDiagnostics(e.diagnostics, "text"));
|
|
3313
|
+
process.exit(code);
|
|
3314
|
+
}
|
|
3315
|
+
function cliFreshTags(platformDir) {
|
|
3316
|
+
const fresh = coldFreshTags(platformDir);
|
|
3317
|
+
return async (reqId) => {
|
|
3318
|
+
try {
|
|
3319
|
+
return await fresh(reqId);
|
|
3320
|
+
} catch (e) {
|
|
3321
|
+
handleInvalidDomainFiles(e, false, EXIT.USAGE);
|
|
3322
|
+
throw e;
|
|
3323
|
+
}
|
|
3324
|
+
};
|
|
3325
|
+
}
|
|
3118
3326
|
function handleStorageUnavailable(e, dbPath) {
|
|
3119
3327
|
const info = describeStorageError(e);
|
|
3120
3328
|
if (info === null)
|
|
@@ -3143,7 +3351,7 @@ function exitOnFailure(cmdName, failure) {
|
|
|
3143
3351
|
process.exit(EXIT.USAGE);
|
|
3144
3352
|
}
|
|
3145
3353
|
async function withReadStorage(opts, fn) {
|
|
3146
|
-
const { platformDir, dbPath, fresh } = opts;
|
|
3354
|
+
const { platformDir, dbPath, fresh, json = false } = opts;
|
|
3147
3355
|
try {
|
|
3148
3356
|
await withIndex({ platformDir, dbPath, build: fresh ? "fresh" : "missing" }, async (h) => {
|
|
3149
3357
|
for (const w of h.warnings)
|
|
@@ -3152,6 +3360,7 @@ async function withReadStorage(opts, fn) {
|
|
|
3152
3360
|
});
|
|
3153
3361
|
} catch (e) {
|
|
3154
3362
|
handleStorageUnavailable(e, dbPath);
|
|
3363
|
+
handleInvalidDomainFiles(e, json);
|
|
3155
3364
|
handleNotAPlatform(e);
|
|
3156
3365
|
}
|
|
3157
3366
|
}
|
|
@@ -3358,7 +3567,7 @@ function listFlag(rawArgs, name, parsed) {
|
|
|
3358
3567
|
}
|
|
3359
3568
|
return raw.flatMap((v) => v.split(",")).map((v) => v.trim()).filter((v) => v !== "");
|
|
3360
3569
|
}
|
|
3361
|
-
var platformDirArg, outArg, jsonArg, freshArg, noPromptArg, livesArg;
|
|
3570
|
+
var platformDirArg, outArg, jsonArg, freshArg, noPromptArg, livesArg, relatesArg;
|
|
3362
3571
|
var init__args = __esm(() => {
|
|
3363
3572
|
init_constants();
|
|
3364
3573
|
platformDirArg = {
|
|
@@ -3383,6 +3592,10 @@ var init__args = __esm(() => {
|
|
|
3383
3592
|
type: "string",
|
|
3384
3593
|
description: "Lives in (livesIn) paths: repeat the flag or comma-separate; an empty value clears the list"
|
|
3385
3594
|
};
|
|
3595
|
+
relatesArg = {
|
|
3596
|
+
type: "string",
|
|
3597
|
+
description: "Related requirement ids (relates): repeat the flag or comma-separate; an empty value clears the list"
|
|
3598
|
+
};
|
|
3386
3599
|
});
|
|
3387
3600
|
|
|
3388
3601
|
// src/commands/index.ts
|
|
@@ -3457,6 +3670,7 @@ var init_commands = __esm(() => {
|
|
|
3457
3670
|
console.error(formatNotASpecPlatform(err.platformDir));
|
|
3458
3671
|
process.exit(EXIT.USAGE);
|
|
3459
3672
|
}
|
|
3673
|
+
handleInvalidDomainFiles(err, !!args.json);
|
|
3460
3674
|
console.error("spec index FAILED:", err);
|
|
3461
3675
|
process.exit(EXIT.FAILURE);
|
|
3462
3676
|
} finally {
|
|
@@ -3466,49 +3680,6 @@ var init_commands = __esm(() => {
|
|
|
3466
3680
|
});
|
|
3467
3681
|
});
|
|
3468
3682
|
|
|
3469
|
-
// src/check/format.ts
|
|
3470
|
-
function compareRepoNullsLast(a, b) {
|
|
3471
|
-
if (a === b)
|
|
3472
|
-
return 0;
|
|
3473
|
-
if (a === null)
|
|
3474
|
-
return 1;
|
|
3475
|
-
if (b === null)
|
|
3476
|
-
return -1;
|
|
3477
|
-
return a.localeCompare(b);
|
|
3478
|
-
}
|
|
3479
|
-
function compareDiagnostics(a, b) {
|
|
3480
|
-
const codeCmp = a.code.localeCompare(b.code);
|
|
3481
|
-
if (codeCmp !== 0)
|
|
3482
|
-
return codeCmp;
|
|
3483
|
-
const repoCmp = compareRepoNullsLast(a.repo, b.repo);
|
|
3484
|
-
if (repoCmp !== 0)
|
|
3485
|
-
return repoCmp;
|
|
3486
|
-
const fileCmp = (a.source_file ?? "").localeCompare(b.source_file ?? "");
|
|
3487
|
-
if (fileCmp !== 0)
|
|
3488
|
-
return fileCmp;
|
|
3489
|
-
const lineCmp = (a.line ?? 0) - (b.line ?? 0);
|
|
3490
|
-
if (lineCmp !== 0)
|
|
3491
|
-
return lineCmp;
|
|
3492
|
-
return (a.req_id ?? "").localeCompare(b.req_id ?? "");
|
|
3493
|
-
}
|
|
3494
|
-
function sortDiagnostics(rows) {
|
|
3495
|
-
return [...rows].sort(compareDiagnostics);
|
|
3496
|
-
}
|
|
3497
|
-
function renderDiagnostics(rows, mode) {
|
|
3498
|
-
const sorted = sortDiagnostics(rows);
|
|
3499
|
-
if (mode === "json") {
|
|
3500
|
-
return JSON.stringify(sorted);
|
|
3501
|
-
}
|
|
3502
|
-
return sorted.map((d) => {
|
|
3503
|
-
const repo = d.repo ?? "";
|
|
3504
|
-
const file = d.source_file ?? "";
|
|
3505
|
-
const line = d.line ?? "";
|
|
3506
|
-
const reqId = d.req_id ?? "";
|
|
3507
|
-
return `${d.code} ${repo} ${file}:${line} ${reqId} ${d.detail}`;
|
|
3508
|
-
}).join(`
|
|
3509
|
-
`);
|
|
3510
|
-
}
|
|
3511
|
-
|
|
3512
3683
|
// src/check/proven.ts
|
|
3513
3684
|
function normalizeToSegments(path) {
|
|
3514
3685
|
const out = [];
|
|
@@ -3634,7 +3805,7 @@ var init_proven = __esm(() => {
|
|
|
3634
3805
|
|
|
3635
3806
|
// src/base/gitBase.ts
|
|
3636
3807
|
function isSafeRef(ref) {
|
|
3637
|
-
return /^[A-Za-z0-9._/-]
|
|
3808
|
+
return /^[A-Za-z0-9._/][A-Za-z0-9._/-]*$/.test(ref);
|
|
3638
3809
|
}
|
|
3639
3810
|
function gitRepoPrefix(platformDir) {
|
|
3640
3811
|
const proc = Bun.spawnSync(["git", "rev-parse", "--show-prefix"], { cwd: platformDir });
|
|
@@ -3672,7 +3843,7 @@ function gitLsTree(platformDir, ref, dir) {
|
|
|
3672
3843
|
function gitDiffNameStatus(platformDir, ref) {
|
|
3673
3844
|
if (!isSafeRef(ref))
|
|
3674
3845
|
return [];
|
|
3675
|
-
const proc = Bun.spawnSync(["git", "diff", "--name-status", "--no-renames", "--relative", ref], {
|
|
3846
|
+
const proc = Bun.spawnSync(["git", "diff", "--name-status", "--no-renames", "--relative", ref, "--"], {
|
|
3676
3847
|
cwd: platformDir
|
|
3677
3848
|
});
|
|
3678
3849
|
if (proc.exitCode !== 0)
|
|
@@ -3691,6 +3862,14 @@ function gitDiffNameStatus(platformDir, ref) {
|
|
|
3691
3862
|
}
|
|
3692
3863
|
return rows;
|
|
3693
3864
|
}
|
|
3865
|
+
function newestReleaseTag(platformDir) {
|
|
3866
|
+
const proc = Bun.spawnSync(["git", "tag", "--list", "v*", "--merged", "HEAD", "--sort=-v:refname"], { cwd: platformDir });
|
|
3867
|
+
if (proc.exitCode !== 0)
|
|
3868
|
+
return null;
|
|
3869
|
+
const tag = proc.stdout.toString().split(`
|
|
3870
|
+
`)[0]?.trim() ?? "";
|
|
3871
|
+
return tag !== "" && isSafeRef(tag) ? tag : null;
|
|
3872
|
+
}
|
|
3694
3873
|
|
|
3695
3874
|
// src/check/codeowners.ts
|
|
3696
3875
|
function parseCodeowners(text) {
|
|
@@ -3972,8 +4151,7 @@ function rowsForDomain2(key, domain2) {
|
|
|
3972
4151
|
source_file: specPaths("", key).rel,
|
|
3973
4152
|
line: found >= 0 ? found + 1 : 0,
|
|
3974
4153
|
req_id: req.id,
|
|
3975
|
-
detail: `${result.problem}.
|
|
3976
|
-
${result.expected}`
|
|
4154
|
+
detail: `${result.problem}. ${result.expected}`
|
|
3977
4155
|
});
|
|
3978
4156
|
}
|
|
3979
4157
|
return rows;
|
|
@@ -4063,6 +4241,60 @@ var init_removed = __esm(() => {
|
|
|
4063
4241
|
init_src();
|
|
4064
4242
|
});
|
|
4065
4243
|
|
|
4244
|
+
// src/check/severity.ts
|
|
4245
|
+
import { existsSync as existsSync7, readFileSync } from "fs";
|
|
4246
|
+
import { join as join8 } from "path";
|
|
4247
|
+
function readCheckConfig(platformDir) {
|
|
4248
|
+
const path = join8(platformDir, CHECK_CONFIG_REL);
|
|
4249
|
+
if (!existsSync7(path))
|
|
4250
|
+
return { ok: true, config: { severity: [] } };
|
|
4251
|
+
return parseCheckConfigText(readFileSync(path, "utf-8"), CHECK_CONFIG_REL);
|
|
4252
|
+
}
|
|
4253
|
+
function specificity(rule, row) {
|
|
4254
|
+
if (rule.code !== row.code)
|
|
4255
|
+
return Specificity.none;
|
|
4256
|
+
if (rule.match === undefined)
|
|
4257
|
+
return Specificity.code;
|
|
4258
|
+
const id = row.req_id;
|
|
4259
|
+
if (id === null)
|
|
4260
|
+
return Specificity.none;
|
|
4261
|
+
if (rule.match.includes(id))
|
|
4262
|
+
return Specificity.id;
|
|
4263
|
+
const key = id.slice(0, id.lastIndexOf("-"));
|
|
4264
|
+
return rule.match.includes(key) ? Specificity.domain : Specificity.none;
|
|
4265
|
+
}
|
|
4266
|
+
function applySeverity(rows, rules) {
|
|
4267
|
+
const out = [];
|
|
4268
|
+
for (const row of rows) {
|
|
4269
|
+
let best;
|
|
4270
|
+
let bestScore = Specificity.none;
|
|
4271
|
+
for (const rule of rules) {
|
|
4272
|
+
const score = specificity(rule, row);
|
|
4273
|
+
if (score !== Specificity.none && score >= bestScore) {
|
|
4274
|
+
best = rule;
|
|
4275
|
+
bestScore = score;
|
|
4276
|
+
}
|
|
4277
|
+
}
|
|
4278
|
+
if (best === undefined)
|
|
4279
|
+
out.push(row);
|
|
4280
|
+
else if (best.severity !== "off")
|
|
4281
|
+
out.push({ ...row, severity: best.severity });
|
|
4282
|
+
}
|
|
4283
|
+
return out;
|
|
4284
|
+
}
|
|
4285
|
+
var CHECK_CONFIG_REL, Specificity;
|
|
4286
|
+
var init_severity = __esm(() => {
|
|
4287
|
+
init_src();
|
|
4288
|
+
init_constants();
|
|
4289
|
+
CHECK_CONFIG_REL = `${CANONICAL_SPECS_DIR}/check.json`;
|
|
4290
|
+
Specificity = {
|
|
4291
|
+
none: -1,
|
|
4292
|
+
code: 0,
|
|
4293
|
+
domain: 1,
|
|
4294
|
+
id: 2
|
|
4295
|
+
};
|
|
4296
|
+
});
|
|
4297
|
+
|
|
4066
4298
|
// src/check/sqlDiagnostics.ts
|
|
4067
4299
|
function collectDiagnostics(storage2) {
|
|
4068
4300
|
const semantic = storage2.listSemanticDiagnostics();
|
|
@@ -4501,7 +4733,7 @@ function localToday() {
|
|
|
4501
4733
|
}
|
|
4502
4734
|
|
|
4503
4735
|
// src/operations/_envelope.ts
|
|
4504
|
-
import { existsSync as
|
|
4736
|
+
import { existsSync as existsSync8 } from "fs";
|
|
4505
4737
|
function domainKeyOf(id) {
|
|
4506
4738
|
return id.slice(0, id.indexOf("-"));
|
|
4507
4739
|
}
|
|
@@ -4510,7 +4742,7 @@ function displayStatus(raw) {
|
|
|
4510
4742
|
}
|
|
4511
4743
|
async function readEnvelope(platformDir, key, missingDetail) {
|
|
4512
4744
|
const { abs: specPath, rel: relFile } = specPaths(platformDir, key);
|
|
4513
|
-
if (!
|
|
4745
|
+
if (!existsSync8(specPath)) {
|
|
4514
4746
|
return fail("not_found", missingDetail ?? `no domain ${key} (expected ${relFile} under ${platformDir})`);
|
|
4515
4747
|
}
|
|
4516
4748
|
const parsed = parseDomainText(await Bun.file(specPath).text(), relFile);
|
|
@@ -4526,32 +4758,58 @@ async function readEnvelope(platformDir, key, missingDetail) {
|
|
|
4526
4758
|
const domain2 = parsed.data;
|
|
4527
4759
|
return { ok: true, key, specPath, relFile, domain: domain2, requirements: domain2.requirements };
|
|
4528
4760
|
}
|
|
4761
|
+
async function scanForEntry(platformDir, id, skipKey) {
|
|
4762
|
+
for (const key of listDomainKeys(platformDir)) {
|
|
4763
|
+
if (key === skipKey)
|
|
4764
|
+
continue;
|
|
4765
|
+
const file = await readEnvelope(platformDir, key);
|
|
4766
|
+
if (!file.ok)
|
|
4767
|
+
continue;
|
|
4768
|
+
const req = file.requirements.find((r) => r.id === id);
|
|
4769
|
+
if (req !== undefined)
|
|
4770
|
+
return { ...file, req };
|
|
4771
|
+
}
|
|
4772
|
+
return null;
|
|
4773
|
+
}
|
|
4529
4774
|
async function locateEntry(platformDir, id, missingDomainDetail) {
|
|
4530
|
-
const
|
|
4531
|
-
|
|
4775
|
+
const prefixKey = domainKeyOf(id);
|
|
4776
|
+
const file = await readEnvelope(platformDir, prefixKey, missingDomainDetail);
|
|
4777
|
+
if (!file.ok && file.reason !== "not_found")
|
|
4532
4778
|
return file;
|
|
4533
|
-
const req = file.requirements.find((r) => r.id === id);
|
|
4534
|
-
if (req
|
|
4535
|
-
return
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
return {
|
|
4779
|
+
const req = file.ok ? file.requirements.find((r) => r.id === id) : undefined;
|
|
4780
|
+
if (file.ok && req !== undefined)
|
|
4781
|
+
return { ...file, req };
|
|
4782
|
+
const scanned = await scanForEntry(platformDir, id, prefixKey);
|
|
4783
|
+
if (scanned !== null)
|
|
4784
|
+
return scanned;
|
|
4785
|
+
return file.ok ? fail("not_found", `no entry ${id} in ${file.relFile}`) : file;
|
|
4786
|
+
}
|
|
4787
|
+
function splitList(values) {
|
|
4788
|
+
return values.flatMap((v) => v.split(",")).map((v) => v.trim()).filter((v) => v !== "");
|
|
4789
|
+
}
|
|
4790
|
+
function successorEntry(id, pred, fields) {
|
|
4791
|
+
const entry = {
|
|
4540
4792
|
id,
|
|
4541
4793
|
status: "active",
|
|
4542
4794
|
statement: fields.statement,
|
|
4543
4795
|
why: fields.why === "" ? null : fields.why,
|
|
4544
4796
|
supersedes: null,
|
|
4545
4797
|
supersededBy: null,
|
|
4546
|
-
relates:
|
|
4798
|
+
relates: fields.relates,
|
|
4547
4799
|
livesIn: fields.livesIn,
|
|
4548
4800
|
issues: fields.issue ? [{ role: "created", id: fields.issue }] : [],
|
|
4549
|
-
aliases: [],
|
|
4550
|
-
cites:
|
|
4801
|
+
aliases: [...pred.aliases],
|
|
4802
|
+
cites: pred.cites.map((c) => ({ ...c }))
|
|
4551
4803
|
};
|
|
4804
|
+
if (pred.term !== undefined)
|
|
4805
|
+
entry.term = pred.term;
|
|
4806
|
+
if (pred.section !== undefined)
|
|
4807
|
+
entry.section = pred.section;
|
|
4808
|
+
return entry;
|
|
4552
4809
|
}
|
|
4553
4810
|
var init__envelope = __esm(() => {
|
|
4554
4811
|
init_src();
|
|
4812
|
+
init_domains();
|
|
4555
4813
|
init_constants();
|
|
4556
4814
|
init__result();
|
|
4557
4815
|
});
|
|
@@ -4588,8 +4846,7 @@ async function judgeStatementGrammar(platformDir, key, statement) {
|
|
|
4588
4846
|
severity: cfg.severity,
|
|
4589
4847
|
key,
|
|
4590
4848
|
message: `the statement does not match the domain's EARS shape \u2014 ${result.problem}.
|
|
4591
|
-
`
|
|
4592
|
-
${result.expected}`
|
|
4849
|
+
${result.expected}`
|
|
4593
4850
|
};
|
|
4594
4851
|
}
|
|
4595
4852
|
function grammarWarningText(cmdName, v) {
|
|
@@ -4606,7 +4863,7 @@ var init_grammar2 = __esm(() => {
|
|
|
4606
4863
|
});
|
|
4607
4864
|
|
|
4608
4865
|
// src/operations/mint.ts
|
|
4609
|
-
import { existsSync as
|
|
4866
|
+
import { existsSync as existsSync9 } from "fs";
|
|
4610
4867
|
function unresolvableRefWarnings(platformDir, fieldValues) {
|
|
4611
4868
|
const out = [];
|
|
4612
4869
|
for (const value of fieldValues) {
|
|
@@ -4621,7 +4878,7 @@ function unresolvableRefWarnings(platformDir, fieldValues) {
|
|
|
4621
4878
|
async function mint(input) {
|
|
4622
4879
|
const { platformDir, key } = input;
|
|
4623
4880
|
const { abs: specPath, rel: relFile } = specPaths(platformDir, key);
|
|
4624
|
-
if (!
|
|
4881
|
+
if (!existsSync9(specPath)) {
|
|
4625
4882
|
return fail("not_found", `no domain ${key} (expected ${relFile} under ${platformDir})`);
|
|
4626
4883
|
}
|
|
4627
4884
|
const warnings = unresolvableRefWarnings(platformDir, [
|
|
@@ -4649,7 +4906,7 @@ async function mint(input) {
|
|
|
4649
4906
|
why: input.why || null,
|
|
4650
4907
|
supersedes: null,
|
|
4651
4908
|
supersededBy: null,
|
|
4652
|
-
relates: input.relates ?? [],
|
|
4909
|
+
relates: splitList(input.relates ?? []),
|
|
4653
4910
|
livesIn: input.livesIn,
|
|
4654
4911
|
issues: input.issue ? [{ role: "created", id: input.issue }] : [],
|
|
4655
4912
|
aliases: [],
|
|
@@ -4808,8 +5065,8 @@ var init_term = __esm(() => {
|
|
|
4808
5065
|
});
|
|
4809
5066
|
|
|
4810
5067
|
// src/operations/glossary.ts
|
|
4811
|
-
import { existsSync as
|
|
4812
|
-
import { join as
|
|
5068
|
+
import { existsSync as existsSync10, readFileSync as readFileSync2 } from "fs";
|
|
5069
|
+
import { join as join9 } from "path";
|
|
4813
5070
|
function parseGlossary(md) {
|
|
4814
5071
|
const terms = [];
|
|
4815
5072
|
let section = null;
|
|
@@ -4861,15 +5118,15 @@ function generateGlossary(terms) {
|
|
|
4861
5118
|
}
|
|
4862
5119
|
function readStoreTerms(platformDir) {
|
|
4863
5120
|
const { abs: specPath, rel: relFile } = specPaths(platformDir, TERM_KEY);
|
|
4864
|
-
if (!
|
|
5121
|
+
if (!existsSync10(specPath))
|
|
4865
5122
|
return [];
|
|
4866
|
-
const parsed = parseDomainText(
|
|
5123
|
+
const parsed = parseDomainText(readFileSync2(specPath, "utf8"), relFile);
|
|
4867
5124
|
if (!parsed.ok)
|
|
4868
5125
|
return [];
|
|
4869
5126
|
return parsed.data.requirements.filter((r) => r.status.toLowerCase() === "active").sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0).map((r) => ({ term: r.term ?? "", statement: r.statement, section: r.section ?? null }));
|
|
4870
5127
|
}
|
|
4871
5128
|
function glossaryPath(platformDir) {
|
|
4872
|
-
return
|
|
5129
|
+
return join9(platformDir, "GLOSSARY.md");
|
|
4873
5130
|
}
|
|
4874
5131
|
async function migrateGlossary(platformDir) {
|
|
4875
5132
|
const store = await termStore(platformDir);
|
|
@@ -4880,10 +5137,10 @@ async function migrateGlossary(platformDir) {
|
|
|
4880
5137
|
return { ok: true, skipped: true, existing: domain2.requirements.length };
|
|
4881
5138
|
}
|
|
4882
5139
|
const gPath = glossaryPath(platformDir);
|
|
4883
|
-
if (!
|
|
5140
|
+
if (!existsSync10(gPath)) {
|
|
4884
5141
|
return fail("not_found", `no GLOSSARY.md to migrate (expected ${gPath})`);
|
|
4885
5142
|
}
|
|
4886
|
-
const parsed = parseGlossary(
|
|
5143
|
+
const parsed = parseGlossary(readFileSync2(gPath, "utf8"));
|
|
4887
5144
|
domain2.requirements = parsed.map((t, i) => ({
|
|
4888
5145
|
id: `${TERM_KEY}-${String(i + 1).padStart(3, "0")}`,
|
|
4889
5146
|
status: "active",
|
|
@@ -4920,10 +5177,10 @@ function glossaryDriftDiagnostic(platformDir) {
|
|
|
4920
5177
|
if (terms.length === 0)
|
|
4921
5178
|
return null;
|
|
4922
5179
|
const gPath = glossaryPath(platformDir);
|
|
4923
|
-
if (!
|
|
5180
|
+
if (!existsSync10(gPath))
|
|
4924
5181
|
return null;
|
|
4925
5182
|
const generated = generateGlossary(terms);
|
|
4926
|
-
const committed =
|
|
5183
|
+
const committed = readFileSync2(gPath, "utf8");
|
|
4927
5184
|
if (generated === committed)
|
|
4928
5185
|
return null;
|
|
4929
5186
|
return {
|
|
@@ -4939,7 +5196,7 @@ function glossaryDriftDiagnostic(platformDir) {
|
|
|
4939
5196
|
function checkGlossary(platformDir) {
|
|
4940
5197
|
const generated = generateGlossary(readStoreTerms(platformDir));
|
|
4941
5198
|
const gPath = glossaryPath(platformDir);
|
|
4942
|
-
const committed =
|
|
5199
|
+
const committed = existsSync10(gPath) ? readFileSync2(gPath, "utf8") : "";
|
|
4943
5200
|
return { ok: true, clean: generated === committed };
|
|
4944
5201
|
}
|
|
4945
5202
|
var GLOSSARY_INTRO = `Canonical names for Spec Engine concepts. When prose, code comments, diagnostics, or
|
|
@@ -4952,27 +5209,221 @@ var init_glossary = __esm(() => {
|
|
|
4952
5209
|
init_term();
|
|
4953
5210
|
});
|
|
4954
5211
|
|
|
4955
|
-
// src/
|
|
4956
|
-
import {
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
const path = join9(platformDir, ".github", "CODEOWNERS");
|
|
4960
|
-
if (!existsSync10(path))
|
|
4961
|
-
return null;
|
|
4962
|
-
return readFileSync2(path, "utf-8");
|
|
5212
|
+
// src/guard/collect.ts
|
|
5213
|
+
import { join as join10 } from "path";
|
|
5214
|
+
function isIgnored(path) {
|
|
5215
|
+
return isPathIgnored(path);
|
|
4963
5216
|
}
|
|
4964
|
-
function
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
return
|
|
5217
|
+
function isSpecFile(path) {
|
|
5218
|
+
return path.startsWith(`${CANONICAL_SPECS_DIR}/`) && path.endsWith("/SPEC.json") && !isIgnored(path);
|
|
5219
|
+
}
|
|
5220
|
+
function isCodeFile(path) {
|
|
5221
|
+
return !isSpecFile(path) && !isIgnored(path) && isTagSource(path);
|
|
5222
|
+
}
|
|
5223
|
+
function parseReqs(text) {
|
|
5224
|
+
const v = parseDomainText(text, "");
|
|
5225
|
+
return v.ok ? v.data.requirements : [];
|
|
5226
|
+
}
|
|
5227
|
+
function collectBaseReqs(platformDir, ref, specPaths2) {
|
|
5228
|
+
const reqs = [];
|
|
5229
|
+
const path = new Map;
|
|
5230
|
+
for (const p of specPaths2) {
|
|
5231
|
+
const bytes = gitShow(platformDir, ref, p);
|
|
5232
|
+
if (bytes === null)
|
|
5233
|
+
continue;
|
|
5234
|
+
for (const r of parseReqs(bytes)) {
|
|
5235
|
+
reqs.push(r);
|
|
5236
|
+
path.set(r.id, p);
|
|
5237
|
+
}
|
|
5238
|
+
}
|
|
5239
|
+
return { reqs, path };
|
|
5240
|
+
}
|
|
5241
|
+
function collectBaseTags(platformDir, ref, codePaths) {
|
|
5242
|
+
const implSite = new Map;
|
|
5243
|
+
const verifySite = new Map;
|
|
5244
|
+
for (const p of codePaths) {
|
|
5245
|
+
const bytes = gitShow(platformDir, ref, p);
|
|
5246
|
+
if (bytes === null)
|
|
5247
|
+
continue;
|
|
5248
|
+
for (const tag of scanTagsInFile("", p, bytes)) {
|
|
5249
|
+
const target = tag.kind === "verifies" ? verifySite : implSite;
|
|
5250
|
+
if (!target.has(tag.req_id))
|
|
5251
|
+
target.set(tag.req_id, { file: tag.file, line: tag.line });
|
|
5252
|
+
}
|
|
5253
|
+
}
|
|
5254
|
+
return { implSite, verifySite };
|
|
5255
|
+
}
|
|
5256
|
+
async function collectWorktreeReqs(canonicalDir) {
|
|
5257
|
+
const ids = new Set;
|
|
5258
|
+
const activeIds = new Set;
|
|
5259
|
+
const supersedesTargets = new Set;
|
|
5260
|
+
for (const rel of await findDomainJsonFiles(canonicalDir)) {
|
|
5261
|
+
const text = await Bun.file(join10(canonicalDir, rel)).text();
|
|
5262
|
+
for (const r of parseReqs(text)) {
|
|
5263
|
+
ids.add(r.id);
|
|
5264
|
+
if (r.status.toLowerCase() === "active")
|
|
5265
|
+
activeIds.add(r.id);
|
|
5266
|
+
if (r.supersedes != null)
|
|
5267
|
+
supersedesTargets.add(r.supersedes);
|
|
5268
|
+
}
|
|
5269
|
+
}
|
|
5270
|
+
return { ids, activeIds, supersedesTargets };
|
|
5271
|
+
}
|
|
5272
|
+
function tallyWorktreeTags(storage2) {
|
|
5273
|
+
const implCount = new Map;
|
|
5274
|
+
const verifyCount = new Map;
|
|
5275
|
+
for (const t of storage2.listTags()) {
|
|
5276
|
+
const bucket = t.kind === "implements" ? implCount : t.kind === "verifies" ? verifyCount : null;
|
|
5277
|
+
if (bucket !== null)
|
|
5278
|
+
bucket.set(t.req_id, (bucket.get(t.req_id) ?? 0) + 1);
|
|
5279
|
+
}
|
|
5280
|
+
return { implCount, verifyCount };
|
|
5281
|
+
}
|
|
5282
|
+
async function collectFacts(platformDir, ref, storage2) {
|
|
5283
|
+
const changed = gitDiffNameStatus(platformDir, ref);
|
|
5284
|
+
const specPaths2 = changed.filter((e) => isSpecFile(e.path)).map((e) => e.path);
|
|
5285
|
+
const codePaths = changed.filter((e) => isCodeFile(e.path)).map((e) => e.path);
|
|
5286
|
+
const deletedSpecFiles = changed.filter((e) => e.status === "D" && isSpecFile(e.path)).map((e) => e.path);
|
|
5287
|
+
const base = collectBaseReqs(platformDir, ref, specPaths2);
|
|
5288
|
+
const baseTags = collectBaseTags(platformDir, ref, codePaths);
|
|
5289
|
+
const worktree = await collectWorktreeReqs(join10(platformDir, CANONICAL_SPECS_DIR));
|
|
5290
|
+
const counts = tallyWorktreeTags(storage2);
|
|
5291
|
+
return {
|
|
5292
|
+
baseReqs: base.reqs,
|
|
5293
|
+
baseReqPath: base.path,
|
|
5294
|
+
baseImplSite: baseTags.implSite,
|
|
5295
|
+
baseVerifySite: baseTags.verifySite,
|
|
5296
|
+
worktreeReqIds: worktree.ids,
|
|
5297
|
+
worktreeActiveIds: worktree.activeIds,
|
|
5298
|
+
worktreeSupersedesTargets: worktree.supersedesTargets,
|
|
5299
|
+
worktreeImplCount: counts.implCount,
|
|
5300
|
+
worktreeVerifyCount: counts.verifyCount,
|
|
5301
|
+
deletedSpecFiles
|
|
5302
|
+
};
|
|
5303
|
+
}
|
|
5304
|
+
var init_collect = __esm(() => {
|
|
5305
|
+
init_src();
|
|
5306
|
+
init_constants();
|
|
5307
|
+
init_fs();
|
|
5308
|
+
init_tags();
|
|
5309
|
+
});
|
|
5310
|
+
|
|
5311
|
+
// src/guard/losses.ts
|
|
5312
|
+
function isActive(status) {
|
|
5313
|
+
return status.toLowerCase() === "active";
|
|
5314
|
+
}
|
|
5315
|
+
function isSuperseded(base, f) {
|
|
5316
|
+
const forward = base.supersededBy != null && f.worktreeReqIds.has(base.supersededBy);
|
|
5317
|
+
return forward || f.worktreeSupersedesTargets.has(base.id);
|
|
5318
|
+
}
|
|
5319
|
+
function reqDeletedLoss(base, f) {
|
|
5320
|
+
return {
|
|
5321
|
+
kind: "REQUIREMENT_REMOVED",
|
|
5322
|
+
req_id: base.id,
|
|
5323
|
+
file: f.baseReqPath.get(base.id) ?? "",
|
|
5324
|
+
line: 0,
|
|
5325
|
+
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`
|
|
5326
|
+
};
|
|
5327
|
+
}
|
|
5328
|
+
function historyRemovedLoss(base, f) {
|
|
5329
|
+
const display = base.status.charAt(0).toUpperCase() + base.status.slice(1).toLowerCase();
|
|
5330
|
+
return {
|
|
5331
|
+
kind: "REQUIREMENT_REMOVED",
|
|
5332
|
+
req_id: base.id,
|
|
5333
|
+
file: f.baseReqPath.get(base.id) ?? "",
|
|
5334
|
+
line: 0,
|
|
5335
|
+
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`
|
|
5336
|
+
};
|
|
5337
|
+
}
|
|
5338
|
+
function implLostLoss(id, site) {
|
|
5339
|
+
return {
|
|
5340
|
+
kind: "IMPL_LOST",
|
|
5341
|
+
req_id: id,
|
|
5342
|
+
file: site.file,
|
|
5343
|
+
line: site.line,
|
|
5344
|
+
detail: `${id} lost its last implementing @spec tag (was ${site.file}:${site.line})`
|
|
5345
|
+
};
|
|
5346
|
+
}
|
|
5347
|
+
function verifyLostLoss(id, site) {
|
|
5348
|
+
return {
|
|
5349
|
+
kind: "VERIFY_LOST",
|
|
5350
|
+
req_id: id,
|
|
5351
|
+
file: site.file,
|
|
5352
|
+
line: site.line,
|
|
5353
|
+
detail: `${id} lost its last verifying @spec tag (was ${site.file}:${site.line})`
|
|
5354
|
+
};
|
|
5355
|
+
}
|
|
5356
|
+
function specFileDeletedLoss(path) {
|
|
5357
|
+
return {
|
|
5358
|
+
kind: "SPEC_FILE_DELETED",
|
|
5359
|
+
req_id: null,
|
|
5360
|
+
file: path,
|
|
5361
|
+
line: 0,
|
|
5362
|
+
detail: `canonical spec file ${path} present at the ref is absent from the working tree`
|
|
5363
|
+
};
|
|
5364
|
+
}
|
|
5365
|
+
function reqDeletedFor(base, f) {
|
|
5366
|
+
if (f.worktreeReqIds.has(base.id))
|
|
5367
|
+
return null;
|
|
5368
|
+
if (!isActive(base.status))
|
|
5369
|
+
return historyRemovedLoss(base, f);
|
|
5370
|
+
if (isSuperseded(base, f))
|
|
5371
|
+
return null;
|
|
5372
|
+
return reqDeletedLoss(base, f);
|
|
5373
|
+
}
|
|
5374
|
+
function lastTagGone(id, count, f) {
|
|
5375
|
+
return f.worktreeActiveIds.has(id) && count === 0;
|
|
5376
|
+
}
|
|
5377
|
+
function classifyLosses(f) {
|
|
5378
|
+
const out = [];
|
|
5379
|
+
for (const base of f.baseReqs) {
|
|
5380
|
+
const loss = reqDeletedFor(base, f);
|
|
5381
|
+
if (loss !== null)
|
|
5382
|
+
out.push(loss);
|
|
5383
|
+
}
|
|
5384
|
+
for (const [id, site] of f.baseImplSite) {
|
|
5385
|
+
if (lastTagGone(id, f.worktreeImplCount.get(id) ?? 0, f))
|
|
5386
|
+
out.push(implLostLoss(id, site));
|
|
5387
|
+
}
|
|
5388
|
+
for (const [id, site] of f.baseVerifySite) {
|
|
5389
|
+
if (lastTagGone(id, f.worktreeVerifyCount.get(id) ?? 0, f))
|
|
5390
|
+
out.push(verifyLostLoss(id, site));
|
|
5391
|
+
}
|
|
5392
|
+
for (const path of f.deletedSpecFiles)
|
|
5393
|
+
out.push(specFileDeletedLoss(path));
|
|
5394
|
+
return out;
|
|
5395
|
+
}
|
|
5396
|
+
|
|
5397
|
+
// src/operations/guard.ts
|
|
5398
|
+
async function guard(storage2, platformDir, ref) {
|
|
5399
|
+
const losses = classifyLosses(await collectFacts(platformDir, ref, storage2));
|
|
5400
|
+
return { ok: true, ref, losses };
|
|
5401
|
+
}
|
|
5402
|
+
var init_guard = __esm(() => {
|
|
5403
|
+
init_collect();
|
|
5404
|
+
});
|
|
5405
|
+
|
|
5406
|
+
// src/operations/check.ts
|
|
5407
|
+
import { existsSync as existsSync11, readFileSync as readFileSync3 } from "fs";
|
|
5408
|
+
import { join as join11 } from "path";
|
|
5409
|
+
function readCodeownersText(platformDir) {
|
|
5410
|
+
const path = join11(platformDir, ".github", "CODEOWNERS");
|
|
5411
|
+
if (!existsSync11(path))
|
|
5412
|
+
return null;
|
|
5413
|
+
return readFileSync3(path, "utf-8");
|
|
5414
|
+
}
|
|
5415
|
+
function domainReqsFromText(text, platformRel) {
|
|
5416
|
+
const validated = parseDomainText(text, platformRel);
|
|
5417
|
+
if (!validated.ok)
|
|
5418
|
+
return { reqs: [], diagnostics: validated.diagnostics };
|
|
5419
|
+
return { reqs: validated.data.requirements, diagnostics: [] };
|
|
4969
5420
|
}
|
|
4970
5421
|
async function collectChangeReqs(canonicalDir) {
|
|
4971
5422
|
const reqs = [];
|
|
4972
5423
|
const relPathById = new Map;
|
|
4973
5424
|
for (const rel of await findDomainJsonFiles(canonicalDir)) {
|
|
4974
5425
|
const platformRel = `${CANONICAL_SPECS_DIR}/${rel}`;
|
|
4975
|
-
const text = await Bun.file(
|
|
5426
|
+
const text = await Bun.file(join11(canonicalDir, rel)).text();
|
|
4976
5427
|
const parsed = domainReqsFromText(text, platformRel);
|
|
4977
5428
|
for (const r of parsed.reqs) {
|
|
4978
5429
|
reqs.push(r);
|
|
@@ -4981,7 +5432,7 @@ async function collectChangeReqs(canonicalDir) {
|
|
|
4981
5432
|
}
|
|
4982
5433
|
return { reqs, relPathById };
|
|
4983
5434
|
}
|
|
4984
|
-
function
|
|
5435
|
+
function collectBaseReqs2(platformDir, ref, diagnostics2) {
|
|
4985
5436
|
const reqs = [];
|
|
4986
5437
|
const relPathById = new Map;
|
|
4987
5438
|
for (const path of gitLsTree(platformDir, ref, CANONICAL_SPECS_DIR)) {
|
|
@@ -5013,8 +5464,8 @@ async function ingestResults(storage2, resultsPath, diagnostics2) {
|
|
|
5013
5464
|
}
|
|
5014
5465
|
async function governanceGate(input, ref, diagnostics2, results) {
|
|
5015
5466
|
const { platformDir } = input;
|
|
5016
|
-
const change = await collectChangeReqs(
|
|
5017
|
-
const base =
|
|
5467
|
+
const change = await collectChangeReqs(join11(platformDir, CANONICAL_SPECS_DIR));
|
|
5468
|
+
const base = collectBaseReqs2(platformDir, ref, diagnostics2);
|
|
5018
5469
|
diagnostics2.push(...requirementRemoved(base.reqs, change.reqs, (id) => base.relPathById.get(id) ?? null));
|
|
5019
5470
|
const codeowners = parseCodeowners(readCodeownersText(platformDir) ?? "");
|
|
5020
5471
|
const approvedBy = (input.approvedBy ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -5023,12 +5474,33 @@ async function governanceGate(input, ref, diagnostics2, results) {
|
|
|
5023
5474
|
diagnostics2.push(...partialPropagation(changedRules(base.reqs, change.reqs), results.verifyingTags, results.resultsParsed, (id) => change.relPathById.get(id) ?? null));
|
|
5024
5475
|
}
|
|
5025
5476
|
}
|
|
5477
|
+
async function lossDiagnostics(storage2, platformDir, ref) {
|
|
5478
|
+
const { losses } = await guard(storage2, platformDir, ref);
|
|
5479
|
+
const rows = [];
|
|
5480
|
+
for (const loss of losses) {
|
|
5481
|
+
if (loss.kind === "REQUIREMENT_REMOVED")
|
|
5482
|
+
continue;
|
|
5483
|
+
rows.push({
|
|
5484
|
+
code: loss.kind,
|
|
5485
|
+
severity: LOSS_SEVERITY[loss.kind],
|
|
5486
|
+
repo: null,
|
|
5487
|
+
source_file: loss.file === "" ? null : loss.file,
|
|
5488
|
+
line: loss.line,
|
|
5489
|
+
req_id: loss.req_id,
|
|
5490
|
+
detail: loss.detail
|
|
5491
|
+
});
|
|
5492
|
+
}
|
|
5493
|
+
return rows;
|
|
5494
|
+
}
|
|
5026
5495
|
async function check(input, storage2) {
|
|
5027
5496
|
const { platformDir } = input;
|
|
5028
5497
|
if (input.base && !gitRefResolves(platformDir, input.base)) {
|
|
5029
5498
|
return fail("usage", `--base ref '${input.base}' does not resolve in ${platformDir} ` + "(not a git repo, or the ref is unfetched/misspelled) \u2014 refusing to run the governance gate fail-open");
|
|
5030
5499
|
}
|
|
5031
5500
|
const result = await runIndex({ platformDir, storage: storage2 });
|
|
5501
|
+
const checkConfig2 = readCheckConfig(platformDir);
|
|
5502
|
+
if (!checkConfig2.ok)
|
|
5503
|
+
return fail("usage", checkConfig2.detail);
|
|
5032
5504
|
const diagnostics2 = collectDiagnostics(storage2);
|
|
5033
5505
|
if (input.unsourcedChange) {
|
|
5034
5506
|
const superseded = storage2.listRequirements({ status: "Superseded" });
|
|
@@ -5043,20 +5515,24 @@ async function check(input, storage2) {
|
|
|
5043
5515
|
const results = await ingestResults(storage2, input.resultsPath, diagnostics2);
|
|
5044
5516
|
if (input.base) {
|
|
5045
5517
|
await governanceGate(input, input.base, diagnostics2, results);
|
|
5518
|
+
diagnostics2.push(...await lossDiagnostics(storage2, platformDir, input.base));
|
|
5046
5519
|
} else if (gitRefResolves(platformDir, "HEAD")) {
|
|
5047
|
-
const change = await collectChangeReqs(
|
|
5520
|
+
const change = await collectChangeReqs(join11(platformDir, CANONICAL_SPECS_DIR));
|
|
5048
5521
|
const baseParseSink = [];
|
|
5049
|
-
const base =
|
|
5522
|
+
const base = collectBaseReqs2(platformDir, "HEAD", baseParseSink);
|
|
5050
5523
|
diagnostics2.push(...requirementRemoved(base.reqs, change.reqs, (id) => base.relPathById.get(id) ?? null));
|
|
5524
|
+
diagnostics2.push(...await lossDiagnostics(storage2, platformDir, "HEAD"));
|
|
5051
5525
|
}
|
|
5526
|
+
const graded = applySeverity(diagnostics2, checkConfig2.config.severity);
|
|
5052
5527
|
return {
|
|
5053
5528
|
ok: true,
|
|
5054
|
-
diagnostics:
|
|
5529
|
+
diagnostics: graded,
|
|
5055
5530
|
buildId: result.build_id,
|
|
5056
|
-
failing:
|
|
5531
|
+
failing: graded.some((d) => d.severity === "error"),
|
|
5057
5532
|
proofsUnconfirmed: !input.resultsPath
|
|
5058
5533
|
};
|
|
5059
5534
|
}
|
|
5535
|
+
var LOSS_SEVERITY;
|
|
5060
5536
|
var init_check = __esm(() => {
|
|
5061
5537
|
init_src();
|
|
5062
5538
|
init_filerefs2();
|
|
@@ -5064,6 +5540,7 @@ var init_check = __esm(() => {
|
|
|
5064
5540
|
init_propagation_teeth();
|
|
5065
5541
|
init_proven();
|
|
5066
5542
|
init_removed();
|
|
5543
|
+
init_severity();
|
|
5067
5544
|
init_sqlDiagnostics();
|
|
5068
5545
|
init_statusflip();
|
|
5069
5546
|
init_supersedes();
|
|
@@ -5074,6 +5551,12 @@ var init_check = __esm(() => {
|
|
|
5074
5551
|
init_fs();
|
|
5075
5552
|
init__result();
|
|
5076
5553
|
init_glossary();
|
|
5554
|
+
init_guard();
|
|
5555
|
+
LOSS_SEVERITY = {
|
|
5556
|
+
IMPL_LOST: "error",
|
|
5557
|
+
SPEC_FILE_DELETED: "error",
|
|
5558
|
+
VERIFY_LOST: "warning"
|
|
5559
|
+
};
|
|
5077
5560
|
});
|
|
5078
5561
|
|
|
5079
5562
|
// src/commands/check.ts
|
|
@@ -5123,17 +5606,17 @@ var init_check2 = __esm(() => {
|
|
|
5123
5606
|
out: outArg,
|
|
5124
5607
|
ci: {
|
|
5125
5608
|
type: "boolean",
|
|
5126
|
-
description: "Force cold rebuild: rm db+wal+shm BEFORE indexing
|
|
5609
|
+
description: "Force cold rebuild: rm db+wal+shm BEFORE indexing"
|
|
5127
5610
|
},
|
|
5128
5611
|
json: jsonArg,
|
|
5129
5612
|
noPrompt: noPromptArg,
|
|
5130
5613
|
unsourcedChange: {
|
|
5131
5614
|
type: "boolean",
|
|
5132
|
-
description: "Opt-in: emit warning-severity UNSOURCED_CHANGE for Superseded requirements lacking a supersedes-via issue (OFF by default
|
|
5615
|
+
description: "Opt-in: emit warning-severity UNSOURCED_CHANGE for Superseded requirements lacking a supersedes-via issue (OFF by default)"
|
|
5133
5616
|
},
|
|
5134
5617
|
results: {
|
|
5135
5618
|
type: "string",
|
|
5136
|
-
description: "Ingest a JUnit XML results file; enforce the trusted-red PROVEN gate
|
|
5619
|
+
description: "Ingest a JUnit XML results file; enforce the trusted-red PROVEN gate"
|
|
5137
5620
|
},
|
|
5138
5621
|
base: {
|
|
5139
5622
|
type: "string",
|
|
@@ -5184,6 +5667,7 @@ var init_check2 = __esm(() => {
|
|
|
5184
5667
|
return result;
|
|
5185
5668
|
});
|
|
5186
5669
|
} catch (e) {
|
|
5670
|
+
handleInvalidDomainFiles(e, jsonMode);
|
|
5187
5671
|
exitAfterCrash(e);
|
|
5188
5672
|
}
|
|
5189
5673
|
if (outcome === undefined) {
|
|
@@ -5207,8 +5691,8 @@ var init_check2 = __esm(() => {
|
|
|
5207
5691
|
});
|
|
5208
5692
|
|
|
5209
5693
|
// src/operations/domain.ts
|
|
5210
|
-
import { existsSync as
|
|
5211
|
-
import { dirname as dirname6, join as
|
|
5694
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync4 } from "fs";
|
|
5695
|
+
import { dirname as dirname6, join as join12, resolve as resolve9 } from "path";
|
|
5212
5696
|
function definedFields(options) {
|
|
5213
5697
|
const out = {};
|
|
5214
5698
|
if (options.scope !== undefined)
|
|
@@ -5228,9 +5712,9 @@ async function newDomain(platformDir, key, options = {}) {
|
|
|
5228
5712
|
if (!(resolvedDest === resolvedRoot || resolvedDest.startsWith(`${resolvedRoot}/`))) {
|
|
5229
5713
|
return fail("usage", `refusing to write outside platformDir (${resolvedDest})`);
|
|
5230
5714
|
}
|
|
5231
|
-
if (
|
|
5715
|
+
if (existsSync12(dest))
|
|
5232
5716
|
return fail("conflict", `refusing to overwrite ${dest}`);
|
|
5233
|
-
if (
|
|
5717
|
+
if (existsSync12(join12(dirname6(dest), "SPEC.md"))) {
|
|
5234
5718
|
return fail("conflict", `refusing to create ${dest}: domain ${key} already exists as spec-engine/${key}/SPEC.md ` + "\u2014 migrate the SPEC.md first (a fresh empty SPEC.json would shadow it on the next index)");
|
|
5235
5719
|
}
|
|
5236
5720
|
mkdirSync4(dirname6(dest), { recursive: true });
|
|
@@ -5247,10 +5731,34 @@ async function newDomain(platformDir, key, options = {}) {
|
|
|
5247
5731
|
async function listDomains(platformDir) {
|
|
5248
5732
|
return { ok: true, domains: await domainsWithScope(platformDir) };
|
|
5249
5733
|
}
|
|
5734
|
+
async function setDomainGrammar(platformDir, key, options) {
|
|
5735
|
+
if (options.grammar === undefined && options.grammarSeverity === undefined) {
|
|
5736
|
+
return fail("usage", "pass --grammar, --severity, or both");
|
|
5737
|
+
}
|
|
5738
|
+
const file = await readEnvelope(platformDir, key);
|
|
5739
|
+
if (!file.ok)
|
|
5740
|
+
return file;
|
|
5741
|
+
const domain2 = { ...file.domain, ...definedFields(options) };
|
|
5742
|
+
const res = await validateAndWrite(file.specPath, domain2, file.relFile);
|
|
5743
|
+
if (!res.ok) {
|
|
5744
|
+
return fail("invalid_domain_file", res.diagnostics.map((d) => d.detail).join(`
|
|
5745
|
+
`), {
|
|
5746
|
+
diagnostics: res.diagnostics
|
|
5747
|
+
});
|
|
5748
|
+
}
|
|
5749
|
+
return {
|
|
5750
|
+
ok: true,
|
|
5751
|
+
key,
|
|
5752
|
+
grammar: domain2.grammar,
|
|
5753
|
+
grammarSeverity: domain2.grammarSeverity,
|
|
5754
|
+
file: file.relFile
|
|
5755
|
+
};
|
|
5756
|
+
}
|
|
5250
5757
|
var init_domain2 = __esm(() => {
|
|
5251
5758
|
init_src();
|
|
5252
5759
|
init_domains();
|
|
5253
5760
|
init_constants();
|
|
5761
|
+
init__envelope();
|
|
5254
5762
|
init__result();
|
|
5255
5763
|
});
|
|
5256
5764
|
|
|
@@ -5259,6 +5767,7 @@ var exports_domain = {};
|
|
|
5259
5767
|
__export(exports_domain, {
|
|
5260
5768
|
domainNewCommand: () => domainNewCommand,
|
|
5261
5769
|
domainListCommand: () => domainListCommand,
|
|
5770
|
+
domainGrammarCommand: () => domainGrammarCommand,
|
|
5262
5771
|
domainCommand: () => domainCommand
|
|
5263
5772
|
});
|
|
5264
5773
|
import { defineCommand as defineCommand4 } from "citty";
|
|
@@ -5269,7 +5778,15 @@ function exitOnNewFailure(failure) {
|
|
|
5269
5778
|
}
|
|
5270
5779
|
exitOnFailure("spec domain new", failure);
|
|
5271
5780
|
}
|
|
5272
|
-
|
|
5781
|
+
function enumFlag(flag, value, allowed) {
|
|
5782
|
+
if (value === undefined)
|
|
5783
|
+
return;
|
|
5784
|
+
if (allowed.includes(value))
|
|
5785
|
+
return value;
|
|
5786
|
+
console.error(`spec domain grammar: --${flag} must be one of ${allowed.join(", ")}`);
|
|
5787
|
+
process.exit(EXIT.USAGE);
|
|
5788
|
+
}
|
|
5789
|
+
var domainNewCommand, domainListCommand, GRAMMARS, SEVERITIES, domainGrammarCommand, domainCommand;
|
|
5273
5790
|
var init_domain3 = __esm(() => {
|
|
5274
5791
|
init_domains();
|
|
5275
5792
|
init_constants();
|
|
@@ -5337,6 +5854,55 @@ var init_domain3 = __esm(() => {
|
|
|
5337
5854
|
}
|
|
5338
5855
|
}
|
|
5339
5856
|
});
|
|
5857
|
+
GRAMMARS = ["ears", "freeform"];
|
|
5858
|
+
SEVERITIES = ["warning", "error"];
|
|
5859
|
+
domainGrammarCommand = defineCommand4({
|
|
5860
|
+
meta: {
|
|
5861
|
+
name: "grammar",
|
|
5862
|
+
description: "Set a domain's statement grammar and its severity"
|
|
5863
|
+
},
|
|
5864
|
+
args: {
|
|
5865
|
+
name: {
|
|
5866
|
+
type: "positional",
|
|
5867
|
+
required: true,
|
|
5868
|
+
description: "Domain key (case-insensitive)"
|
|
5869
|
+
},
|
|
5870
|
+
platformDir: platformDirArg,
|
|
5871
|
+
grammar: {
|
|
5872
|
+
type: "string",
|
|
5873
|
+
description: "ears | freeform"
|
|
5874
|
+
},
|
|
5875
|
+
severity: {
|
|
5876
|
+
type: "string",
|
|
5877
|
+
description: "warning | error \u2014 how STATEMENT_GRAMMAR and write-time judging treat a miss"
|
|
5878
|
+
},
|
|
5879
|
+
json: {
|
|
5880
|
+
type: "boolean",
|
|
5881
|
+
description: "Print { key, grammar, grammar_severity, file }"
|
|
5882
|
+
}
|
|
5883
|
+
},
|
|
5884
|
+
async run({ args }) {
|
|
5885
|
+
const platformDir = resolvePlatformDir(args);
|
|
5886
|
+
const grammar = enumFlag("grammar", args.grammar, GRAMMARS);
|
|
5887
|
+
const grammarSeverity = enumFlag("severity", args.severity, SEVERITIES);
|
|
5888
|
+
const result = await setDomainGrammar(platformDir, normalizeDomainKey(args.name), {
|
|
5889
|
+
grammar,
|
|
5890
|
+
grammarSeverity
|
|
5891
|
+
});
|
|
5892
|
+
if (!result.ok)
|
|
5893
|
+
exitOnFailure("spec domain grammar", result);
|
|
5894
|
+
if (args.json) {
|
|
5895
|
+
console.log(JSON.stringify({
|
|
5896
|
+
key: result.key,
|
|
5897
|
+
grammar: result.grammar ?? null,
|
|
5898
|
+
grammar_severity: result.grammarSeverity ?? null,
|
|
5899
|
+
file: result.file
|
|
5900
|
+
}));
|
|
5901
|
+
return;
|
|
5902
|
+
}
|
|
5903
|
+
console.log(`${result.file}: grammar ${result.grammar ?? "freeform"}, severity ${result.grammarSeverity ?? "warning"}`);
|
|
5904
|
+
}
|
|
5905
|
+
});
|
|
5340
5906
|
domainCommand = defineCommand4({
|
|
5341
5907
|
meta: {
|
|
5342
5908
|
name: "domain",
|
|
@@ -5344,7 +5910,8 @@ var init_domain3 = __esm(() => {
|
|
|
5344
5910
|
},
|
|
5345
5911
|
subCommands: {
|
|
5346
5912
|
new: domainNewCommand,
|
|
5347
|
-
list: domainListCommand
|
|
5913
|
+
list: domainListCommand,
|
|
5914
|
+
grammar: domainGrammarCommand
|
|
5348
5915
|
}
|
|
5349
5916
|
});
|
|
5350
5917
|
});
|
|
@@ -5528,7 +6095,7 @@ var init_map = __esm(() => {
|
|
|
5528
6095
|
noPrompt: args.noPrompt
|
|
5529
6096
|
}
|
|
5530
6097
|
});
|
|
5531
|
-
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, (storage2) => {
|
|
6098
|
+
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, (storage2) => {
|
|
5532
6099
|
const { rows } = coverageMatrix(storage2);
|
|
5533
6100
|
if (!args.json && rows.length === 0) {
|
|
5534
6101
|
console.error(formatNoRequirementsIndexed(platformDir));
|
|
@@ -5641,7 +6208,7 @@ var init_get = __esm(() => {
|
|
|
5641
6208
|
platformDir,
|
|
5642
6209
|
args: { noPrompt: args.noPrompt }
|
|
5643
6210
|
});
|
|
5644
|
-
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, (storage2) => {
|
|
6211
|
+
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, (storage2) => {
|
|
5645
6212
|
const result = getRecord(storage2, reqId);
|
|
5646
6213
|
if (result.row === null) {
|
|
5647
6214
|
console.error(result.platformEmpty ? formatNoRequirementsIndexed(platformDir) : `spec get: no requirement ${reqId} in the index under ${platformDir} (check the id, or re-run with --fresh after editing specs)`);
|
|
@@ -5715,7 +6282,7 @@ var init_list = __esm(() => {
|
|
|
5715
6282
|
platformDir,
|
|
5716
6283
|
args: { noPrompt: args.noPrompt }
|
|
5717
6284
|
});
|
|
5718
|
-
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, (storage2) => {
|
|
6285
|
+
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, (storage2) => {
|
|
5719
6286
|
const result = listRecords(storage2, filter);
|
|
5720
6287
|
if (!args.json && result.rows.length === 0) {
|
|
5721
6288
|
console.error(result.platformEmpty ? formatNoRequirementsIndexed(platformDir) : `spec list: no requirements match under ${platformDir}`);
|
|
@@ -5792,7 +6359,7 @@ var init_propagation2 = __esm(() => {
|
|
|
5792
6359
|
platformDir,
|
|
5793
6360
|
args: { noPrompt: args.noPrompt }
|
|
5794
6361
|
});
|
|
5795
|
-
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, (storage2) => {
|
|
6362
|
+
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, (storage2) => {
|
|
5796
6363
|
const result = propagation2(storage2, reqId);
|
|
5797
6364
|
if (!args.json && result.platformEmpty) {
|
|
5798
6365
|
console.error(formatNoRequirementsIndexed(platformDir));
|
|
@@ -5915,7 +6482,7 @@ var init_query = __esm(() => {
|
|
|
5915
6482
|
platformDir,
|
|
5916
6483
|
args: { noPrompt: args.noPrompt }
|
|
5917
6484
|
});
|
|
5918
|
-
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, (storage2) => {
|
|
6485
|
+
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, (storage2) => {
|
|
5919
6486
|
const result = query(storage2, text, limit);
|
|
5920
6487
|
if (!result.ok) {
|
|
5921
6488
|
console.error(`spec query: ${result.detail}`);
|
|
@@ -6037,7 +6604,7 @@ var init_relations = __esm(() => {
|
|
|
6037
6604
|
noPrompt: args.noPrompt
|
|
6038
6605
|
}
|
|
6039
6606
|
});
|
|
6040
|
-
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, (storage2) => {
|
|
6607
|
+
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, (storage2) => {
|
|
6041
6608
|
const { rows } = relations(storage2);
|
|
6042
6609
|
if (!args.json && rows.length === 0) {
|
|
6043
6610
|
console.error(formatNoRelations(platformDir));
|
|
@@ -6162,7 +6729,7 @@ var init_adapter = () => {};
|
|
|
6162
6729
|
|
|
6163
6730
|
// ../tracker/src/cache.ts
|
|
6164
6731
|
import { mkdirSync as mkdirSync5 } from "fs";
|
|
6165
|
-
import { dirname as dirname7, join as
|
|
6732
|
+
import { dirname as dirname7, join as join13 } from "path";
|
|
6166
6733
|
function isFresh(entry, now, ttlMs = CACHE_TTL_MS) {
|
|
6167
6734
|
const t = Date.parse(entry.fetched_at);
|
|
6168
6735
|
if (Number.isNaN(t))
|
|
@@ -6176,7 +6743,7 @@ function isValidEntry(e) {
|
|
|
6176
6743
|
return typeof r.title === "string" && typeof r.status === "string" && typeof r.url === "string" && typeof r.fetched_at === "string";
|
|
6177
6744
|
}
|
|
6178
6745
|
function sidecarPath(platformDir) {
|
|
6179
|
-
return
|
|
6746
|
+
return join13(platformDir, ".spec-engine", SIDECAR_FILE);
|
|
6180
6747
|
}
|
|
6181
6748
|
async function readCache(platformDir) {
|
|
6182
6749
|
try {
|
|
@@ -6443,7 +7010,7 @@ var init_provenance = __esm(() => {
|
|
|
6443
7010
|
noPrompt: args.noPrompt
|
|
6444
7011
|
}
|
|
6445
7012
|
});
|
|
6446
|
-
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, async (storage2) => {
|
|
7013
|
+
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, async (storage2) => {
|
|
6447
7014
|
const { rows } = provenance(storage2, issueId || undefined);
|
|
6448
7015
|
if (!args.json && rows.length === 0) {
|
|
6449
7016
|
console.error(issueId ? formatNoProvenanceForIssue(issueId, platformDir) : formatNoProvenance(platformDir));
|
|
@@ -6507,6 +7074,7 @@ async function mintFromFlags(platformDir, key, textFlag, args, rawArgs) {
|
|
|
6507
7074
|
statement,
|
|
6508
7075
|
why: (args.why ?? "").trim(),
|
|
6509
7076
|
livesIn: listFlag(rawArgs, "lives", args.lives) ?? [],
|
|
7077
|
+
relates: listFlag(rawArgs, "relates", args.relates) ?? [],
|
|
6510
7078
|
issue: (args.issue ?? "").trim() || undefined,
|
|
6511
7079
|
status: statusOf(args),
|
|
6512
7080
|
json: Boolean(args.json)
|
|
@@ -6585,6 +7153,7 @@ var init_req = __esm(() => {
|
|
|
6585
7153
|
},
|
|
6586
7154
|
why: { type: "string", description: "Why it matters field (only with --text)" },
|
|
6587
7155
|
lives: livesArg,
|
|
7156
|
+
relates: relatesArg,
|
|
6588
7157
|
issue: {
|
|
6589
7158
|
type: "string",
|
|
6590
7159
|
description: "Originating ticket id, recorded as created-provenance on the entry (only with --text; opaque \u2014 never a requirement id)"
|
|
@@ -6617,9 +7186,9 @@ var init_req = __esm(() => {
|
|
|
6617
7186
|
await mintFromFlags(platformDir, key, textFlag, args, rawArgs);
|
|
6618
7187
|
return;
|
|
6619
7188
|
}
|
|
6620
|
-
const hasFieldFlags = typeof args.why === "string" || typeof args.lives === "string" || typeof args.issue === "string";
|
|
7189
|
+
const hasFieldFlags = typeof args.why === "string" || typeof args.lives === "string" || typeof args.relates === "string" || typeof args.issue === "string";
|
|
6621
7190
|
if (hasFieldFlags) {
|
|
6622
|
-
console.error("spec req: --why/--lives/--issue require --text (the Requirement field is mandatory)");
|
|
7191
|
+
console.error("spec req: --why/--lives/--relates/--issue require --text (the Requirement field is mandatory)");
|
|
6623
7192
|
process.exit(EXIT.USAGE);
|
|
6624
7193
|
return;
|
|
6625
7194
|
}
|
|
@@ -6755,6 +7324,9 @@ async function confirmTermCommand(opts) {
|
|
|
6755
7324
|
exitOnFailure("spec term confirm", result);
|
|
6756
7325
|
await withIndex({ platformDir, build: "fresh" }, () => {
|
|
6757
7326
|
return;
|
|
7327
|
+
}).catch((e) => {
|
|
7328
|
+
handleInvalidDomainFiles(e, false, EXIT.USAGE);
|
|
7329
|
+
throw e;
|
|
6758
7330
|
});
|
|
6759
7331
|
const { pinned, file } = result;
|
|
6760
7332
|
if (opts.json) {
|
|
@@ -7027,14 +7599,18 @@ async function supersedeTarget(platformDir, id) {
|
|
|
7027
7599
|
return located;
|
|
7028
7600
|
}
|
|
7029
7601
|
function successorFields(input, req) {
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7602
|
+
return {
|
|
7603
|
+
why: (input.why ?? req.why ?? "").trim(),
|
|
7604
|
+
livesIn: input.livesIn ?? [...req.livesIn],
|
|
7605
|
+
relates: input.relates === undefined ? [...req.relates] : splitList(input.relates)
|
|
7606
|
+
};
|
|
7034
7607
|
}
|
|
7035
|
-
function
|
|
7036
|
-
const term =
|
|
7037
|
-
|
|
7608
|
+
function applyTermOverrides(successor, input) {
|
|
7609
|
+
const term = input.term?.trim();
|
|
7610
|
+
if (term)
|
|
7611
|
+
successor.term = term;
|
|
7612
|
+
if (input.aliases !== undefined)
|
|
7613
|
+
successor.aliases = input.aliases;
|
|
7038
7614
|
}
|
|
7039
7615
|
function flipPredecessor(req, newId, issue) {
|
|
7040
7616
|
req.status = "superseded";
|
|
@@ -7070,18 +7646,19 @@ async function supersede(input, freshTags) {
|
|
|
7070
7646
|
}
|
|
7071
7647
|
warnings.push({ kind: "grammar", text: grammarWarningText("", verdict).slice(2) });
|
|
7072
7648
|
}
|
|
7073
|
-
const { why, livesIn } = successorFields(input, req);
|
|
7649
|
+
const { why, livesIn, relates } = successorFields(input, req);
|
|
7074
7650
|
warnings.push(...unresolvableRefWarnings(platformDir, [input.statement, why, ...livesIn]));
|
|
7075
7651
|
const newId = await nextRequirementId(platformDir, key);
|
|
7076
7652
|
const issue = input.issue?.trim() || undefined;
|
|
7653
|
+
const successor = successorEntry(newId, req, {
|
|
7654
|
+
statement: input.statement,
|
|
7655
|
+
why,
|
|
7656
|
+
livesIn,
|
|
7657
|
+
relates,
|
|
7658
|
+
issue
|
|
7659
|
+
});
|
|
7660
|
+
applyTermOverrides(successor, input);
|
|
7077
7661
|
flipPredecessor(req, newId, issue);
|
|
7078
|
-
const successor = successorEntry(newId, { statement: input.statement, why, livesIn, issue });
|
|
7079
|
-
if (key === "TERM") {
|
|
7080
|
-
const termFields = successorTermFields(input, req);
|
|
7081
|
-
if (termFields.term !== undefined)
|
|
7082
|
-
successor.term = termFields.term;
|
|
7083
|
-
successor.aliases = termFields.aliases;
|
|
7084
|
-
}
|
|
7085
7662
|
requirements.push(successor);
|
|
7086
7663
|
const specVersion = applyVersionStage(domain2, req, successor, Boolean(input.noBump));
|
|
7087
7664
|
domain2.updated = localToday();
|
|
@@ -7190,6 +7767,9 @@ function inputFromArgs(args, rawArgs, platformDir, id, statement) {
|
|
|
7190
7767
|
const livesIn = listFlag(rawArgs, "lives", args.lives);
|
|
7191
7768
|
if (livesIn !== undefined)
|
|
7192
7769
|
input.livesIn = livesIn;
|
|
7770
|
+
const relates = listFlag(rawArgs, "relates", args.relates);
|
|
7771
|
+
if (relates !== undefined)
|
|
7772
|
+
input.relates = relates;
|
|
7193
7773
|
if (typeof args.term === "string")
|
|
7194
7774
|
input.term = args.term;
|
|
7195
7775
|
if (typeof args.aliases === "string")
|
|
@@ -7203,7 +7783,6 @@ var supersedeArgs, supersedeCommand;
|
|
|
7203
7783
|
var init_supersede2 = __esm(() => {
|
|
7204
7784
|
init_constants();
|
|
7205
7785
|
init_discover();
|
|
7206
|
-
init__index();
|
|
7207
7786
|
init_supersede();
|
|
7208
7787
|
init_grammar3();
|
|
7209
7788
|
init__args();
|
|
@@ -7228,6 +7807,10 @@ var init_supersede2 = __esm(() => {
|
|
|
7228
7807
|
type: "string",
|
|
7229
7808
|
description: "Successor's Lives in: repeat or comma-separate; empty clears (default: copied from the old entry)"
|
|
7230
7809
|
},
|
|
7810
|
+
relates: {
|
|
7811
|
+
type: "string",
|
|
7812
|
+
description: "Successor's related ids: repeat or comma-separate; empty clears (default: copied from the old entry)"
|
|
7813
|
+
},
|
|
7231
7814
|
term: {
|
|
7232
7815
|
type: "string",
|
|
7233
7816
|
description: "TERM successor's headword (default: copied from the old entry; TERM ids only)"
|
|
@@ -7268,7 +7851,7 @@ var init_supersede2 = __esm(() => {
|
|
|
7268
7851
|
handleNotAPlatform(e);
|
|
7269
7852
|
}
|
|
7270
7853
|
const statement = await resolveSuccessorText(args, platformDir, id);
|
|
7271
|
-
const result = await supersede(inputFromArgs(args, rawArgs, platformDir, id, statement),
|
|
7854
|
+
const result = await supersede(inputFromArgs(args, rawArgs, platformDir, id, statement), cliFreshTags(platformDir));
|
|
7272
7855
|
if (!result.ok)
|
|
7273
7856
|
exitOnFailure("spec supersede", result);
|
|
7274
7857
|
printWarnings("spec supersede", result.warnings);
|
|
@@ -7335,7 +7918,6 @@ var deprecateCommand;
|
|
|
7335
7918
|
var init_deprecate2 = __esm(() => {
|
|
7336
7919
|
init_constants();
|
|
7337
7920
|
init_discover();
|
|
7338
|
-
init__index();
|
|
7339
7921
|
init_deprecate();
|
|
7340
7922
|
init_grammar3();
|
|
7341
7923
|
init__args();
|
|
@@ -7378,7 +7960,7 @@ var init_deprecate2 = __esm(() => {
|
|
|
7378
7960
|
} catch (e) {
|
|
7379
7961
|
handleNotAPlatform(e);
|
|
7380
7962
|
}
|
|
7381
|
-
const result = await deprecate({ platformDir, id, reason },
|
|
7963
|
+
const result = await deprecate({ platformDir, id, reason }, cliFreshTags(platformDir));
|
|
7382
7964
|
if (!result.ok)
|
|
7383
7965
|
exitOnFailure("spec deprecate", result);
|
|
7384
7966
|
const { file, sites } = result;
|
|
@@ -7397,30 +7979,28 @@ var init_deprecate2 = __esm(() => {
|
|
|
7397
7979
|
|
|
7398
7980
|
// src/operations/move.ts
|
|
7399
7981
|
async function locateMove(platformDir, id, targetKey) {
|
|
7400
|
-
const
|
|
7401
|
-
|
|
7982
|
+
const prefixKey = domainKeyOf(id);
|
|
7983
|
+
const located = await locateEntry(platformDir, id, `no domain ${prefixKey} (expected spec-engine/${prefixKey}/SPEC.json)`);
|
|
7984
|
+
if (!located.ok)
|
|
7985
|
+
return located;
|
|
7986
|
+
const { req, ...source } = located;
|
|
7987
|
+
if (source.key === targetKey) {
|
|
7402
7988
|
return fail("usage", `${id} is already in ${targetKey} \u2014 use spec supersede for an in-domain revision`);
|
|
7403
7989
|
}
|
|
7404
|
-
const source = await readEnvelope(platformDir, sourceKey, `no domain ${sourceKey} (expected spec-engine/${sourceKey}/SPEC.json)`);
|
|
7405
|
-
if (!source.ok)
|
|
7406
|
-
return source;
|
|
7407
7990
|
const target = await readEnvelope(platformDir, targetKey, `no target domain ${targetKey} \u2014 run \`spec domain new ${targetKey}\` first`);
|
|
7408
7991
|
if (!target.ok)
|
|
7409
7992
|
return target;
|
|
7410
|
-
const req = source.requirements.find((r) => r.id === id);
|
|
7411
|
-
if (req === undefined)
|
|
7412
|
-
return fail("not_found", `no entry ${id} in ${source.relFile}`);
|
|
7413
7993
|
if (req.status.toLowerCase() !== "active") {
|
|
7414
7994
|
return fail("conflict", `${id} is ${req.status} \u2014 only Active requirements move (superseded/retired entries stay as history)`);
|
|
7415
7995
|
}
|
|
7416
7996
|
return { ok: true, source, target, req };
|
|
7417
7997
|
}
|
|
7418
7998
|
function successorFields2(input, req) {
|
|
7419
|
-
const srcLives = (req.livesIn[0] ?? "").trim();
|
|
7420
7999
|
return {
|
|
7421
8000
|
statement: (input.statement ?? req.statement).trim(),
|
|
7422
8001
|
why: (input.why ?? req.why ?? "").trim(),
|
|
7423
|
-
livesIn: input.livesIn ??
|
|
8002
|
+
livesIn: input.livesIn ?? [...req.livesIn],
|
|
8003
|
+
relates: [...req.relates]
|
|
7424
8004
|
};
|
|
7425
8005
|
}
|
|
7426
8006
|
function versionSide(domain2, key, noBump) {
|
|
@@ -7469,7 +8049,7 @@ async function move(input, freshTags) {
|
|
|
7469
8049
|
const sourceCurrent = source.domain.specVersion ?? 1;
|
|
7470
8050
|
req.status = "superseded";
|
|
7471
8051
|
req.supersededBy = newId;
|
|
7472
|
-
target.requirements.push(successorEntry(newId, fields));
|
|
8052
|
+
target.requirements.push(successorEntry(newId, req, fields));
|
|
7473
8053
|
const noBump = Boolean(input.noBump);
|
|
7474
8054
|
const sourceSpecVersion = versionSide(source.domain, source.key, noBump);
|
|
7475
8055
|
const targetSpecVersion = versionSide(target.domain, target.key, noBump);
|
|
@@ -7530,7 +8110,6 @@ var init_move2 = __esm(() => {
|
|
|
7530
8110
|
init_domains();
|
|
7531
8111
|
init_constants();
|
|
7532
8112
|
init_discover();
|
|
7533
|
-
init__index();
|
|
7534
8113
|
init_move();
|
|
7535
8114
|
init_grammar3();
|
|
7536
8115
|
init__args();
|
|
@@ -7592,7 +8171,7 @@ var init_move2 = __esm(() => {
|
|
|
7592
8171
|
} catch (e) {
|
|
7593
8172
|
handleNotAPlatform(e);
|
|
7594
8173
|
}
|
|
7595
|
-
const result = await move(inputFromArgs2(args, rawArgs, platformDir, id, targetKey),
|
|
8174
|
+
const result = await move(inputFromArgs2(args, rawArgs, platformDir, id, targetKey), cliFreshTags(platformDir));
|
|
7596
8175
|
if (!result.ok)
|
|
7597
8176
|
exitOnFailure("spec move", result);
|
|
7598
8177
|
printWarnings("spec move", result.warnings);
|
|
@@ -7626,18 +8205,30 @@ var init_move2 = __esm(() => {
|
|
|
7626
8205
|
});
|
|
7627
8206
|
|
|
7628
8207
|
// src/operations/amend.ts
|
|
7629
|
-
|
|
8208
|
+
function releaseHolding(platformDir, relFile, id) {
|
|
8209
|
+
const tag = newestReleaseTag(platformDir);
|
|
8210
|
+
if (tag === null)
|
|
8211
|
+
return null;
|
|
8212
|
+
const text = gitShow(platformDir, tag, relFile);
|
|
8213
|
+
if (text === null)
|
|
8214
|
+
return null;
|
|
8215
|
+
const parsed = parseDomainText(text, relFile);
|
|
8216
|
+
return parsed.ok && parsed.data.requirements.some((r) => r.id === id) ? tag : null;
|
|
8217
|
+
}
|
|
8218
|
+
function gateRejection(platformDir, relFile, req, fields) {
|
|
8219
|
+
const { id } = req;
|
|
7630
8220
|
const statusLc = req.status.toLowerCase();
|
|
7631
8221
|
if (statusLc !== "active" && statusLc !== "draft") {
|
|
7632
8222
|
return fail("conflict", `${id} is ${displayStatus(req.status)} \u2014 only Active/Draft entries amend (a superseded entry is history; supersede its successor instead)`);
|
|
7633
8223
|
}
|
|
7634
8224
|
if (statusLc !== "active")
|
|
7635
8225
|
return null;
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
|
|
8226
|
+
if (!PROMISE_FIELDS.some((f) => fields[f] !== undefined))
|
|
8227
|
+
return null;
|
|
8228
|
+
const tag = releaseHolding(platformDir, relFile, id);
|
|
8229
|
+
if (tag === null)
|
|
7639
8230
|
return null;
|
|
7640
|
-
return fail("conflict", `${id} is
|
|
8231
|
+
return fail("conflict", `${id} is released in ${tag} \u2014 its promise is immutable. ` + `Run \`spec supersede ${id}\` to change the statement or why; ` + "pass only --lives or --relates to amend its metadata in place.");
|
|
7641
8232
|
}
|
|
7642
8233
|
function applyFields(req, fields) {
|
|
7643
8234
|
const fieldsChanged = [];
|
|
@@ -7657,6 +8248,10 @@ function applyFields(req, fields) {
|
|
|
7657
8248
|
fieldsChanged.push("lives");
|
|
7658
8249
|
refValues.push(...fields.livesIn);
|
|
7659
8250
|
}
|
|
8251
|
+
if (fields.relates !== undefined) {
|
|
8252
|
+
req.relates = splitList(fields.relates);
|
|
8253
|
+
fieldsChanged.push("relates");
|
|
8254
|
+
}
|
|
7660
8255
|
if (fields.term !== undefined) {
|
|
7661
8256
|
req.term = fields.term;
|
|
7662
8257
|
fieldsChanged.push("term");
|
|
@@ -7672,18 +8267,17 @@ function applyFields(req, fields) {
|
|
|
7672
8267
|
}
|
|
7673
8268
|
return { fieldsChanged, refValues };
|
|
7674
8269
|
}
|
|
7675
|
-
async function amend(input
|
|
8270
|
+
async function amend(input) {
|
|
7676
8271
|
const { platformDir, id, fields } = input;
|
|
7677
8272
|
const located = await locateEntry(platformDir, id);
|
|
7678
8273
|
if (!located.ok)
|
|
7679
8274
|
return located;
|
|
7680
|
-
const { specPath, relFile, domain: domain2, req } = located;
|
|
7681
|
-
const rejection =
|
|
8275
|
+
const { key, specPath, relFile, domain: domain2, req } = located;
|
|
8276
|
+
const rejection = gateRejection(platformDir, relFile, req, fields);
|
|
7682
8277
|
if (rejection !== null)
|
|
7683
8278
|
return rejection;
|
|
7684
8279
|
const warnings = [];
|
|
7685
8280
|
if (fields.statement !== undefined) {
|
|
7686
|
-
const key = domainKeyOf(id);
|
|
7687
8281
|
const verdict = await judgeStatementGrammar(platformDir, key, fields.statement);
|
|
7688
8282
|
if (!verdict.ok) {
|
|
7689
8283
|
if (verdict.severity === "error") {
|
|
@@ -7705,12 +8299,14 @@ async function amend(input, freshTags) {
|
|
|
7705
8299
|
}
|
|
7706
8300
|
return { ok: true, id, file: relFile, fieldsChanged: [...fieldsChanged].sort(), warnings };
|
|
7707
8301
|
}
|
|
8302
|
+
var PROMISE_FIELDS;
|
|
7708
8303
|
var init_amend = __esm(() => {
|
|
7709
8304
|
init_src();
|
|
7710
8305
|
init_grammar2();
|
|
7711
8306
|
init__envelope();
|
|
7712
8307
|
init__result();
|
|
7713
8308
|
init_mint();
|
|
8309
|
+
PROMISE_FIELDS = ["statement", "why", "term", "aliases"];
|
|
7714
8310
|
});
|
|
7715
8311
|
|
|
7716
8312
|
// src/commands/amend.ts
|
|
@@ -7721,9 +8317,9 @@ __export(exports_amend, {
|
|
|
7721
8317
|
import { defineCommand as defineCommand18 } from "citty";
|
|
7722
8318
|
function assertAmendFlags(args) {
|
|
7723
8319
|
const hasIssue = args.issue !== undefined && args.issue.trim() !== "";
|
|
7724
|
-
const hasField = args.text !== undefined || args.why !== undefined || args.lives !== undefined || args.term !== undefined || args.aliases !== undefined;
|
|
8320
|
+
const hasField = args.text !== undefined || args.why !== undefined || args.lives !== undefined || args.relates !== undefined || args.term !== undefined || args.aliases !== undefined;
|
|
7725
8321
|
if (!hasField && !hasIssue) {
|
|
7726
|
-
console.error("spec amend: nothing to amend \u2014 pass at least one of --text / --why / --lives / --term / --aliases");
|
|
8322
|
+
console.error("spec amend: nothing to amend \u2014 pass at least one of --text / --why / --lives / --relates / --term / --aliases");
|
|
7727
8323
|
process.exit(EXIT.USAGE);
|
|
7728
8324
|
}
|
|
7729
8325
|
if (args.text !== undefined && args.text.trim() === "") {
|
|
@@ -7750,6 +8346,9 @@ function fieldsFromArgs(args, rawArgs) {
|
|
|
7750
8346
|
const livesIn = listFlag(rawArgs, "lives", args.lives);
|
|
7751
8347
|
if (livesIn !== undefined)
|
|
7752
8348
|
fields.livesIn = livesIn;
|
|
8349
|
+
const relates = listFlag(rawArgs, "relates", args.relates);
|
|
8350
|
+
if (relates !== undefined)
|
|
8351
|
+
fields.relates = relates;
|
|
7753
8352
|
if (args.term !== undefined)
|
|
7754
8353
|
fields.term = args.term.trim();
|
|
7755
8354
|
if (args.issue !== undefined && args.issue.trim() !== "")
|
|
@@ -7762,7 +8361,6 @@ var amendArgs, amendCommand;
|
|
|
7762
8361
|
var init_amend2 = __esm(() => {
|
|
7763
8362
|
init_constants();
|
|
7764
8363
|
init_discover();
|
|
7765
|
-
init__index();
|
|
7766
8364
|
init_amend();
|
|
7767
8365
|
init_grammar3();
|
|
7768
8366
|
init__args();
|
|
@@ -7777,6 +8375,7 @@ var init_amend2 = __esm(() => {
|
|
|
7777
8375
|
text: { type: "string", description: "New Requirement (statement) field value" },
|
|
7778
8376
|
why: { type: "string", description: "New Why it matters field value" },
|
|
7779
8377
|
lives: livesArg,
|
|
8378
|
+
relates: relatesArg,
|
|
7780
8379
|
issue: {
|
|
7781
8380
|
type: "string",
|
|
7782
8381
|
description: "Ticket that motivated this amendment \u2014 recorded as amends-via provenance (opaque, never a requirement id)"
|
|
@@ -7791,7 +8390,7 @@ var init_amend2 = __esm(() => {
|
|
|
7791
8390
|
amendCommand = defineCommand18({
|
|
7792
8391
|
meta: {
|
|
7793
8392
|
name: "amend",
|
|
7794
|
-
description: "Revise
|
|
8393
|
+
description: "Revise a requirement's fields in place (same id, no version bump). A released requirement's statement and why are superseded instead; --lives and --relates still amend."
|
|
7795
8394
|
},
|
|
7796
8395
|
args: amendArgs,
|
|
7797
8396
|
async run({ args, rawArgs }) {
|
|
@@ -7808,7 +8407,7 @@ var init_amend2 = __esm(() => {
|
|
|
7808
8407
|
handleNotAPlatform(e);
|
|
7809
8408
|
}
|
|
7810
8409
|
const fields = fieldsFromArgs(args, rawArgs);
|
|
7811
|
-
const result = await amend({ platformDir, id, fields }
|
|
8410
|
+
const result = await amend({ platformDir, id, fields });
|
|
7812
8411
|
if (!result.ok)
|
|
7813
8412
|
exitOnFailure("spec amend", result);
|
|
7814
8413
|
printWarnings("spec amend", result.warnings);
|
|
@@ -7930,13 +8529,27 @@ Target domain: ${opts.domain} (no charter/scope recorded \u2014 check placement
|
|
|
7930
8529
|
|
|
7931
8530
|
// src/server/mcp.ts
|
|
7932
8531
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7933
|
-
import { z as
|
|
8532
|
+
import { z as z4 } from "zod";
|
|
7934
8533
|
function jsonResult(value) {
|
|
7935
8534
|
return { content: [{ type: "text", text: JSON.stringify(value) }] };
|
|
7936
8535
|
}
|
|
7937
8536
|
function errorResult(message) {
|
|
7938
8537
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
7939
8538
|
}
|
|
8539
|
+
function amendFieldsOf(args) {
|
|
8540
|
+
const fields = {};
|
|
8541
|
+
if (args.text !== undefined)
|
|
8542
|
+
fields.statement = args.text;
|
|
8543
|
+
if (args.why !== undefined)
|
|
8544
|
+
fields.why = args.why || null;
|
|
8545
|
+
if (args.lives !== undefined)
|
|
8546
|
+
fields.livesIn = args.lives;
|
|
8547
|
+
if (args.relates !== undefined)
|
|
8548
|
+
fields.relates = args.relates;
|
|
8549
|
+
if (args.issue)
|
|
8550
|
+
fields.issue = args.issue;
|
|
8551
|
+
return fields;
|
|
8552
|
+
}
|
|
7940
8553
|
function buildMcpServer(platformDir) {
|
|
7941
8554
|
const server = new McpServer({ name: "spec", version: package_default.version });
|
|
7942
8555
|
const fresh = (fn) => withIndex({ platformDir, build: "fresh" }, (h) => fn(h.storage));
|
|
@@ -7944,8 +8557,8 @@ function buildMcpServer(platformDir) {
|
|
|
7944
8557
|
title: "Full-text requirement retrieval",
|
|
7945
8558
|
description: "Search requirements by full text (SQLite FTS5 MATCH syntax; wrap phrases in double quotes). Returns ranked hits \u2014 use this to load the requirements a task touches instead of re-asking the user.",
|
|
7946
8559
|
inputSchema: {
|
|
7947
|
-
text:
|
|
7948
|
-
limit:
|
|
8560
|
+
text: z4.string().min(1).describe("FTS5 query text"),
|
|
8561
|
+
limit: z4.number().int().min(1).max(LIMIT_MAX).optional().describe(`Max hits (default ${DEFAULT_QUERY_LIMIT})`)
|
|
7949
8562
|
}
|
|
7950
8563
|
}, async ({ text, limit }) => fresh((s) => {
|
|
7951
8564
|
const r = query(s, text, limit ?? DEFAULT_QUERY_LIMIT);
|
|
@@ -7955,13 +8568,13 @@ function buildMcpServer(platformDir) {
|
|
|
7955
8568
|
title: "Files \u2192 requirements",
|
|
7956
8569
|
description: "The requirements tagged in the given files (platform-relative paths, e.g. api/src/renew.ts). Load these before changing the files.",
|
|
7957
8570
|
inputSchema: {
|
|
7958
|
-
files:
|
|
8571
|
+
files: z4.array(z4.string().min(1)).min(1).max(100).describe("Platform-relative file paths")
|
|
7959
8572
|
}
|
|
7960
8573
|
}, async ({ files }) => fresh((s) => jsonResult(resolveFiles(s, files).rows)));
|
|
7961
8574
|
server.registerTool("spec_req_tags", {
|
|
7962
8575
|
title: "Requirement \u2192 tag sites (reverse query)",
|
|
7963
8576
|
description: "Every code/test/doc site tagging the given requirement, across all repos \u2014 impact analysis and retag worklists.",
|
|
7964
|
-
inputSchema: { req_id:
|
|
8577
|
+
inputSchema: { req_id: z4.string().describe("Requirement id (KEY-NNN)") }
|
|
7965
8578
|
}, async ({ req_id }) => {
|
|
7966
8579
|
if (!ID_RE.test(req_id)) {
|
|
7967
8580
|
return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
|
|
@@ -7984,7 +8597,7 @@ function buildMcpServer(platformDir) {
|
|
|
7984
8597
|
server.registerTool("spec_propagation", {
|
|
7985
8598
|
title: "Cross-repo propagation for a superseded requirement",
|
|
7986
8599
|
description: "Per member repo: migrated to the successor (verified or not), still on the predecessor, or unrelated \u2014 plus the drift flag.",
|
|
7987
|
-
inputSchema: { req_id:
|
|
8600
|
+
inputSchema: { req_id: z4.string().describe("Requirement id (KEY-NNN)") }
|
|
7988
8601
|
}, async ({ req_id }) => {
|
|
7989
8602
|
if (!ID_RE.test(req_id)) {
|
|
7990
8603
|
return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
|
|
@@ -7994,7 +8607,7 @@ function buildMcpServer(platformDir) {
|
|
|
7994
8607
|
server.registerTool("spec_next_id", {
|
|
7995
8608
|
title: "Next unused requirement id",
|
|
7996
8609
|
description: "Allocate-preview the next unused id in a domain (filesystem-derived; nothing is written). Use before authoring a new requirement.",
|
|
7997
|
-
inputSchema: { domain:
|
|
8610
|
+
inputSchema: { domain: z4.string().min(1).describe("Domain key (e.g. BILLING)") }
|
|
7998
8611
|
}, async ({ domain: domain2 }) => {
|
|
7999
8612
|
const r = await nextId(platformDir, domain2);
|
|
8000
8613
|
return r.ok ? jsonResult({ domain: r.key, next_id: r.nextId }) : errorResult(r.detail);
|
|
@@ -8002,7 +8615,7 @@ function buildMcpServer(platformDir) {
|
|
|
8002
8615
|
server.registerTool("spec_get", {
|
|
8003
8616
|
title: "One requirement's full record",
|
|
8004
8617
|
description: "The full record for one requirement id: status, key, seq, statement, why, versions, and source location. An unknown id returns [] (the `spec get --json` shape).",
|
|
8005
|
-
inputSchema: { req_id:
|
|
8618
|
+
inputSchema: { req_id: z4.string().describe("Requirement id (KEY-NNN)") }
|
|
8006
8619
|
}, async ({ req_id }) => {
|
|
8007
8620
|
if (!ID_RE.test(req_id)) {
|
|
8008
8621
|
return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
|
|
@@ -8016,8 +8629,8 @@ function buildMcpServer(platformDir) {
|
|
|
8016
8629
|
title: "Every requirement record",
|
|
8017
8630
|
description: "Every requirement record in (key, seq) order, superseded and deprecated history included. Optional domain and status filters. The `spec list --json` shape.",
|
|
8018
8631
|
inputSchema: {
|
|
8019
|
-
domain:
|
|
8020
|
-
status:
|
|
8632
|
+
domain: z4.string().optional().describe("Only this domain key (e.g. BILLING)"),
|
|
8633
|
+
status: z4.string().optional().describe(`Only this status: ${STATUS_WORDS.join(" | ")}`)
|
|
8021
8634
|
}
|
|
8022
8635
|
}, async ({ domain: domain2, status }) => {
|
|
8023
8636
|
const filter = {};
|
|
@@ -8036,17 +8649,18 @@ function buildMcpServer(platformDir) {
|
|
|
8036
8649
|
title: "Supersede a shipped requirement",
|
|
8037
8650
|
description: "Flip an Active requirement to superseded, mint its successor in the same domain, and return the retag worklist (the tag sites spec check reports as SUPERSEDED_REFERENCED until retagged). The same JSON as `spec supersede --json`.",
|
|
8038
8651
|
inputSchema: {
|
|
8039
|
-
req_id:
|
|
8040
|
-
statement:
|
|
8041
|
-
why:
|
|
8042
|
-
lives_in:
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8652
|
+
req_id: z4.string().describe("The Active requirement id to supersede (KEY-NNN)"),
|
|
8653
|
+
statement: z4.string().min(1).describe("The successor's statement"),
|
|
8654
|
+
why: z4.string().optional().describe("Successor's why (default: copied from the predecessor)"),
|
|
8655
|
+
lives_in: z4.array(z4.string()).optional().describe("Successor's livesIn paths (default: copied from the predecessor)"),
|
|
8656
|
+
relates: z4.array(z4.string()).optional().describe("Successor's related ids, each comma-split; an empty list clears (default: copied from the predecessor)"),
|
|
8657
|
+
issue: z4.string().optional().describe("Ticket recorded as supersedes-via on the predecessor and created on the successor")
|
|
8658
|
+
}
|
|
8659
|
+
}, async ({ req_id, statement, why, lives_in, relates, issue }) => {
|
|
8046
8660
|
if (!ID_RE.test(req_id)) {
|
|
8047
8661
|
return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
|
|
8048
8662
|
}
|
|
8049
|
-
const r = await supersede({ platformDir, id: req_id, statement, why, livesIn: lives_in, issue }, coldFreshTags(platformDir));
|
|
8663
|
+
const r = await supersede({ platformDir, id: req_id, statement, why, livesIn: lives_in, relates, issue }, coldFreshTags(platformDir));
|
|
8050
8664
|
if (!r.ok)
|
|
8051
8665
|
return errorResult(r.detail);
|
|
8052
8666
|
for (const w of r.warnings)
|
|
@@ -8063,8 +8677,8 @@ function buildMcpServer(platformDir) {
|
|
|
8063
8677
|
title: "Deprecate a requirement",
|
|
8064
8678
|
description: "Mark an Active or Draft requirement deprecated with a recorded reason and return the code tags still bound to it (each a DEPRECATED_REFERENCED error in spec check until removed). The same JSON as `spec deprecate --json`.",
|
|
8065
8679
|
inputSchema: {
|
|
8066
|
-
req_id:
|
|
8067
|
-
reason:
|
|
8680
|
+
req_id: z4.string().describe("The requirement id to deprecate (KEY-NNN)"),
|
|
8681
|
+
reason: z4.string().min(1).describe("Why the requirement ended; the durable record")
|
|
8068
8682
|
}
|
|
8069
8683
|
}, async ({ req_id, reason }) => {
|
|
8070
8684
|
if (!ID_RE.test(req_id)) {
|
|
@@ -8080,21 +8694,109 @@ function buildMcpServer(platformDir) {
|
|
|
8080
8694
|
title: "Accept a draft requirement",
|
|
8081
8695
|
description: "Promote a Draft requirement to Active, leaving every other field untouched. The same JSON as `spec accept --json`.",
|
|
8082
8696
|
inputSchema: {
|
|
8083
|
-
req_id:
|
|
8697
|
+
req_id: z4.string().describe("The Draft requirement id to accept (KEY-NNN)")
|
|
8698
|
+
}
|
|
8699
|
+
}, async ({ req_id }) => {
|
|
8700
|
+
if (!ID_RE.test(req_id)) {
|
|
8701
|
+
return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
|
|
8702
|
+
}
|
|
8703
|
+
const r = await accept({ platformDir, id: req_id });
|
|
8704
|
+
return r.ok ? jsonResult({ id: r.id, file: r.file }) : errorResult(r.detail);
|
|
8705
|
+
});
|
|
8706
|
+
server.registerTool("spec_req", {
|
|
8707
|
+
title: "Author a requirement",
|
|
8708
|
+
description: "Append a new requirement to a domain (Active, or Draft with draft: true). The same JSON as `spec req --text \u2026 --json`: { id, file }, plus clauses when the statement parses as EARS.",
|
|
8709
|
+
inputSchema: {
|
|
8710
|
+
domain: z4.string().min(1).describe("Domain key or unique case-insensitive prefix"),
|
|
8711
|
+
text: z4.string().trim().min(1).describe("The requirement statement"),
|
|
8712
|
+
why: z4.string().trim().default("").describe("What breaks without it"),
|
|
8713
|
+
lives: z4.array(z4.string()).default([]).describe("livesIn paths"),
|
|
8714
|
+
relates: z4.array(z4.string()).default([]).describe("Related requirement ids, each comma-split"),
|
|
8715
|
+
issue: z4.string().trim().optional().describe("Originating ticket, recorded as created"),
|
|
8716
|
+
draft: z4.boolean().default(false).describe("Author as Draft instead of Active")
|
|
8717
|
+
}
|
|
8718
|
+
}, async ({ domain: domain2, text, why, lives, relates, issue, draft }) => {
|
|
8719
|
+
const allocated = await nextId(platformDir, domain2);
|
|
8720
|
+
if (!allocated.ok)
|
|
8721
|
+
return errorResult(allocated.detail);
|
|
8722
|
+
const r = await mint({
|
|
8723
|
+
platformDir,
|
|
8724
|
+
key: allocated.key,
|
|
8725
|
+
statement: text,
|
|
8726
|
+
why,
|
|
8727
|
+
livesIn: lives,
|
|
8728
|
+
relates,
|
|
8729
|
+
issue: issue || undefined,
|
|
8730
|
+
status: draft ? "draft" : "active"
|
|
8731
|
+
});
|
|
8732
|
+
if (!r.ok)
|
|
8733
|
+
return errorResult(r.detail);
|
|
8734
|
+
for (const w of r.warnings)
|
|
8735
|
+
console.error(`spec_req: ${w.text}`);
|
|
8736
|
+
return jsonResult(r.clauses ? { id: r.id, file: r.file, clauses: r.clauses } : { id: r.id, file: r.file });
|
|
8737
|
+
});
|
|
8738
|
+
server.registerTool("spec_amend", {
|
|
8739
|
+
title: "Amend a requirement in place",
|
|
8740
|
+
description: "Revise an Active or Draft requirement's fields in place (same id, no version change); a shipped requirement's statement and why are superseded instead. The same JSON as `spec amend --json`.",
|
|
8741
|
+
inputSchema: {
|
|
8742
|
+
id: z4.string().describe("The requirement id to amend (KEY-NNN)"),
|
|
8743
|
+
text: z4.string().trim().min(1).optional().describe("New statement"),
|
|
8744
|
+
why: z4.string().trim().optional().describe("New why; empty clears it"),
|
|
8745
|
+
lives: z4.array(z4.string()).optional().describe("Replacement livesIn list; empty clears"),
|
|
8746
|
+
relates: z4.array(z4.string()).optional().describe("Replacement related ids, each comma-split; empty clears"),
|
|
8747
|
+
issue: z4.string().trim().optional().describe("Ticket recorded as amends-via")
|
|
8748
|
+
}
|
|
8749
|
+
}, async ({ id, text, why, lives, relates, issue }) => {
|
|
8750
|
+
if (!ID_RE.test(id))
|
|
8751
|
+
return errorResult(`id must be a requirement id (KEY-NNN); got ${id}`);
|
|
8752
|
+
const fields = amendFieldsOf({ text, why, lives, relates, issue });
|
|
8753
|
+
if (Object.keys(fields).length === 0) {
|
|
8754
|
+
return errorResult("nothing to amend \u2014 pass at least one of text, why, lives, relates");
|
|
8084
8755
|
}
|
|
8085
|
-
|
|
8086
|
-
if (!
|
|
8087
|
-
return errorResult(
|
|
8756
|
+
const r = await amend({ platformDir, id, fields });
|
|
8757
|
+
if (!r.ok)
|
|
8758
|
+
return errorResult(r.detail);
|
|
8759
|
+
for (const w of r.warnings)
|
|
8760
|
+
console.error(`spec_amend: ${w.text}`);
|
|
8761
|
+
return jsonResult({ id, file: r.file, fields_changed: r.fieldsChanged });
|
|
8762
|
+
});
|
|
8763
|
+
server.registerTool("spec_move", {
|
|
8764
|
+
title: "Move a requirement to another domain",
|
|
8765
|
+
description: "Mint the successor in newDomain carrying the source's fields, mark the source superseded, and return the retag worklist. The same JSON as `spec move --json`.",
|
|
8766
|
+
inputSchema: {
|
|
8767
|
+
id: z4.string().describe("The Active requirement id to move (KEY-NNN)"),
|
|
8768
|
+
newDomain: z4.string().min(1).describe("The existing target domain key"),
|
|
8769
|
+
text: z4.string().optional().describe("Successor statement (default: copied)"),
|
|
8770
|
+
why: z4.string().optional().describe("Successor why (default: copied)"),
|
|
8771
|
+
lives: z4.array(z4.string()).optional().describe("Successor livesIn list; empty clears (default: copied)")
|
|
8088
8772
|
}
|
|
8089
|
-
|
|
8090
|
-
|
|
8773
|
+
}, async ({ id, newDomain: newDomain2, text, why, lives }) => {
|
|
8774
|
+
if (!ID_RE.test(id))
|
|
8775
|
+
return errorResult(`id must be a requirement id (KEY-NNN); got ${id}`);
|
|
8776
|
+
const targetKey = normalizeDomainKey(newDomain2);
|
|
8777
|
+
if (targetKey === "")
|
|
8778
|
+
return errorResult(`newDomain must be a domain key; got ${newDomain2}`);
|
|
8779
|
+
const r = await move({ platformDir, id, targetKey, statement: text, why, livesIn: lives }, coldFreshTags(platformDir));
|
|
8780
|
+
if (!r.ok)
|
|
8781
|
+
return errorResult(r.detail);
|
|
8782
|
+
for (const w of r.warnings)
|
|
8783
|
+
console.error(`spec_move: ${w.text}`);
|
|
8784
|
+
return jsonResult({
|
|
8785
|
+
old_id: r.oldId,
|
|
8786
|
+
new_id: r.newId,
|
|
8787
|
+
from_file: r.fromFile,
|
|
8788
|
+
to_file: r.toFile,
|
|
8789
|
+
source_spec_version: r.sourceSpecVersion,
|
|
8790
|
+
target_spec_version: r.targetSpecVersion,
|
|
8791
|
+
retag: r.retag
|
|
8792
|
+
});
|
|
8091
8793
|
});
|
|
8092
8794
|
server.registerPrompt("author_requirements", {
|
|
8093
8795
|
title: "Author requirements from a brief",
|
|
8094
8796
|
description: "Turn a vague brief/ticket into well-formed Spec Engine requirements: one requirement per testable promise, placed by domain charter, deduped via spec query, drafted to the GUARD template + cold-read rubric, minted with spec req. The engine returns a static playbook template \u2014 your model runs it.",
|
|
8095
8797
|
argsSchema: {
|
|
8096
|
-
brief:
|
|
8097
|
-
domain:
|
|
8798
|
+
brief: z4.string().min(1).describe("The vague brief / ticket text to author from"),
|
|
8799
|
+
domain: z4.string().optional().describe("Optional target domain KEY \u2014 its charter/scope is injected when given")
|
|
8098
8800
|
}
|
|
8099
8801
|
}, async ({ brief, domain: domain2 }) => {
|
|
8100
8802
|
let resolvedDomain;
|
|
@@ -8125,8 +8827,11 @@ var init_mcp = __esm(() => {
|
|
|
8125
8827
|
init_domains();
|
|
8126
8828
|
init__index();
|
|
8127
8829
|
init_accept();
|
|
8830
|
+
init_amend();
|
|
8128
8831
|
init_check();
|
|
8129
8832
|
init_deprecate();
|
|
8833
|
+
init_mint();
|
|
8834
|
+
init_move();
|
|
8130
8835
|
init_nextId();
|
|
8131
8836
|
init_reads2();
|
|
8132
8837
|
init_records();
|
|
@@ -8175,7 +8880,7 @@ __export(exports_resolve, {
|
|
|
8175
8880
|
resolveCommand: () => resolveCommand
|
|
8176
8881
|
});
|
|
8177
8882
|
import { statSync as statSync5 } from "fs";
|
|
8178
|
-
import { join as
|
|
8883
|
+
import { join as join14, relative as relative2, resolve as resolve13, sep as sep3 } from "path";
|
|
8179
8884
|
import { defineCommand as defineCommand21 } from "citty";
|
|
8180
8885
|
function classifyArg(a) {
|
|
8181
8886
|
if (a.startsWith("--")) {
|
|
@@ -8214,7 +8919,7 @@ function splitFilesAndPlatformDir(positionals, fallbackPlatformDir) {
|
|
|
8214
8919
|
const resolved = resolve13(last);
|
|
8215
8920
|
if (statSync5(resolved).isDirectory()) {
|
|
8216
8921
|
try {
|
|
8217
|
-
if (statSync5(
|
|
8922
|
+
if (statSync5(join14(resolved, "spec-engine")).isDirectory()) {
|
|
8218
8923
|
lastIsPlatformDir = true;
|
|
8219
8924
|
} else {
|
|
8220
8925
|
lastIsDirWithoutSpecEngine = true;
|
|
@@ -8254,7 +8959,7 @@ async function runReverseQuery(opts) {
|
|
|
8254
8959
|
if (outArg2)
|
|
8255
8960
|
assertContainedPath(dbPath, platformDir, "spec resolve: --out");
|
|
8256
8961
|
await maybePromptForOnboarding({ platformDir, args: { noPrompt } });
|
|
8257
|
-
await withReadStorage({ platformDir, dbPath, fresh }, (storage2) => {
|
|
8962
|
+
await withReadStorage({ platformDir, dbPath, fresh, json }, (storage2) => {
|
|
8258
8963
|
const { rows, known } = reqTags(storage2, reqId);
|
|
8259
8964
|
if (rows.length === 0) {
|
|
8260
8965
|
if (!known) {
|
|
@@ -8343,7 +9048,7 @@ var init_resolve2 = __esm(() => {
|
|
|
8343
9048
|
},
|
|
8344
9049
|
req: {
|
|
8345
9050
|
type: "string",
|
|
8346
|
-
description: "Reverse query
|
|
9051
|
+
description: "Reverse query: list every tag site (repo, file, line, kind, level) for the given requirement id instead of resolving files. Takes no file positionals."
|
|
8347
9052
|
},
|
|
8348
9053
|
out: {
|
|
8349
9054
|
type: "string",
|
|
@@ -8365,7 +9070,7 @@ var init_resolve2 = __esm(() => {
|
|
|
8365
9070
|
resolveCommand = defineCommand21({
|
|
8366
9071
|
meta: {
|
|
8367
9072
|
name: "resolve",
|
|
8368
|
-
description: "Return the requirements tagged in the given files
|
|
9073
|
+
description: "Return the requirements tagged in the given files. Accepts multiple positional file paths (e.g. `spec resolve api/src/renew.ts api/src/charge.ts`) and a comma-split fallback inside a single positional (`spec resolve a.ts,b.ts`). Paths may be platform-relative or absolute under the platform tree."
|
|
8369
9074
|
},
|
|
8370
9075
|
args: resolveArgs,
|
|
8371
9076
|
async run({ args, rawArgs }) {
|
|
@@ -8398,7 +9103,7 @@ var init_resolve2 = __esm(() => {
|
|
|
8398
9103
|
noPrompt: args.noPrompt
|
|
8399
9104
|
}
|
|
8400
9105
|
});
|
|
8401
|
-
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh }, (storage2) => {
|
|
9106
|
+
await withReadStorage({ platformDir, dbPath, fresh: !!args.fresh, json: !!args.json }, (storage2) => {
|
|
8402
9107
|
const rows = storage2.resolveByFiles(normalized);
|
|
8403
9108
|
if (!args.json && rows.length === 0 && storage2.listRequirements().length === 0) {
|
|
8404
9109
|
console.error(formatNoRequirementsIndexed(platformDir));
|
|
@@ -8525,8 +9230,8 @@ var exports_gate = {};
|
|
|
8525
9230
|
__export(exports_gate, {
|
|
8526
9231
|
gateCommand: () => gateCommand
|
|
8527
9232
|
});
|
|
8528
|
-
import { existsSync as
|
|
8529
|
-
import { join as
|
|
9233
|
+
import { existsSync as existsSync13 } from "fs";
|
|
9234
|
+
import { join as join15 } from "path";
|
|
8530
9235
|
import { defineCommand as defineCommand22 } from "citty";
|
|
8531
9236
|
function isEpipe(e) {
|
|
8532
9237
|
if (e === null || typeof e !== "object" || !("code" in e))
|
|
@@ -8550,7 +9255,7 @@ function resolveGateArgs(args) {
|
|
|
8550
9255
|
process.exit(EXIT.USAGE);
|
|
8551
9256
|
}
|
|
8552
9257
|
const platformDir = resolvePlatformDir(args);
|
|
8553
|
-
if (!
|
|
9258
|
+
if (!existsSync13(join15(platformDir, "spec-engine"))) {
|
|
8554
9259
|
console.error(formatNotASpecPlatform(platformDir));
|
|
8555
9260
|
process.exit(EXIT.USAGE);
|
|
8556
9261
|
}
|
|
@@ -8623,6 +9328,7 @@ var init_gate2 = __esm(() => {
|
|
|
8623
9328
|
writeStdoutTolerant(`build_id: ${gated.buildId}`);
|
|
8624
9329
|
}
|
|
8625
9330
|
} catch (e) {
|
|
9331
|
+
handleInvalidDomainFiles(e, Boolean(args.json));
|
|
8626
9332
|
const msg = e instanceof Error ? e.message : String(e);
|
|
8627
9333
|
console.error(`spec gate: crashed: ${msg}`);
|
|
8628
9334
|
process.exit(EXIT.USAGE);
|
|
@@ -8708,205 +9414,6 @@ function renderGuard(losses, mode, ref) {
|
|
|
8708
9414
|
}
|
|
8709
9415
|
var STOP = "\uD83D\uDED1";
|
|
8710
9416
|
|
|
8711
|
-
// src/guard/collect.ts
|
|
8712
|
-
import { join as join14 } from "path";
|
|
8713
|
-
function isIgnored(path) {
|
|
8714
|
-
return isPathIgnored(path);
|
|
8715
|
-
}
|
|
8716
|
-
function isSpecFile(path) {
|
|
8717
|
-
return path.startsWith(`${CANONICAL_SPECS_DIR}/`) && path.endsWith("/SPEC.json") && !isIgnored(path);
|
|
8718
|
-
}
|
|
8719
|
-
function isCodeFile(path) {
|
|
8720
|
-
if (isSpecFile(path) || isIgnored(path))
|
|
8721
|
-
return false;
|
|
8722
|
-
const ext = path.includes(".") ? path.split(".").pop() ?? "" : "";
|
|
8723
|
-
return CODE_EXTS.has(ext);
|
|
8724
|
-
}
|
|
8725
|
-
function parseReqs(text) {
|
|
8726
|
-
const v = parseDomainText(text, "");
|
|
8727
|
-
return v.ok ? v.data.requirements : [];
|
|
8728
|
-
}
|
|
8729
|
-
function collectBaseReqs2(platformDir, ref, specPaths2) {
|
|
8730
|
-
const reqs = [];
|
|
8731
|
-
const path = new Map;
|
|
8732
|
-
for (const p of specPaths2) {
|
|
8733
|
-
const bytes = gitShow(platformDir, ref, p);
|
|
8734
|
-
if (bytes === null)
|
|
8735
|
-
continue;
|
|
8736
|
-
for (const r of parseReqs(bytes)) {
|
|
8737
|
-
reqs.push(r);
|
|
8738
|
-
path.set(r.id, p);
|
|
8739
|
-
}
|
|
8740
|
-
}
|
|
8741
|
-
return { reqs, path };
|
|
8742
|
-
}
|
|
8743
|
-
function collectBaseTags(platformDir, ref, codePaths) {
|
|
8744
|
-
const implSite = new Map;
|
|
8745
|
-
const verifySite = new Map;
|
|
8746
|
-
for (const p of codePaths) {
|
|
8747
|
-
const bytes = gitShow(platformDir, ref, p);
|
|
8748
|
-
if (bytes === null)
|
|
8749
|
-
continue;
|
|
8750
|
-
for (const tag of scanTagsInFile("", p, bytes)) {
|
|
8751
|
-
const target = tag.kind === "verifies" ? verifySite : implSite;
|
|
8752
|
-
if (!target.has(tag.req_id))
|
|
8753
|
-
target.set(tag.req_id, { file: tag.file, line: tag.line });
|
|
8754
|
-
}
|
|
8755
|
-
}
|
|
8756
|
-
return { implSite, verifySite };
|
|
8757
|
-
}
|
|
8758
|
-
async function collectWorktreeReqs(canonicalDir) {
|
|
8759
|
-
const ids = new Set;
|
|
8760
|
-
const activeIds = new Set;
|
|
8761
|
-
const supersedesTargets = new Set;
|
|
8762
|
-
for (const rel of await findDomainJsonFiles(canonicalDir)) {
|
|
8763
|
-
const text = await Bun.file(join14(canonicalDir, rel)).text();
|
|
8764
|
-
for (const r of parseReqs(text)) {
|
|
8765
|
-
ids.add(r.id);
|
|
8766
|
-
if (r.status.toLowerCase() === "active")
|
|
8767
|
-
activeIds.add(r.id);
|
|
8768
|
-
if (r.supersedes != null)
|
|
8769
|
-
supersedesTargets.add(r.supersedes);
|
|
8770
|
-
}
|
|
8771
|
-
}
|
|
8772
|
-
return { ids, activeIds, supersedesTargets };
|
|
8773
|
-
}
|
|
8774
|
-
function tallyWorktreeTags(storage2) {
|
|
8775
|
-
const implCount = new Map;
|
|
8776
|
-
const verifyCount = new Map;
|
|
8777
|
-
for (const t of storage2.listTags()) {
|
|
8778
|
-
const bucket = t.kind === "implements" ? implCount : t.kind === "verifies" ? verifyCount : null;
|
|
8779
|
-
if (bucket !== null)
|
|
8780
|
-
bucket.set(t.req_id, (bucket.get(t.req_id) ?? 0) + 1);
|
|
8781
|
-
}
|
|
8782
|
-
return { implCount, verifyCount };
|
|
8783
|
-
}
|
|
8784
|
-
async function collectFacts(platformDir, ref, storage2) {
|
|
8785
|
-
const changed = gitDiffNameStatus(platformDir, ref);
|
|
8786
|
-
const specPaths2 = changed.filter((e) => isSpecFile(e.path)).map((e) => e.path);
|
|
8787
|
-
const codePaths = changed.filter((e) => isCodeFile(e.path)).map((e) => e.path);
|
|
8788
|
-
const deletedSpecFiles = changed.filter((e) => e.status === "D" && isSpecFile(e.path)).map((e) => e.path);
|
|
8789
|
-
const base = collectBaseReqs2(platformDir, ref, specPaths2);
|
|
8790
|
-
const baseTags = collectBaseTags(platformDir, ref, codePaths);
|
|
8791
|
-
const worktree = await collectWorktreeReqs(join14(platformDir, CANONICAL_SPECS_DIR));
|
|
8792
|
-
const counts = tallyWorktreeTags(storage2);
|
|
8793
|
-
return {
|
|
8794
|
-
baseReqs: base.reqs,
|
|
8795
|
-
baseReqPath: base.path,
|
|
8796
|
-
baseImplSite: baseTags.implSite,
|
|
8797
|
-
baseVerifySite: baseTags.verifySite,
|
|
8798
|
-
worktreeReqIds: worktree.ids,
|
|
8799
|
-
worktreeActiveIds: worktree.activeIds,
|
|
8800
|
-
worktreeSupersedesTargets: worktree.supersedesTargets,
|
|
8801
|
-
worktreeImplCount: counts.implCount,
|
|
8802
|
-
worktreeVerifyCount: counts.verifyCount,
|
|
8803
|
-
deletedSpecFiles
|
|
8804
|
-
};
|
|
8805
|
-
}
|
|
8806
|
-
var CODE_EXTS;
|
|
8807
|
-
var init_collect = __esm(() => {
|
|
8808
|
-
init_src();
|
|
8809
|
-
init_constants();
|
|
8810
|
-
init_fs();
|
|
8811
|
-
init_tags();
|
|
8812
|
-
CODE_EXTS = new Set(DEFAULT_EXTS);
|
|
8813
|
-
});
|
|
8814
|
-
|
|
8815
|
-
// src/guard/losses.ts
|
|
8816
|
-
function isActive(status) {
|
|
8817
|
-
return status.toLowerCase() === "active";
|
|
8818
|
-
}
|
|
8819
|
-
function isSuperseded(base, f) {
|
|
8820
|
-
const forward = base.supersededBy != null && f.worktreeReqIds.has(base.supersededBy);
|
|
8821
|
-
return forward || f.worktreeSupersedesTargets.has(base.id);
|
|
8822
|
-
}
|
|
8823
|
-
function reqDeletedLoss(base, f) {
|
|
8824
|
-
return {
|
|
8825
|
-
kind: "REQUIREMENT_REMOVED",
|
|
8826
|
-
req_id: base.id,
|
|
8827
|
-
file: f.baseReqPath.get(base.id) ?? "",
|
|
8828
|
-
line: 0,
|
|
8829
|
-
detail: `${base.id} was Active at the ref but is absent from the working-tree spec and was not superseded \u2014 a requirement is never deleted`
|
|
8830
|
-
};
|
|
8831
|
-
}
|
|
8832
|
-
function historyRemovedLoss(base, f) {
|
|
8833
|
-
const display = base.status.charAt(0).toUpperCase() + base.status.slice(1).toLowerCase();
|
|
8834
|
-
return {
|
|
8835
|
-
kind: "REQUIREMENT_REMOVED",
|
|
8836
|
-
req_id: base.id,
|
|
8837
|
-
file: f.baseReqPath.get(base.id) ?? "",
|
|
8838
|
-
line: 0,
|
|
8839
|
-
detail: `${base.id} was ${display} at the ref but is absent from the working-tree spec \u2014 a requirement is never deleted, and its history is the record`
|
|
8840
|
-
};
|
|
8841
|
-
}
|
|
8842
|
-
function implLostLoss(id, site) {
|
|
8843
|
-
return {
|
|
8844
|
-
kind: "IMPL_LOST",
|
|
8845
|
-
req_id: id,
|
|
8846
|
-
file: site.file,
|
|
8847
|
-
line: site.line,
|
|
8848
|
-
detail: `${id} lost its last implementing @spec tag (was ${site.file}:${site.line})`
|
|
8849
|
-
};
|
|
8850
|
-
}
|
|
8851
|
-
function verifyLostLoss(id, site) {
|
|
8852
|
-
return {
|
|
8853
|
-
kind: "VERIFY_LOST",
|
|
8854
|
-
req_id: id,
|
|
8855
|
-
file: site.file,
|
|
8856
|
-
line: site.line,
|
|
8857
|
-
detail: `${id} lost its last verifying @spec tag (was ${site.file}:${site.line})`
|
|
8858
|
-
};
|
|
8859
|
-
}
|
|
8860
|
-
function specFileDeletedLoss(path) {
|
|
8861
|
-
return {
|
|
8862
|
-
kind: "SPEC_FILE_DELETED",
|
|
8863
|
-
req_id: null,
|
|
8864
|
-
file: path,
|
|
8865
|
-
line: 0,
|
|
8866
|
-
detail: `canonical spec file ${path} present at the ref is absent from the working tree`
|
|
8867
|
-
};
|
|
8868
|
-
}
|
|
8869
|
-
function reqDeletedFor(base, f) {
|
|
8870
|
-
if (f.worktreeReqIds.has(base.id))
|
|
8871
|
-
return null;
|
|
8872
|
-
if (!isActive(base.status))
|
|
8873
|
-
return historyRemovedLoss(base, f);
|
|
8874
|
-
if (isSuperseded(base, f))
|
|
8875
|
-
return null;
|
|
8876
|
-
return reqDeletedLoss(base, f);
|
|
8877
|
-
}
|
|
8878
|
-
function lastTagGone(id, count, f) {
|
|
8879
|
-
return f.worktreeActiveIds.has(id) && count === 0;
|
|
8880
|
-
}
|
|
8881
|
-
function classifyLosses(f) {
|
|
8882
|
-
const out = [];
|
|
8883
|
-
for (const base of f.baseReqs) {
|
|
8884
|
-
const loss = reqDeletedFor(base, f);
|
|
8885
|
-
if (loss !== null)
|
|
8886
|
-
out.push(loss);
|
|
8887
|
-
}
|
|
8888
|
-
for (const [id, site] of f.baseImplSite) {
|
|
8889
|
-
if (lastTagGone(id, f.worktreeImplCount.get(id) ?? 0, f))
|
|
8890
|
-
out.push(implLostLoss(id, site));
|
|
8891
|
-
}
|
|
8892
|
-
for (const [id, site] of f.baseVerifySite) {
|
|
8893
|
-
if (lastTagGone(id, f.worktreeVerifyCount.get(id) ?? 0, f))
|
|
8894
|
-
out.push(verifyLostLoss(id, site));
|
|
8895
|
-
}
|
|
8896
|
-
for (const path of f.deletedSpecFiles)
|
|
8897
|
-
out.push(specFileDeletedLoss(path));
|
|
8898
|
-
return out;
|
|
8899
|
-
}
|
|
8900
|
-
|
|
8901
|
-
// src/operations/guard.ts
|
|
8902
|
-
async function guard(storage2, platformDir, ref) {
|
|
8903
|
-
const losses = classifyLosses(await collectFacts(platformDir, ref, storage2));
|
|
8904
|
-
return { ok: true, ref, losses };
|
|
8905
|
-
}
|
|
8906
|
-
var init_guard = __esm(() => {
|
|
8907
|
-
init_collect();
|
|
8908
|
-
});
|
|
8909
|
-
|
|
8910
9417
|
// src/commands/guard.ts
|
|
8911
9418
|
var exports_guard = {};
|
|
8912
9419
|
__export(exports_guard, {
|
|
@@ -8945,7 +9452,7 @@ var init_guard2 = __esm(() => {
|
|
|
8945
9452
|
process.exit(EXIT.OK);
|
|
8946
9453
|
}
|
|
8947
9454
|
let losses = [];
|
|
8948
|
-
await withReadStorage({ platformDir, dbPath: defaultIndexPath(platformDir), fresh: true }, async (storage2) => {
|
|
9455
|
+
await withReadStorage({ platformDir, dbPath: defaultIndexPath(platformDir), fresh: true, json: jsonMode }, async (storage2) => {
|
|
8949
9456
|
losses = (await guard(storage2, platformDir, ref)).losses;
|
|
8950
9457
|
});
|
|
8951
9458
|
console.log(renderGuard(losses, jsonMode ? "json" : "text", ref));
|
|
@@ -10962,6 +11469,10 @@ function mountEditor(app, bindHost = DEFAULT_BIND_HOST) {
|
|
|
10962
11469
|
Lives in (optional, single path)
|
|
10963
11470
|
<input type="text" name="livesIn" placeholder="path/to/impl.ts" />
|
|
10964
11471
|
</label>
|
|
11472
|
+
<label>
|
|
11473
|
+
Relates to (optional, comma-separated ids)
|
|
11474
|
+
<input type="text" name="relates" placeholder="BILLING-001, BILLING-002" />
|
|
11475
|
+
</label>
|
|
10965
11476
|
<button type="submit">Create</button>
|
|
10966
11477
|
</form>
|
|
10967
11478
|
</section>
|
|
@@ -10996,6 +11507,7 @@ function mountEditor(app, bindHost = DEFAULT_BIND_HOST) {
|
|
|
10996
11507
|
const statement = field(form, "statement");
|
|
10997
11508
|
const why = field(form, "why");
|
|
10998
11509
|
const livesIn = field(form, "livesIn");
|
|
11510
|
+
const relates = field(form, "relates");
|
|
10999
11511
|
const payload = {
|
|
11000
11512
|
key: field(form, "key"),
|
|
11001
11513
|
statement
|
|
@@ -11004,6 +11516,8 @@ function mountEditor(app, bindHost = DEFAULT_BIND_HOST) {
|
|
|
11004
11516
|
payload.why = why;
|
|
11005
11517
|
if (livesIn.length > 0)
|
|
11006
11518
|
payload.livesIn = livesIn;
|
|
11519
|
+
if (relates.length > 0)
|
|
11520
|
+
payload.relates = relates;
|
|
11007
11521
|
const res = await app.request("/api/requirements", {
|
|
11008
11522
|
method: "POST",
|
|
11009
11523
|
headers: { "content-type": "application/json" },
|
|
@@ -11840,6 +12354,9 @@ function guarded(handler) {
|
|
|
11840
12354
|
try {
|
|
11841
12355
|
return await handler(c);
|
|
11842
12356
|
} catch (err) {
|
|
12357
|
+
if (err instanceof InvalidDomainFilesError) {
|
|
12358
|
+
return c.json({ error: "invalid_domain_file", diagnostics: sortDiagnostics(err.diagnostics) }, 422);
|
|
12359
|
+
}
|
|
11843
12360
|
const info = describeStorageError(err);
|
|
11844
12361
|
if (info === null)
|
|
11845
12362
|
throw err;
|
|
@@ -11904,7 +12421,7 @@ async function readJsonBody(c) {
|
|
|
11904
12421
|
}
|
|
11905
12422
|
return { ok: true, body: parsed };
|
|
11906
12423
|
}
|
|
11907
|
-
function
|
|
12424
|
+
function toStringList(v) {
|
|
11908
12425
|
if (Array.isArray(v))
|
|
11909
12426
|
return v.filter((x) => typeof x === "string");
|
|
11910
12427
|
if (typeof v === "string" && v.length > 0)
|
|
@@ -11915,11 +12432,20 @@ function mintFieldsFromBody(body) {
|
|
|
11915
12432
|
return {
|
|
11916
12433
|
statement: typeof body.statement === "string" ? body.statement : "",
|
|
11917
12434
|
why: typeof body.why === "string" ? body.why : "",
|
|
11918
|
-
livesIn:
|
|
12435
|
+
livesIn: toStringList(body.livesIn),
|
|
12436
|
+
relates: toStringList(body.relates),
|
|
11919
12437
|
issue: typeof body.issue === "string" && body.issue.trim() !== "" ? body.issue.trim() : undefined,
|
|
11920
12438
|
status: body.status === "draft" ? "draft" : "active"
|
|
11921
12439
|
};
|
|
11922
12440
|
}
|
|
12441
|
+
function successorFieldsFromBody(body) {
|
|
12442
|
+
return {
|
|
12443
|
+
why: typeof body.why === "string" ? body.why : undefined,
|
|
12444
|
+
livesIn: "livesIn" in body ? toStringList(body.livesIn) : undefined,
|
|
12445
|
+
relates: "relates" in body ? toStringList(body.relates) : undefined,
|
|
12446
|
+
issue: typeof body.issue === "string" && body.issue.trim() !== "" ? body.issue.trim() : undefined
|
|
12447
|
+
};
|
|
12448
|
+
}
|
|
11923
12449
|
function amendFieldsFromBody(body) {
|
|
11924
12450
|
const fields = {};
|
|
11925
12451
|
if (typeof body.statement === "string")
|
|
@@ -11927,7 +12453,9 @@ function amendFieldsFromBody(body) {
|
|
|
11927
12453
|
if ("why" in body)
|
|
11928
12454
|
fields.why = typeof body.why === "string" ? body.why : null;
|
|
11929
12455
|
if ("livesIn" in body)
|
|
11930
|
-
fields.livesIn =
|
|
12456
|
+
fields.livesIn = toStringList(body.livesIn);
|
|
12457
|
+
if ("relates" in body)
|
|
12458
|
+
fields.relates = toStringList(body.relates);
|
|
11931
12459
|
return Object.keys(fields).length === 0 ? null : fields;
|
|
11932
12460
|
}
|
|
11933
12461
|
function failureResponse(c, failure) {
|
|
@@ -12034,12 +12562,9 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
|
|
|
12034
12562
|
return withWriteLock(async () => {
|
|
12035
12563
|
const fields = amendFieldsFromBody(body);
|
|
12036
12564
|
if (fields === null) {
|
|
12037
|
-
return c.json({ error: "nothing to amend \u2014 provide statement, why, or
|
|
12565
|
+
return c.json({ error: "nothing to amend \u2014 provide statement, why, livesIn, or relates" }, 400);
|
|
12038
12566
|
}
|
|
12039
|
-
const r = await amend({ platformDir, id, fields }
|
|
12040
|
-
await runIndex({ platformDir, storage: storage2 });
|
|
12041
|
-
return storage2.listTags({ req_id: reqId });
|
|
12042
|
-
});
|
|
12567
|
+
const r = await amend({ platformDir, id, fields });
|
|
12043
12568
|
if (!r.ok)
|
|
12044
12569
|
return failureResponse(c, r);
|
|
12045
12570
|
await runIndex({ platformDir, storage: storage2 });
|
|
@@ -12067,13 +12592,50 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
|
|
|
12067
12592
|
if (statement === "")
|
|
12068
12593
|
return c.json({ error: "statement is required (non-empty)" }, 400);
|
|
12069
12594
|
return withWriteLock(async () => {
|
|
12070
|
-
const r = await supersede({
|
|
12595
|
+
const r = await supersede({ platformDir, id, statement, ...successorFieldsFromBody(body) }, async (reqId) => {
|
|
12596
|
+
await runIndex({ platformDir, storage: storage2 });
|
|
12597
|
+
return storage2.listTags({ req_id: reqId });
|
|
12598
|
+
});
|
|
12599
|
+
if (!r.ok)
|
|
12600
|
+
return failureResponse(c, r);
|
|
12601
|
+
return c.json({
|
|
12602
|
+
ok: true,
|
|
12603
|
+
old_id: r.oldId,
|
|
12604
|
+
new_id: r.newId,
|
|
12605
|
+
file: r.file,
|
|
12606
|
+
spec_version: r.specVersion,
|
|
12607
|
+
retag: r.retag
|
|
12608
|
+
}, 201);
|
|
12609
|
+
});
|
|
12610
|
+
}));
|
|
12611
|
+
app.post("/api/requirements/:id/move", guarded(async function moveRequirement(c) {
|
|
12612
|
+
if (!featureEnabled("editor")) {
|
|
12613
|
+
return c.json({ error: featureDisabledMessage("editor") }, 404);
|
|
12614
|
+
}
|
|
12615
|
+
const originReject = rejectCrossOrigin(c, bindHost);
|
|
12616
|
+
if (originReject)
|
|
12617
|
+
return originReject;
|
|
12618
|
+
const headerReject = rejectBadWriteHeaders(c);
|
|
12619
|
+
if (headerReject)
|
|
12620
|
+
return headerReject;
|
|
12621
|
+
const parsed = await readJsonBody(c);
|
|
12622
|
+
if (!parsed.ok)
|
|
12623
|
+
return parsed.res;
|
|
12624
|
+
const body = parsed.body;
|
|
12625
|
+
const id = c.req.param("id") ?? "";
|
|
12626
|
+
if (!ID_RE.test(id))
|
|
12627
|
+
return c.json({ error: "id must be a requirement id (KEY-NNN)" }, 400);
|
|
12628
|
+
const targetKey = normalizeDomainKey(typeof body.newDomain === "string" ? body.newDomain : "");
|
|
12629
|
+
if (targetKey === "")
|
|
12630
|
+
return c.json({ error: "newDomain is required (a domain key)" }, 400);
|
|
12631
|
+
return withWriteLock(async () => {
|
|
12632
|
+
const r = await move({
|
|
12071
12633
|
platformDir,
|
|
12072
12634
|
id,
|
|
12073
|
-
|
|
12635
|
+
targetKey,
|
|
12636
|
+
statement: typeof body.text === "string" ? body.text : undefined,
|
|
12074
12637
|
why: typeof body.why === "string" ? body.why : undefined,
|
|
12075
|
-
livesIn: "
|
|
12076
|
-
issue: typeof body.issue === "string" && body.issue.trim() !== "" ? body.issue.trim() : undefined
|
|
12638
|
+
livesIn: "lives" in body ? toStringList(body.lives) : undefined
|
|
12077
12639
|
}, async (reqId) => {
|
|
12078
12640
|
await runIndex({ platformDir, storage: storage2 });
|
|
12079
12641
|
return storage2.listTags({ req_id: reqId });
|
|
@@ -12084,8 +12646,10 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
|
|
|
12084
12646
|
ok: true,
|
|
12085
12647
|
old_id: r.oldId,
|
|
12086
12648
|
new_id: r.newId,
|
|
12087
|
-
|
|
12088
|
-
|
|
12649
|
+
from_file: r.fromFile,
|
|
12650
|
+
to_file: r.toFile,
|
|
12651
|
+
source_spec_version: r.sourceSpecVersion,
|
|
12652
|
+
target_spec_version: r.targetSpecVersion,
|
|
12089
12653
|
retag: r.retag
|
|
12090
12654
|
}, 201);
|
|
12091
12655
|
});
|
|
@@ -12228,6 +12792,7 @@ var init_api = __esm(() => {
|
|
|
12228
12792
|
init_amend();
|
|
12229
12793
|
init_deprecate();
|
|
12230
12794
|
init_mint();
|
|
12795
|
+
init_move();
|
|
12231
12796
|
init_reads2();
|
|
12232
12797
|
init_records();
|
|
12233
12798
|
init_supersede();
|
|
@@ -12358,7 +12923,7 @@ __export(exports_serve, {
|
|
|
12358
12923
|
resolveBind: () => resolveBind,
|
|
12359
12924
|
composeServeApp: () => composeServeApp
|
|
12360
12925
|
});
|
|
12361
|
-
import { existsSync as
|
|
12926
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync6 } from "fs";
|
|
12362
12927
|
import { dirname as dirname8 } from "path";
|
|
12363
12928
|
import { defineCommand as defineCommand24 } from "citty";
|
|
12364
12929
|
import { Hono as Hono2 } from "hono";
|
|
@@ -12411,7 +12976,7 @@ async function runProbe() {
|
|
|
12411
12976
|
}
|
|
12412
12977
|
const body = await res.text();
|
|
12413
12978
|
if (!body.includes("Spec Engine \u2014 coming online")) {
|
|
12414
|
-
console.error(`serve --probe FAILED: body did not contain the placeholder string
|
|
12979
|
+
console.error(`serve --probe FAILED: body did not contain the placeholder string`);
|
|
12415
12980
|
server.stop();
|
|
12416
12981
|
process.exit(EXIT.FAILURE);
|
|
12417
12982
|
}
|
|
@@ -12440,7 +13005,7 @@ var init_serve = __esm(() => {
|
|
|
12440
13005
|
serveCommand = defineCommand24({
|
|
12441
13006
|
meta: {
|
|
12442
13007
|
name: "serve",
|
|
12443
|
-
description: "Run the local webapp. Binds 127.0.0.1 on --port, or one private --host address behind an access token. --probe is the
|
|
13008
|
+
description: "Run the local webapp. Binds 127.0.0.1 on --port, or one private --host address behind an access token. --probe is the asset-embedding smoke."
|
|
12444
13009
|
},
|
|
12445
13010
|
args: {
|
|
12446
13011
|
platformDir: platformDirArg,
|
|
@@ -12510,7 +13075,7 @@ var init_serve = __esm(() => {
|
|
|
12510
13075
|
}
|
|
12511
13076
|
});
|
|
12512
13077
|
mkdirSync6(dirname8(dbPath), { recursive: true });
|
|
12513
|
-
const needsIndex = !
|
|
13078
|
+
const needsIndex = !existsSync14(dbPath);
|
|
12514
13079
|
let storage2;
|
|
12515
13080
|
try {
|
|
12516
13081
|
storage2 = openStorage(dbPath);
|
|
@@ -12531,6 +13096,7 @@ var init_serve = __esm(() => {
|
|
|
12531
13096
|
});
|
|
12532
13097
|
} catch (err) {
|
|
12533
13098
|
storage2.close();
|
|
13099
|
+
handleInvalidDomainFiles(err, false);
|
|
12534
13100
|
handleStorageUnavailable(err, dbPath);
|
|
12535
13101
|
console.error(`spec serve: failed to start on ${urlHost(host, port)}:`, err);
|
|
12536
13102
|
process.exit(EXIT.FAILURE);
|
|
@@ -12557,13 +13123,13 @@ __export(exports_docs, {
|
|
|
12557
13123
|
docsCommand: () => docsCommand,
|
|
12558
13124
|
createDocsFetchHandler: () => createDocsFetchHandler
|
|
12559
13125
|
});
|
|
12560
|
-
import { existsSync as
|
|
12561
|
-
import { join as
|
|
13126
|
+
import { existsSync as existsSync15, statSync as statSync6 } from "fs";
|
|
13127
|
+
import { join as join16, normalize, sep as sep4 } from "path";
|
|
12562
13128
|
import { fileURLToPath } from "url";
|
|
12563
13129
|
import { defineCommand as defineCommand25 } from "citty";
|
|
12564
13130
|
function resolveDocsRootFrom(candidates) {
|
|
12565
13131
|
for (const candidate of candidates) {
|
|
12566
|
-
if (
|
|
13132
|
+
if (existsSync15(join16(candidate, "index.html")))
|
|
12567
13133
|
return candidate;
|
|
12568
13134
|
}
|
|
12569
13135
|
return null;
|
|
@@ -12587,17 +13153,17 @@ function createDocsFetchHandler(root) {
|
|
|
12587
13153
|
if (pathname.includes("\x00")) {
|
|
12588
13154
|
return new Response("Bad request", { status: 400 });
|
|
12589
13155
|
}
|
|
12590
|
-
const target = normalize(
|
|
13156
|
+
const target = normalize(join16(normalRoot, pathname));
|
|
12591
13157
|
if (target !== normalRoot && !target.startsWith(rootPrefix)) {
|
|
12592
13158
|
return new Response("Not found", { status: 404 });
|
|
12593
13159
|
}
|
|
12594
|
-
for (const candidate of [target,
|
|
12595
|
-
if (
|
|
13160
|
+
for (const candidate of [target, join16(target, "index.html"), `${target}.html`]) {
|
|
13161
|
+
if (existsSync15(candidate) && statSync6(candidate).isFile()) {
|
|
12596
13162
|
return new Response(Bun.file(candidate));
|
|
12597
13163
|
}
|
|
12598
13164
|
}
|
|
12599
|
-
const notFoundPage =
|
|
12600
|
-
if (
|
|
13165
|
+
const notFoundPage = join16(normalRoot, "404.html");
|
|
13166
|
+
if (existsSync15(notFoundPage)) {
|
|
12601
13167
|
return new Response(Bun.file(notFoundPage), { status: 404 });
|
|
12602
13168
|
}
|
|
12603
13169
|
return new Response("Not found", { status: 404 });
|
|
@@ -12647,7 +13213,7 @@ var init_docs2 = __esm(() => {
|
|
|
12647
13213
|
port: {
|
|
12648
13214
|
type: "string",
|
|
12649
13215
|
default: "4100",
|
|
12650
|
-
description: "Port (default 4100; 0 = ephemeral). Hostname is hardcoded 127.0.0.1
|
|
13216
|
+
description: "Port (default 4100; 0 = ephemeral). Hostname is hardcoded 127.0.0.1; no --host accepted."
|
|
12651
13217
|
},
|
|
12652
13218
|
probe: {
|
|
12653
13219
|
type: "boolean",
|