@spec-engine/spec-engine 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/agent/AGENTS.md +678 -0
- package/dist/agent/mcp.json +11 -0
- package/dist/agent/pre-commit +3 -0
- package/dist/agent/skills/req-author/SKILL.md +102 -0
- package/dist/docs/agent-reference/index.html +1280 -33
- package/dist/docs/architecture/index.html +6 -6
- package/dist/docs/checks/index.html +19 -266
- package/dist/docs/commands/index.html +32 -164
- package/dist/docs/format/index.html +2 -9
- package/dist/docs/getting-started/index.html +3 -3
- package/dist/docs/guides/platform-setup/index.html +2 -2
- package/dist/docs/pagefind/fragment/en_10e3da6.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_37cae5e.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_7fc2fcd.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_96d3bc1.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_a26aa07.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_b7e68ef.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_d8a5bcd.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_de67937.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_fc3c55b.pf_fragment +0 -0
- package/dist/docs/pagefind/index/en_1664b45.pf_index +0 -0
- package/dist/docs/pagefind/index/en_a8b8bb6.pf_index +0 -0
- package/dist/docs/pagefind/pagefind-entry.json +1 -1
- package/dist/docs/pagefind/pagefind.en_bc3d4a7766.pf_meta +0 -0
- package/dist/docs/platform-mapping/index.html +2 -1
- package/dist/docs/tags/index.html +9 -92
- package/dist/impl.js +2011 -703
- package/package.json +1 -1
- package/dist/docs/pagefind/fragment/en_1f98c2d.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_24f76b6.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_4bd9de6.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_9cc1878.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_9cfc306.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_b2867b9.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_b81dc52.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_c3abf3d.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_c3e92c6.pf_fragment +0 -0
- package/dist/docs/pagefind/index/en_6e50ed4.pf_index +0 -0
- package/dist/docs/pagefind/index/en_afc98e5.pf_index +0 -0
- package/dist/docs/pagefind/pagefind.en_d0b5c328bc.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.9.0",
|
|
24
24
|
description: "Cross-repo spec engine \u2014 durable requirements with permanent IDs, bound to code via @spec tags, checked in CI. Ships the spec CLI, the local coverage webapp (spec serve), and offline docs (spec docs).",
|
|
25
25
|
license: "MIT",
|
|
26
26
|
type: "module",
|
|
@@ -73,6 +73,31 @@ var init_package = __esm(() => {
|
|
|
73
73
|
};
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
+
// src/stdout.ts
|
|
77
|
+
import { format } from "util";
|
|
78
|
+
function isEpipe(e) {
|
|
79
|
+
return typeof e === "object" && e !== null && "code" in e && (e.code === "EPIPE" || e.code === "ERR_STREAM_DESTROYED");
|
|
80
|
+
}
|
|
81
|
+
function routeStdout() {
|
|
82
|
+
process.stdout.on("error", (e) => {
|
|
83
|
+
if (!isEpipe(e))
|
|
84
|
+
throw e;
|
|
85
|
+
});
|
|
86
|
+
console.log = (...args) => {
|
|
87
|
+
const text = `${format(...args)}
|
|
88
|
+
`;
|
|
89
|
+
lastWrite = new Promise((written) => process.stdout.write(text, () => written()));
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function exit(code) {
|
|
93
|
+
await lastWrite;
|
|
94
|
+
process.exit(code);
|
|
95
|
+
}
|
|
96
|
+
var lastWrite;
|
|
97
|
+
var init_stdout = __esm(() => {
|
|
98
|
+
lastWrite = Promise.resolve();
|
|
99
|
+
});
|
|
100
|
+
|
|
76
101
|
// ../shared/src/diagnostics.ts
|
|
77
102
|
var DiagnosticCode, NotASpecPlatformError, InvalidDomainFilesError;
|
|
78
103
|
var init_diagnostics = __esm(() => {
|
|
@@ -84,15 +109,13 @@ var init_diagnostics = __esm(() => {
|
|
|
84
109
|
SUPERSEDED_REFERENCED: "SUPERSEDED_REFERENCED",
|
|
85
110
|
DEPRECATED_REFERENCED: "DEPRECATED_REFERENCED",
|
|
86
111
|
STATEMENT_GRAMMAR: "STATEMENT_GRAMMAR",
|
|
112
|
+
STATEMENT_RULES: "STATEMENT_RULES",
|
|
87
113
|
DRAFT_REFERENCED: "DRAFT_REFERENCED",
|
|
88
114
|
GLOSSARY_DRIFT: "GLOSSARY_DRIFT",
|
|
89
115
|
ORPHAN_REQ: "ORPHAN_REQ",
|
|
90
116
|
UNVERIFIED_REQ: "UNVERIFIED_REQ",
|
|
91
117
|
DRIFT: "DRIFT",
|
|
92
118
|
NO_SPEC_CONFIG: "NO_SPEC_CONFIG",
|
|
93
|
-
BROKEN_FILE_REF: "BROKEN_FILE_REF",
|
|
94
|
-
UNIMPLEMENTED_IN_FILE: "UNIMPLEMENTED_IN_FILE",
|
|
95
|
-
UNVERIFIED_IN_FILE: "UNVERIFIED_IN_FILE",
|
|
96
119
|
BROKEN_RELATES: "BROKEN_RELATES",
|
|
97
120
|
RELATES_SUPERSEDED: "RELATES_SUPERSEDED",
|
|
98
121
|
CYCLIC_SUPERSEDE: "CYCLIC_SUPERSEDE",
|
|
@@ -100,6 +123,7 @@ var init_diagnostics = __esm(() => {
|
|
|
100
123
|
UNKNOWN_ROLE: "UNKNOWN_ROLE",
|
|
101
124
|
UNSOURCED_CHANGE: "UNSOURCED_CHANGE",
|
|
102
125
|
INVALID_DOMAIN_FILE: "INVALID_DOMAIN_FILE",
|
|
126
|
+
MIGRATION_PENDING: "MIGRATION_PENDING",
|
|
103
127
|
UNPROVEN_REQ: "UNPROVEN_REQ",
|
|
104
128
|
PROOFS_UNCONFIRMED: "PROOFS_UNCONFIRMED",
|
|
105
129
|
REQUIREMENT_REMOVED: "REQUIREMENT_REMOVED",
|
|
@@ -214,8 +238,6 @@ function orderDomain(d) {
|
|
|
214
238
|
supersedes: r.supersedes ?? null,
|
|
215
239
|
supersededBy: r.supersededBy ?? null,
|
|
216
240
|
relates: r.relates,
|
|
217
|
-
livesIn: r.livesIn,
|
|
218
|
-
verifiedIn: r.verifiedIn,
|
|
219
241
|
issues: r.issues.map((iss) => ({ role: iss.role, id: iss.id })),
|
|
220
242
|
changedAtVersion: r.changedAtVersion,
|
|
221
243
|
supersededAtVersion: r.supersededAtVersion,
|
|
@@ -262,6 +284,10 @@ function preserveShape(raw, rawDomain, domain) {
|
|
|
262
284
|
const next = orderDomain(domain);
|
|
263
285
|
patchFields(raw, before, next);
|
|
264
286
|
const rawEntries = raw.requirements;
|
|
287
|
+
for (const entry of rawEntries) {
|
|
288
|
+
for (const key of LEGACY_ENTRY_KEYS)
|
|
289
|
+
delete entry[key];
|
|
290
|
+
}
|
|
265
291
|
const byId = new Map;
|
|
266
292
|
rawDomain.requirements.forEach((r, i) => {
|
|
267
293
|
byId.set(r.id, [...byId.get(r.id) ?? [], i]);
|
|
@@ -332,7 +358,7 @@ async function validateAndWrite(path, domain, sourceFile) {
|
|
|
332
358
|
await Bun.write(path, serialized);
|
|
333
359
|
return { ok: true };
|
|
334
360
|
}
|
|
335
|
-
var ID_RE, KEY_RE, SpecIssueSchema, SpecCiteSchema, SpecRequirementSchema, SpecDomainSchema, sameValue = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
361
|
+
var ID_RE, KEY_RE, SpecIssueSchema, SpecCiteSchema, LEGACY_ENTRY_KEYS, SpecRequirementSchema, SpecDomainSchema, sameValue = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
336
362
|
var init_domain = __esm(() => {
|
|
337
363
|
init_diagnostics();
|
|
338
364
|
ID_RE = /^[A-Z][A-Z0-9]*-\d+$/;
|
|
@@ -345,6 +371,7 @@ var init_domain = __esm(() => {
|
|
|
345
371
|
term: z.string(),
|
|
346
372
|
pinned: z.number().int()
|
|
347
373
|
}).strict();
|
|
374
|
+
LEGACY_ENTRY_KEYS = ["livesIn", "verifiedIn"];
|
|
348
375
|
SpecRequirementSchema = z.object({
|
|
349
376
|
id: z.string().regex(ID_RE),
|
|
350
377
|
status: z.string().min(1),
|
|
@@ -353,8 +380,8 @@ var init_domain = __esm(() => {
|
|
|
353
380
|
supersedes: z.string().nullable().optional(),
|
|
354
381
|
supersededBy: z.string().nullable().optional(),
|
|
355
382
|
relates: z.array(z.string()).optional().default([]),
|
|
356
|
-
livesIn: z.
|
|
357
|
-
verifiedIn: z.
|
|
383
|
+
livesIn: z.unknown().optional(),
|
|
384
|
+
verifiedIn: z.unknown().optional(),
|
|
358
385
|
issues: z.array(SpecIssueSchema).optional().default([]),
|
|
359
386
|
changedAtVersion: z.number().int().optional(),
|
|
360
387
|
supersededAtVersion: z.number().int().optional(),
|
|
@@ -363,7 +390,7 @@ var init_domain = __esm(() => {
|
|
|
363
390
|
aliases: z.array(z.string()).optional().default([]),
|
|
364
391
|
cites: z.array(SpecCiteSchema).optional().default([]),
|
|
365
392
|
section: z.string().nullable().optional()
|
|
366
|
-
}).strict();
|
|
393
|
+
}).strict().transform(({ livesIn: _l, verifiedIn: _v, ...entry }) => entry);
|
|
367
394
|
SpecDomainSchema = z.object({
|
|
368
395
|
key: z.string().regex(KEY_RE),
|
|
369
396
|
owner: z.string().nullable().optional(),
|
|
@@ -376,6 +403,159 @@ var init_domain = __esm(() => {
|
|
|
376
403
|
}).strict();
|
|
377
404
|
});
|
|
378
405
|
|
|
406
|
+
// ../shared/src/statementRules.ts
|
|
407
|
+
function jargonPatterns(words) {
|
|
408
|
+
if (words.length === 0)
|
|
409
|
+
return [];
|
|
410
|
+
return [new RegExp(`\\b(?:${words.map(escapeRegExp).join("|")})\\b`, "i")];
|
|
411
|
+
}
|
|
412
|
+
function withoutExemptNames(statement, names) {
|
|
413
|
+
let out = statement;
|
|
414
|
+
for (const name of names) {
|
|
415
|
+
const tail = name.endsWith("/") ? "[^\\s`]*" : "(?![\\w-])";
|
|
416
|
+
out = out.replace(new RegExp(`(?<![\\w.-])${escapeRegExp(name)}${tail}`, "g"), "X");
|
|
417
|
+
}
|
|
418
|
+
return out;
|
|
419
|
+
}
|
|
420
|
+
function sentenceDetail(statement) {
|
|
421
|
+
if (/[\r\n]/.test(statement))
|
|
422
|
+
return "contains a line break";
|
|
423
|
+
const prose = statement.replace(QUOTED, "Q").replace(NON_TERMINAL, "N");
|
|
424
|
+
const terminals = prose.match(TERMINAL)?.length ?? 0;
|
|
425
|
+
if (terminals === 1 && /[.?!]$/.test(prose.trimEnd()))
|
|
426
|
+
return null;
|
|
427
|
+
return `${terminals} sentence terminators; exactly one, at the end`;
|
|
428
|
+
}
|
|
429
|
+
function hitsDetail(text, patterns) {
|
|
430
|
+
const hits = patterns.map((pattern) => text.match(pattern)?.[0]).filter((hit) => hit !== undefined);
|
|
431
|
+
return hits.length > 0 ? hits.map((hit) => `"${hit}"`).join(", ") : null;
|
|
432
|
+
}
|
|
433
|
+
function statementFindings(statement, config = {}) {
|
|
434
|
+
const maxLength = config.maxLength ?? DEFAULT_MAX_STATEMENT_LENGTH;
|
|
435
|
+
const details = {
|
|
436
|
+
"one-sentence": sentenceDetail(statement),
|
|
437
|
+
length: statement.length > maxLength ? `${statement.length} characters; at most ${maxLength}` : null,
|
|
438
|
+
"no-file-path": hitsDetail(withoutExemptNames(statement, config.exemptNames ?? []), FILE_PATH),
|
|
439
|
+
"no-test-narration": null,
|
|
440
|
+
"no-toolchain-layout": null,
|
|
441
|
+
"no-history": null,
|
|
442
|
+
"no-jargon": hitsDetail(statement, jargonPatterns(config.jargon ?? DEFAULT_JARGON)),
|
|
443
|
+
"no-tool-statement": hitsDetail(statement, TOOL_STATEMENT)
|
|
444
|
+
};
|
|
445
|
+
for (const { rule, patterns } of FIXED_RULES)
|
|
446
|
+
details[rule] = hitsDetail(statement, patterns);
|
|
447
|
+
const findings = [];
|
|
448
|
+
for (const rule of STATEMENT_RULES) {
|
|
449
|
+
const detail = details[rule];
|
|
450
|
+
if (detail !== null && config.severity?.[rule] !== "off") {
|
|
451
|
+
findings.push({ rule, detail, reason: STATEMENT_RULE_REASONS[rule] });
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return findings;
|
|
455
|
+
}
|
|
456
|
+
function formatStatementFinding(f) {
|
|
457
|
+
return `${f.rule}: ${f.detail} \u2014 ${f.reason}`;
|
|
458
|
+
}
|
|
459
|
+
var RULE_ZERO = "A requirement is one rule of the product's behavior, stated from the user's point of view.", STATEMENT_RULES, DEFAULT_MAX_STATEMENT_LENGTH = 280, DEFAULT_JARGON, STATEMENT_RULE_REASONS, EXT = "ts|tsx|js|mjs|json|yml|yaml|sql|md|toml|css|astro|sh", FILE_PATH, FIXED_RULES, TOOL_STATEMENT, escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), QUOTED, NON_TERMINAL, TERMINAL;
|
|
460
|
+
var init_statementRules = __esm(() => {
|
|
461
|
+
STATEMENT_RULES = [
|
|
462
|
+
"one-sentence",
|
|
463
|
+
"length",
|
|
464
|
+
"no-file-path",
|
|
465
|
+
"no-test-narration",
|
|
466
|
+
"no-toolchain-layout",
|
|
467
|
+
"no-history",
|
|
468
|
+
"no-jargon",
|
|
469
|
+
"no-tool-statement"
|
|
470
|
+
];
|
|
471
|
+
DEFAULT_JARGON = [
|
|
472
|
+
"seam",
|
|
473
|
+
"seams",
|
|
474
|
+
"rung",
|
|
475
|
+
"rungs",
|
|
476
|
+
"residue",
|
|
477
|
+
"matrix entry",
|
|
478
|
+
"stage runner",
|
|
479
|
+
"the stage"
|
|
480
|
+
];
|
|
481
|
+
STATEMENT_RULE_REASONS = {
|
|
482
|
+
"one-sentence": "a statement is one testable sentence",
|
|
483
|
+
length: "a sentence a reader holds at once",
|
|
484
|
+
"no-file-path": "which file holds what is layout, not behaviour",
|
|
485
|
+
"no-test-narration": "how a test is written is not what the product does",
|
|
486
|
+
"no-toolchain-layout": "how CI and tooling are arranged is not a product behaviour",
|
|
487
|
+
"no-history": "a statement says what holds now, not how it got there",
|
|
488
|
+
"no-jargon": "a reader without the codebase cannot parse project jargon",
|
|
489
|
+
"no-tool-statement": "naming the tool in use is a decision record, not a requirement"
|
|
490
|
+
};
|
|
491
|
+
FILE_PATH = [
|
|
492
|
+
/(?<![\w/@-])(?:apps|packages|tooling|modules|spec-engine|\.github)\/[\w.@/-]*/i,
|
|
493
|
+
/`(?![@/<])(?![^`\s]*:\/\/)(?![^`\s./]*\/[^`\s./]*`)[^`\s]*\/[^`\s]*`/,
|
|
494
|
+
new RegExp(`\\b(?!(?:next|node|postgres)\\.js\\b|auth\\.md\\b)[\\w-]+(?:\\.[\\w-]+)*\\.(?:${EXT})\\b`, "i")
|
|
495
|
+
];
|
|
496
|
+
FIXED_RULES = [
|
|
497
|
+
{
|
|
498
|
+
rule: "no-test-narration",
|
|
499
|
+
patterns: [
|
|
500
|
+
/\b(?:describe|it|test)\(/,
|
|
501
|
+
/\.(?:test|spec)\./i,
|
|
502
|
+
/\btest files?\b/i,
|
|
503
|
+
/\bthe tests?\b/i,
|
|
504
|
+
/\bvitest\b/i,
|
|
505
|
+
/\bplaywright\b/i,
|
|
506
|
+
/\bfixtures?\b(?! (?:accounts?|seed))/i,
|
|
507
|
+
/\bplanted\b/i
|
|
508
|
+
]
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
rule: "no-toolchain-layout",
|
|
512
|
+
patterns: [
|
|
513
|
+
/\bworkflows?\b/i,
|
|
514
|
+
/\b(?:test|full|CI|job|build|RLS|per-table) matrix\b|\bmatrix (?:entry|entries|job)\b/i,
|
|
515
|
+
/\brunners?\b/i,
|
|
516
|
+
/\bGitHub Actions\b/i,
|
|
517
|
+
/\bci\.yml\b/i,
|
|
518
|
+
/(?<!`)\bpnpm\s/i,
|
|
519
|
+
/\bturbo\b/i,
|
|
520
|
+
/\bbiome\b/i,
|
|
521
|
+
/\bdrizzle-kit\b/i,
|
|
522
|
+
/\btsx\s/i,
|
|
523
|
+
/\bbun\s/i,
|
|
524
|
+
/\bShellCheck\b/i,
|
|
525
|
+
/\bcoverage thresholds?\b/i,
|
|
526
|
+
/`(?:pnpm )?ci:/i,
|
|
527
|
+
/@spec(?![\w-])/i,
|
|
528
|
+
/\btag(?:ged)? in\b/i
|
|
529
|
+
]
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
rule: "no-history",
|
|
533
|
+
patterns: [
|
|
534
|
+
/\bamended\b/i,
|
|
535
|
+
/\bsupersed\w*(?: by)? `?[A-Z][A-Z0-9]*-\d/,
|
|
536
|
+
/\breplaced by\b|\breplace[sd] `?(?:ADR-|[A-Z][A-Z0-9]*-\d)/i,
|
|
537
|
+
/\bpreviously\b/i,
|
|
538
|
+
/\bformerly\b/i,
|
|
539
|
+
/\bused to\b/i,
|
|
540
|
+
/\bsince `?\d/i,
|
|
541
|
+
/\bmigration \d/i,
|
|
542
|
+
/\b0\d{3}_/,
|
|
543
|
+
/\bPORT-T/,
|
|
544
|
+
/\bADR-/
|
|
545
|
+
]
|
|
546
|
+
}
|
|
547
|
+
];
|
|
548
|
+
TOOL_STATEMENT = [
|
|
549
|
+
/\bwe use\b/i,
|
|
550
|
+
/\bis used for\b/i,
|
|
551
|
+
/\bis the tool\b/i,
|
|
552
|
+
/\bis pinned (?:to|at) (?:`?[\w@./-]+`? )?v?\d/i
|
|
553
|
+
];
|
|
554
|
+
QUOTED = /`[^`]*`|"[^"]*"|\u201C[^\u201D]*\u201D/g;
|
|
555
|
+
NON_TERMINAL = /\b(?:e\.g|i\.e|etc|vs|cf)\.|\d+(?:\.\d+)+/gi;
|
|
556
|
+
TERMINAL = /[.?!](?=\s|$)/g;
|
|
557
|
+
});
|
|
558
|
+
|
|
379
559
|
// ../shared/src/checkConfig.ts
|
|
380
560
|
import { z as z2 } from "zod";
|
|
381
561
|
function parseCheckConfigText(text, sourceFile) {
|
|
@@ -393,10 +573,11 @@ function parseCheckConfigText(text, sourceFile) {
|
|
|
393
573
|
const path = issue && issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
394
574
|
return { ok: false, detail: `${sourceFile}: ${path}: ${issue?.message ?? "invalid"}` };
|
|
395
575
|
}
|
|
396
|
-
var CODES, matchEntry, SeverityRuleSchema, CheckConfigSchema;
|
|
576
|
+
var CODES, matchEntry, SeverityRuleSchema, StatementRulesConfigSchema, CheckConfigSchema;
|
|
397
577
|
var init_checkConfig = __esm(() => {
|
|
398
578
|
init_diagnostics();
|
|
399
579
|
init_domain();
|
|
580
|
+
init_statementRules();
|
|
400
581
|
CODES = Object.values(DiagnosticCode);
|
|
401
582
|
matchEntry = z2.string().refine((s) => KEY_RE.test(s) || ID_RE.test(s), "must be a domain key or a requirement id");
|
|
402
583
|
SeverityRuleSchema = z2.object({
|
|
@@ -404,7 +585,16 @@ var init_checkConfig = __esm(() => {
|
|
|
404
585
|
severity: z2.enum(["error", "warning", "off"]),
|
|
405
586
|
match: z2.array(matchEntry).optional()
|
|
406
587
|
}).strict();
|
|
407
|
-
|
|
588
|
+
StatementRulesConfigSchema = z2.object({
|
|
589
|
+
maxLength: z2.number().int().positive().optional(),
|
|
590
|
+
jargon: z2.array(z2.string().min(1)).optional(),
|
|
591
|
+
exemptNames: z2.array(z2.string().min(1)).optional(),
|
|
592
|
+
severity: z2.partialRecord(z2.enum(STATEMENT_RULES), z2.enum(["error", "warning", "off"])).optional()
|
|
593
|
+
}).strict();
|
|
594
|
+
CheckConfigSchema = z2.object({
|
|
595
|
+
severity: z2.array(SeverityRuleSchema).default([]),
|
|
596
|
+
statementRules: StatementRulesConfigSchema.optional()
|
|
597
|
+
}).strict();
|
|
408
598
|
});
|
|
409
599
|
|
|
410
600
|
// ../shared/src/config.ts
|
|
@@ -844,6 +1034,7 @@ var init_src = __esm(() => {
|
|
|
844
1034
|
init_net();
|
|
845
1035
|
init_propagation();
|
|
846
1036
|
init_schema();
|
|
1037
|
+
init_statementRules();
|
|
847
1038
|
init_storage();
|
|
848
1039
|
});
|
|
849
1040
|
|
|
@@ -1113,9 +1304,6 @@ function isPathIgnored(path, extra = []) {
|
|
|
1113
1304
|
function isHiddenEntry(name) {
|
|
1114
1305
|
return name.startsWith(".") && name !== ".github";
|
|
1115
1306
|
}
|
|
1116
|
-
function isHiddenPath(rel) {
|
|
1117
|
-
return rel.split("/").some(isHiddenEntry);
|
|
1118
|
-
}
|
|
1119
1307
|
async function walkInto(rootDir, relDir, keep, extraIgnore, out) {
|
|
1120
1308
|
const absDir = relDir ? join2(rootDir, relDir) : rootDir;
|
|
1121
1309
|
let entries;
|
|
@@ -2193,33 +2381,9 @@ class SqliteStorage {
|
|
|
2193
2381
|
if (files.length > FILES_MAX) {
|
|
2194
2382
|
throw new Error(`resolveByFiles: too many files (max ${FILES_MAX} per call; got ${files.length})`);
|
|
2195
2383
|
}
|
|
2196
|
-
const
|
|
2197
|
-
if (queryFiles.length > 32000) {
|
|
2198
|
-
throw new Error(`resolveByFiles: expanded bind count ${queryFiles.length} would exceed ` + "SQLITE_MAX_VARIABLE_NUMBER (32766); lower FILES_MAX");
|
|
2199
|
-
}
|
|
2200
|
-
const placeholders = queryFiles.map(() => "?").join(",");
|
|
2384
|
+
const placeholders = files.map(() => "?").join(",");
|
|
2201
2385
|
const sql = RESOLVE_BY_FILES_SQL_PREFIX + placeholders + RESOLVE_BY_FILES_SQL_SUFFIX;
|
|
2202
|
-
return this.#db.query(sql).all(...
|
|
2203
|
-
}
|
|
2204
|
-
#selfMemberName() {
|
|
2205
|
-
const canonical = this.getRepo("spec-engine");
|
|
2206
|
-
if (!canonical)
|
|
2207
|
-
return null;
|
|
2208
|
-
const platformRoot = dirname2(canonical.path);
|
|
2209
|
-
const row = this.#db.query("SELECT name FROM repos WHERE name != 'spec-engine' AND path = $path").get({ path: platformRoot });
|
|
2210
|
-
return row?.name ?? null;
|
|
2211
|
-
}
|
|
2212
|
-
#expandSelfMemberFiles(files) {
|
|
2213
|
-
const self = this.#selfMemberName();
|
|
2214
|
-
if (!self)
|
|
2215
|
-
return files;
|
|
2216
|
-
const prefix = `${self}/`;
|
|
2217
|
-
const expanded = [...files];
|
|
2218
|
-
for (const f of files) {
|
|
2219
|
-
if (!f.startsWith(prefix))
|
|
2220
|
-
expanded.push(prefix + f);
|
|
2221
|
-
}
|
|
2222
|
-
return expanded;
|
|
2386
|
+
return this.#db.query(sql).all(...files);
|
|
2223
2387
|
}
|
|
2224
2388
|
withWriteTx(fn) {
|
|
2225
2389
|
const wrapped = this.#db.transaction((arg) => fn(arg));
|
|
@@ -2685,24 +2849,8 @@ function repoExtraIgnore(member) {
|
|
|
2685
2849
|
const repoIgnore = (member.ignore ?? []).map((e) => e.endsWith("/") ? e : `${e}/`);
|
|
2686
2850
|
return [...member.selfMember ? ["spec-engine/"] : [], ...repoIgnore];
|
|
2687
2851
|
}
|
|
2688
|
-
function
|
|
2689
|
-
|
|
2690
|
-
let owner = null;
|
|
2691
|
-
for (const member2 of members) {
|
|
2692
|
-
const rel2 = relative(member2.path, abs).split(sep2).join("/");
|
|
2693
|
-
if (rel2 === "" || rel2 === ".." || rel2.startsWith("../") || isAbsolute(rel2))
|
|
2694
|
-
continue;
|
|
2695
|
-
if (owner === null || member2.path.length > owner.member.path.length)
|
|
2696
|
-
owner = { member: member2, rel: rel2 };
|
|
2697
|
-
}
|
|
2698
|
-
if (owner === null)
|
|
2699
|
-
return null;
|
|
2700
|
-
const { member, rel } = owner;
|
|
2701
|
-
if (isHiddenPath(rel) || isPathIgnored(rel, repoExtraIgnore(member)))
|
|
2702
|
-
return null;
|
|
2703
|
-
if (!isTagSource(rel) && !isDocSource(rel))
|
|
2704
|
-
return null;
|
|
2705
|
-
return `${member.name}/${rel}`;
|
|
2852
|
+
function tagFile(member, rel) {
|
|
2853
|
+
return member.selfMember ? rel : `${member.name}/${rel}`;
|
|
2706
2854
|
}
|
|
2707
2855
|
async function scanMemberCode(member, extraIgnore) {
|
|
2708
2856
|
const hitsOut = [];
|
|
@@ -2710,7 +2858,7 @@ async function scanMemberCode(member, extraIgnore) {
|
|
|
2710
2858
|
for (const rel of codePaths) {
|
|
2711
2859
|
const absPath = join4(member.path, rel);
|
|
2712
2860
|
const text = await Bun.file(absPath).text();
|
|
2713
|
-
const platformRelative =
|
|
2861
|
+
const platformRelative = tagFile(member, rel);
|
|
2714
2862
|
const hits = scanTagsInFile(member.name, platformRelative, text);
|
|
2715
2863
|
for (const h of hits)
|
|
2716
2864
|
hitsOut.push(h);
|
|
@@ -2724,7 +2872,7 @@ async function scanMemberDocs(member, extraIgnore) {
|
|
|
2724
2872
|
for (const rel of docPaths) {
|
|
2725
2873
|
const absPath = join4(member.path, rel);
|
|
2726
2874
|
const text = await Bun.file(absPath).text();
|
|
2727
|
-
const platformRelative =
|
|
2875
|
+
const platformRelative = tagFile(member, rel);
|
|
2728
2876
|
const { tags, mentions } = scanDocFile(member.name, platformRelative, text);
|
|
2729
2877
|
for (const t of tags)
|
|
2730
2878
|
tagsOut.push(t);
|
|
@@ -2915,6 +3063,944 @@ var init_pipeline = __esm(() => {
|
|
|
2915
3063
|
init_discover();
|
|
2916
3064
|
});
|
|
2917
3065
|
|
|
3066
|
+
// ../../.claude/skills/req-author/SKILL.md
|
|
3067
|
+
var SKILL_default = `---
|
|
3068
|
+
name: req-author
|
|
3069
|
+
description: Turn a rough brief or ticket into Spec Engine requirements \u2014 draft them in the fixed EARS shape, tie every draft to the exact words it came from, present the batch for human approval, and only then mint via the real \`spec\` CLI.
|
|
3070
|
+
---
|
|
3071
|
+
|
|
3072
|
+
# req-author
|
|
3073
|
+
|
|
3074
|
+
> A requirement is one rule of the product's behavior, stated from the user's point of view.
|
|
3075
|
+
|
|
3076
|
+
The full requirement authoring standard is the "Requirement authoring
|
|
3077
|
+
standard" section of the agent reference
|
|
3078
|
+
(\`packages/site/src/content/docs/agent-reference.md\`); this skill applies it.
|
|
3079
|
+
|
|
3080
|
+
This skill drives the real \`spec\` CLI (\`spec req\`, \`spec query\`,
|
|
3081
|
+
\`spec domain list\`) and never reimplements minting. It is the front half of
|
|
3082
|
+
the lifecycle: brief in, approved requirements out; the route \u2192 tag \u2192 check
|
|
3083
|
+
loop consumes what it produces.
|
|
3084
|
+
|
|
3085
|
+
**Two non-negotiable rules:**
|
|
3086
|
+
|
|
3087
|
+
1. **Never write without approval.** Draft, self-check, present the batch \u2014
|
|
3088
|
+
then STOP. No \`spec req\` runs until the human explicitly approves.
|
|
3089
|
+
2. **Every draft quotes its source.** Each drafted requirement carries the
|
|
3090
|
+
exact phrase from the brief it derives from, verbatim. A requirement you
|
|
3091
|
+
cannot anchor to the author's words is labeled **inferred** so the human
|
|
3092
|
+
can accept or reject the inference knowingly. This exists because
|
|
3093
|
+
agent-drafted requirements have shipped that the product owner could not
|
|
3094
|
+
trace back to anything they said.
|
|
3095
|
+
|
|
3096
|
+
## Procedure
|
|
3097
|
+
|
|
3098
|
+
1. **Read the brief.** A ticket is temporary work; the requirements outlive
|
|
3099
|
+
it. One brief usually splits into several requirements \u2014 one per behavior,
|
|
3100
|
+
never one per ticket.
|
|
3101
|
+
|
|
3102
|
+
2. **Draft each requirement in the fixed shape.** Every statement uses one of
|
|
3103
|
+
the five EARS shapes (the word "shall" separates who acts from the
|
|
3104
|
+
observable result):
|
|
3105
|
+
|
|
3106
|
+
| Situation | Shape |
|
|
3107
|
+
|---|---|
|
|
3108
|
+
| Always true | The \`<system>\` shall \`<result>\` |
|
|
3109
|
+
| Triggered by an event | When \`<trigger>\`, the \`<system>\` shall \`<result>\` |
|
|
3110
|
+
| Only in a certain state | While \`<state>\`, the \`<system>\` shall \`<result>\` |
|
|
3111
|
+
| Handling something going wrong | If \`<failure>\`, then the \`<system>\` shall \`<result>\` |
|
|
3112
|
+
| Only when a feature is present | Where \`<feature>\`, the \`<system>\` shall \`<result>\` |
|
|
3113
|
+
|
|
3114
|
+
Example: "When an unknown requirement id is passed, spec resolve shall
|
|
3115
|
+
print [] and exit 0." If a draft fits no row, you have not yet decided
|
|
3116
|
+
what triggers the behavior or what observably happens \u2014 decide, or mark
|
|
3117
|
+
the draft as a question for the human.
|
|
3118
|
+
|
|
3119
|
+
One promise per statement: a result chaining two behaviors with \`and\` is
|
|
3120
|
+
two drafts. Prefer \`When\`, \`If \u2026 then\`, and the bare \`shall\` form; \`While\`
|
|
3121
|
+
and \`Where\` only when a state or feature gates the behavior. Rationale
|
|
3122
|
+
belongs in the why, never in the statement.
|
|
3123
|
+
|
|
3124
|
+
The statement rules are checked the same way: one sentence ending in a
|
|
3125
|
+
period, at most 280 characters, and no repository file or folder, test
|
|
3126
|
+
mechanics, CI or tooling layout, history, project jargon, or tool choice.
|
|
3127
|
+
\`spec req\` refuses a draft that breaks one; \`spec lint <KEY-NNN> .\` lists
|
|
3128
|
+
the findings for an existing requirement.
|
|
3129
|
+
|
|
3130
|
+
3. **Write a why that names what breaks.** The \`why\` answers "what goes wrong
|
|
3131
|
+
if this stops being true" \u2014 never a restatement of the statement in
|
|
3132
|
+
different words.
|
|
3133
|
+
|
|
3134
|
+
4. **Place and dedup.** \`spec domain list --json\` shows every domain's
|
|
3135
|
+
charter; put each requirement in the domain whose promise it protects (not
|
|
3136
|
+
where the code file sits). Then \`spec query "<phrase>" . --json\` \u2014 if a
|
|
3137
|
+
close requirement already exists, propose amending or relating it instead
|
|
3138
|
+
of a new id.
|
|
3139
|
+
|
|
3140
|
+
5. **Present the batch and STOP.** For each candidate show: the domain, the
|
|
3141
|
+
statement, the why, and the **source quote** (or the
|
|
3142
|
+
\`inferred\` label). Write nothing until the human approves. This gate is
|
|
3143
|
+
mandatory.
|
|
3144
|
+
|
|
3145
|
+
6. **On approval, mint via the real CLI:**
|
|
3146
|
+
|
|
3147
|
+
\`\`\`
|
|
3148
|
+
spec req <domain> --text "<statement>" --why "<what breaks>"
|
|
3149
|
+
\`\`\`
|
|
3150
|
+
|
|
3151
|
+
(or the equivalent \`spec mcp\` tools). \`spec req\` validates through the one
|
|
3152
|
+
shared schema \u2014 in a domain that declares the ears grammar it also checks
|
|
3153
|
+
the statement shape at write time, and with \`--json\` it returns the parsed
|
|
3154
|
+
clauses.
|
|
3155
|
+
|
|
3156
|
+
7. **Record the ticket as provenance only.** The originating ticket id is
|
|
3157
|
+
history, never a requirement id and never a code \`@spec\` tag.
|
|
3158
|
+
|
|
3159
|
+
8. **Verify.** \`spec index . && spec check . --ci\` (exit 0) and
|
|
3160
|
+
\`spec guard .\` (no loss) before the change ships.
|
|
3161
|
+
|
|
3162
|
+
## Invocation note
|
|
3163
|
+
|
|
3164
|
+
The repo's \`.mcp.json\` registers the local \`spec mcp\` server (stdio,
|
|
3165
|
+
loopback), so the tools and the \`author_requirements\` prompt are callable
|
|
3166
|
+
natively from the harness. From a source checkout the server runs as
|
|
3167
|
+
\`bun packages/engine/src/cli.ts mcp .\`; a compiled \`spec\` binary on PATH runs
|
|
3168
|
+
as \`spec mcp .\`.
|
|
3169
|
+
`;
|
|
3170
|
+
var init_SKILL = () => {};
|
|
3171
|
+
|
|
3172
|
+
// ../site/src/content/docs/agent-reference.md
|
|
3173
|
+
var agent_reference_default = `---
|
|
3174
|
+
title: Agent Reference
|
|
3175
|
+
description: The model, the authoring standard, and the exact contract of the spec CLI, for coding agents and the people who direct them
|
|
3176
|
+
---
|
|
3177
|
+
|
|
3178
|
+
The reference for a coding agent working in a repository that uses Spec Engine. It has three parts: the model and the working loop, the standard every requirement is written against, and the exact contract of every \`spec\` command.
|
|
3179
|
+
|
|
3180
|
+
## What Spec Engine is
|
|
3181
|
+
|
|
3182
|
+
Spec Engine helps developers, product teams, and agents write better specs and keep them current, so the software changes accurately.
|
|
3183
|
+
|
|
3184
|
+
It works with four things:
|
|
3185
|
+
|
|
3186
|
+
| Noun | What it is | Where it lives | Authored or derived |
|
|
3187
|
+
| --- | --- | --- | --- |
|
|
3188
|
+
| Domain | A named subject area of requirements with an UPPERCASE key (\`BILLING\`), an owner, and a charter that says what belongs in it | \`spec-engine/<KEY>/SPEC.json\`, one file per domain | Authored |
|
|
3189
|
+
| Requirement | One durable rule of the product's behavior, addressed by a permanent id \`KEY-NNN\` (\`BILLING-009\`), with a statement, a why, and a status | An entry in its domain's \`SPEC.json\` | Authored |
|
|
3190
|
+
| Tag | A comment \`@spec KEY-NNN\` that binds a line of code or a test to a requirement | Source and test files, in comments | Authored in code |
|
|
3191
|
+
| Index | A SQLite database of every domain, requirement, and tag, which the read and check commands query | \`.spec-engine/index.sqlite\` at the platform root | Derived |
|
|
3192
|
+
|
|
3193
|
+
- The \`SPEC.json\` files are the only authored truth about what the product promises. Write them through the \`spec\` commands. An id is never reused or renumbered: a changed promise is superseded by a new id, and an ended one is deprecated.
|
|
3194
|
+
- Tags in code are the only truth about where a requirement is implemented and where it is verified. A tag in implementation code implements; a tag in a test file verifies. The kind comes from the file path, never from the tag text.
|
|
3195
|
+
- The index is derived and disposable. Every command rebuilds it from the \`SPEC.json\` files and the tags, and deleting \`.spec-engine/\` is always safe.
|
|
3196
|
+
- Requirements are not tickets. One ticket usually becomes several requirements. A ticket id is provenance only: it goes in \`--issue\`, never in a requirement id or a tag.
|
|
3197
|
+
|
|
3198
|
+
## The loop
|
|
3199
|
+
|
|
3200
|
+
\`\`\`shell
|
|
3201
|
+
spec query "renewal charge" . --json # 1. route: requirements on the topic
|
|
3202
|
+
spec resolve src/billing/renew.ts . --json # or the ones tagged in a file
|
|
3203
|
+
# 2. implement the change
|
|
3204
|
+
# 3. tag the function that produces the behavior and the test that proves it
|
|
3205
|
+
spec check . --ci --json # 4. exit 1: fix before the PR
|
|
3206
|
+
spec guard . --json # 5. exit 1: the change loses a requirement
|
|
3207
|
+
\`\`\`
|
|
3208
|
+
|
|
3209
|
+
When \`spec guard\` blocks, relay its \`\uD83D\uDED1 spec-guard:\` block verbatim to the user. Then either \`spec supersede\` the requirement with a successor or get the user's explicit decision to \`spec deprecate\` it. Never delete a requirement to make the gate pass.
|
|
3210
|
+
|
|
3211
|
+
## What to tag
|
|
3212
|
+
|
|
3213
|
+
\`\`\`ts
|
|
3214
|
+
export function renew() { /* \u2026 */ } // @spec BILLING-009
|
|
3215
|
+
it("charges current price", () => {}) // @spec BILLING-009 unit
|
|
3216
|
+
\`\`\`
|
|
3217
|
+
|
|
3218
|
+
- A tag binds the one function that produces the behavior and the one test that proves it.
|
|
3219
|
+
- Helpers, glue code, and configuration carry no tag.
|
|
3220
|
+
- If you cannot name the behavior a line produces, do not tag it.
|
|
3221
|
+
- A test tag may end with a level: \`unit\`, \`integration\`, or \`e2e\`.
|
|
3222
|
+
|
|
3223
|
+
## In CI
|
|
3224
|
+
|
|
3225
|
+
\`\`\`shell
|
|
3226
|
+
spec check . --ci --base "$(git merge-base HEAD origin/main)"
|
|
3227
|
+
\`\`\`
|
|
3228
|
+
|
|
3229
|
+
Without \`--base\`, \`check\` diffs against \`HEAD\`, which in a CI checkout already holds the change, so the removal and loss checks find nothing to report.
|
|
3230
|
+
|
|
3231
|
+
## Requirement authoring standard
|
|
3232
|
+
|
|
3233
|
+
<!-- @spec CHRT-024 -->
|
|
3234
|
+
|
|
3235
|
+
The rules every new requirement is written against. The statement shapes and the statement rules are also checked at write time and by \`spec check\`.
|
|
3236
|
+
|
|
3237
|
+
0. **A requirement is one rule of the product's behavior, stated from the user's point of view.** The statement rules below are the mechanical half of this rule: \`spec req\`, \`amend\`, \`supersede\`, \`move\`, and \`accept\` refuse a statement that breaks one, \`spec check\` reports an Active statement that breaks one as \`STATEMENT_RULES\`, and \`spec lint\` lists every finding. The judgment half stays with the author.
|
|
3238
|
+
1. **One behavior per requirement.** A ticket usually splits into several requirements. Never mint one requirement per ticket.
|
|
3239
|
+
2. **Write the statement in a fixed shape.** Pick the sentence shape that matches the situation. The word "shall" separates who acts from the observable result:
|
|
3240
|
+
|
|
3241
|
+
| Situation | Shape | Example |
|
|
3242
|
+
| --- | --- | --- |
|
|
3243
|
+
| Always true | The \`<system>\` shall \`<result>\` | The spec CLI shall write JSON output to stdout only. |
|
|
3244
|
+
| Triggered by an event | When \`<trigger>\`, the \`<system>\` shall \`<result>\` | When an unknown requirement id is passed, spec resolve shall print \`[]\` and exit 0. |
|
|
3245
|
+
| Only in a certain state | While \`<state>\`, the \`<system>\` shall \`<result>\` | While the index file is missing, read commands shall rebuild it before answering. |
|
|
3246
|
+
| Handling something going wrong | If \`<failure>\`, then the \`<system>\` shall \`<result>\` | If the results file cannot be parsed, then spec check shall report the file and exit 2. |
|
|
3247
|
+
| Only when a feature is present | Where \`<feature>\`, the \`<system>\` shall \`<result>\` | Where a member config lists ignore directories, the scanner shall skip them. |
|
|
3248
|
+
|
|
3249
|
+
A statement that fits no row has not yet decided what triggers the behavior or what observably happens.
|
|
3250
|
+
|
|
3251
|
+
A statement may end with a trailing \`so <observable consequence>\` clause ("\u2026shall report NO_SPEC_CONFIG at warning severity, so a lone warning never fails the gate") when the consequence is itself observable. The reason the behavior exists belongs in the why, never in the statement.
|
|
3252
|
+
|
|
3253
|
+
One promise per statement. A result that chains two behaviors with \`and\`, or that sets two severities, is two requirements: a single test must be able to prove the whole statement. Prefer \`When\`, \`If \u2026 then\`, and the bare \`shall\` form. Use \`While\` and \`Where\` only when a state or a feature is what gates the behavior.
|
|
3254
|
+
3. **It must make sense on its own.** A reader who sees only this one statement, in search results or a report, understands it. A statement that says "the normalization" or "that prefix" without naming the subject fails.
|
|
3255
|
+
4. **Describe what it does, not how.** Say what a user or calling agent observes. If renaming an internal function would make the statement wrong, it is written at the wrong level.
|
|
3256
|
+
5. **No expiration dates.** No "currently", no "the new behavior", no references to things that were removed.
|
|
3257
|
+
6. **The why states what breaks.** Answer "what goes wrong if this stops being true". Never restate the requirement in other words.
|
|
3258
|
+
7. **Three link types, three jobs.** Tests prove it. Code implements it. Tickets are only history: never an id, never proof.
|
|
3259
|
+
8. **File it where the promise lives, and search first.** Pick the domain whose charter covers the behavior, and run \`spec query\` before minting. If something close exists, amend it instead.
|
|
3260
|
+
|
|
3261
|
+
### Requirement id format
|
|
3262
|
+
|
|
3263
|
+
<!-- @spec CHRT-023 -->
|
|
3264
|
+
|
|
3265
|
+
- Ids are \`KEY-NNN\`: the domain key, a hyphen, and a three-digit zero-padded number (\`BILLING-010\`).
|
|
3266
|
+
- Ids are permanent. A requirement is superseded or deprecated, never renumbered and never deleted.
|
|
3267
|
+
|
|
3268
|
+
### The one-promise lint
|
|
3269
|
+
|
|
3270
|
+
A domain envelope may declare \`grammar: "ears"\` with \`grammarSeverity: "warning" | "error"\` (default warning). Set both with \`spec domain grammar <KEY>\`, never by hand. In such a domain a statement must fit one of the five shapes, and a statement that fits a shape still fails when:
|
|
3271
|
+
|
|
3272
|
+
| Rule | Fails | Passes |
|
|
3273
|
+
| --- | --- | --- |
|
|
3274
|
+
| Semicolon clause | \`; \` anywhere after the first \`shall\` | \`When a; b holds, X shall \u2026\` |
|
|
3275
|
+
| Second subject | \`, and <subject> shall\` after the first \`shall\` | \`X shall A, and shall B\` |
|
|
3276
|
+
| Rationale | \`because\`, \`so that\`, \`in order to\` anywhere | a trailing \`, so <observable consequence>\` |
|
|
3277
|
+
|
|
3278
|
+
- \`spec check\` reports a nonconforming Active or Draft statement as \`STATEMENT_GRAMMAR\` at the declared severity, one row per statement, the detail naming the first failed shape or rule.
|
|
3279
|
+
- \`spec req\`, \`amend\`, \`supersede\`, and \`move\` judge new text at write time: they warn and write under warning, and refuse (exit 2) under error.
|
|
3280
|
+
- The lint is a pattern, not a reading: a statement that passes can still chain two behaviors, and the reviewer still owns that call.
|
|
3281
|
+
- The TERM domain is exempt.
|
|
3282
|
+
|
|
3283
|
+
### The statement rules
|
|
3284
|
+
|
|
3285
|
+
Every domain is judged by these rules, whatever its grammar. \`spec req\`, \`amend\`, \`supersede\`, \`move\` (with new text), and \`accept\` refuse a statement that breaks a rule graded \`error\` (the default): exit 2, one \`<rule>: <detail> \u2014 <reason>\` line per finding, rule 0 last. A rule graded \`warning\` writes the statement and prints the same finding lines on stderr. There is no override flag; the \`statementRules\` block of \`check.json\` grades a rule, tunes it, or turns it off. \`spec check\` reports an Active statement that breaks one as \`STATEMENT_RULES\`, one row per rule per statement. The TERM domain is never judged.
|
|
3286
|
+
|
|
3287
|
+
| Rule | Reports | Printed reason |
|
|
3288
|
+
| --- | --- | --- |
|
|
3289
|
+
| \`one-sentence\` | more than one sentence, no closing period, or a line break | a statement is one testable sentence |
|
|
3290
|
+
| \`length\` | over \`maxLength\` characters (280 by default) | a sentence a reader holds at once |
|
|
3291
|
+
| \`no-file-path\` | a repository path or file name | which file holds what is layout, not behaviour |
|
|
3292
|
+
| \`no-test-narration\` | how a test is written | how a test is written is not what the product does |
|
|
3293
|
+
| \`no-toolchain-layout\` | CI or tooling layout | how CI and tooling are arranged is not a product behaviour |
|
|
3294
|
+
| \`no-history\` | how the requirement changed | a statement says what holds now, not how it got there |
|
|
3295
|
+
| \`no-jargon\` | a word from the jargon list | a reader without the codebase cannot parse project jargon |
|
|
3296
|
+
| \`no-tool-statement\` | which tool is in use | naming the tool in use is a decision record, not a requirement |
|
|
3297
|
+
|
|
3298
|
+
## Authoring requirements (brief \u2192 mint)
|
|
3299
|
+
|
|
3300
|
+
<!-- @spec AUTHOR-013 -->
|
|
3301
|
+
|
|
3302
|
+
<!-- @spec AUTHOR-012 -->
|
|
3303
|
+
|
|
3304
|
+
The procedure for turning a brief or ticket into requirements. Each draft is written to the requirement authoring standard above.
|
|
3305
|
+
|
|
3306
|
+
1. **Split the brief.** One requirement per behavior, never one per ticket.
|
|
3307
|
+
2. **Place each.** \`spec domain list --json\` prints every charter. File the requirement where the promise lives.
|
|
3308
|
+
3. **Dedup.** \`spec query "<phrase>" . --json\` before minting. Overlap means \`spec amend\` or relate the existing requirement.
|
|
3309
|
+
4. **Draft to the standard.** Every draft quotes the phrase of the brief it derives from.
|
|
3310
|
+
5. **Mint.** \`spec req <domain> --text "<statement>" --why "<what breaks>" [--issue <ticket>]\`.
|
|
3311
|
+
6. **Provenance, not identity.** The ticket goes in \`--issue\`, never in an id or a tag.
|
|
3312
|
+
7. **Verify.** \`spec index . && spec check . --ci\` (exit 0) and \`spec guard .\` before the PR.
|
|
3313
|
+
|
|
3314
|
+
## Command reference
|
|
3315
|
+
|
|
3316
|
+
### Invocation
|
|
3317
|
+
|
|
3318
|
+
\`\`\`shell
|
|
3319
|
+
spec <command> [...] # after: bun add -g @spec-engine/spec-engine
|
|
3320
|
+
bunx @spec-engine/spec-engine <command> [...] # without a global install (requires Bun)
|
|
3321
|
+
\`\`\`
|
|
3322
|
+
|
|
3323
|
+
- Almost every command takes an optional trailing \`platformDir\` positional (default: cwd). The platform dir is the directory that contains \`spec-engine/\`.
|
|
3324
|
+
- Every command is non-interactive when stdin is not a TTY.
|
|
3325
|
+
- \`--json\` goes to **stdout only**. Guidance, warnings, and diagnostics chrome go to **stderr**. Parse stdout, surface stderr.
|
|
3326
|
+
|
|
3327
|
+
### Platforms, members, and pins
|
|
3328
|
+
|
|
3329
|
+
- A platform is the directory holding \`spec-engine/\`. When declared, its platform file \`platform-map.json\` is \`{ name, members, ignore }\`.
|
|
3330
|
+
- A member is a repository named in that file, carrying a \`{ platform, member }\` marker. A monorepo's coverage columns are its workspace packages, named by path (\`packages/engine\`). A lone repository with no platform file is one column or its packages. A monorepo's files outside every package are one more column, named by the member or, for a lone monorepo, the directory.
|
|
3331
|
+
- \`spec-engine.member.json\` adds one fact: the member's pin, \`spec-engine@N\`. \`spec init <name>\` declares and pins in one step.
|
|
3332
|
+
- A requirement domain's version is derived from its supersede history. The platform version is the max domain version. Drift compares a member's pin against each referenced requirement's \`changed_at_version\`. A stray \`spec-engine.platform.json\` is ignored with a warning.
|
|
3333
|
+
- A member's \`dependsOn\` (the platform's own package names in its manifest) orders \`spec propagation\`: providers before consumers.
|
|
3334
|
+
|
|
3335
|
+
### Tags
|
|
3336
|
+
|
|
3337
|
+
- The tag is \`@spec KEY-NNN\`, optionally followed by a level token \`unit\` | \`integration\` | \`e2e\` on a test tag. Never write "implements" or "verifies" in the tag.
|
|
3338
|
+
- The scanner reads \`.ts\`, \`.tsx\`, \`.js\`, \`.jsx\`, \`.mjs\`, \`.sh\`, \`.lua\`, \`.luau\`, \`.go\`, \`.rs\`, \`.tf\`, \`.toml\`, \`.sql\`, \`.css\`, \`.yml\`, \`.yaml\`, and \`.astro\` files, any file named \`Dockerfile\` or \`Dockerfile.<suffix>\`, and \`.md\` documents, whose tags document.
|
|
3339
|
+
- The tag regex ignores the comment leader, so \`--\`, \`//\`, \`#\`, \`<!-- -->\`, and \`/* \u2026 */\` all work.
|
|
3340
|
+
- Dot-prefixed directories are skipped except \`.github/\`. \`fixtures/\` directories are ignored everywhere.
|
|
3341
|
+
- A tag in implementation code implements; a tag in a test path verifies. A test path contains \`.test.\`, \`.spec.\`, \`__tests__/\`, \`/tests/\`, \`/e2e/\`, or \`.e2e.\`, or ends in \`_test.go\`, \`_test.rs\`, \`_spec.lua\`, or \`_test.lua\`, or lives under \`spec/\` (busted).
|
|
3342
|
+
- Rust tests inline under \`#[cfg(test)]\` sit in the implementation file and count as implements; put a verifying tag in \`tests/\` or a \`*_test.rs\` file.
|
|
3343
|
+
|
|
3344
|
+
### Exit code contract
|
|
3345
|
+
|
|
3346
|
+
| Code | Meaning |
|
|
3347
|
+
| --- | --- |
|
|
3348
|
+
| 0 | Success. Empty results are success (\`[]\` on stdout, guidance on stderr). |
|
|
3349
|
+
| 1 | Data-level failure: \`check\` found an error-severity diagnostic; \`guard\` found a loss; \`gate\` failed; \`index\` crashed mid-build. |
|
|
3350
|
+
| 2 | Usage/environment error: bad args, not a Spec Engine platform (no \`spec-engine/\`), path-containment violation, invalid \`--limit\`/\`--port\`, FTS5 syntax error. |
|
|
3351
|
+
|
|
3352
|
+
Branch on exit codes, not on output text.
|
|
3353
|
+
|
|
3354
|
+
### Commands
|
|
3355
|
+
|
|
3356
|
+
| Command | Synopsis | \`--json\` | Exits |
|
|
3357
|
+
| --- | --- | --- | --- |
|
|
3358
|
+
| \`spec index [platformDir]\` | Build or refresh the derived index | yes (IndexResult) | 0 / 1 / 2 |
|
|
3359
|
+
| \`spec check [platformDir] [--ci]\` | Integrity + coverage + drift diagnostics | yes (array) | 0 / 1 / 2 |
|
|
3360
|
+
| \`spec guard [platformDir] [--against <ref>]\` | Block a change about to lose a requirement, its last implementation, or its last test | yes (array) | 0 / 1 / 2 |
|
|
3361
|
+
| \`spec map [platformDir]\` | Requirement \xD7 repo coverage matrix | yes (array) | 0 / 2 |
|
|
3362
|
+
| \`spec get <KEY-NNN> [platformDir]\` | One requirement's full record and its tag sites; unknown id is \`[]\` | yes (object, or \`[]\`) | 0 / 2 |
|
|
3363
|
+
| \`spec list [platformDir] [--domain KEY] [--status S]\` | Every requirement record in (key, seq) order, history included | yes (array) | 0 / 2 |
|
|
3364
|
+
| \`spec query <text> [platformDir]\` | Full-text retrieval over requirements | yes (array) | 0 / 2 |
|
|
3365
|
+
| \`spec relations [platformDir]\` | Mermaid graph of \`relates\` links | yes (array) | 0 / 2 |
|
|
3366
|
+
| \`spec provenance [issueId] [platformDir]\` | Per-requirement provenance matrix | yes (array) | 0 / 2 |
|
|
3367
|
+
| \`spec resolve <files\u2026> [platformDir]\` | Requirements tagged in the given files | yes (array) | 0 / 2 |
|
|
3368
|
+
| \`spec propagation <KEY-NNN> [platformDir]\` | Per-repo migrated/drifted state for a superseded requirement | yes (array) | 0 / 2 |
|
|
3369
|
+
| \`spec gate <repo> <KEY-NNN> [platformDir]\` | Approval gate: pass iff Active and the pin covers it | yes (object) | 0 / 1 / 2 |
|
|
3370
|
+
| \`spec init [repo]\` | Declare a member through platform-map and write its \`spec-engine.member.json\` pin | yes (object) | 0 / 2 |
|
|
3371
|
+
| \`spec domain new <KEY>\` / \`spec domain list\` / \`spec domain grammar <KEY>\` | Scaffold / list \`spec-engine/<KEY>/SPEC.json\` / set its statement grammar and severity | \`list\`: yes (array); \`grammar\`: yes (object) | 0 / 2 |
|
|
3372
|
+
| \`spec migrate [platformDir]\` | Strip the retired \`livesIn\` and \`verifiedIn\` fields from every \`SPEC.json\` (idempotent) | yes (object) | 0 / 2 |
|
|
3373
|
+
| \`spec req <domain-prefix> [platformDir]\` | Piped: next unused id. \`--text\`: author non-interactively | yes (object) | 0 / 2 |
|
|
3374
|
+
| \`spec term \u2026\` | Author, list, revise, and confirm glossary terms | yes | 0 / 2 |
|
|
3375
|
+
| \`spec glossary [platformDir]\` | Generate \`GLOSSARY.md\` from the TERM store; \`--migrate\`, \`--check\` | yes (object) | 0 / 1 / 2 |
|
|
3376
|
+
| \`spec deprecate <KEY-NNN> --reason "\u2026" [platformDir]\` | End an Active/Draft requirement with a recorded reason | yes (object) | 0 / 2 |
|
|
3377
|
+
| \`spec supersede <KEY-NNN> [platformDir]\` | Flip to superseded, mint the successor, retag every site to it | yes (object) | 0 / 2 |
|
|
3378
|
+
| \`spec move <KEY-NNN> <NEW-DOMAIN> [platformDir]\` | Cross-domain supersede | yes (object) | 0 / 2 |
|
|
3379
|
+
| \`spec amend <KEY-NNN> [platformDir]\` | Revise an unshipped entry in place | yes (object) | 0 / 2 |
|
|
3380
|
+
| \`spec accept <KEY-NNN> [platformDir]\` | Promote a Draft requirement to Active | yes (object) | 0 / 2 |
|
|
3381
|
+
| \`spec lint [KEY \\| KEY-NNN] [platformDir] [--all]\` | Every statement-rule finding for the platform, a domain, or a requirement | yes (array) | 0 / 1 / 2 |
|
|
3382
|
+
| \`spec serve [platformDir] [--port N] [--host IP]\` | Local webapp + \`/api/*\` over the index | n/a (HTTP) | 0 / 1 / 2 |
|
|
3383
|
+
| \`spec docs [--port N]\` | Serve the bundled docs site offline | n/a (HTTP) | 0 / 1 / 2 |
|
|
3384
|
+
| \`spec mcp [platformDir]\` | MCP server over stdio | n/a (JSON-RPC) | 0 / 2 |
|
|
3385
|
+
|
|
3386
|
+
Common flags on the index-touching commands (\`index\`, \`check\`, \`map\`, \`get\`, \`list\`, \`query\`, \`relations\`, \`resolve\`, \`propagation\`, \`provenance\`, \`gate\`, \`serve\`):
|
|
3387
|
+
|
|
3388
|
+
| Flag | Effect |
|
|
3389
|
+
| --- | --- |
|
|
3390
|
+
| \`--out <path>\` | Index DB path override. Must resolve inside \`platformDir\` (exit 2 otherwise). |
|
|
3391
|
+
| \`--no-prompt\` | Suppress the member-onboarding prompt. |
|
|
3392
|
+
| \`--json\` | Machine output (all except \`serve\`). |
|
|
3393
|
+
| \`--fresh\` | Read commands only: delete the DB plus WAL/SHM and reindex before answering. |
|
|
3394
|
+
|
|
3395
|
+
\`init\`, \`domain list\`, \`req\` register \`--json\` only. \`domain new\` registers none.
|
|
3396
|
+
|
|
3397
|
+
**Index freshness.** Read commands (\`map\`, \`get\`, \`list\`, \`query\`, \`relations\`, \`resolve\`, \`propagation\`, \`provenance\`) build the index when it is missing and otherwise trust it. A warm index older than a changed \`SPEC.json\` produces a stderr warning. Tag-only code edits are not detected. \`gate\` always rebuilds cold, and so does \`check --ci\`. After editing specs or tags, pass \`--fresh\`.
|
|
3398
|
+
|
|
3399
|
+
### spec index
|
|
3400
|
+
|
|
3401
|
+
Build or refresh the derived index.
|
|
3402
|
+
|
|
3403
|
+
\`--json\`: \`{ build_id, repos, domains, requirements, tags, diagnostics }\`.
|
|
3404
|
+
|
|
3405
|
+
Exit: 0 built / 1 an invalid SPEC.json refused the build, or the build crashed / 2 not a platform or bad args.
|
|
3406
|
+
|
|
3407
|
+
- \`build_id\` is deterministic for identical inputs (cold-rebuild equivalence).
|
|
3408
|
+
- A \`SPEC.json\` that is not JSON or fails the shared schema refuses the whole build, and the index on disk is left as it was. Every file is parsed first, so one run reports every invalid file. Under \`--json\` stdout holds only the \`INVALID_DOMAIN_FILE\` rows in the \`spec check --json\` row shape; text mode prints only those rows on stderr. Every other index-building command prints only the same rows and exits 1 (\`check\`, \`gate\`, \`guard\`, \`serve\`, and the read commands); a lifecycle command that reindexes for its retag (\`supersede\`, \`amend\`, \`deprecate\`, \`move\`, \`term confirm\`) exits 2 after its write.
|
|
3409
|
+
- Also writes \`.spec-engine/doctor.md\`, a list of doc lines that mention a requirement id without an explicit \`<!-- @spec ID -->\` binding.
|
|
3410
|
+
|
|
3411
|
+
### spec check
|
|
3412
|
+
|
|
3413
|
+
Integrity, coverage, drift, and governance diagnostics.
|
|
3414
|
+
|
|
3415
|
+
| Flag | Effect |
|
|
3416
|
+
| --- | --- |
|
|
3417
|
+
| \`--ci\` | Delete the DB first. Correctness never trusts a warm index. |
|
|
3418
|
+
| \`--results <junit.xml>\` | The trusted-red gate: a verifying tag counts only when its test passed. Path resolves inside \`platformDir\`. |
|
|
3419
|
+
| \`--base <ref>\` | Diff the requirement set against a git ref (deletion and status-flip governance). |
|
|
3420
|
+
| \`--approved-by <list>\` | Approver set for the status-flip check. |
|
|
3421
|
+
| \`--require-owner-approval\` | Escalate \`UNAPPROVED_STATUS_FLIP\` from warning to error. |
|
|
3422
|
+
| \`--unsourced-change\` | Also warn on superseded requirements with no \`supersedes-via\` issue. |
|
|
3423
|
+
|
|
3424
|
+
\`--json\` rows: \`{ code, severity, repo, source_file, line, req_id, detail }\`, nullable except \`code\`, \`severity\`, \`detail\`, sorted deterministically.
|
|
3425
|
+
|
|
3426
|
+
Exit: 0 clean or warnings only / 1 any error-severity row / 2 usage.
|
|
3427
|
+
|
|
3428
|
+
<!-- @spec CHCK-053 -->
|
|
3429
|
+
|
|
3430
|
+
Codes, in \`DiagnosticCode\` order:
|
|
3431
|
+
|
|
3432
|
+
| Code | Severity | Condition |
|
|
3433
|
+
| --- | --- | --- |
|
|
3434
|
+
| \`DUP_ID\` | error | The same requirement id appears twice |
|
|
3435
|
+
| \`BROKEN_SUPERSEDE\` | error | A \`supersededBy\` names a missing id, or a \`supersedes\` names a missing id or the entry itself |
|
|
3436
|
+
| \`BAD_STATUS\` | error | An entry status outside \`active\` / \`draft\` / \`superseded\` / \`deprecated\` |
|
|
3437
|
+
| \`DANGLING_TAG\` | error | A tag references no requirement |
|
|
3438
|
+
| \`SUPERSEDED_REFERENCED\` | error | Code still tags a superseded requirement (retag to the successor) |
|
|
3439
|
+
| \`DEPRECATED_REFERENCED\` | error | Code still tags a deprecated requirement |
|
|
3440
|
+
| \`STATEMENT_GRAMMAR\` | per domain, default warning | A statement fails its domain's declared grammar |
|
|
3441
|
+
| \`STATEMENT_RULES\` | error; per rule from \`statementRules.severity\` | An Active statement breaks a statement rule: one row per rule per statement, the detail \`<rule>: <detail> \u2014 <reason>\` |
|
|
3442
|
+
| \`DRAFT_REFERENCED\` | error on a verifying tag, warning on an implementing or documenting tag | A tag references a Draft requirement |
|
|
3443
|
+
| \`GLOSSARY_DRIFT\` | warning | The committed glossary differs from the one generated from the term store |
|
|
3444
|
+
| \`ORPHAN_REQ\` | error | An Active requirement has no implementing tag |
|
|
3445
|
+
| \`UNVERIFIED_REQ\` | warning | An Active requirement is implemented but has no verifying tag |
|
|
3446
|
+
| \`DRIFT\` | error | A member is pinned behind the version at which a requirement it references changed |
|
|
3447
|
+
| \`NO_SPEC_CONFIG\` | warning | A declared member is present on disk with no \`spec-engine.member.json\` |
|
|
3448
|
+
| \`BROKEN_RELATES\` | warning | A \`relates\` entry names a nonexistent requirement |
|
|
3449
|
+
| \`RELATES_SUPERSEDED\` | warning | A \`relates\` entry names a superseded requirement |
|
|
3450
|
+
| \`CYCLIC_SUPERSEDE\` | error | A circular supersession chain |
|
|
3451
|
+
| \`SELF_RELATES\` | warning | A \`relates\` entry names its own requirement (dropped, but surfaced) |
|
|
3452
|
+
| \`UNKNOWN_ROLE\` | warning | An \`issues\` entry uses a role outside \`created\` / \`supersedes-via\` / \`amends-via\` (surfaced and dropped, never stored) |
|
|
3453
|
+
| \`UNSOURCED_CHANGE\` | warning, only with \`--unsourced-change\` | A superseded requirement carries no \`supersedes-via\` issue |
|
|
3454
|
+
| \`INVALID_DOMAIN_FILE\` | error | A \`SPEC.json\` is not JSON or fails the shared schema; one row per failing field |
|
|
3455
|
+
| \`MIGRATION_PENDING\` | warning | A \`SPEC.json\` still carries the retired \`livesIn\` or \`verifiedIn\` fields; run \`spec migrate\` |
|
|
3456
|
+
| \`UNPROVEN_REQ\` | error, only with \`--results\` | An Active requirement has verifying tags but no passing correlated test |
|
|
3457
|
+
| \`PROOFS_UNCONFIRMED\` | warning, only without \`--results\` | The proof gate fell back to presence-only mode |
|
|
3458
|
+
| \`REQUIREMENT_REMOVED\` | error | An Active requirement present at the base ref is gone with no same-change supersession |
|
|
3459
|
+
| \`IMPL_LOST\` | error, when a base ref resolves | The change removes the last implementing tag of a surviving Active requirement |
|
|
3460
|
+
| \`VERIFY_LOST\` | warning, when a base ref resolves | The change removes the last verifying tag of a surviving Active requirement |
|
|
3461
|
+
| \`SPEC_FILE_DELETED\` | error, when a base ref resolves | A \`SPEC.json\` present at the base ref is gone |
|
|
3462
|
+
| \`UNAPPROVED_STATUS_FLIP\` | warning; error with \`--require-owner-approval\` | A flip to superseded or deprecated whose CODEOWNERS domain owner is absent from \`--approved-by\` |
|
|
3463
|
+
| \`PARTIAL_PROPAGATION\` | error, needs \`--base\` and \`--results\` | A changed requirement where some bound tests pass and some fail |
|
|
3464
|
+
| \`MALFORMED_FILE\` | platform-map's severity: error, or warning for a manifest | A platform file, a package or workspace manifest, or platform-map's per-user file failed to parse or validate |
|
|
3465
|
+
| \`MEMBER_MISSING\` | warning | A declared member is not on this machine; it is not scanned |
|
|
3466
|
+
| \`MARKER_MISSING\` | warning | A declared member has no marker |
|
|
3467
|
+
| \`MARKER_MISMATCH\` | error | A member's marker names a different platform |
|
|
3468
|
+
| \`UNLISTED_REPO\` | warning (platform-map info, promoted) | A repository in the platform folder is not a member; its tags are not scanned |
|
|
3469
|
+
| \`PLATFORM_NOT_LOCATED\` | warning | A marker names a platform this machine cannot find |
|
|
3470
|
+
| \`UNDECLARED_PLATFORM\` | warning (platform-map info, promoted) | A folder of repositories with no platform file; nothing in it is a member |
|
|
3471
|
+
| \`SCAN_TRUNCATED\` | warning | A workspace walk hit its depth or entry cap |
|
|
3472
|
+
| \`UNDEFINED_TERM\` | error | A \`cites\` entry resolves to no glossary term |
|
|
3473
|
+
| \`ORPHAN_TERM\` | warning | An Active term that nothing cites |
|
|
3474
|
+
| \`TERM_DRIFT\` | warning | A citation's pin is behind the term's current version |
|
|
3475
|
+
| \`SUPERSEDED_TERM_REFERENCED\` | error | A citation names a superseded term |
|
|
3476
|
+
|
|
3477
|
+
The four TERM codes are the last four rows. Only \`error\` rows drive exit 1.
|
|
3478
|
+
|
|
3479
|
+
The eight codes before them are platform-map's, surfaced with the platform-map code: every platform-map error and warning keeps its severity; \`UNLISTED_REPO\` and \`UNDECLARED_PLATFORM\` are promoted from info to warning because they mean a repository's tags are not scanned; \`UNMATCHED_PATTERN\` and \`AMBIGUOUS_ECOSYSTEM\` are not surfaced. \`repo\` is the diagnostic's subject when it names a member; \`source_file\` is the subject. \`NO_SPEC_CONFIG\` stays Spec Engine's own code: a declared member present on disk with no \`spec-engine.member.json\`.
|
|
3480
|
+
|
|
3481
|
+
- Deletion detection runs by default: with no \`--base\`, the working tree is diffed against \`HEAD\` whenever git resolves. An Active requirement absent from the tree without a same-change supersession is \`REQUIREMENT_REMOVED\`. History (superseded and deprecated entries) may never be deleted. Outside git the check stays quiet.
|
|
3482
|
+
- Without \`--results\`, \`check\` emits \`PROOFS_UNCONFIRMED\` and skips proof enforcement. The results file is never hashed into \`build_id\`. Generate it with the test runner's JUnit reporter, for example \`bun test --reporter=junit --reporter-outfile=.spec-engine/results.xml\`.
|
|
3483
|
+
- Term citations: \`UNDEFINED_TERM\`, \`SUPERSEDED_TERM_REFERENCED\`, and \`TERM_DRIFT\` clear with \`spec term confirm\`.
|
|
3484
|
+
- Losses: whenever a base ref resolves (\`--base\`, else \`HEAD\`), \`check\` reports the \`IMPL_LOST\`, \`VERIFY_LOST\`, and \`SPEC_FILE_DELETED\` rows \`spec guard\` would report against that ref. Guard's \`REQUIREMENT_REMOVED\` rows are dropped; the removal check already reports that id.
|
|
3485
|
+
- Under \`--ci\` with no \`--base\`, a working tree whose tracked files equal \`HEAD\` makes every loss check pass vacuously, so \`check\` warns on stderr with the line to run instead: \`spec check . --ci --base "$(git merge-base HEAD origin/<default>)"\`. The exit code and stdout are unchanged. <!-- @spec GUARD-031 -->
|
|
3486
|
+
- Coverage counts only Active requirements: \`ORPHAN_REQ\` and \`UNVERIFIED_REQ\` fire for Active entries only. <!-- @spec CHCK-032 -->
|
|
3487
|
+
|
|
3488
|
+
#### check.json
|
|
3489
|
+
|
|
3490
|
+
\`spec-engine/check.json\` regrades diagnostics. Absent means no rules.
|
|
3491
|
+
|
|
3492
|
+
\`\`\`json
|
|
3493
|
+
{ "severity": [
|
|
3494
|
+
{ "code": "UNVERIFIED_REQ", "severity": "error" },
|
|
3495
|
+
{ "code": "UNVERIFIED_REQ", "severity": "warning", "match": ["LEGACY", "BILLING-004"] }
|
|
3496
|
+
] }
|
|
3497
|
+
\`\`\`
|
|
3498
|
+
|
|
3499
|
+
- \`code\` is any diagnostic code; \`severity\` is \`error\`, \`warning\`, or \`off\`.
|
|
3500
|
+
- \`match\` lists domain keys and requirement ids. A scoped rule applies only to rows whose \`req_id\` is listed or carries a listed key.
|
|
3501
|
+
- The most specific rule wins: an id match, then a domain match, then an unscoped rule. Among equals, the later rule wins.
|
|
3502
|
+
- \`off\` drops the row from every output.
|
|
3503
|
+
- Grading runs inside the operation, so the CLI, \`spec_check\`, and every other surface agree, and exit 1 follows the graded severities.
|
|
3504
|
+
- A file that fails the schema (unknown code or key, a bad severity, a malformed match entry) exits 2 with \`spec-engine/check.json: <path>: <reason>\`.
|
|
3505
|
+
|
|
3506
|
+
\`statementRules\` configures the statement rules on every surface: the write-time refusal, \`STATEMENT_RULES\`, and \`spec lint\`.
|
|
3507
|
+
|
|
3508
|
+
\`\`\`json
|
|
3509
|
+
{ "severity": [],
|
|
3510
|
+
"statementRules": {
|
|
3511
|
+
"maxLength": 280,
|
|
3512
|
+
"jargon": ["seam", "rung"],
|
|
3513
|
+
"exemptNames": ["SPEC.json", "spec-engine/"],
|
|
3514
|
+
"severity": { "no-jargon": "off", "length": "error" }
|
|
3515
|
+
} }
|
|
3516
|
+
\`\`\`
|
|
3517
|
+
|
|
3518
|
+
- \`maxLength\` defaults to 280. \`jargon\` replaces the default list (\`seam\`, \`seams\`, \`rung\`, \`rungs\`, \`residue\`, \`matrix entry\`, \`stage runner\`, \`the stage\`).
|
|
3519
|
+
- \`exemptNames\` are product names \`no-file-path\` never reports. A name ending in \`/\` exempts every path under it.
|
|
3520
|
+
- \`severity\` maps a rule to \`error\`, \`warning\`, or \`off\`. A rule with no entry is \`error\`. \`error\` refuses the write and grades the \`STATEMENT_RULES\` row error; \`warning\` writes, prints the finding on stderr, and grades the row warning; \`off\` turns the rule off everywhere. The \`severity\` array regrades by code after it.
|
|
3521
|
+
- The rules: \`one-sentence\`, \`length\`, \`no-file-path\`, \`no-test-narration\`, \`no-toolchain-layout\`, \`no-history\`, \`no-jargon\`, \`no-tool-statement\`. Unknown keys exit 2.
|
|
3522
|
+
|
|
3523
|
+
### spec guard
|
|
3524
|
+
|
|
3525
|
+
The pre-commit loss gate. Diffs the requirement derivation at \`--against <ref>\` (default \`HEAD\`) against the working tree, scoped to the files in \`git diff\`. Rebuilds the worktree index cold every run.
|
|
3526
|
+
|
|
3527
|
+
\`--json\` rows: \`{ kind, req_id, file, line, detail }\`, sorted by \`(req_id, kind, file, line)\`. \`kind\` is one of \`REQUIREMENT_REMOVED\`, \`IMPL_LOST\`, \`VERIFY_LOST\`, \`SPEC_FILE_DELETED\` (\`req_id\` null).
|
|
3528
|
+
|
|
3529
|
+
Exit: 0 clean / 1 any loss / 2 usage.
|
|
3530
|
+
|
|
3531
|
+
- A loss is suppressed only by ending the requirement in the same change: supersede it (either direction) or \`spec deprecate\` it. There is no override comment.
|
|
3532
|
+
- A non-git tree, a repo with no \`HEAD\`, or an unknown ref prints \`NOT_A_GIT_REPO\` to stderr and exits 0.
|
|
3533
|
+
- A ref that begins with \`-\` or holds a character outside \`A-Z a-z 0-9 . _ / -\` is refused before git runs: \`guard\` treats it as an unknown ref, and \`check --base\` exits 2.
|
|
3534
|
+
- Text mode prints a \`\uD83D\uDED1 spec-guard:\` block per requirement, written in the second person. Relay it verbatim to the user before proceeding.
|
|
3535
|
+
|
|
3536
|
+
### spec map
|
|
3537
|
+
|
|
3538
|
+
\`--json\` rows keyed \`(req_id, repo)\`: \`{ req_id, domain_key, req_status, req_spec_version, req_changed_at_version, repo, repo_pin, implemented: 0|1, verified: 0|1, test_levels }\`. Text mode renders \`src\`, \`test\`, \`src+test\`, \`\u2014\`.
|
|
3539
|
+
|
|
3540
|
+
Exit: 0 / 2.
|
|
3541
|
+
|
|
3542
|
+
### spec get
|
|
3543
|
+
|
|
3544
|
+
One requirement's full record from the derived index, with the code sites its tags name. Tags are the only record of where a requirement is implemented and verified.
|
|
3545
|
+
|
|
3546
|
+
\`--json\`: the \`Requirement\` row \`{ id, key, seq, status, superseded_by, text, why, source_file, line, spec_version, changed_at_version, superseded_at_version }\` plus \`implemented_in: [{ repo, file, line, kind }]\` (implementing and documenting tags) and \`verified_in: [{ repo, file, line, level }]\` (verifying tags), each sorted by (repo, file, line). The same object is served by \`GET /api/requirements/:id\` and \`spec_get\`, and shown on the webapp \`/requirements/:id\` page. Text mode is a one-row table (\`ID STATUS KEY SEQ CHANGED_AT TEXT\`) followed by an \`implemented in:\` and a \`verified in:\` list of \`file:line\`.
|
|
3547
|
+
|
|
3548
|
+
Exit: 0 / 2. An unknown id prints \`[]\` on stdout, guidance on stderr, and exits 0. A malformed id exits 2.
|
|
3549
|
+
|
|
3550
|
+
### spec list
|
|
3551
|
+
|
|
3552
|
+
Every requirement record in (key, seq) order. Unlike \`query\`, superseded and deprecated rows are included and nothing is ranked.
|
|
3553
|
+
|
|
3554
|
+
| Flag | Effect |
|
|
3555
|
+
| --- | --- |
|
|
3556
|
+
| \`--domain KEY\` | Only that domain (case-insensitive key). |
|
|
3557
|
+
| \`--status S\` | Only that status: \`active\`, \`draft\`, \`superseded\`, \`deprecated\`. Any other word exits 2. |
|
|
3558
|
+
|
|
3559
|
+
\`--json\`: an array of \`Requirement\` rows, the same rows \`GET /api/requirements\` serves. Text mode is the \`ID STATUS KEY SEQ CHANGED_AT TEXT\` table.
|
|
3560
|
+
|
|
3561
|
+
Exit: 0 / 2. An empty result is \`[]\` under \`--json\`, guidance on stderr in text mode, exit 0.
|
|
3562
|
+
|
|
3563
|
+
### spec query
|
|
3564
|
+
|
|
3565
|
+
FTS5 \`MATCH\` syntax. \`--limit N\` (default 10, max 1000).
|
|
3566
|
+
|
|
3567
|
+
Exit: 0 / 2 (bare \`AND\` or \`OR\` is an FTS5 grammar error).
|
|
3568
|
+
|
|
3569
|
+
- Wrap multi-word phrases in double quotes inside the shell-quoted arg.
|
|
3570
|
+
- Results rank ascending (best first). Superseded requirements are excluded.
|
|
3571
|
+
- Glossary term definitions ride the same index. A matching term appears in a separate Terms group in text mode, discriminated by \`key\` under \`--json\`.
|
|
3572
|
+
|
|
3573
|
+
### spec relations
|
|
3574
|
+
|
|
3575
|
+
Text mode: mermaid \`graph LR\`, one node per requirement in a \`relates\` field, one undirected deduped edge per pair. \`--json\` rows: \`{ from_id, to_id, source_file, line }\`, sorted by (from, to).
|
|
3576
|
+
|
|
3577
|
+
Exit: 0 / 2. Empty graph is \`[]\` and exit 0.
|
|
3578
|
+
|
|
3579
|
+
### spec provenance
|
|
3580
|
+
|
|
3581
|
+
Per requirement: the creating issue, the revising issues (\`supersedes-via\`, \`amends-via\`), the backing tests, and the \`source_file:line\` pointer.
|
|
3582
|
+
|
|
3583
|
+
| Flag | Effect |
|
|
3584
|
+
| --- | --- |
|
|
3585
|
+
| \`[issueId]\` positional | Display-only reverse filter. Never a join key. |
|
|
3586
|
+
| \`--resolve-issues\` | Overlay tracker title/status/URL. Needs \`SPEC_TRACKER_TOKEN\`; degrades to bare ids without it. |
|
|
3587
|
+
|
|
3588
|
+
\`--json\` rows: \`{ req_id, role, issue_id, source_file, line, req_status, implemented, verified, test_levels }\`, sorted on the full composite key.
|
|
3589
|
+
|
|
3590
|
+
Exit: 0 / 2. Empty matrix is \`[]\` and exit 0.
|
|
3591
|
+
|
|
3592
|
+
### spec resolve
|
|
3593
|
+
|
|
3594
|
+
Accepts multiple positionals, comma-split inside one positional, and absolute paths under the platform dir.
|
|
3595
|
+
|
|
3596
|
+
| Flag | Effect |
|
|
3597
|
+
| --- | --- |
|
|
3598
|
+
| \`--req KEY-NNN\` | Reverse query: every tag site for that requirement. Takes no file positionals. |
|
|
3599
|
+
|
|
3600
|
+
\`--json\` rows (\`--req\`): \`{ req_id, repo, file, line, kind, level }\` sorted by (repo, file, line). \`kind\` is \`implements\`, \`verifies\`, or \`documents\`.
|
|
3601
|
+
|
|
3602
|
+
Exit: 0 / 2. Empty result is \`[]\` and exit 0. Unknown id under \`--req\` is \`[]\`.
|
|
3603
|
+
|
|
3604
|
+
Path rules. \`tags.file\` is platform-relative: \`<member>/<path>\` for a file inside a declared member or a workspace package, and the bare \`<path>\` for a file a lone repository owns itself (its directory-named or \`root\` column). The key never carries the checkout's folder name.
|
|
3605
|
+
|
|
3606
|
+
- Multi-repo platform: pass \`<repo>/<path>\`, e.g. \`spec resolve api/src/renew.ts .\`
|
|
3607
|
+
- Single repo: pass the path from the platform root, e.g. \`spec resolve src/orders.ts .\` or \`spec resolve .github/workflows/ci.yml .\`
|
|
3608
|
+
- Absolute paths are normalized against \`platformDir\`. A path outside it exits 2.
|
|
3609
|
+
|
|
3610
|
+
### spec propagation
|
|
3611
|
+
|
|
3612
|
+
One row per member repo: \`{ repo, state, via_req_id, drifted }\`. \`state\` is one of \`MIGRATED_VERIFIED\`, \`MIGRATED_UNVERIFIED\`, \`ON_PREDECESSOR\`, \`ON_OTHER_DOMAIN_REQ\`, \`NO_DOMAIN_REFERENCE\`.
|
|
3613
|
+
|
|
3614
|
+
Rows are in dependency order: a member comes after every member it depends on, read from platform-map's \`dependsOn\` (the platform's own package names in a member's manifest), and members at the same depth come by name. The same order is served by \`/api/propagation/:id\`, \`spec_propagation\`, and the webapp's \`/propagation/:id\` page. A platform with no in-platform dependencies lists members by name.
|
|
3615
|
+
|
|
3616
|
+
Exit: 0 / 2.
|
|
3617
|
+
|
|
3618
|
+
### spec gate
|
|
3619
|
+
|
|
3620
|
+
\`--json\`: \`{ pass, reason, repo, req_id, detail, status, changed_at_version, pinned_spec_version }\`. \`reason\` is one of \`PASS\`, \`NOT_FOUND\`, \`DRAFT\`, \`SUPERSEDED\`, \`DEPRECATED\`, \`VERSION_PIN\`, decided in that order. Pin equality passes.
|
|
3621
|
+
|
|
3622
|
+
Exit: 0 pass / 1 fail / 2 unknown repo name or bad args.
|
|
3623
|
+
|
|
3624
|
+
### spec init
|
|
3625
|
+
|
|
3626
|
+
Declares a member through platform-map and writes \`spec-engine.member.json\`.
|
|
3627
|
+
|
|
3628
|
+
| Flag | Effect |
|
|
3629
|
+
| --- | --- |
|
|
3630
|
+
| \`--specs spec-engine@N\` | Override the pin. |
|
|
3631
|
+
| \`--force\` | Rewrite an existing config, preserving \`ignore\`. Any other key refuses, as does a malformed \`ignore\`. |
|
|
3632
|
+
| \`--platform <dir>\` | The platform root, for a declared member checked out outside the platform folder. Its location is recorded in platform-map's per-user file first. |
|
|
3633
|
+
|
|
3634
|
+
\`--json\`: \`{ action: "wrote", path, pin, source, declared, linked, agent_surface }\`, \`{ action: "already-configured", path, pin, extra_fields, declared, linked, agent_surface }\`, or, on a platform directory, \`{ action: "surface-only", agent_surface }\`. \`declared\` lists the platform-map files written (the platform file, the marker), empty when the member was already declared; \`linked\` is the per-user file written under \`--platform\`, else null; \`agent_surface\` lists the agent-surface files written or changed, \`[]\` on a re-run.
|
|
3635
|
+
|
|
3636
|
+
Exit: 0 wrote or already configured / 2. Branch on \`action\`, not the exit code.
|
|
3637
|
+
|
|
3638
|
+
- Where the target sits decides what is written:
|
|
3639
|
+
- a repository directly under a platform folder (a directory holding \`spec-engine/\`) that the platform file does not list is declared: a \`platform-map.json\` platform file entry at the root (the file is created when absent) and a \`{ platform, member }\` marker in the repository, then the pin. platform-map declares only a directory with a \`.git\` entry or a \`package.json\`; a plain folder is refused with exit 2;
|
|
3640
|
+
- a declared member only gets the pin (a missing marker is written back);
|
|
3641
|
+
- a workspace package of a monorepo gets a nested pin and no declaration; a repository inside a lone monorepo that is not a workspace package is refused;
|
|
3642
|
+
- a directory under no platform gets the \`spec-engine@1\` fallback pin;
|
|
3643
|
+
- a platform directory (holding \`spec-engine/\`) gets the agent surface and no pin.
|
|
3644
|
+
- Every action then installs the agent surface into the repository (the member; the monorepo root for a workspace package), bundled in the npm package under \`dist/agent/\`. Hints for anything left alone go to stderr.
|
|
3645
|
+
- \`AGENTS.md\`: the agent reference between a \`spec-engine:agent-reference:begin\` and a \`spec-engine:agent-reference:end\` HTML comment marker, appended or created, replaced in place when the markers exist; text outside them is never touched. A \`CLAUDE.md\` that does not mention \`AGENTS.md\` gets a hint only.
|
|
3646
|
+
- \`.claude/skills/req-author/\`: written when absent; a differing copy is replaced only under \`--force\`.
|
|
3647
|
+
- \`.mcp.json\`: a \`spec\` server entry (\`spec mcp <platform>\`) merged in; other keys and an existing \`spec\` entry are kept.
|
|
3648
|
+
- \`pre-commit\`: \`spec guard <platform>\`, written to \`.githooks/\` or else \`.git/hooks/\` when no hook exists there; an existing hook gets a hint.
|
|
3649
|
+
- With no platform located, the MCP entry and the hook are skipped.
|
|
3650
|
+
- The platform root is platform-map's \`locate()\`: the nearest \`platform-map.json\` or \`.git\` boundary, a marker resolved through the parent directory or the per-user file; the parent directory when it holds \`spec-engine/\`. A \`.git\` boundary between the target and an outer \`spec-engine/\` hides that platform.
|
|
3651
|
+
- Default pin: \`--specs\`, else the derived platform version, else \`spec-engine@1\` with a printed note.
|
|
3652
|
+
- A resolved path with a \`spec-engine\` segment is refused, except on the engine's own checkout (root package named \`@spec-engine/spec-engine\`), where only the path below the platform root is judged.
|
|
3653
|
+
- \`ignore: ["dir", \u2026]\` excludes repo-relative directory prefixes from that repo's scans: \`vendor\` hides \`vendor/x.ts\` but not \`src/vendor/x.ts\`. A monorepo root column excludes its workspace package paths the same way. A monorepo's columns are its workspace manifest's packages.
|
|
3654
|
+
|
|
3655
|
+
### spec domain
|
|
3656
|
+
|
|
3657
|
+
\`spec domain new <KEY>\` scaffolds \`spec-engine/<KEY>/SPEC.json\`. \`spec domain list\` prints one key per line; \`--json\` prints a sorted array of \`{ key, scope }\` (scope is the charter sentence or null), read from the filesystem.
|
|
3658
|
+
|
|
3659
|
+
\`spec domain grammar <KEY> [platformDir] [--grammar ears|freeform] [--severity warning|error]\` sets the envelope's \`grammar\` and/or \`grammarSeverity\` through the validating write path; every other field is written back as read. \`--json\` prints \`{ key, grammar, grammar_severity, file }\` (an unset field is null). An unknown domain, neither flag, or a value outside the enums exits 2. CLI only: no MCP tool or HTTP route.
|
|
3660
|
+
|
|
3661
|
+
\`\`\`shell
|
|
3662
|
+
spec domain grammar CHCK --severity error
|
|
3663
|
+
\`\`\`
|
|
3664
|
+
|
|
3665
|
+
Exit: 0 / 2.
|
|
3666
|
+
|
|
3667
|
+
### spec migrate
|
|
3668
|
+
|
|
3669
|
+
Rewrites every \`SPEC.json\` that still carries the retired \`livesIn\` or \`verifiedIn\` fields through the write seam, which drops them. A file without them is not touched, so a second run writes nothing. Until a file is migrated, the fields are read and ignored, and the next write of a file drops them.
|
|
3670
|
+
|
|
3671
|
+
\`--json\`: \`{ files }\`, the platform-relative paths rewritten.
|
|
3672
|
+
|
|
3673
|
+
Exit: 0 / 2.
|
|
3674
|
+
|
|
3675
|
+
### spec req
|
|
3676
|
+
|
|
3677
|
+
| Mode | Behavior |
|
|
3678
|
+
| --- | --- |
|
|
3679
|
+
| stdin not a TTY, no \`--text\` | Print the bare next unused id (e.g. \`BILLING-010\`), exit 0. No prompts, no writes. |
|
|
3680
|
+
| \`--json\`, no \`--text\` | Print \`{ domain, next_id }\`. Same zero-write contract, even on a TTY. |
|
|
3681
|
+
| \`--text "<statement>"\` | Append an Active entry with zero prompts. \`--why\`, \`--relates\`, \`--issue <ticket>\` fill the other fields and error without \`--text\`. \`--relates\` repeats and comma-separates. |
|
|
3682
|
+
| \`--draft\` | Author the entry as Draft (with \`--text\`, or on a TTY). Off a TTY without \`--text\` it exits 2. |
|
|
3683
|
+
| TTY, no flags | Interactive authoring. |
|
|
3684
|
+
|
|
3685
|
+
\`--json\` with \`--text\`: \`{ id, file }\`, plus \`clauses: { pattern, condition, system, response }\` when the statement parses as EARS.
|
|
3686
|
+
|
|
3687
|
+
Exit: 0 / 2 (ambiguous prefix, unknown domain, grammar refusal, statement-rule refusal).
|
|
3688
|
+
|
|
3689
|
+
- The domain prefix is case-insensitive: \`bil\` resolves to \`BILLING\`.
|
|
3690
|
+
- The resolved domain's charter is echoed to stderr on every authoring path.
|
|
3691
|
+
- Unresolvable \`@<path>\` refs warn on stderr and never block.
|
|
3692
|
+
|
|
3693
|
+
### spec term
|
|
3694
|
+
|
|
3695
|
+
A term is a requirement row in the reserved \`TERM\` domain: definition in \`statement\`, headword in \`term\`, synonyms in \`aliases\`.
|
|
3696
|
+
|
|
3697
|
+
| Form | Effect |
|
|
3698
|
+
| --- | --- |
|
|
3699
|
+
| \`spec term "<name>" --def "<definition>" [--aliases a,b] [--section s]\` | Append a \`TERM-NNN\`. \`--json\`: \`{ id, file }\`. |
|
|
3700
|
+
| \`spec term\` with no \`--def\` | Pure id query, mirrors \`spec req\`: bare next \`TERM\` id, or \`{ domain, next_id }\`. |
|
|
3701
|
+
| \`spec term list\` | \`id name status\` per line, or \`[{ id, term, status }]\`. Reads the filesystem. |
|
|
3702
|
+
| \`spec term revise <TERM-NNN> --def "\u2026"\` | Rewrite the definition in place and bump the TERM envelope's \`specVersion\`. \`--no-bump\` opts out. \`--json\`: \`{ id, file, spec_version }\`. |
|
|
3703
|
+
| \`spec term confirm <KEY-NNN> <TERM-NNN>\` | Re-pin a citation to the term's current version (clears \`TERM_DRIFT\`); re-point to the successor when the term is superseded (clears \`SUPERSEDED_TERM_REFERENCED\`). \`--json\`: \`{ req_id, term_id, pinned, file }\`. |
|
|
3704
|
+
|
|
3705
|
+
\`spec supersede TERM-NNN\` and \`spec amend TERM-NNN --term/--aliases\` work on term ids as on requirements.
|
|
3706
|
+
|
|
3707
|
+
Exit: 0 / 2.
|
|
3708
|
+
|
|
3709
|
+
### spec glossary
|
|
3710
|
+
|
|
3711
|
+
| Form | Effect |
|
|
3712
|
+
| --- | --- |
|
|
3713
|
+
| \`spec glossary\` | Overwrite \`GLOSSARY.md\` from the TERM store. Byte-stable: fixed header, terms in id order, \`## {section}\` once per change, one \`- **{term}** \u2014 {statement}\` bullet each. |
|
|
3714
|
+
| \`--migrate\` | One-time parse of an existing \`GLOSSARY.md\` into \`TERM-001..N\`. Skips when the TERM domain already has entries. |
|
|
3715
|
+
| \`--check\` | Regenerate into a buffer and diff against the committed file. Exit 1 on drift. |
|
|
3716
|
+
|
|
3717
|
+
\`--json\`: \`{ generated }\`, \`{ migrated }\`, or \`{ ok }\`.
|
|
3718
|
+
|
|
3719
|
+
Exit: 0 / 1 (\`--check\` drift) / 2.
|
|
3720
|
+
|
|
3721
|
+
### spec deprecate
|
|
3722
|
+
|
|
3723
|
+
Marks an Active/Draft requirement \`deprecated\`, records \`--reason\` on the entry, and removes every \`@spec\` tag of the id from the code. A line that holds only the tag (and its comment leader) is deleted; on a line with other text, only the tag and its level token go. Text mode prints the removed sites.
|
|
3724
|
+
|
|
3725
|
+
\`--json\`: \`{ id, file, reason, removed: [{ req_id, repo, file, line, kind, level }] }\`, the sites as they stood before the removal.
|
|
3726
|
+
|
|
3727
|
+
Tag sites come from the freshly built index, so \`fixtures/\` and dot-prefixed directories are never touched. Each rewritten line keeps its comment leader, level token, and line ending, and no other line changes. Running the command again finds no sites.
|
|
3728
|
+
|
|
3729
|
+
Exit: 0 / 2.
|
|
3730
|
+
|
|
3731
|
+
### spec supersede
|
|
3732
|
+
|
|
3733
|
+
Requires an Active target. Flips it to \`superseded by NEW\`, mints the successor in the domain file that holds the target, reindexes fresh, and rewrites every \`@spec OLD\` tag in the code to \`@spec NEW\`, keeping the level token and the rest of the line. Text mode prints the rewritten sites. Tag sites come from the freshly built index, so \`fixtures/\` and dot-prefixed directories are never touched. Each rewritten line keeps its comment leader, level token, and line ending, and no other line changes. Running the command again finds no sites.
|
|
3734
|
+
|
|
3735
|
+
The successor copies the predecessor's \`why\`, \`relates\`, \`cites\`, \`aliases\`, \`term\`, and \`section\` unless a flag overrides the field. Its \`issues\` hold only the \`created\` row from \`--issue\`.
|
|
3736
|
+
|
|
3737
|
+
| Flag | Effect |
|
|
3738
|
+
| --- | --- |
|
|
3739
|
+
| \`--text\` | The successor statement. Required when not a TTY. |
|
|
3740
|
+
| \`--why\`, \`--relates\` | Successor fields. Default to copies of the predecessor's. \`--relates\` repeats or comma-separates and replaces the whole list; an empty value clears it. |
|
|
3741
|
+
| \`--issue <ticket>\` | Recorded as \`supersedes-via\` on the predecessor and \`created\` on the successor. |
|
|
3742
|
+
| \`--no-bump\` | TERM domain only: keep the authored \`specVersion\`. No-op elsewhere. |
|
|
3743
|
+
|
|
3744
|
+
\`--json\`: \`{ old_id, new_id, file, spec_version, retagged: [{ req_id, repo, file, line, kind, level }] }\`, the rewritten sites (\`req_id\` is the old id). On a requirement domain \`spec_version\` is the derived domain version after the supersession, and the same number is stamped as the predecessor's \`supersededAtVersion\`.
|
|
3745
|
+
|
|
3746
|
+
Exit: 0 / 2. All guards run before the first byte is written.
|
|
3747
|
+
|
|
3748
|
+
### spec move
|
|
3749
|
+
|
|
3750
|
+
Cross-domain supersede. Mints the successor as the next id in \`<NEW-DOMAIN>\` (which must exist), copies \`statement\`, \`why\`, \`relates\`, \`cites\`, \`aliases\`, \`term\`, and \`section\` (\`--text\` and \`--why\` override), flips the source to superseded with a cross-domain \`supersededBy\`, and rewrites every tag of the source to the successor as \`spec supersede\` does. The successor carries no \`issues\`.
|
|
3751
|
+
|
|
3752
|
+
\`--json\`: \`{ old_id, new_id, from_file, to_file, source_spec_version, target_spec_version, retagged: [...] }\`.
|
|
3753
|
+
|
|
3754
|
+
Exit: 0 / 2 (non-Active source, or the target is the source's own domain).
|
|
3755
|
+
|
|
3756
|
+
- Both envelopes validate before either is written.
|
|
3757
|
+
- The source version advances; the target version is unchanged.
|
|
3758
|
+
|
|
3759
|
+
### spec amend
|
|
3760
|
+
|
|
3761
|
+
Revise an Active or Draft entry in place. Same id, no version change.
|
|
3762
|
+
|
|
3763
|
+
| Flag | Effect |
|
|
3764
|
+
| --- | --- |
|
|
3765
|
+
| \`--text\`, \`--why\`, \`--relates\` | Field to change. At least one flag is required. Untouched fields stay byte-identical. \`--relates\` repeats or comma-separates and replaces the whole list (the \`--aliases\` idiom); an empty value clears it. |
|
|
3766
|
+
| \`--issue <ticket>\` | Append \`amends-via\` provenance. |
|
|
3767
|
+
| \`--term\`, \`--aliases\` | TERM ids only. |
|
|
3768
|
+
|
|
3769
|
+
\`--json\`: \`{ id, file, fields_changed }\`.
|
|
3770
|
+
|
|
3771
|
+
A released requirement is an Active entry present in its domain file at the newest \`v*\` tag merged into HEAD. Its promise does not change in place: a \`--text\`, \`--why\`, \`--term\`, or \`--aliases\` amend refuses with exit 2, naming the tag. Two paths stay open: \`spec supersede\` changes the promise, and an amend of only \`--relates\` or \`--issue\` writes in place. With no \`v*\` tag merged into HEAD, or outside git, every entry is unreleased. A Draft entry always amends.
|
|
3772
|
+
|
|
3773
|
+
Exit: 0 / 2 (superseded or deprecated target; a released entry's promise fields).
|
|
3774
|
+
|
|
3775
|
+
### spec accept
|
|
3776
|
+
|
|
3777
|
+
Promotes a Draft requirement to Active. Every other field stays byte-identical and the domain version does not move. Reword a draft with \`spec amend\` first; end one with \`spec deprecate\`.
|
|
3778
|
+
|
|
3779
|
+
\`--json\`: \`{ id, file }\`.
|
|
3780
|
+
|
|
3781
|
+
Exit: 0 / 2 (the target is not Draft: nothing is written).
|
|
3782
|
+
|
|
3783
|
+
- A Draft never raises \`ORPHAN_REQ\` or \`UNVERIFIED_REQ\` and is not counted in the coverage report. A code tag on one is \`DRAFT_REFERENCED\` (warning).
|
|
3784
|
+
|
|
3785
|
+
### spec lint
|
|
3786
|
+
|
|
3787
|
+
Every statement-rule finding. Reads \`SPEC.json\` directly, not the index.
|
|
3788
|
+
|
|
3789
|
+
| Form | Scope |
|
|
3790
|
+
| --- | --- |
|
|
3791
|
+
| \`spec lint [platformDir]\` | the platform |
|
|
3792
|
+
| \`spec lint <KEY> [platformDir]\` | one domain |
|
|
3793
|
+
| \`spec lint <KEY-NNN> [platformDir]\` | one requirement |
|
|
3794
|
+
| \`--all\` | Draft requirements as well as Active ones |
|
|
3795
|
+
|
|
3796
|
+
Text mode prints \`<id> <rule>: <detail> \u2014 <reason>\` per finding on stdout, then rule 0 on stderr. \`--json\` prints a sorted array of \`{ req_id, rule, detail, reason, source_file, line }\`. The TERM domain is never judged. A lone positional that holds \`spec-engine/\` is the platform dir. CLI only: no MCP tool or HTTP route.
|
|
3797
|
+
|
|
3798
|
+
Exit: 0 clean / 1 any finding / 2 usage, unknown domain or id, or a bad \`check.json\`.
|
|
3799
|
+
|
|
3800
|
+
### spec serve
|
|
3801
|
+
|
|
3802
|
+
Binds one address: \`127.0.0.1\`, or \`--host <ip>\` (a literal loopback, RFC 1918, or \`100.64/10\` address only; anything else exits 2). \`--port 0\` (default) picks a free port. \`--probe\` boots on loopback, fetches \`/\`, and exits 0 or 1. The startup line, \`/api/platform\` (\`name\`), and every page's title and sidebar name the platform (platform file \`name\`, else the directory name), never a path; \`spec mcp\` logs the same name on stderr.
|
|
3803
|
+
|
|
3804
|
+
A non-loopback \`--host\` is LAN mode: stdout prints \`http://<ip>:<port>/?token=\u2026\`; every request needs that token (the query parameter is exchanged for an \`HttpOnly; SameSite=Strict\` cookie, then a 302 drops it), else 401. A \`Host\` other than loopback or the bound address is 403. Read-only unless \`--allow-writes\`: non-GET requests, \`/provenance\`, and \`/api/provenance?resolve=1\` answer 403. \`--token\` / \`SPEC_SERVE_TOKEN\` pin the token (\u2265 16 of \`A-Z a-z 0-9 _ -\`); otherwise it is 256 random bits.
|
|
3805
|
+
|
|
3806
|
+
When the reindex after a write refuses on an invalid \`SPEC.json\` elsewhere on the platform, the route answers 422 \`{ error: "invalid_domain_file", diagnostics: [...] }\`, the rows in the \`check --json\` shape (the CLI exits 2 with the same rows).
|
|
3807
|
+
|
|
3808
|
+
Always-on routes: \`/\`, \`/requirements[/:id]\`, \`/propagation/:id\`, \`/api/coverage\`, \`/api/report\`, \`/api/repos\`, \`/api/platform\`, \`/api/requirements[/:id]\`, \`/api/propagation/:id\`, \`/api/resolve?files=\u2026\`.
|
|
3809
|
+
|
|
3810
|
+
Flag-gated, 404 until enabled. \`SPEC_FLAGS\` is comma-separated; unknown keys are ignored.
|
|
3811
|
+
|
|
3812
|
+
| \`SPEC_FLAGS\` key | Routes |
|
|
3813
|
+
| --- | --- |
|
|
3814
|
+
| \`query\` | \`/query\`, \`/api/query?q=&limit=\` |
|
|
3815
|
+
| \`relations\` | \`/relations\`, \`/api/relations[?format=mermaid]\` |
|
|
3816
|
+
| \`provenance\` | \`/provenance\`, \`/api/provenance[?resolve=1]\`, \`/api/provenance/by-issue?issue=\` |
|
|
3817
|
+
| \`editor\` | \`/editor\`, \`POST\` and \`PUT /api/requirements\`, \`POST /api/requirements/:id/supersede\`, \`POST /api/requirements/:id/move\`, \`POST /api/requirements/:id/deprecate\`, \`POST /api/requirements/:id/accept\` |
|
|
3818
|
+
| \`glossary\`, \`logs\` | Placeholder pages |
|
|
3819
|
+
|
|
3820
|
+
Exit: 0 / 1 / 2.
|
|
3821
|
+
|
|
3822
|
+
### spec docs
|
|
3823
|
+
|
|
3824
|
+
Serves the prebuilt docs site on \`127.0.0.1\`, fully offline.
|
|
3825
|
+
|
|
3826
|
+
- Takes no platform dir. Registers none of \`--json\`, \`--out\`, \`--no-prompt\`.
|
|
3827
|
+
- The npm artifact ships the site at \`dist/docs/\`. A source checkout serves \`packages/site/dist\`; run \`bun run build:site\` first, else exit 2.
|
|
3828
|
+
- \`--probe\` mirrors \`serve --probe\`.
|
|
3829
|
+
|
|
3830
|
+
Exit: 0 / 1 / 2.
|
|
3831
|
+
|
|
3832
|
+
### spec mcp
|
|
3833
|
+
|
|
3834
|
+
MCP server over stdio. Register it as \`{ "command": "spec", "args": ["mcp", "<platformDir>"] }\`.
|
|
3835
|
+
|
|
3836
|
+
| Tool | Returns |
|
|
3837
|
+
| --- | --- |
|
|
3838
|
+
| \`spec_query\` | FTS hits, the \`spec query --json\` shape |
|
|
3839
|
+
| \`spec_resolve\` | files \u2192 requirements |
|
|
3840
|
+
| \`spec_req_tags\` | requirement \u2192 tag sites |
|
|
3841
|
+
| \`spec_coverage_report\` | per-domain rollup \`{ domain, active, implemented, verified, orphans, unverified }\` |
|
|
3842
|
+
| \`spec_check\` | diagnostic rows |
|
|
3843
|
+
| \`spec_propagation\` | per-repo migration state |
|
|
3844
|
+
| \`spec_next_id\` | \`{ domain, next_id }\`, nothing written |
|
|
3845
|
+
| \`spec_get\` | one \`Requirement\` row with \`implemented_in\` and \`verified_in\`, or \`[]\` for an unknown id; the \`spec get --json\` shape |
|
|
3846
|
+
| \`spec_list\` | \`Requirement\` rows in (key, seq) order, \`domain\` and \`status\` filters; the \`spec list --json\` shape |
|
|
3847
|
+
| \`spec_supersede\` | \`{ old_id, new_id, file, spec_version, retagged }\`, the \`spec supersede --json\` shape; optional \`why\`, \`relates\`, \`issue\` arguments |
|
|
3848
|
+
| \`spec_deprecate\` | \`{ id, file, reason, removed }\`, the \`spec deprecate --json\` shape |
|
|
3849
|
+
| \`spec_accept\` | \`{ id, file }\`, the \`spec accept --json\` shape |
|
|
3850
|
+
| \`spec_req\` | \`{ id, file, clauses? }\`, the \`spec req --text --json\` shape; \`domain\` (key or prefix) and \`text\` required, optional \`why\`, \`relates\`, \`issue\`, \`draft\` |
|
|
3851
|
+
| \`spec_amend\` | \`{ id, file, fields_changed }\`, the \`spec amend --json\` shape; \`id\` required, optional \`text\`, \`why\`, \`relates\`, \`issue\` |
|
|
3852
|
+
| \`spec_move\` | \`{ old_id, new_id, from_file, to_file, source_spec_version, target_spec_version, retagged }\`, the \`spec move --json\` shape; \`id\` and \`newDomain\` required, optional \`text\`, \`why\` |
|
|
3853
|
+
|
|
3854
|
+
Every tool call reindexes fresh. stdout is the protocol channel; chrome goes to stderr. One prompt, \`author_requirements\` (\`brief\` required, \`domain\` optional), returns a static authoring playbook with the brief and the domain's charter substituted. The engine runs no model.
|
|
3855
|
+
|
|
3856
|
+
Exit: 0 / 2.
|
|
3857
|
+
|
|
3858
|
+
### Gotchas
|
|
3859
|
+
|
|
3860
|
+
- **\`fixtures/\` is globally ignored by the tag scanner.** Tags under any \`fixtures/\` folder are test data, not coverage claims.
|
|
3861
|
+
- **The index owns nothing.** Never edit \`.spec-engine/index.sqlite\`. Edit the \`SPEC.json\` source and re-run \`spec index\`.
|
|
3862
|
+
- **\`storage_unavailable\` / \`SQLITE_IOERR*\` means your environment.** The index runs in WAL mode and needs real file locks. A sandboxed process gets \`SQLITE_IOERR_VNODE\`; re-run unsandboxed or grant write and lock access to \`<platformDir>/.spec-engine/\`. \`SQLITE_BUSY\` means another \`spec\` process holds the index.
|
|
3863
|
+
- **Empty is not an error.** Read commands exit 0 with \`[]\` and guidance on stderr.
|
|
3864
|
+
- **Determinism is contractual.** JSON outputs are sorted and byte-stable.
|
|
3865
|
+
- A directory passed as the last positional is \`platformDir\` only if it contains \`spec-engine/\`. Otherwise it is treated as a file, with a warning.
|
|
3866
|
+
`;
|
|
3867
|
+
var init_agent_reference = () => {};
|
|
3868
|
+
|
|
3869
|
+
// src/operations/agentSurface.ts
|
|
3870
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
|
|
3871
|
+
import { dirname as dirname4, join as join5, relative as relative2 } from "path";
|
|
3872
|
+
function mcpTemplate(platformArg) {
|
|
3873
|
+
const body = { mcpServers: { spec: { command: "spec", args: ["mcp", platformArg] } } };
|
|
3874
|
+
return `${JSON.stringify(body, null, 2)}
|
|
3875
|
+
`;
|
|
3876
|
+
}
|
|
3877
|
+
function shellArg(arg) {
|
|
3878
|
+
return /^[\w./-]+$/.test(arg) ? arg : `'${arg.replaceAll("'", "'\\''")}'`;
|
|
3879
|
+
}
|
|
3880
|
+
function writeIfChanged(path, content, out) {
|
|
3881
|
+
if (existsSync3(path) && readFileSync(path, "utf8") === content)
|
|
3882
|
+
return;
|
|
3883
|
+
mkdirSync2(dirname4(path), { recursive: true });
|
|
3884
|
+
writeFileSync(path, content);
|
|
3885
|
+
out.written.push(path);
|
|
3886
|
+
}
|
|
3887
|
+
function writeReferenceBlock(dir, out) {
|
|
3888
|
+
const path = join5(dir, "AGENTS.md");
|
|
3889
|
+
const block = `${AGENT_BLOCK_BEGIN}
|
|
3890
|
+
${AGENT_REFERENCE.trimEnd()}
|
|
3891
|
+
${AGENT_BLOCK_END}`;
|
|
3892
|
+
const text = existsSync3(path) ? readFileSync(path, "utf8") : "";
|
|
3893
|
+
const begin = text.indexOf(AGENT_BLOCK_BEGIN);
|
|
3894
|
+
const end = text.indexOf(AGENT_BLOCK_END, begin);
|
|
3895
|
+
let next;
|
|
3896
|
+
if (begin !== -1 && end !== -1) {
|
|
3897
|
+
next = text.slice(0, begin) + block + text.slice(end + AGENT_BLOCK_END.length);
|
|
3898
|
+
} else if (begin !== -1 || text.includes(AGENT_BLOCK_END)) {
|
|
3899
|
+
out.hints.push(`${path} holds only one agent-reference marker; restore the pair to let spec init refresh the block`);
|
|
3900
|
+
return;
|
|
3901
|
+
} else {
|
|
3902
|
+
const gap = text === "" ? "" : text.endsWith(`
|
|
3903
|
+
`) ? `
|
|
3904
|
+
` : `
|
|
3905
|
+
|
|
3906
|
+
`;
|
|
3907
|
+
next = `${text}${gap}${block}
|
|
3908
|
+
`;
|
|
3909
|
+
}
|
|
3910
|
+
writeIfChanged(path, next, out);
|
|
3911
|
+
const claude = join5(dir, "CLAUDE.md");
|
|
3912
|
+
if (existsSync3(claude) && !readFileSync(claude, "utf8").includes("AGENTS.md")) {
|
|
3913
|
+
out.hints.push(`${claude} does not reference AGENTS.md; add the line @AGENTS.md so Claude Code loads the spec reference`);
|
|
3914
|
+
}
|
|
3915
|
+
}
|
|
3916
|
+
function writeSkill(dir, force, out) {
|
|
3917
|
+
for (const [rel, content] of Object.entries(SKILL_FILES)) {
|
|
3918
|
+
const path = join5(dir, ".claude", "skills", rel);
|
|
3919
|
+
if (existsSync3(path) && !force && readFileSync(path, "utf8") !== content) {
|
|
3920
|
+
out.hints.push(`${path} differs from the bundled skill; re-run with --force to replace it`);
|
|
3921
|
+
continue;
|
|
3922
|
+
}
|
|
3923
|
+
writeIfChanged(path, content, out);
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
function isRecord(value) {
|
|
3927
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3928
|
+
}
|
|
3929
|
+
function writeMcpEntry(dir, platformArg, out) {
|
|
3930
|
+
const path = join5(dir, ".mcp.json");
|
|
3931
|
+
const entry = { command: "spec", args: ["mcp", platformArg] };
|
|
3932
|
+
const wanted = `add to its mcpServers: "spec": ${JSON.stringify(entry)}`;
|
|
3933
|
+
if (!existsSync3(path)) {
|
|
3934
|
+
writeIfChanged(path, mcpTemplate(platformArg), out);
|
|
3935
|
+
return;
|
|
3936
|
+
}
|
|
3937
|
+
let config2;
|
|
3938
|
+
try {
|
|
3939
|
+
config2 = JSON.parse(readFileSync(path, "utf8"));
|
|
3940
|
+
} catch {
|
|
3941
|
+
out.hints.push(`${path} is not valid JSON; left unchanged. ${wanted}`);
|
|
3942
|
+
return;
|
|
3943
|
+
}
|
|
3944
|
+
if (!isRecord(config2) || config2.mcpServers !== undefined && !isRecord(config2.mcpServers)) {
|
|
3945
|
+
out.hints.push(`${path} is not an MCP server config object; left unchanged. ${wanted}`);
|
|
3946
|
+
return;
|
|
3947
|
+
}
|
|
3948
|
+
const servers = config2.mcpServers ?? {};
|
|
3949
|
+
if (servers.spec !== undefined) {
|
|
3950
|
+
if (JSON.stringify(servers.spec) !== JSON.stringify(entry)) {
|
|
3951
|
+
out.hints.push(`${path} already has a spec server entry; left unchanged. Expected ${wanted}`);
|
|
3952
|
+
}
|
|
3953
|
+
return;
|
|
3954
|
+
}
|
|
3955
|
+
config2.mcpServers = { ...servers, spec: entry };
|
|
3956
|
+
writeIfChanged(path, `${JSON.stringify(config2, null, 2)}
|
|
3957
|
+
`, out);
|
|
3958
|
+
}
|
|
3959
|
+
function writeHook(dir, platformArg, out) {
|
|
3960
|
+
const command = `spec guard ${shellArg(platformArg)}`;
|
|
3961
|
+
const hooksDir = [join5(dir, ".githooks"), join5(dir, ".git", "hooks")].find(isExistingDir);
|
|
3962
|
+
if (hooksDir === undefined) {
|
|
3963
|
+
out.hints.push(`${dir} has no hooks directory; add a pre-commit hook that runs: ${command}`);
|
|
3964
|
+
return;
|
|
3965
|
+
}
|
|
3966
|
+
const path = join5(hooksDir, "pre-commit");
|
|
3967
|
+
if (existsSync3(path)) {
|
|
3968
|
+
if (!readFileSync(path, "utf8").includes(command)) {
|
|
3969
|
+
out.hints.push(`${path} already exists; left unchanged. Add this line to it: ${command}`);
|
|
3970
|
+
}
|
|
3971
|
+
return;
|
|
3972
|
+
}
|
|
3973
|
+
writeFileSync(path, PRE_COMMIT.replace("spec guard .", command), { mode: 493 });
|
|
3974
|
+
out.written.push(path);
|
|
3975
|
+
if (hooksDir.endsWith(".githooks")) {
|
|
3976
|
+
out.hints.push(`git runs ${path} only after: git config core.hooksPath .githooks`);
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
function writeAgentSurface(input) {
|
|
3980
|
+
const out = { written: [], hints: [] };
|
|
3981
|
+
writeReferenceBlock(input.dir, out);
|
|
3982
|
+
writeSkill(input.dir, input.force, out);
|
|
3983
|
+
if (input.platformDir === null) {
|
|
3984
|
+
out.hints.push("no platform was located, so the MCP server and the pre-commit hook were skipped; re-run spec init once the repository belongs to a platform");
|
|
3985
|
+
return out;
|
|
3986
|
+
}
|
|
3987
|
+
const platformArg = relative2(input.dir, input.platformDir) || ".";
|
|
3988
|
+
writeMcpEntry(input.dir, platformArg, out);
|
|
3989
|
+
writeHook(input.dir, platformArg, out);
|
|
3990
|
+
return out;
|
|
3991
|
+
}
|
|
3992
|
+
var AGENT_BLOCK_BEGIN = "<!-- spec-engine:agent-reference:begin -->", AGENT_BLOCK_END = "<!-- spec-engine:agent-reference:end -->", AGENT_REFERENCE, SKILL_FILES, PRE_COMMIT = `#!/bin/sh
|
|
3993
|
+
spec guard .
|
|
3994
|
+
exit $?
|
|
3995
|
+
`;
|
|
3996
|
+
var init_agentSurface = __esm(() => {
|
|
3997
|
+
init_SKILL();
|
|
3998
|
+
init_agent_reference();
|
|
3999
|
+
init_constants();
|
|
4000
|
+
AGENT_REFERENCE = agent_reference_default.replace(/^---\n[\s\S]*?\n---\n+/, "").replace(/^<!-- @spec [A-Z][A-Z0-9]*-\d+ -->\n\n?/gm, "").replace(/ <!-- @spec [A-Z][A-Z0-9]*-\d+ -->/g, "");
|
|
4001
|
+
SKILL_FILES = { "req-author/SKILL.md": SKILL_default };
|
|
4002
|
+
});
|
|
4003
|
+
|
|
2918
4004
|
// src/operations/_result.ts
|
|
2919
4005
|
function fail(reason, detail, extra = {}) {
|
|
2920
4006
|
const out = { ok: false, reason, detail };
|
|
@@ -2936,8 +4022,8 @@ var init__result = __esm(() => {
|
|
|
2936
4022
|
});
|
|
2937
4023
|
|
|
2938
4024
|
// src/operations/init.ts
|
|
2939
|
-
import { existsSync as
|
|
2940
|
-
import { basename as basename3, join as
|
|
4025
|
+
import { existsSync as existsSync4, realpathSync, statSync as statSync2 } from "fs";
|
|
4026
|
+
import { basename as basename3, join as join6, relative as relative3, resolve as resolve4, sep as sep3 } from "path";
|
|
2941
4027
|
import {
|
|
2942
4028
|
applyInit,
|
|
2943
4029
|
applyLink,
|
|
@@ -2971,7 +4057,7 @@ function canonicalize(repoDir) {
|
|
|
2971
4057
|
function judgeSpecTree(repoDir, canonical, root) {
|
|
2972
4058
|
if (!hasSpecEngineSegment(canonical))
|
|
2973
4059
|
return { ok: true };
|
|
2974
|
-
const judged = root !== null && isEnginePlatform(root, ENGINE_PACKAGE_NAME) ?
|
|
4060
|
+
const judged = root !== null && isEnginePlatform(root, ENGINE_PACKAGE_NAME) ? relative3(root, canonical) : canonical;
|
|
2975
4061
|
if (hasSpecEngineSegment(judged)) {
|
|
2976
4062
|
return fail("usage", `${repoDir} resolves to ${canonical}, which is inside a spec-engine/ tree \u2014 refusing to scaffold there.`);
|
|
2977
4063
|
}
|
|
@@ -3019,7 +4105,7 @@ function place(repoDir, canonical) {
|
|
|
3019
4105
|
return fail("usage", `${canonical} is neither a declared member nor a workspace package of the platform at ${root}`);
|
|
3020
4106
|
}
|
|
3021
4107
|
function link(canonical, platformRoot) {
|
|
3022
|
-
if (!
|
|
4108
|
+
if (!existsSync4(join6(canonical, PLATFORM_MAP_FILENAME))) {
|
|
3023
4109
|
return fail("usage", `${canonical} carries no platform-map.json marker, so it is not a declared member; declare it from the platform folder with \`spec init <name>\``);
|
|
3024
4110
|
}
|
|
3025
4111
|
const plan = planLink(canonical, { root: resolve4(platformRoot) });
|
|
@@ -3034,7 +4120,7 @@ async function resolveMemberPin(input) {
|
|
|
3034
4120
|
if (!canon.ok)
|
|
3035
4121
|
return canon;
|
|
3036
4122
|
const { canonical } = canon;
|
|
3037
|
-
if (isExistingDir(
|
|
4123
|
+
if (isExistingDir(join6(canonical, CANONICAL_SPECS_DIR))) {
|
|
3038
4124
|
return fail("usage", `${canonical} is a platform dir (contains spec-engine/) \u2014 pass a member subdir or cd into one`);
|
|
3039
4125
|
}
|
|
3040
4126
|
let linked = null;
|
|
@@ -3084,7 +4170,7 @@ function planDeclare(platformDir, name) {
|
|
|
3084
4170
|
}
|
|
3085
4171
|
const candidate = plan.candidates.find((c) => c.name === name);
|
|
3086
4172
|
if (candidate === undefined) {
|
|
3087
|
-
return fail("usage", `${
|
|
4173
|
+
return fail("usage", `${join6(platformDir, name)} has no .git entry or package.json, so platform-map cannot declare it as a member`);
|
|
3088
4174
|
}
|
|
3089
4175
|
if (candidate.marker !== undefined && candidate.marker !== plan.platformName) {
|
|
3090
4176
|
return fail("conflict", `${name} carries a platform-map.json marker for platform "${candidate.marker}", not "${plan.platformName}"`);
|
|
@@ -3127,8 +4213,8 @@ async function write(configPath, specs, ignore) {
|
|
|
3127
4213
|
}
|
|
3128
4214
|
async function writeMemberConfig(input) {
|
|
3129
4215
|
const { canonical, pin } = input;
|
|
3130
|
-
const configPath =
|
|
3131
|
-
if (!
|
|
4216
|
+
const configPath = join6(canonical, MEMBER_CONFIG_FILENAME);
|
|
4217
|
+
if (!existsSync4(configPath)) {
|
|
3132
4218
|
await write(configPath, pin, input.ignore);
|
|
3133
4219
|
return { ok: true, action: "wrote", path: configPath, pin };
|
|
3134
4220
|
}
|
|
@@ -3251,16 +4337,16 @@ function renderDiagnostics(rows, mode) {
|
|
|
3251
4337
|
}
|
|
3252
4338
|
|
|
3253
4339
|
// src/operations/_index.ts
|
|
3254
|
-
import { existsSync as
|
|
3255
|
-
import { dirname as
|
|
4340
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync, statSync as statSync3 } from "fs";
|
|
4341
|
+
import { dirname as dirname5, join as join7 } from "path";
|
|
3256
4342
|
async function withIndex(opts, fn) {
|
|
3257
4343
|
const { platformDir, build } = opts;
|
|
3258
4344
|
const dbPath = opts.dbPath ?? defaultIndexPath(platformDir);
|
|
3259
4345
|
assertSpecPlatform(platformDir);
|
|
3260
|
-
|
|
4346
|
+
mkdirSync3(dirname5(dbPath), { recursive: true });
|
|
3261
4347
|
if (build === "fresh" || build === "reset")
|
|
3262
4348
|
coldResetDb(dbPath);
|
|
3263
|
-
const wasMissing = !
|
|
4349
|
+
const wasMissing = !existsSync5(dbPath);
|
|
3264
4350
|
const storage2 = openStorage(dbPath);
|
|
3265
4351
|
try {
|
|
3266
4352
|
const warnings = [];
|
|
@@ -3281,12 +4367,12 @@ async function withIndex(opts, fn) {
|
|
|
3281
4367
|
function staleIndexWarning(platformDir, dbPath) {
|
|
3282
4368
|
try {
|
|
3283
4369
|
const dbMtime = statSync3(dbPath).mtimeMs;
|
|
3284
|
-
const specsDir =
|
|
4370
|
+
const specsDir = join7(platformDir, CANONICAL_SPECS_DIR);
|
|
3285
4371
|
for (const entry of readdirSync(specsDir, { withFileTypes: true })) {
|
|
3286
4372
|
if (!entry.isDirectory())
|
|
3287
4373
|
continue;
|
|
3288
|
-
const specPath =
|
|
3289
|
-
if (!
|
|
4374
|
+
const specPath = join7(specsDir, entry.name, SPEC_FILENAME);
|
|
4375
|
+
if (!existsSync5(specPath))
|
|
3290
4376
|
continue;
|
|
3291
4377
|
if (statSync3(specPath).mtimeMs > dbMtime) {
|
|
3292
4378
|
return `spec: warning \u2014 ${CANONICAL_SPECS_DIR}/${entry.name}/${SPEC_FILENAME} changed after the index was built; results may be stale. Pass --fresh to rebuild.`;
|
|
@@ -3348,14 +4434,14 @@ function handleNotAPlatform(e) {
|
|
|
3348
4434
|
}
|
|
3349
4435
|
throw e;
|
|
3350
4436
|
}
|
|
3351
|
-
function handleInvalidDomainFiles(e, json, code = EXIT.FAILURE) {
|
|
4437
|
+
async function handleInvalidDomainFiles(e, json, code = EXIT.FAILURE) {
|
|
3352
4438
|
if (!(e instanceof InvalidDomainFilesError))
|
|
3353
4439
|
return;
|
|
3354
4440
|
if (json)
|
|
3355
4441
|
console.log(renderDiagnostics(e.diagnostics, "json"));
|
|
3356
4442
|
else
|
|
3357
4443
|
console.error(renderDiagnostics(e.diagnostics, "text"));
|
|
3358
|
-
|
|
4444
|
+
await exit(code);
|
|
3359
4445
|
}
|
|
3360
4446
|
function cliFreshTags(platformDir) {
|
|
3361
4447
|
const fresh = coldFreshTags(platformDir);
|
|
@@ -3363,7 +4449,7 @@ function cliFreshTags(platformDir) {
|
|
|
3363
4449
|
try {
|
|
3364
4450
|
return await fresh(reqId);
|
|
3365
4451
|
} catch (e) {
|
|
3366
|
-
handleInvalidDomainFiles(e, false, EXIT.USAGE);
|
|
4452
|
+
await handleInvalidDomainFiles(e, false, EXIT.USAGE);
|
|
3367
4453
|
throw e;
|
|
3368
4454
|
}
|
|
3369
4455
|
};
|
|
@@ -3379,7 +4465,7 @@ function printWarnings(cmdName, warnings) {
|
|
|
3379
4465
|
for (const w of warnings ?? []) {
|
|
3380
4466
|
if (w.kind === "ref")
|
|
3381
4467
|
console.error(`spec req: warning \u2014 ${w.text}`);
|
|
3382
|
-
else if (w.kind === "grammar")
|
|
4468
|
+
else if (w.kind === "grammar" || w.kind === "statement")
|
|
3383
4469
|
console.error(`${cmdName}: ${w.text}`);
|
|
3384
4470
|
else
|
|
3385
4471
|
console.error(w.text);
|
|
@@ -3405,7 +4491,7 @@ async function withReadStorage(opts, fn) {
|
|
|
3405
4491
|
});
|
|
3406
4492
|
} catch (e) {
|
|
3407
4493
|
handleStorageUnavailable(e, dbPath);
|
|
3408
|
-
handleInvalidDomainFiles(e, json);
|
|
4494
|
+
await handleInvalidDomainFiles(e, json);
|
|
3409
4495
|
handleNotAPlatform(e);
|
|
3410
4496
|
}
|
|
3411
4497
|
}
|
|
@@ -3414,6 +4500,7 @@ var init__shared = __esm(() => {
|
|
|
3414
4500
|
init_constants();
|
|
3415
4501
|
init_discover();
|
|
3416
4502
|
init__index();
|
|
4503
|
+
init_stdout();
|
|
3417
4504
|
init_errors();
|
|
3418
4505
|
init_sqlite();
|
|
3419
4506
|
});
|
|
@@ -3424,7 +4511,7 @@ __export(exports_init, {
|
|
|
3424
4511
|
runInit: () => runInit,
|
|
3425
4512
|
initCommand: () => initCommand
|
|
3426
4513
|
});
|
|
3427
|
-
import { resolve as resolve5 } from "path";
|
|
4514
|
+
import { dirname as dirname6, join as join8, resolve as resolve5 } from "path";
|
|
3428
4515
|
import { defineCommand } from "citty";
|
|
3429
4516
|
function errMessage2(err) {
|
|
3430
4517
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -3456,19 +4543,54 @@ function sourceText(result, json) {
|
|
|
3456
4543
|
}
|
|
3457
4544
|
return "fallback @1";
|
|
3458
4545
|
}
|
|
4546
|
+
function printSurface(surface, json) {
|
|
4547
|
+
for (const hint of surface.hints)
|
|
4548
|
+
console.error(`spec init: ${hint}`);
|
|
4549
|
+
if (json)
|
|
4550
|
+
return;
|
|
4551
|
+
for (const file of surface.written)
|
|
4552
|
+
console.log(` agent surface: ${file}`);
|
|
4553
|
+
}
|
|
4554
|
+
function surfaceFor(result, force) {
|
|
4555
|
+
const { placement } = result;
|
|
4556
|
+
return writeAgentSurface({
|
|
4557
|
+
dir: placement.kind === "package" ? placement.platformDir : dirname6(result.path),
|
|
4558
|
+
platformDir: placement.kind === "loose" ? null : placement.platformDir,
|
|
4559
|
+
force
|
|
4560
|
+
});
|
|
4561
|
+
}
|
|
4562
|
+
function isPlatformDir(dir) {
|
|
4563
|
+
try {
|
|
4564
|
+
return isExistingDir(join8(dir, CANONICAL_SPECS_DIR));
|
|
4565
|
+
} catch {
|
|
4566
|
+
return false;
|
|
4567
|
+
}
|
|
4568
|
+
}
|
|
4569
|
+
function initPlatformDir(dir, force, json) {
|
|
4570
|
+
const surface = writeAgentSurface({ dir, platformDir: dir, force });
|
|
4571
|
+
if (json) {
|
|
4572
|
+
console.log(JSON.stringify({ action: "surface-only", agent_surface: surface.written }));
|
|
4573
|
+
} else {
|
|
4574
|
+
console.log(`spec init: ${dir} is a platform directory; wrote no member config`);
|
|
4575
|
+
}
|
|
4576
|
+
printSurface(surface, json);
|
|
4577
|
+
}
|
|
3459
4578
|
async function runInit(args) {
|
|
3460
4579
|
const json = Boolean(args.json);
|
|
4580
|
+
const force = Boolean(args.force);
|
|
3461
4581
|
const override = typeof args.specs === "string" ? validateOverride(args.specs) : undefined;
|
|
3462
4582
|
const result = await initMember({
|
|
3463
4583
|
repoDir: resolveRepoArg(args.repo),
|
|
3464
4584
|
override,
|
|
3465
4585
|
platformRoot: typeof args.platform === "string" ? args.platform : undefined,
|
|
3466
|
-
force
|
|
4586
|
+
force
|
|
3467
4587
|
});
|
|
3468
4588
|
if (!result.ok)
|
|
3469
4589
|
exitOnFailure("spec init", result);
|
|
3470
4590
|
const source = sourceText(result, json);
|
|
3471
4591
|
const { declared, linked } = result;
|
|
4592
|
+
const surface = surfaceFor(result, force);
|
|
4593
|
+
const agent_surface = surface.written;
|
|
3472
4594
|
if (result.action === "already-configured") {
|
|
3473
4595
|
if (json) {
|
|
3474
4596
|
console.log(JSON.stringify({
|
|
@@ -3477,8 +4599,10 @@ async function runInit(args) {
|
|
|
3477
4599
|
pin: result.pin,
|
|
3478
4600
|
extra_fields: result.extraFields,
|
|
3479
4601
|
declared,
|
|
3480
|
-
linked
|
|
4602
|
+
linked,
|
|
4603
|
+
agent_surface
|
|
3481
4604
|
}));
|
|
4605
|
+
printSurface(surface, json);
|
|
3482
4606
|
return;
|
|
3483
4607
|
}
|
|
3484
4608
|
console.log("spec init: already configured");
|
|
@@ -3491,6 +4615,7 @@ async function runInit(args) {
|
|
|
3491
4615
|
console.log(` declared: ${file}`);
|
|
3492
4616
|
if (linked !== null)
|
|
3493
4617
|
console.log(` linked: ${linked}`);
|
|
4618
|
+
printSurface(surface, json);
|
|
3494
4619
|
return;
|
|
3495
4620
|
}
|
|
3496
4621
|
if (json) {
|
|
@@ -3500,8 +4625,10 @@ async function runInit(args) {
|
|
|
3500
4625
|
pin: result.pin,
|
|
3501
4626
|
source,
|
|
3502
4627
|
declared,
|
|
3503
|
-
linked
|
|
4628
|
+
linked,
|
|
4629
|
+
agent_surface
|
|
3504
4630
|
}));
|
|
4631
|
+
printSurface(surface, json);
|
|
3505
4632
|
return;
|
|
3506
4633
|
}
|
|
3507
4634
|
console.log(`spec init: wrote ${result.path}`);
|
|
@@ -3511,17 +4638,19 @@ async function runInit(args) {
|
|
|
3511
4638
|
console.log(` declared: ${file}`);
|
|
3512
4639
|
if (linked !== null)
|
|
3513
4640
|
console.log(` linked: ${linked}`);
|
|
4641
|
+
printSurface(surface, json);
|
|
3514
4642
|
}
|
|
3515
4643
|
var initCommand;
|
|
3516
4644
|
var init_init2 = __esm(() => {
|
|
3517
4645
|
init_src();
|
|
3518
4646
|
init_constants();
|
|
4647
|
+
init_agentSurface();
|
|
3519
4648
|
init_init();
|
|
3520
4649
|
init__shared();
|
|
3521
4650
|
initCommand = defineCommand({
|
|
3522
4651
|
meta: {
|
|
3523
4652
|
name: "init",
|
|
3524
|
-
description: "Declare a member repo
|
|
4653
|
+
description: "Declare a member repo, scaffold its spec-engine.member.json, and install the agent surface"
|
|
3525
4654
|
},
|
|
3526
4655
|
args: {
|
|
3527
4656
|
repo: {
|
|
@@ -3539,7 +4668,7 @@ var init_init2 = __esm(() => {
|
|
|
3539
4668
|
},
|
|
3540
4669
|
force: {
|
|
3541
4670
|
type: "boolean",
|
|
3542
|
-
description: "Overwrite an existing spec-engine.member.json (refuses if existing has extra keys)."
|
|
4671
|
+
description: "Overwrite an existing spec-engine.member.json (refuses if existing has extra keys) and a differing req-author skill."
|
|
3543
4672
|
},
|
|
3544
4673
|
json: {
|
|
3545
4674
|
type: "boolean",
|
|
@@ -3547,7 +4676,11 @@ var init_init2 = __esm(() => {
|
|
|
3547
4676
|
}
|
|
3548
4677
|
},
|
|
3549
4678
|
async run({ args }) {
|
|
3550
|
-
|
|
4679
|
+
const repoDir = resolveRepoArg(args.repo);
|
|
4680
|
+
if (isPlatformDir(repoDir))
|
|
4681
|
+
initPlatformDir(repoDir, Boolean(args.force), Boolean(args.json));
|
|
4682
|
+
else
|
|
4683
|
+
await runInit(args);
|
|
3551
4684
|
}
|
|
3552
4685
|
});
|
|
3553
4686
|
});
|
|
@@ -3612,7 +4745,7 @@ function listFlag(rawArgs, name, parsed) {
|
|
|
3612
4745
|
}
|
|
3613
4746
|
return raw.flatMap((v) => v.split(",")).map((v) => v.trim()).filter((v) => v !== "");
|
|
3614
4747
|
}
|
|
3615
|
-
var platformDirArg, outArg, jsonArg, freshArg, noPromptArg,
|
|
4748
|
+
var platformDirArg, outArg, jsonArg, freshArg, noPromptArg, relatesArg;
|
|
3616
4749
|
var init__args = __esm(() => {
|
|
3617
4750
|
init_constants();
|
|
3618
4751
|
platformDirArg = {
|
|
@@ -3633,14 +4766,6 @@ var init__args = __esm(() => {
|
|
|
3633
4766
|
type: "boolean",
|
|
3634
4767
|
description: "Suppress the interactive onboarding prompt for siblings missing spec-engine.member.json (they surface as NO_SPEC_CONFIG warnings)"
|
|
3635
4768
|
};
|
|
3636
|
-
livesArg = {
|
|
3637
|
-
type: "string",
|
|
3638
|
-
description: "Lives in (livesIn) paths: repeat the flag or comma-separate; an empty value clears the list"
|
|
3639
|
-
};
|
|
3640
|
-
testsArg = {
|
|
3641
|
-
type: "string",
|
|
3642
|
-
description: "Verified in (verifiedIn) test paths: repeat the flag or comma-separate; an empty value clears the list"
|
|
3643
|
-
};
|
|
3644
4769
|
relatesArg = {
|
|
3645
4770
|
type: "string",
|
|
3646
4771
|
description: "Related requirement ids (relates): repeat the flag or comma-separate; an empty value clears the list"
|
|
@@ -3652,8 +4777,8 @@ var exports_commands = {};
|
|
|
3652
4777
|
__export(exports_commands, {
|
|
3653
4778
|
indexCommand: () => indexCommand
|
|
3654
4779
|
});
|
|
3655
|
-
import { mkdirSync as
|
|
3656
|
-
import { dirname as
|
|
4780
|
+
import { mkdirSync as mkdirSync4 } from "fs";
|
|
4781
|
+
import { dirname as dirname7 } from "path";
|
|
3657
4782
|
import { defineCommand as defineCommand2 } from "citty";
|
|
3658
4783
|
var indexCommand;
|
|
3659
4784
|
var init_commands = __esm(() => {
|
|
@@ -3700,7 +4825,7 @@ var init_commands = __esm(() => {
|
|
|
3700
4825
|
let storage2;
|
|
3701
4826
|
try {
|
|
3702
4827
|
assertSpecPlatform(platformDir);
|
|
3703
|
-
|
|
4828
|
+
mkdirSync4(dirname7(dbPath), { recursive: true });
|
|
3704
4829
|
storage2 = openStorage(dbPath);
|
|
3705
4830
|
const result = await runIndex({ platformDir, storage: storage2 });
|
|
3706
4831
|
if (args.json) {
|
|
@@ -3719,7 +4844,7 @@ var init_commands = __esm(() => {
|
|
|
3719
4844
|
console.error(formatNotASpecPlatform(err.platformDir));
|
|
3720
4845
|
process.exit(EXIT.USAGE);
|
|
3721
4846
|
}
|
|
3722
|
-
handleInvalidDomainFiles(err, !!args.json);
|
|
4847
|
+
await handleInvalidDomainFiles(err, !!args.json);
|
|
3723
4848
|
console.error("spec index FAILED:", err);
|
|
3724
4849
|
process.exit(EXIT.FAILURE);
|
|
3725
4850
|
} finally {
|
|
@@ -3729,6 +4854,87 @@ var init_commands = __esm(() => {
|
|
|
3729
4854
|
});
|
|
3730
4855
|
});
|
|
3731
4856
|
|
|
4857
|
+
// src/base/gitBase.ts
|
|
4858
|
+
function gitEnv() {
|
|
4859
|
+
return Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.startsWith("GIT_") || k.startsWith("GIT_CONFIG")));
|
|
4860
|
+
}
|
|
4861
|
+
function isSafeRef(ref) {
|
|
4862
|
+
return /^[A-Za-z0-9._/][A-Za-z0-9._/-]*$/.test(ref);
|
|
4863
|
+
}
|
|
4864
|
+
function gitRepoPrefix(platformDir) {
|
|
4865
|
+
const proc = Bun.spawnSync(["git", "rev-parse", "--show-prefix"], {
|
|
4866
|
+
cwd: platformDir,
|
|
4867
|
+
env: gitEnv()
|
|
4868
|
+
});
|
|
4869
|
+
if (proc.exitCode !== 0)
|
|
4870
|
+
return "";
|
|
4871
|
+
return proc.stdout.toString().trim();
|
|
4872
|
+
}
|
|
4873
|
+
function gitRefResolves(platformDir, ref) {
|
|
4874
|
+
if (!isSafeRef(ref))
|
|
4875
|
+
return false;
|
|
4876
|
+
const proc = Bun.spawnSync(["git", "rev-parse", "--verify", "--quiet", `${ref}^{commit}`], {
|
|
4877
|
+
cwd: platformDir,
|
|
4878
|
+
env: gitEnv()
|
|
4879
|
+
});
|
|
4880
|
+
return proc.exitCode === 0;
|
|
4881
|
+
}
|
|
4882
|
+
function gitShow(platformDir, ref, relPath) {
|
|
4883
|
+
if (!isSafeRef(ref))
|
|
4884
|
+
return null;
|
|
4885
|
+
const full = `${gitRepoPrefix(platformDir)}${relPath}`;
|
|
4886
|
+
const proc = Bun.spawnSync(["git", "show", `${ref}:${full}`], {
|
|
4887
|
+
cwd: platformDir,
|
|
4888
|
+
env: gitEnv()
|
|
4889
|
+
});
|
|
4890
|
+
if (proc.exitCode !== 0)
|
|
4891
|
+
return null;
|
|
4892
|
+
return proc.stdout.toString();
|
|
4893
|
+
}
|
|
4894
|
+
function gitLsTree(platformDir, ref, dir) {
|
|
4895
|
+
if (!isSafeRef(ref))
|
|
4896
|
+
return [];
|
|
4897
|
+
const prefix = gitRepoPrefix(platformDir);
|
|
4898
|
+
const proc = Bun.spawnSync(["git", "ls-tree", "-r", "--name-only", ref, "--", `${prefix}${dir}`], { cwd: platformDir, env: gitEnv() });
|
|
4899
|
+
if (proc.exitCode !== 0)
|
|
4900
|
+
return [];
|
|
4901
|
+
return proc.stdout.toString().split(`
|
|
4902
|
+
`).map((l) => l.trim()).filter((l) => l.length > 0).map((l) => prefix.length > 0 && l.startsWith(prefix) ? l.slice(prefix.length) : l);
|
|
4903
|
+
}
|
|
4904
|
+
function gitTreeMatchesHead(platformDir) {
|
|
4905
|
+
return Bun.spawnSync(["git", "diff", "--quiet", "HEAD", "--", ".", ":!.spec-engine"], {
|
|
4906
|
+
cwd: platformDir
|
|
4907
|
+
}).exitCode === 0;
|
|
4908
|
+
}
|
|
4909
|
+
function gitDiffNameStatus(platformDir, ref) {
|
|
4910
|
+
if (!isSafeRef(ref))
|
|
4911
|
+
return [];
|
|
4912
|
+
const proc = Bun.spawnSync(["git", "diff", "--name-status", "--no-renames", "--relative", ref, "--"], { cwd: platformDir, env: gitEnv() });
|
|
4913
|
+
if (proc.exitCode !== 0)
|
|
4914
|
+
return [];
|
|
4915
|
+
const rows = [];
|
|
4916
|
+
for (const line of proc.stdout.toString().split(`
|
|
4917
|
+
`)) {
|
|
4918
|
+
if (line.length === 0)
|
|
4919
|
+
continue;
|
|
4920
|
+
const parts = line.split("\t");
|
|
4921
|
+
const status = (parts[0] ?? "").trim().toUpperCase();
|
|
4922
|
+
const path = (parts[parts.length - 1] ?? "").trim();
|
|
4923
|
+
if (status.length === 0 || path.length === 0)
|
|
4924
|
+
continue;
|
|
4925
|
+
rows.push({ status, path });
|
|
4926
|
+
}
|
|
4927
|
+
return rows;
|
|
4928
|
+
}
|
|
4929
|
+
function newestReleaseTag(platformDir) {
|
|
4930
|
+
const proc = Bun.spawnSync(["git", "tag", "--list", "v*", "--merged", "HEAD", "--sort=-v:refname"], { cwd: platformDir, env: gitEnv() });
|
|
4931
|
+
if (proc.exitCode !== 0)
|
|
4932
|
+
return null;
|
|
4933
|
+
const tag = proc.stdout.toString().split(`
|
|
4934
|
+
`)[0]?.trim() ?? "";
|
|
4935
|
+
return tag !== "" && isSafeRef(tag) ? tag : null;
|
|
4936
|
+
}
|
|
4937
|
+
|
|
3732
4938
|
// src/check/proven.ts
|
|
3733
4939
|
function normalizeToSegments(path) {
|
|
3734
4940
|
const out = [];
|
|
@@ -3852,74 +5058,6 @@ var init_proven = __esm(() => {
|
|
|
3852
5058
|
init_src();
|
|
3853
5059
|
});
|
|
3854
5060
|
|
|
3855
|
-
// src/base/gitBase.ts
|
|
3856
|
-
function isSafeRef(ref) {
|
|
3857
|
-
return /^[A-Za-z0-9._/][A-Za-z0-9._/-]*$/.test(ref);
|
|
3858
|
-
}
|
|
3859
|
-
function gitRepoPrefix(platformDir) {
|
|
3860
|
-
const proc = Bun.spawnSync(["git", "rev-parse", "--show-prefix"], { cwd: platformDir });
|
|
3861
|
-
if (proc.exitCode !== 0)
|
|
3862
|
-
return "";
|
|
3863
|
-
return proc.stdout.toString().trim();
|
|
3864
|
-
}
|
|
3865
|
-
function gitRefResolves(platformDir, ref) {
|
|
3866
|
-
if (!isSafeRef(ref))
|
|
3867
|
-
return false;
|
|
3868
|
-
const proc = Bun.spawnSync(["git", "rev-parse", "--verify", "--quiet", `${ref}^{commit}`], {
|
|
3869
|
-
cwd: platformDir
|
|
3870
|
-
});
|
|
3871
|
-
return proc.exitCode === 0;
|
|
3872
|
-
}
|
|
3873
|
-
function gitShow(platformDir, ref, relPath) {
|
|
3874
|
-
if (!isSafeRef(ref))
|
|
3875
|
-
return null;
|
|
3876
|
-
const full = `${gitRepoPrefix(platformDir)}${relPath}`;
|
|
3877
|
-
const proc = Bun.spawnSync(["git", "show", `${ref}:${full}`], { cwd: platformDir });
|
|
3878
|
-
if (proc.exitCode !== 0)
|
|
3879
|
-
return null;
|
|
3880
|
-
return proc.stdout.toString();
|
|
3881
|
-
}
|
|
3882
|
-
function gitLsTree(platformDir, ref, dir) {
|
|
3883
|
-
if (!isSafeRef(ref))
|
|
3884
|
-
return [];
|
|
3885
|
-
const prefix = gitRepoPrefix(platformDir);
|
|
3886
|
-
const proc = Bun.spawnSync(["git", "ls-tree", "-r", "--name-only", ref, "--", `${prefix}${dir}`], { cwd: platformDir });
|
|
3887
|
-
if (proc.exitCode !== 0)
|
|
3888
|
-
return [];
|
|
3889
|
-
return proc.stdout.toString().split(`
|
|
3890
|
-
`).map((l) => l.trim()).filter((l) => l.length > 0).map((l) => prefix.length > 0 && l.startsWith(prefix) ? l.slice(prefix.length) : l);
|
|
3891
|
-
}
|
|
3892
|
-
function gitDiffNameStatus(platformDir, ref) {
|
|
3893
|
-
if (!isSafeRef(ref))
|
|
3894
|
-
return [];
|
|
3895
|
-
const proc = Bun.spawnSync(["git", "diff", "--name-status", "--no-renames", "--relative", ref, "--"], {
|
|
3896
|
-
cwd: platformDir
|
|
3897
|
-
});
|
|
3898
|
-
if (proc.exitCode !== 0)
|
|
3899
|
-
return [];
|
|
3900
|
-
const rows = [];
|
|
3901
|
-
for (const line of proc.stdout.toString().split(`
|
|
3902
|
-
`)) {
|
|
3903
|
-
if (line.length === 0)
|
|
3904
|
-
continue;
|
|
3905
|
-
const parts = line.split("\t");
|
|
3906
|
-
const status = (parts[0] ?? "").trim().toUpperCase();
|
|
3907
|
-
const path = (parts[parts.length - 1] ?? "").trim();
|
|
3908
|
-
if (status.length === 0 || path.length === 0)
|
|
3909
|
-
continue;
|
|
3910
|
-
rows.push({ status, path });
|
|
3911
|
-
}
|
|
3912
|
-
return rows;
|
|
3913
|
-
}
|
|
3914
|
-
function newestReleaseTag(platformDir) {
|
|
3915
|
-
const proc = Bun.spawnSync(["git", "tag", "--list", "v*", "--merged", "HEAD", "--sort=-v:refname"], { cwd: platformDir });
|
|
3916
|
-
if (proc.exitCode !== 0)
|
|
3917
|
-
return null;
|
|
3918
|
-
const tag = proc.stdout.toString().split(`
|
|
3919
|
-
`)[0]?.trim() ?? "";
|
|
3920
|
-
return tag !== "" && isSafeRef(tag) ? tag : null;
|
|
3921
|
-
}
|
|
3922
|
-
|
|
3923
5061
|
// src/check/codeowners.ts
|
|
3924
5062
|
function parseCodeowners(text) {
|
|
3925
5063
|
const rules = [];
|
|
@@ -3997,18 +5135,18 @@ function matchesSegment(pat, seg) {
|
|
|
3997
5135
|
}
|
|
3998
5136
|
|
|
3999
5137
|
// src/authoring/domains.ts
|
|
4000
|
-
import { existsSync as
|
|
4001
|
-
import { join as
|
|
5138
|
+
import { existsSync as existsSync6, readdirSync as readdirSync2 } from "fs";
|
|
5139
|
+
import { join as join9 } from "path";
|
|
4002
5140
|
function normalizeDomainKey(raw) {
|
|
4003
5141
|
return raw.toUpperCase().replace(/\s+/g, "");
|
|
4004
5142
|
}
|
|
4005
5143
|
function listDomainKeys(platformDir) {
|
|
4006
|
-
const specsDir =
|
|
4007
|
-
return readdirSync2(specsDir, { withFileTypes: true }).filter((e) => e.isDirectory() &&
|
|
5144
|
+
const specsDir = join9(platformDir, "spec-engine");
|
|
5145
|
+
return readdirSync2(specsDir, { withFileTypes: true }).filter((e) => e.isDirectory() && existsSync6(join9(specsDir, e.name, "SPEC.json"))).map((e) => e.name).sort();
|
|
4008
5146
|
}
|
|
4009
5147
|
async function domainScope(platformDir, key) {
|
|
4010
5148
|
const { abs: jsonPath, rel: relFile } = specPaths(platformDir, key);
|
|
4011
|
-
if (!
|
|
5149
|
+
if (!existsSync6(jsonPath))
|
|
4012
5150
|
return null;
|
|
4013
5151
|
let text;
|
|
4014
5152
|
try {
|
|
@@ -4049,166 +5187,28 @@ function requirementsOf(text, sourceFile) {
|
|
|
4049
5187
|
}
|
|
4050
5188
|
async function headMaxSeq(platformDir, key) {
|
|
4051
5189
|
try {
|
|
4052
|
-
const proc = Bun.spawn(["git", "-C",
|
|
5190
|
+
const proc = Bun.spawn(["git", "-C", join9(platformDir, "spec-engine", key), "show", "HEAD:./SPEC.json"], { stdout: "pipe", stderr: "ignore", env: gitEnv() });
|
|
4053
5191
|
const text = await new Response(proc.stdout).text();
|
|
4054
5192
|
if (await proc.exited !== 0)
|
|
4055
|
-
return 0;
|
|
4056
|
-
return maxSeqOf(requirementsOf(text, specPaths(platformDir, key).rel));
|
|
4057
|
-
} catch {
|
|
4058
|
-
return 0;
|
|
4059
|
-
}
|
|
4060
|
-
}
|
|
4061
|
-
async function nextRequirementId(platformDir, key) {
|
|
4062
|
-
const { abs: jsonPath, rel: relFile } = specPaths(platformDir, key);
|
|
4063
|
-
if (!existsSync5(jsonPath))
|
|
4064
|
-
return `${key}-001`;
|
|
4065
|
-
const requirements = requirementsOf(await Bun.file(jsonPath).text(), relFile);
|
|
4066
|
-
const maxSeq = Math.max(maxSeqOf(requirements), await headMaxSeq(platformDir, key));
|
|
4067
|
-
const next = String(maxSeq + 1).padStart(3, "0");
|
|
4068
|
-
return `${key}-${next}`;
|
|
4069
|
-
}
|
|
4070
|
-
var init_domains = __esm(() => {
|
|
4071
|
-
init_src();
|
|
4072
|
-
init_constants();
|
|
4073
|
-
init_src();
|
|
4074
|
-
});
|
|
4075
|
-
|
|
4076
|
-
// src/authoring/filerefs.ts
|
|
4077
|
-
import { existsSync as existsSync6 } from "fs";
|
|
4078
|
-
import { resolve as resolve7 } from "path";
|
|
4079
|
-
function extractRefsFromText(s) {
|
|
4080
|
-
const out = [];
|
|
4081
|
-
for (const m of s.matchAll(FILE_REF_RE)) {
|
|
4082
|
-
const candidate = (m[1] ?? "").replace(/\.+$/, "");
|
|
4083
|
-
if (candidate.includes("/"))
|
|
4084
|
-
out.push(candidate);
|
|
4085
|
-
}
|
|
4086
|
-
return out;
|
|
4087
|
-
}
|
|
4088
|
-
function resolveFileRef(platformDir, ref) {
|
|
4089
|
-
const root = resolve7(platformDir);
|
|
4090
|
-
const abs = resolve7(root, ref);
|
|
4091
|
-
if (!(abs === root || abs.startsWith(`${root}/`)))
|
|
4092
|
-
return false;
|
|
4093
|
-
return existsSync6(abs);
|
|
4094
|
-
}
|
|
4095
|
-
var FILE_REF_RE;
|
|
4096
|
-
var init_filerefs = __esm(() => {
|
|
4097
|
-
FILE_REF_RE = /(?<![^\s])@([A-Za-z0-9_][A-Za-z0-9_\-./]*)/g;
|
|
4098
|
-
});
|
|
4099
|
-
|
|
4100
|
-
// src/check/filerefs.ts
|
|
4101
|
-
function locateInDomain(raw, sourceFile, requirements, statuses) {
|
|
4102
|
-
const out = [];
|
|
4103
|
-
const rawLines = raw.split(`
|
|
4104
|
-
`);
|
|
4105
|
-
let lineCursor = 0;
|
|
4106
|
-
for (const req of requirements) {
|
|
4107
|
-
const found = findRequirementIdLine(rawLines, req.id, lineCursor);
|
|
4108
|
-
if (found >= 0)
|
|
4109
|
-
lineCursor = found + 1;
|
|
4110
|
-
if (statuses.has(req.status.toLowerCase())) {
|
|
4111
|
-
out.push({ sourceFile, line: found >= 0 ? found + 1 : 0, req });
|
|
4112
|
-
}
|
|
4113
|
-
}
|
|
4114
|
-
return out;
|
|
4115
|
-
}
|
|
4116
|
-
async function locatedEntries(platformDir, statuses) {
|
|
4117
|
-
const out = [];
|
|
4118
|
-
for (const key of listDomainKeys(platformDir)) {
|
|
4119
|
-
const { abs, rel } = specPaths(platformDir, key);
|
|
4120
|
-
let raw;
|
|
4121
|
-
try {
|
|
4122
|
-
raw = await Bun.file(abs).text();
|
|
4123
|
-
} catch {
|
|
4124
|
-
continue;
|
|
4125
|
-
}
|
|
4126
|
-
const parsed = parseDomainText(raw, rel);
|
|
4127
|
-
if (parsed.ok)
|
|
4128
|
-
out.push(...locateInDomain(raw, rel, parsed.data.requirements, statuses));
|
|
4129
|
-
}
|
|
4130
|
-
return out;
|
|
4131
|
-
}
|
|
4132
|
-
function sortRows(rows) {
|
|
4133
|
-
return rows.sort((a, b) => (a.source_file ?? "").localeCompare(b.source_file ?? "") || (a.line ?? 0) - (b.line ?? 0) || (a.req_id ?? "").localeCompare(b.req_id ?? "") || a.code.localeCompare(b.code) || a.detail.localeCompare(b.detail));
|
|
4134
|
-
}
|
|
4135
|
-
function row(at, code, severity, detail) {
|
|
4136
|
-
return {
|
|
4137
|
-
code,
|
|
4138
|
-
severity,
|
|
4139
|
-
repo: null,
|
|
4140
|
-
source_file: at.sourceFile,
|
|
4141
|
-
line: at.line,
|
|
4142
|
-
req_id: at.req.id,
|
|
4143
|
-
detail
|
|
4144
|
-
};
|
|
4145
|
-
}
|
|
4146
|
-
async function brokenFileRefDiagnostics(platformDir) {
|
|
4147
|
-
const rows = [];
|
|
4148
|
-
for (const at of await locatedEntries(platformDir, FILE_REF_STATUSES)) {
|
|
4149
|
-
for (const [field, entries] of [
|
|
4150
|
-
["livesIn", at.req.livesIn],
|
|
4151
|
-
["verifiedIn", at.req.verifiedIn]
|
|
4152
|
-
]) {
|
|
4153
|
-
for (const entry of entries) {
|
|
4154
|
-
const ref = refOf(entry);
|
|
4155
|
-
if (ref.length === 0 || resolveFileRef(platformDir, ref))
|
|
4156
|
-
continue;
|
|
4157
|
-
rows.push(row(at, DiagnosticCode.BROKEN_FILE_REF, "error", `${field} entry ${entry} does not resolve to a file under the platform root`));
|
|
4158
|
-
}
|
|
4159
|
-
}
|
|
4160
|
-
}
|
|
4161
|
-
return sortRows(rows);
|
|
4162
|
-
}
|
|
4163
|
-
function tagKeys(tags, kinds) {
|
|
4164
|
-
return new Set(tags.filter((t) => kinds.includes(t.kind)).map((t) => `${t.req_id} ${t.file}`));
|
|
4165
|
-
}
|
|
4166
|
-
function inFileRows(platformDir, members, at, check) {
|
|
4167
|
-
const rows = [];
|
|
4168
|
-
for (const entry of at.req[check.field]) {
|
|
4169
|
-
const ref = refOf(entry);
|
|
4170
|
-
if (ref.length === 0 || !resolveFileRef(platformDir, ref))
|
|
4171
|
-
continue;
|
|
4172
|
-
const file = scannedTagFile(members, platformDir, ref);
|
|
4173
|
-
if (file === null || check.keys.has(`${at.req.id} ${file}`))
|
|
4174
|
-
continue;
|
|
4175
|
-
rows.push(row(at, check.code, "warning", `${check.field} entry ${entry} holds no ${check.missing} tag for ${at.req.id}`));
|
|
5193
|
+
return 0;
|
|
5194
|
+
return maxSeqOf(requirementsOf(text, specPaths(platformDir, key).rel));
|
|
5195
|
+
} catch {
|
|
5196
|
+
return 0;
|
|
4176
5197
|
}
|
|
4177
|
-
return rows;
|
|
4178
5198
|
}
|
|
4179
|
-
async function
|
|
4180
|
-
const {
|
|
4181
|
-
|
|
4182
|
-
{
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
},
|
|
4188
|
-
{
|
|
4189
|
-
field: "verifiedIn",
|
|
4190
|
-
code: DiagnosticCode.UNVERIFIED_IN_FILE,
|
|
4191
|
-
keys: tagKeys(tags, ["verifies"]),
|
|
4192
|
-
missing: "verifying"
|
|
4193
|
-
}
|
|
4194
|
-
];
|
|
4195
|
-
const rows = [];
|
|
4196
|
-
for (const at of await locatedEntries(platformDir, IN_FILE_STATUSES)) {
|
|
4197
|
-
for (const check of checks)
|
|
4198
|
-
rows.push(...inFileRows(platformDir, members, at, check));
|
|
4199
|
-
}
|
|
4200
|
-
return sortRows(rows);
|
|
5199
|
+
async function nextRequirementId(platformDir, key) {
|
|
5200
|
+
const { abs: jsonPath, rel: relFile } = specPaths(platformDir, key);
|
|
5201
|
+
if (!existsSync6(jsonPath))
|
|
5202
|
+
return `${key}-001`;
|
|
5203
|
+
const requirements = requirementsOf(await Bun.file(jsonPath).text(), relFile);
|
|
5204
|
+
const maxSeq = Math.max(maxSeqOf(requirements), await headMaxSeq(platformDir, key));
|
|
5205
|
+
const next = String(maxSeq + 1).padStart(3, "0");
|
|
5206
|
+
return `${key}-${next}`;
|
|
4201
5207
|
}
|
|
4202
|
-
var
|
|
4203
|
-
var init_filerefs2 = __esm(() => {
|
|
5208
|
+
var init_domains = __esm(() => {
|
|
4204
5209
|
init_src();
|
|
4205
|
-
init_domains();
|
|
4206
|
-
init_filerefs();
|
|
4207
5210
|
init_constants();
|
|
4208
|
-
|
|
4209
|
-
init_pipeline();
|
|
4210
|
-
FILE_REF_STATUSES = new Set(["active", "draft"]);
|
|
4211
|
-
IN_FILE_STATUSES = new Set(["active"]);
|
|
5211
|
+
init_src();
|
|
4212
5212
|
});
|
|
4213
5213
|
|
|
4214
5214
|
// src/check/grammar.ts
|
|
@@ -4341,20 +5341,20 @@ var init_removed = __esm(() => {
|
|
|
4341
5341
|
});
|
|
4342
5342
|
|
|
4343
5343
|
// src/check/severity.ts
|
|
4344
|
-
import { existsSync as existsSync7, readFileSync } from "fs";
|
|
4345
|
-
import { join as
|
|
5344
|
+
import { existsSync as existsSync7, readFileSync as readFileSync2 } from "fs";
|
|
5345
|
+
import { join as join10 } from "path";
|
|
4346
5346
|
function readCheckConfig(platformDir) {
|
|
4347
|
-
const path =
|
|
5347
|
+
const path = join10(platformDir, CHECK_CONFIG_REL);
|
|
4348
5348
|
if (!existsSync7(path))
|
|
4349
5349
|
return { ok: true, config: { severity: [] } };
|
|
4350
|
-
return parseCheckConfigText(
|
|
5350
|
+
return parseCheckConfigText(readFileSync2(path, "utf-8"), CHECK_CONFIG_REL);
|
|
4351
5351
|
}
|
|
4352
|
-
function specificity(rule,
|
|
4353
|
-
if (rule.code !==
|
|
5352
|
+
function specificity(rule, row) {
|
|
5353
|
+
if (rule.code !== row.code)
|
|
4354
5354
|
return Specificity.none;
|
|
4355
5355
|
if (rule.match === undefined)
|
|
4356
5356
|
return Specificity.code;
|
|
4357
|
-
const id =
|
|
5357
|
+
const id = row.req_id;
|
|
4358
5358
|
if (id === null)
|
|
4359
5359
|
return Specificity.none;
|
|
4360
5360
|
if (rule.match.includes(id))
|
|
@@ -4364,20 +5364,20 @@ function specificity(rule, row2) {
|
|
|
4364
5364
|
}
|
|
4365
5365
|
function applySeverity(rows, rules) {
|
|
4366
5366
|
const out = [];
|
|
4367
|
-
for (const
|
|
5367
|
+
for (const row of rows) {
|
|
4368
5368
|
let best;
|
|
4369
5369
|
let bestScore = Specificity.none;
|
|
4370
5370
|
for (const rule of rules) {
|
|
4371
|
-
const score = specificity(rule,
|
|
5371
|
+
const score = specificity(rule, row);
|
|
4372
5372
|
if (score !== Specificity.none && score >= bestScore) {
|
|
4373
5373
|
best = rule;
|
|
4374
5374
|
bestScore = score;
|
|
4375
5375
|
}
|
|
4376
5376
|
}
|
|
4377
5377
|
if (best === undefined)
|
|
4378
|
-
out.push(
|
|
5378
|
+
out.push(row);
|
|
4379
5379
|
else if (best.severity !== "off")
|
|
4380
|
-
out.push({ ...
|
|
5380
|
+
out.push({ ...row, severity: best.severity });
|
|
4381
5381
|
}
|
|
4382
5382
|
return out;
|
|
4383
5383
|
}
|
|
@@ -4424,6 +5424,60 @@ var init_sqlDiagnostics = __esm(() => {
|
|
|
4424
5424
|
]);
|
|
4425
5425
|
});
|
|
4426
5426
|
|
|
5427
|
+
// src/check/statementRules.ts
|
|
5428
|
+
async function domainRows(platformDir, key, config2, scope) {
|
|
5429
|
+
const { abs, rel } = specPaths(platformDir, key);
|
|
5430
|
+
const raw = await Bun.file(abs).text().catch(() => null);
|
|
5431
|
+
if (raw === null)
|
|
5432
|
+
return [];
|
|
5433
|
+
const parsed = parseDomainText(raw, rel);
|
|
5434
|
+
if (!parsed.ok)
|
|
5435
|
+
return [];
|
|
5436
|
+
const rawLines = raw.split(`
|
|
5437
|
+
`);
|
|
5438
|
+
const rows = [];
|
|
5439
|
+
let lineCursor = 0;
|
|
5440
|
+
for (const req of parsed.data.requirements) {
|
|
5441
|
+
const found = findRequirementIdLine(rawLines, req.id, lineCursor);
|
|
5442
|
+
if (found >= 0)
|
|
5443
|
+
lineCursor = found + 1;
|
|
5444
|
+
if (scope.id !== undefined && req.id !== scope.id)
|
|
5445
|
+
continue;
|
|
5446
|
+
if (!scope.statuses.has(req.status.toLowerCase()))
|
|
5447
|
+
continue;
|
|
5448
|
+
for (const f of statementFindings(req.statement, config2)) {
|
|
5449
|
+
rows.push({ req_id: req.id, ...f, source_file: rel, line: found >= 0 ? found + 1 : 0 });
|
|
5450
|
+
}
|
|
5451
|
+
}
|
|
5452
|
+
return rows;
|
|
5453
|
+
}
|
|
5454
|
+
async function statementRuleRows(platformDir, config2, scope) {
|
|
5455
|
+
const rows = [];
|
|
5456
|
+
for (const key of scope.keys ?? listDomainKeys(platformDir)) {
|
|
5457
|
+
if (key !== "TERM")
|
|
5458
|
+
rows.push(...await domainRows(platformDir, key, config2, scope));
|
|
5459
|
+
}
|
|
5460
|
+
rows.sort((a, b) => a.source_file.localeCompare(b.source_file) || a.line - b.line || a.req_id.localeCompare(b.req_id));
|
|
5461
|
+
return rows;
|
|
5462
|
+
}
|
|
5463
|
+
async function statementRulesDiagnostics(platformDir, config2) {
|
|
5464
|
+
const rows = await statementRuleRows(platformDir, config2, { statuses: new Set(["active"]) });
|
|
5465
|
+
return rows.map((r) => ({
|
|
5466
|
+
code: DiagnosticCode.STATEMENT_RULES,
|
|
5467
|
+
severity: config2?.severity?.[r.rule] === "warning" ? "warning" : "error",
|
|
5468
|
+
repo: null,
|
|
5469
|
+
source_file: r.source_file,
|
|
5470
|
+
line: r.line,
|
|
5471
|
+
req_id: r.req_id,
|
|
5472
|
+
detail: formatStatementFinding(r)
|
|
5473
|
+
}));
|
|
5474
|
+
}
|
|
5475
|
+
var init_statementRules2 = __esm(() => {
|
|
5476
|
+
init_src();
|
|
5477
|
+
init_domains();
|
|
5478
|
+
init_constants();
|
|
5479
|
+
});
|
|
5480
|
+
|
|
4427
5481
|
// src/check/statusflip.ts
|
|
4428
5482
|
function normalizeHandle(h) {
|
|
4429
5483
|
return (h.startsWith("@") ? h.slice(1) : h).toLowerCase();
|
|
@@ -4895,8 +5949,6 @@ function successorEntry(id, pred, fields) {
|
|
|
4895
5949
|
supersedes: null,
|
|
4896
5950
|
supersededBy: null,
|
|
4897
5951
|
relates: fields.relates,
|
|
4898
|
-
livesIn: fields.livesIn,
|
|
4899
|
-
verifiedIn: fields.verifiedIn,
|
|
4900
5952
|
issues: fields.issue ? [{ role: "created", id: fields.issue }] : [],
|
|
4901
5953
|
aliases: [...pred.aliases],
|
|
4902
5954
|
cites: pred.cites.map((c) => ({ ...c }))
|
|
@@ -4914,6 +5966,30 @@ var init__envelope = __esm(() => {
|
|
|
4914
5966
|
init__result();
|
|
4915
5967
|
});
|
|
4916
5968
|
|
|
5969
|
+
// src/authoring/filerefs.ts
|
|
5970
|
+
import { existsSync as existsSync9 } from "fs";
|
|
5971
|
+
import { resolve as resolve7 } from "path";
|
|
5972
|
+
function extractRefsFromText(s) {
|
|
5973
|
+
const out = [];
|
|
5974
|
+
for (const m of s.matchAll(FILE_REF_RE)) {
|
|
5975
|
+
const candidate = (m[1] ?? "").replace(/\.+$/, "");
|
|
5976
|
+
if (candidate.includes("/"))
|
|
5977
|
+
out.push(candidate);
|
|
5978
|
+
}
|
|
5979
|
+
return out;
|
|
5980
|
+
}
|
|
5981
|
+
function resolveFileRef(platformDir, ref) {
|
|
5982
|
+
const root = resolve7(platformDir);
|
|
5983
|
+
const abs = resolve7(root, ref);
|
|
5984
|
+
if (!(abs === root || abs.startsWith(`${root}/`)))
|
|
5985
|
+
return false;
|
|
5986
|
+
return existsSync9(abs);
|
|
5987
|
+
}
|
|
5988
|
+
var FILE_REF_RE;
|
|
5989
|
+
var init_filerefs = __esm(() => {
|
|
5990
|
+
FILE_REF_RE = /(?<![^\s])@([A-Za-z0-9_][A-Za-z0-9_\-./]*)/g;
|
|
5991
|
+
});
|
|
5992
|
+
|
|
4917
5993
|
// src/authoring/grammar.ts
|
|
4918
5994
|
async function grammarConfig(platformDir, key) {
|
|
4919
5995
|
const freeform = { grammar: "freeform", severity: "warning" };
|
|
@@ -4962,8 +6038,35 @@ var init_grammar2 = __esm(() => {
|
|
|
4962
6038
|
init_constants();
|
|
4963
6039
|
});
|
|
4964
6040
|
|
|
6041
|
+
// src/authoring/statementRules.ts
|
|
6042
|
+
function statementRulesVerdict(platformDir, key, statement) {
|
|
6043
|
+
if (key === "TERM" || statement.trim() === "")
|
|
6044
|
+
return { refusal: null, warnings: [] };
|
|
6045
|
+
const config2 = readCheckConfig(platformDir);
|
|
6046
|
+
if (!config2.ok)
|
|
6047
|
+
return { refusal: config2.detail, warnings: [] };
|
|
6048
|
+
const rules = config2.config.statementRules;
|
|
6049
|
+
const findings = statementFindings(statement, rules);
|
|
6050
|
+
const errors = findings.filter((f) => (rules?.severity?.[f.rule] ?? "error") === "error");
|
|
6051
|
+
if (errors.length > 0) {
|
|
6052
|
+
return { refusal: [...errors.map(formatStatementFinding), RULE_ZERO].join(`
|
|
6053
|
+
`), warnings: [] };
|
|
6054
|
+
}
|
|
6055
|
+
return {
|
|
6056
|
+
refusal: null,
|
|
6057
|
+
warnings: findings.map((f) => ({
|
|
6058
|
+
kind: "statement",
|
|
6059
|
+
text: `warning \u2014 ${formatStatementFinding(f)}`
|
|
6060
|
+
}))
|
|
6061
|
+
};
|
|
6062
|
+
}
|
|
6063
|
+
var init_statementRules3 = __esm(() => {
|
|
6064
|
+
init_src();
|
|
6065
|
+
init_severity();
|
|
6066
|
+
});
|
|
6067
|
+
|
|
4965
6068
|
// src/operations/mint.ts
|
|
4966
|
-
import { existsSync as
|
|
6069
|
+
import { existsSync as existsSync10 } from "fs";
|
|
4967
6070
|
function unresolvableRefWarnings(platformDir, fieldValues) {
|
|
4968
6071
|
const out = [];
|
|
4969
6072
|
for (const value of fieldValues) {
|
|
@@ -4978,15 +6081,10 @@ function unresolvableRefWarnings(platformDir, fieldValues) {
|
|
|
4978
6081
|
async function mint(input) {
|
|
4979
6082
|
const { platformDir, key } = input;
|
|
4980
6083
|
const { abs: specPath, rel: relFile } = specPaths(platformDir, key);
|
|
4981
|
-
if (!
|
|
6084
|
+
if (!existsSync10(specPath)) {
|
|
4982
6085
|
return fail("not_found", `no domain ${key} (expected ${relFile} under ${platformDir})`);
|
|
4983
6086
|
}
|
|
4984
|
-
const warnings = unresolvableRefWarnings(platformDir, [
|
|
4985
|
-
input.statement,
|
|
4986
|
-
input.why,
|
|
4987
|
-
...input.livesIn,
|
|
4988
|
-
...input.verifiedIn ?? []
|
|
4989
|
-
]);
|
|
6087
|
+
const warnings = unresolvableRefWarnings(platformDir, [input.statement, input.why]);
|
|
4990
6088
|
const verdict = await judgeStatementGrammar(platformDir, key, input.statement);
|
|
4991
6089
|
if (!verdict.ok) {
|
|
4992
6090
|
if (verdict.severity === "error") {
|
|
@@ -4994,6 +6092,10 @@ async function mint(input) {
|
|
|
4994
6092
|
}
|
|
4995
6093
|
warnings.push({ kind: "grammar", text: grammarWarningText("", verdict).slice(2) });
|
|
4996
6094
|
}
|
|
6095
|
+
const rules = statementRulesVerdict(platformDir, key, input.statement);
|
|
6096
|
+
if (rules.refusal !== null)
|
|
6097
|
+
return fail("usage", rules.refusal, { warnings });
|
|
6098
|
+
warnings.push(...rules.warnings);
|
|
4997
6099
|
const file = await readEnvelope(platformDir, key);
|
|
4998
6100
|
if (!file.ok) {
|
|
4999
6101
|
return fail(file.reason, file.detail, { diagnostics: file.diagnostics, warnings });
|
|
@@ -5008,8 +6110,6 @@ async function mint(input) {
|
|
|
5008
6110
|
supersedes: null,
|
|
5009
6111
|
supersededBy: null,
|
|
5010
6112
|
relates: splitList(input.relates ?? []),
|
|
5011
|
-
livesIn: input.livesIn,
|
|
5012
|
-
verifiedIn: input.verifiedIn ?? [],
|
|
5013
6113
|
issues: input.issue ? [{ role: "created", id: input.issue }] : [],
|
|
5014
6114
|
aliases: [],
|
|
5015
6115
|
cites: input.cites ?? []
|
|
@@ -5031,6 +6131,7 @@ var init_mint = __esm(() => {
|
|
|
5031
6131
|
init_domains();
|
|
5032
6132
|
init_filerefs();
|
|
5033
6133
|
init_grammar2();
|
|
6134
|
+
init_statementRules3();
|
|
5034
6135
|
init_constants();
|
|
5035
6136
|
init__envelope();
|
|
5036
6137
|
init__result();
|
|
@@ -5066,8 +6167,6 @@ async function mintTerm(input) {
|
|
|
5066
6167
|
supersedes: null,
|
|
5067
6168
|
supersededBy: null,
|
|
5068
6169
|
relates: [],
|
|
5069
|
-
livesIn: [],
|
|
5070
|
-
verifiedIn: [],
|
|
5071
6170
|
issues: [],
|
|
5072
6171
|
aliases: input.aliases,
|
|
5073
6172
|
cites: [],
|
|
@@ -5168,8 +6267,8 @@ var init_term = __esm(() => {
|
|
|
5168
6267
|
});
|
|
5169
6268
|
|
|
5170
6269
|
// src/operations/glossary.ts
|
|
5171
|
-
import { existsSync as
|
|
5172
|
-
import { join as
|
|
6270
|
+
import { existsSync as existsSync11, readFileSync as readFileSync3 } from "fs";
|
|
6271
|
+
import { join as join11 } from "path";
|
|
5173
6272
|
function parseGlossary(md) {
|
|
5174
6273
|
const terms = [];
|
|
5175
6274
|
let section = null;
|
|
@@ -5221,15 +6320,15 @@ function generateGlossary(terms) {
|
|
|
5221
6320
|
}
|
|
5222
6321
|
function readStoreTerms(platformDir) {
|
|
5223
6322
|
const { abs: specPath, rel: relFile } = specPaths(platformDir, TERM_KEY);
|
|
5224
|
-
if (!
|
|
6323
|
+
if (!existsSync11(specPath))
|
|
5225
6324
|
return [];
|
|
5226
|
-
const parsed = parseDomainText(
|
|
6325
|
+
const parsed = parseDomainText(readFileSync3(specPath, "utf8"), relFile);
|
|
5227
6326
|
if (!parsed.ok)
|
|
5228
6327
|
return [];
|
|
5229
6328
|
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 }));
|
|
5230
6329
|
}
|
|
5231
6330
|
function glossaryPath(platformDir) {
|
|
5232
|
-
return
|
|
6331
|
+
return join11(platformDir, "GLOSSARY.md");
|
|
5233
6332
|
}
|
|
5234
6333
|
async function migrateGlossary(platformDir) {
|
|
5235
6334
|
const store = await termStore(platformDir);
|
|
@@ -5240,10 +6339,10 @@ async function migrateGlossary(platformDir) {
|
|
|
5240
6339
|
return { ok: true, skipped: true, existing: domain2.requirements.length };
|
|
5241
6340
|
}
|
|
5242
6341
|
const gPath = glossaryPath(platformDir);
|
|
5243
|
-
if (!
|
|
6342
|
+
if (!existsSync11(gPath)) {
|
|
5244
6343
|
return fail("not_found", `no GLOSSARY.md to migrate (expected ${gPath})`);
|
|
5245
6344
|
}
|
|
5246
|
-
const parsed = parseGlossary(
|
|
6345
|
+
const parsed = parseGlossary(readFileSync3(gPath, "utf8"));
|
|
5247
6346
|
domain2.requirements = parsed.map((t, i) => ({
|
|
5248
6347
|
id: `${TERM_KEY}-${String(i + 1).padStart(3, "0")}`,
|
|
5249
6348
|
status: "active",
|
|
@@ -5253,8 +6352,6 @@ async function migrateGlossary(platformDir) {
|
|
|
5253
6352
|
supersedes: null,
|
|
5254
6353
|
supersededBy: null,
|
|
5255
6354
|
relates: [],
|
|
5256
|
-
livesIn: [],
|
|
5257
|
-
verifiedIn: [],
|
|
5258
6355
|
issues: [],
|
|
5259
6356
|
aliases: [],
|
|
5260
6357
|
cites: [],
|
|
@@ -5281,10 +6378,10 @@ function glossaryDriftDiagnostic(platformDir) {
|
|
|
5281
6378
|
if (terms.length === 0)
|
|
5282
6379
|
return null;
|
|
5283
6380
|
const gPath = glossaryPath(platformDir);
|
|
5284
|
-
if (!
|
|
6381
|
+
if (!existsSync11(gPath))
|
|
5285
6382
|
return null;
|
|
5286
6383
|
const generated = generateGlossary(terms);
|
|
5287
|
-
const committed =
|
|
6384
|
+
const committed = readFileSync3(gPath, "utf8");
|
|
5288
6385
|
if (generated === committed)
|
|
5289
6386
|
return null;
|
|
5290
6387
|
return {
|
|
@@ -5300,7 +6397,7 @@ function glossaryDriftDiagnostic(platformDir) {
|
|
|
5300
6397
|
function checkGlossary(platformDir) {
|
|
5301
6398
|
const generated = generateGlossary(readStoreTerms(platformDir));
|
|
5302
6399
|
const gPath = glossaryPath(platformDir);
|
|
5303
|
-
const committed =
|
|
6400
|
+
const committed = existsSync11(gPath) ? readFileSync3(gPath, "utf8") : "";
|
|
5304
6401
|
return { ok: true, clean: generated === committed };
|
|
5305
6402
|
}
|
|
5306
6403
|
var GLOSSARY_INTRO = `Canonical names for Spec Engine concepts. When prose, code comments, diagnostics, or
|
|
@@ -5314,7 +6411,7 @@ var init_glossary = __esm(() => {
|
|
|
5314
6411
|
});
|
|
5315
6412
|
|
|
5316
6413
|
// src/guard/collect.ts
|
|
5317
|
-
import { join as
|
|
6414
|
+
import { join as join12 } from "path";
|
|
5318
6415
|
function isIgnored(path) {
|
|
5319
6416
|
return isPathIgnored(path);
|
|
5320
6417
|
}
|
|
@@ -5362,7 +6459,7 @@ async function collectWorktreeReqs(canonicalDir) {
|
|
|
5362
6459
|
const activeIds = new Set;
|
|
5363
6460
|
const supersedesTargets = new Set;
|
|
5364
6461
|
for (const rel of await findDomainJsonFiles(canonicalDir)) {
|
|
5365
|
-
const text = await Bun.file(
|
|
6462
|
+
const text = await Bun.file(join12(canonicalDir, rel)).text();
|
|
5366
6463
|
for (const r of parseReqs(text)) {
|
|
5367
6464
|
ids.add(r.id);
|
|
5368
6465
|
if (r.status.toLowerCase() === "active")
|
|
@@ -5390,7 +6487,7 @@ async function collectFacts(platformDir, ref, storage2) {
|
|
|
5390
6487
|
const deletedSpecFiles = changed.filter((e) => e.status === "D" && isSpecFile(e.path)).map((e) => e.path);
|
|
5391
6488
|
const base = collectBaseReqs(platformDir, ref, specPaths2);
|
|
5392
6489
|
const baseTags = collectBaseTags(platformDir, ref, codePaths);
|
|
5393
|
-
const worktree = await collectWorktreeReqs(
|
|
6490
|
+
const worktree = await collectWorktreeReqs(join12(platformDir, CANONICAL_SPECS_DIR));
|
|
5394
6491
|
const counts = tallyWorktreeTags(storage2);
|
|
5395
6492
|
return {
|
|
5396
6493
|
baseReqs: base.reqs,
|
|
@@ -5507,14 +6604,63 @@ var init_guard = __esm(() => {
|
|
|
5507
6604
|
init_collect();
|
|
5508
6605
|
});
|
|
5509
6606
|
|
|
6607
|
+
// src/operations/migrate.ts
|
|
6608
|
+
async function migrationPendingDiagnostics(platformDir) {
|
|
6609
|
+
const rows = [];
|
|
6610
|
+
for (const key of listDomainKeys(platformDir)) {
|
|
6611
|
+
const { abs, rel } = specPaths(platformDir, key);
|
|
6612
|
+
const raw = await Bun.file(abs).text().catch(() => "");
|
|
6613
|
+
if (!LEGACY_KEY_RE.test(raw))
|
|
6614
|
+
continue;
|
|
6615
|
+
rows.push({
|
|
6616
|
+
code: DiagnosticCode.MIGRATION_PENDING,
|
|
6617
|
+
severity: "warning",
|
|
6618
|
+
repo: null,
|
|
6619
|
+
source_file: rel,
|
|
6620
|
+
line: null,
|
|
6621
|
+
req_id: null,
|
|
6622
|
+
detail: `${rel} still carries the retired livesIn or verifiedIn fields; run spec migrate to strip them`
|
|
6623
|
+
});
|
|
6624
|
+
}
|
|
6625
|
+
return rows;
|
|
6626
|
+
}
|
|
6627
|
+
async function migrate(platformDir) {
|
|
6628
|
+
const files = [];
|
|
6629
|
+
for (const key of listDomainKeys(platformDir)) {
|
|
6630
|
+
const file = await readEnvelope(platformDir, key);
|
|
6631
|
+
if (!file.ok)
|
|
6632
|
+
return file;
|
|
6633
|
+
if (!LEGACY_KEY_RE.test(await Bun.file(file.specPath).text()))
|
|
6634
|
+
continue;
|
|
6635
|
+
const res = await validateAndWrite(file.specPath, file.domain, file.relFile);
|
|
6636
|
+
if (!res.ok) {
|
|
6637
|
+
return fail("invalid_domain_file", res.diagnostics.map((d) => d.detail).join(`
|
|
6638
|
+
`), {
|
|
6639
|
+
diagnostics: res.diagnostics
|
|
6640
|
+
});
|
|
6641
|
+
}
|
|
6642
|
+
files.push(file.relFile);
|
|
6643
|
+
}
|
|
6644
|
+
return { ok: true, files };
|
|
6645
|
+
}
|
|
6646
|
+
var LEGACY_KEY_RE;
|
|
6647
|
+
var init_migrate = __esm(() => {
|
|
6648
|
+
init_src();
|
|
6649
|
+
init_domains();
|
|
6650
|
+
init_constants();
|
|
6651
|
+
init__envelope();
|
|
6652
|
+
init__result();
|
|
6653
|
+
LEGACY_KEY_RE = /(?<!\\)"(?:livesIn|verifiedIn)"\s*:/;
|
|
6654
|
+
});
|
|
6655
|
+
|
|
5510
6656
|
// src/operations/check.ts
|
|
5511
|
-
import { existsSync as
|
|
5512
|
-
import { join as
|
|
6657
|
+
import { existsSync as existsSync12, readFileSync as readFileSync4 } from "fs";
|
|
6658
|
+
import { join as join13 } from "path";
|
|
5513
6659
|
function readCodeownersText(platformDir) {
|
|
5514
|
-
const path =
|
|
5515
|
-
if (!
|
|
6660
|
+
const path = join13(platformDir, ".github", "CODEOWNERS");
|
|
6661
|
+
if (!existsSync12(path))
|
|
5516
6662
|
return null;
|
|
5517
|
-
return
|
|
6663
|
+
return readFileSync4(path, "utf-8");
|
|
5518
6664
|
}
|
|
5519
6665
|
function domainReqsFromText(text, platformRel) {
|
|
5520
6666
|
const validated = parseDomainText(text, platformRel);
|
|
@@ -5527,7 +6673,7 @@ async function collectChangeReqs(canonicalDir) {
|
|
|
5527
6673
|
const relPathById = new Map;
|
|
5528
6674
|
for (const rel of await findDomainJsonFiles(canonicalDir)) {
|
|
5529
6675
|
const platformRel = `${CANONICAL_SPECS_DIR}/${rel}`;
|
|
5530
|
-
const text = await Bun.file(
|
|
6676
|
+
const text = await Bun.file(join13(canonicalDir, rel)).text();
|
|
5531
6677
|
const parsed = domainReqsFromText(text, platformRel);
|
|
5532
6678
|
for (const r of parsed.reqs) {
|
|
5533
6679
|
reqs.push(r);
|
|
@@ -5568,7 +6714,7 @@ async function ingestResults(storage2, resultsPath, diagnostics2) {
|
|
|
5568
6714
|
}
|
|
5569
6715
|
async function governanceGate(input, ref, diagnostics2, results) {
|
|
5570
6716
|
const { platformDir } = input;
|
|
5571
|
-
const change = await collectChangeReqs(
|
|
6717
|
+
const change = await collectChangeReqs(join13(platformDir, CANONICAL_SPECS_DIR));
|
|
5572
6718
|
const base = collectBaseReqs2(platformDir, ref, diagnostics2);
|
|
5573
6719
|
diagnostics2.push(...requirementRemoved(base.reqs, change.reqs, (id) => base.relPathById.get(id) ?? null));
|
|
5574
6720
|
const codeowners = parseCodeowners(readCodeownersText(platformDir) ?? "");
|
|
@@ -5611,9 +6757,9 @@ async function check(input, storage2) {
|
|
|
5611
6757
|
diagnostics2.push(...unsourcedChanges(superseded, storage2.listProvenance()));
|
|
5612
6758
|
}
|
|
5613
6759
|
diagnostics2.push(...await statementGrammarDiagnostics(platformDir));
|
|
5614
|
-
diagnostics2.push(...await
|
|
5615
|
-
diagnostics2.push(...await inFileTagDiagnostics(platformDir, storage2.listTags()));
|
|
6760
|
+
diagnostics2.push(...await statementRulesDiagnostics(platformDir, checkConfig2.config.statementRules));
|
|
5616
6761
|
diagnostics2.push(...await supersedesPointerDiagnostics(platformDir));
|
|
6762
|
+
diagnostics2.push(...await migrationPendingDiagnostics(platformDir));
|
|
5617
6763
|
const glossaryDrift = glossaryDriftDiagnostic(platformDir);
|
|
5618
6764
|
if (glossaryDrift !== null)
|
|
5619
6765
|
diagnostics2.push(glossaryDrift);
|
|
@@ -5622,7 +6768,7 @@ async function check(input, storage2) {
|
|
|
5622
6768
|
await governanceGate(input, input.base, diagnostics2, results);
|
|
5623
6769
|
diagnostics2.push(...await lossDiagnostics(storage2, platformDir, input.base));
|
|
5624
6770
|
} else if (gitRefResolves(platformDir, "HEAD")) {
|
|
5625
|
-
const change = await collectChangeReqs(
|
|
6771
|
+
const change = await collectChangeReqs(join13(platformDir, CANONICAL_SPECS_DIR));
|
|
5626
6772
|
const baseParseSink = [];
|
|
5627
6773
|
const base = collectBaseReqs2(platformDir, "HEAD", baseParseSink);
|
|
5628
6774
|
diagnostics2.push(...requirementRemoved(base.reqs, change.reqs, (id) => base.relPathById.get(id) ?? null));
|
|
@@ -5640,13 +6786,13 @@ async function check(input, storage2) {
|
|
|
5640
6786
|
var LOSS_SEVERITY;
|
|
5641
6787
|
var init_check = __esm(() => {
|
|
5642
6788
|
init_src();
|
|
5643
|
-
init_filerefs2();
|
|
5644
6789
|
init_grammar();
|
|
5645
6790
|
init_propagation_teeth();
|
|
5646
6791
|
init_proven();
|
|
5647
6792
|
init_removed();
|
|
5648
6793
|
init_severity();
|
|
5649
6794
|
init_sqlDiagnostics();
|
|
6795
|
+
init_statementRules2();
|
|
5650
6796
|
init_statusflip();
|
|
5651
6797
|
init_supersedes();
|
|
5652
6798
|
init_unsourced();
|
|
@@ -5657,6 +6803,7 @@ var init_check = __esm(() => {
|
|
|
5657
6803
|
init__result();
|
|
5658
6804
|
init_glossary();
|
|
5659
6805
|
init_guard();
|
|
6806
|
+
init_migrate();
|
|
5660
6807
|
LOSS_SEVERITY = {
|
|
5661
6808
|
IMPL_LOST: "error",
|
|
5662
6809
|
SPEC_FILE_DELETED: "error",
|
|
@@ -5667,7 +6814,8 @@ var init_check = __esm(() => {
|
|
|
5667
6814
|
// src/commands/check.ts
|
|
5668
6815
|
var exports_check = {};
|
|
5669
6816
|
__export(exports_check, {
|
|
5670
|
-
checkCommand: () => checkCommand
|
|
6817
|
+
checkCommand: () => checkCommand,
|
|
6818
|
+
CI_BASE_WARNING: () => CI_BASE_WARNING
|
|
5671
6819
|
});
|
|
5672
6820
|
import { resolve as resolve8 } from "path";
|
|
5673
6821
|
import { defineCommand as defineCommand3 } from "citty";
|
|
@@ -5690,7 +6838,7 @@ function exitAfterCrash(e) {
|
|
|
5690
6838
|
console.error(`spec check: crashed: ${msg}`);
|
|
5691
6839
|
process.exit(EXIT.USAGE);
|
|
5692
6840
|
}
|
|
5693
|
-
var checkCommand;
|
|
6841
|
+
var CI_BASE_WARNING = 'spec check --ci: losses not checked, the working tree equals HEAD. In CI run: spec check . --ci --base "$(git merge-base HEAD origin/<default>)"', checkCommand;
|
|
5694
6842
|
var init_check2 = __esm(() => {
|
|
5695
6843
|
init_src();
|
|
5696
6844
|
init_proven();
|
|
@@ -5699,6 +6847,7 @@ var init_check2 = __esm(() => {
|
|
|
5699
6847
|
init_prompt();
|
|
5700
6848
|
init__index();
|
|
5701
6849
|
init_check();
|
|
6850
|
+
init_stdout();
|
|
5702
6851
|
init__args();
|
|
5703
6852
|
init__shared();
|
|
5704
6853
|
checkCommand = defineCommand3({
|
|
@@ -5772,13 +6921,16 @@ var init_check2 = __esm(() => {
|
|
|
5772
6921
|
return result;
|
|
5773
6922
|
});
|
|
5774
6923
|
} catch (e) {
|
|
5775
|
-
handleInvalidDomainFiles(e, jsonMode);
|
|
6924
|
+
await handleInvalidDomainFiles(e, jsonMode);
|
|
5776
6925
|
exitAfterCrash(e);
|
|
5777
6926
|
}
|
|
5778
6927
|
if (outcome === undefined) {
|
|
5779
6928
|
process.exit(EXIT.USAGE);
|
|
5780
6929
|
return;
|
|
5781
6930
|
}
|
|
6931
|
+
if (args.ci && !args.base && gitTreeMatchesHead(platformDir)) {
|
|
6932
|
+
console.error(CI_BASE_WARNING);
|
|
6933
|
+
}
|
|
5782
6934
|
const diagnostics2 = [...outcome.diagnostics];
|
|
5783
6935
|
if (outcome.proofsUnconfirmed) {
|
|
5784
6936
|
if (jsonMode) {
|
|
@@ -5790,14 +6942,14 @@ var init_check2 = __esm(() => {
|
|
|
5790
6942
|
console.log(renderDiagnostics(diagnostics2, jsonMode ? "json" : "text"));
|
|
5791
6943
|
if (!jsonMode)
|
|
5792
6944
|
console.log(`build_id: ${outcome.buildId}`);
|
|
5793
|
-
|
|
6945
|
+
await exit(outcome.failing ? EXIT.FAILURE : EXIT.OK);
|
|
5794
6946
|
}
|
|
5795
6947
|
});
|
|
5796
6948
|
});
|
|
5797
6949
|
|
|
5798
6950
|
// src/operations/domain.ts
|
|
5799
|
-
import { existsSync as
|
|
5800
|
-
import { dirname as
|
|
6951
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync5 } from "fs";
|
|
6952
|
+
import { dirname as dirname8, join as join14, resolve as resolve9 } from "path";
|
|
5801
6953
|
function definedFields(options) {
|
|
5802
6954
|
const out = {};
|
|
5803
6955
|
if (options.scope !== undefined)
|
|
@@ -5817,12 +6969,12 @@ async function newDomain(platformDir, key, options = {}) {
|
|
|
5817
6969
|
if (!(resolvedDest === resolvedRoot || resolvedDest.startsWith(`${resolvedRoot}/`))) {
|
|
5818
6970
|
return fail("usage", `refusing to write outside platformDir (${resolvedDest})`);
|
|
5819
6971
|
}
|
|
5820
|
-
if (
|
|
6972
|
+
if (existsSync13(dest))
|
|
5821
6973
|
return fail("conflict", `refusing to overwrite ${dest}`);
|
|
5822
|
-
if (
|
|
6974
|
+
if (existsSync13(join14(dirname8(dest), "SPEC.md"))) {
|
|
5823
6975
|
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)");
|
|
5824
6976
|
}
|
|
5825
|
-
|
|
6977
|
+
mkdirSync5(dirname8(dest), { recursive: true });
|
|
5826
6978
|
const envelope = { ...scaffoldDomainObject(key, localToday()), ...definedFields(options) };
|
|
5827
6979
|
const res = await validateAndWrite(dest, envelope, relFile);
|
|
5828
6980
|
if (!res.ok) {
|
|
@@ -6040,12 +7192,12 @@ function sortMatrix(rows) {
|
|
|
6040
7192
|
return a.repo.localeCompare(b.repo);
|
|
6041
7193
|
});
|
|
6042
7194
|
}
|
|
6043
|
-
function cellStatus(
|
|
6044
|
-
if (
|
|
7195
|
+
function cellStatus(row) {
|
|
7196
|
+
if (row.implemented === 1 && row.verified === 1)
|
|
6045
7197
|
return "src+test";
|
|
6046
|
-
if (
|
|
7198
|
+
if (row.implemented === 1)
|
|
6047
7199
|
return "src";
|
|
6048
|
-
if (
|
|
7200
|
+
if (row.verified === 1)
|
|
6049
7201
|
return "test";
|
|
6050
7202
|
return EMPTY_CELL;
|
|
6051
7203
|
}
|
|
@@ -6058,18 +7210,18 @@ function renderMatrix(rows, mode) {
|
|
|
6058
7210
|
return "";
|
|
6059
7211
|
const repos = Array.from(new Set(sorted.map((r) => r.repo))).sort((a, b) => a.localeCompare(b));
|
|
6060
7212
|
const byReq = new Map;
|
|
6061
|
-
for (const
|
|
6062
|
-
let entry = byReq.get(
|
|
7213
|
+
for (const row of sorted) {
|
|
7214
|
+
let entry = byReq.get(row.req_id);
|
|
6063
7215
|
if (!entry) {
|
|
6064
7216
|
entry = {
|
|
6065
|
-
domain_key:
|
|
6066
|
-
req_id:
|
|
6067
|
-
req_status:
|
|
7217
|
+
domain_key: row.domain_key,
|
|
7218
|
+
req_id: row.req_id,
|
|
7219
|
+
req_status: row.req_status,
|
|
6068
7220
|
cells: new Map
|
|
6069
7221
|
};
|
|
6070
|
-
byReq.set(
|
|
7222
|
+
byReq.set(row.req_id, entry);
|
|
6071
7223
|
}
|
|
6072
|
-
entry.cells.set(
|
|
7224
|
+
entry.cells.set(row.repo, cellStatus(row));
|
|
6073
7225
|
}
|
|
6074
7226
|
const header = ["DOMAIN", "REQUIREMENT", "STATUS", ...repos];
|
|
6075
7227
|
const lines = [header];
|
|
@@ -6077,8 +7229,8 @@ function renderMatrix(rows, mode) {
|
|
|
6077
7229
|
const cells = repos.map((r) => entry.cells.get(r) ?? EMPTY_CELL);
|
|
6078
7230
|
lines.push([entry.domain_key, entry.req_id, entry.req_status, ...cells]);
|
|
6079
7231
|
}
|
|
6080
|
-
const widths = header.map((_, col) => Math.max(...lines.map((
|
|
6081
|
-
return lines.map((
|
|
7232
|
+
const widths = header.map((_, col) => Math.max(...lines.map((row) => row[col]?.length ?? 0)));
|
|
7233
|
+
return lines.map((row) => row.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ")).join(`
|
|
6082
7234
|
`);
|
|
6083
7235
|
}
|
|
6084
7236
|
var EMPTY_CELL = "\u2014";
|
|
@@ -6214,12 +7366,19 @@ var init_map = __esm(() => {
|
|
|
6214
7366
|
|
|
6215
7367
|
// src/operations/records.ts
|
|
6216
7368
|
function getRecord(storage2, id) {
|
|
6217
|
-
const
|
|
6218
|
-
|
|
6219
|
-
ok: true,
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
7369
|
+
const req = storage2.getRequirement(id);
|
|
7370
|
+
if (req === null) {
|
|
7371
|
+
return { ok: true, row: null, platformEmpty: storage2.listRequirements().length === 0 };
|
|
7372
|
+
}
|
|
7373
|
+
const implemented_in = [];
|
|
7374
|
+
const verified_in = [];
|
|
7375
|
+
for (const { repo, file, line, kind, level } of storage2.listTags({ req_id: id })) {
|
|
7376
|
+
if (kind === "verifies")
|
|
7377
|
+
verified_in.push({ repo, file, line, level: level ?? null });
|
|
7378
|
+
else
|
|
7379
|
+
implemented_in.push({ repo, file, line, kind });
|
|
7380
|
+
}
|
|
7381
|
+
return { ok: true, row: { ...req, implemented_in, verified_in }, platformEmpty: false };
|
|
6223
7382
|
}
|
|
6224
7383
|
function listRecords(storage2, filter = {}) {
|
|
6225
7384
|
const rows = storage2.listRequirements(filter);
|
|
@@ -6255,8 +7414,18 @@ function renderRecords(rows, mode) {
|
|
|
6255
7414
|
lines.push([r.id, r.status, r.key, String(r.seq), String(r.changed_at_version), r.text]);
|
|
6256
7415
|
}
|
|
6257
7416
|
const fixed = HEADER.length - 1;
|
|
6258
|
-
const widths = HEADER.slice(0, fixed).map((_, col) => Math.max(...lines.map((
|
|
6259
|
-
return lines.map((
|
|
7417
|
+
const widths = HEADER.slice(0, fixed).map((_, col) => Math.max(...lines.map((row) => row[col]?.length ?? 0)));
|
|
7418
|
+
return lines.map((row) => row.map((cell, i) => i < fixed ? cell.padEnd(widths[i] ?? 0) : cell).join(" ").trimEnd()).join(`
|
|
7419
|
+
`);
|
|
7420
|
+
}
|
|
7421
|
+
function renderRecord(record) {
|
|
7422
|
+
const sites = (label, list) => list.length === 0 ? [`${label}: none`] : [`${label}:`, ...list.map((s) => ` ${s.file}:${s.line}`)];
|
|
7423
|
+
return [
|
|
7424
|
+
renderRecords([record], "text"),
|
|
7425
|
+
"",
|
|
7426
|
+
...sites("implemented in", record.implemented_in),
|
|
7427
|
+
...sites("verified in", record.verified_in)
|
|
7428
|
+
].join(`
|
|
6260
7429
|
`);
|
|
6261
7430
|
}
|
|
6262
7431
|
var HEADER;
|
|
@@ -6283,7 +7452,7 @@ var init_get = __esm(() => {
|
|
|
6283
7452
|
getCommand = defineCommand6({
|
|
6284
7453
|
meta: {
|
|
6285
7454
|
name: "get",
|
|
6286
|
-
description: "Print one requirement's full record (id, status, key, seq, versions, statement) from the derived index."
|
|
7455
|
+
description: "Print one requirement's full record (id, status, key, seq, versions, statement) and the file:line of every tag that implements or verifies it, from the derived index."
|
|
6287
7456
|
},
|
|
6288
7457
|
args: {
|
|
6289
7458
|
reqId: {
|
|
@@ -6320,7 +7489,7 @@ var init_get = __esm(() => {
|
|
|
6320
7489
|
console.log("[]");
|
|
6321
7490
|
return;
|
|
6322
7491
|
}
|
|
6323
|
-
console.log(args.json ? JSON.stringify(result.row) :
|
|
7492
|
+
console.log(args.json ? JSON.stringify(result.row) : renderRecord(result.row));
|
|
6324
7493
|
});
|
|
6325
7494
|
}
|
|
6326
7495
|
});
|
|
@@ -6408,11 +7577,11 @@ function renderPropagation(rows, mode) {
|
|
|
6408
7577
|
return "";
|
|
6409
7578
|
const header = ["REPO", "STATE", "VIA", "DRIFT?"];
|
|
6410
7579
|
const lines = [header];
|
|
6411
|
-
for (const
|
|
6412
|
-
lines.push([
|
|
7580
|
+
for (const row of rows) {
|
|
7581
|
+
lines.push([row.repo, row.state, row.via_req_id ?? EMPTY_CELL2, row.drifted ? "yes" : "no"]);
|
|
6413
7582
|
}
|
|
6414
|
-
const widths = header.map((_, col) => Math.max(...lines.map((
|
|
6415
|
-
return lines.map((
|
|
7583
|
+
const widths = header.map((_, col) => Math.max(...lines.map((row) => row[col]?.length ?? 0)));
|
|
7584
|
+
return lines.map((row) => row.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ")).join(`
|
|
6416
7585
|
`);
|
|
6417
7586
|
}
|
|
6418
7587
|
var EMPTY_CELL2 = "\u2014";
|
|
@@ -6486,8 +7655,8 @@ function excerptOf(raw) {
|
|
|
6486
7655
|
}
|
|
6487
7656
|
function renderTable(header, rows) {
|
|
6488
7657
|
const lines = [header, ...rows];
|
|
6489
|
-
const widths = header.map((_, col) => Math.max(...lines.map((
|
|
6490
|
-
return lines.map((
|
|
7658
|
+
const widths = header.map((_, col) => Math.max(...lines.map((row) => row[col]?.length ?? 0)));
|
|
7659
|
+
return lines.map((row) => row.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ")).join(`
|
|
6491
7660
|
`);
|
|
6492
7661
|
}
|
|
6493
7662
|
function renderQuery(hits, mode) {
|
|
@@ -6742,17 +7911,17 @@ function compareReqIds2(a, b) {
|
|
|
6742
7911
|
function sortProvenance(rows) {
|
|
6743
7912
|
return [...rows].sort((a, b) => compareReqIds2(a.req_id, b.req_id) || a.role.localeCompare(b.role) || a.issue_id.localeCompare(b.issue_id) || a.source_file.localeCompare(b.source_file) || a.line - b.line);
|
|
6744
7913
|
}
|
|
6745
|
-
function backingTests(
|
|
7914
|
+
function backingTests(row) {
|
|
6746
7915
|
let base;
|
|
6747
|
-
if (
|
|
7916
|
+
if (row.implemented === 1 && row.verified === 1)
|
|
6748
7917
|
base = "src+test";
|
|
6749
|
-
else if (
|
|
7918
|
+
else if (row.implemented === 1)
|
|
6750
7919
|
base = "src";
|
|
6751
|
-
else if (
|
|
7920
|
+
else if (row.verified === 1)
|
|
6752
7921
|
base = "test";
|
|
6753
7922
|
else
|
|
6754
7923
|
base = "\u2014";
|
|
6755
|
-
return
|
|
7924
|
+
return row.test_levels ? `${base} (${row.test_levels})` : base;
|
|
6756
7925
|
}
|
|
6757
7926
|
function renderProvenance(rows, mode) {
|
|
6758
7927
|
const sorted = sortProvenance(rows);
|
|
@@ -6762,12 +7931,12 @@ function renderProvenance(rows, mode) {
|
|
|
6762
7931
|
if (sorted.length === 0)
|
|
6763
7932
|
return "";
|
|
6764
7933
|
const byReq = new Map;
|
|
6765
|
-
for (const
|
|
6766
|
-
const bucket = byReq.get(
|
|
7934
|
+
for (const row of sorted) {
|
|
7935
|
+
const bucket = byReq.get(row.req_id);
|
|
6767
7936
|
if (bucket)
|
|
6768
|
-
bucket.push(
|
|
7937
|
+
bucket.push(row);
|
|
6769
7938
|
else
|
|
6770
|
-
byReq.set(
|
|
7939
|
+
byReq.set(row.req_id, [row]);
|
|
6771
7940
|
}
|
|
6772
7941
|
const lines = [];
|
|
6773
7942
|
for (const [reqId, links] of byReq) {
|
|
@@ -6782,8 +7951,8 @@ function renderProvenance(rows, mode) {
|
|
|
6782
7951
|
return lines.join(`
|
|
6783
7952
|
`);
|
|
6784
7953
|
}
|
|
6785
|
-
function decorateRow(
|
|
6786
|
-
const prefix = ` ${
|
|
7954
|
+
function decorateRow(row, resolved) {
|
|
7955
|
+
const prefix = ` ${row.role} ${row.issue_id} ${row.source_file}:${row.line}`;
|
|
6787
7956
|
if (resolved?.ok && resolved.title && resolved.url) {
|
|
6788
7957
|
const staleMark = resolved.stale ? " (cached \u2014 refresh failed)" : "";
|
|
6789
7958
|
return `${prefix} ${resolved.title} [${resolved.status ?? ""}] ${resolved.url}${staleMark}`;
|
|
@@ -6807,12 +7976,12 @@ function renderProvenanceDecorated(rows, resolved, mode) {
|
|
|
6807
7976
|
if (sorted.length === 0)
|
|
6808
7977
|
return "";
|
|
6809
7978
|
const byReq = new Map;
|
|
6810
|
-
for (const
|
|
6811
|
-
const bucket = byReq.get(
|
|
7979
|
+
for (const row of sorted) {
|
|
7980
|
+
const bucket = byReq.get(row.req_id);
|
|
6812
7981
|
if (bucket)
|
|
6813
|
-
bucket.push(
|
|
7982
|
+
bucket.push(row);
|
|
6814
7983
|
else
|
|
6815
|
-
byReq.set(
|
|
7984
|
+
byReq.set(row.req_id, [row]);
|
|
6816
7985
|
}
|
|
6817
7986
|
const lines = [];
|
|
6818
7987
|
for (const [reqId, links] of byReq) {
|
|
@@ -6833,8 +8002,8 @@ var TOKEN_HINT = "set SPEC_TRACKER_TOKEN to resolve issue titles";
|
|
|
6833
8002
|
var init_adapter = () => {};
|
|
6834
8003
|
|
|
6835
8004
|
// ../tracker/src/cache.ts
|
|
6836
|
-
import { mkdirSync as
|
|
6837
|
-
import { dirname as
|
|
8005
|
+
import { mkdirSync as mkdirSync6 } from "fs";
|
|
8006
|
+
import { dirname as dirname9, join as join15 } from "path";
|
|
6838
8007
|
function isFresh(entry, now, ttlMs = CACHE_TTL_MS) {
|
|
6839
8008
|
const t = Date.parse(entry.fetched_at);
|
|
6840
8009
|
if (Number.isNaN(t))
|
|
@@ -6848,7 +8017,7 @@ function isValidEntry(e) {
|
|
|
6848
8017
|
return typeof r.title === "string" && typeof r.status === "string" && typeof r.url === "string" && typeof r.fetched_at === "string";
|
|
6849
8018
|
}
|
|
6850
8019
|
function sidecarPath(platformDir) {
|
|
6851
|
-
return
|
|
8020
|
+
return join15(platformDir, ".spec-engine", SIDECAR_FILE);
|
|
6852
8021
|
}
|
|
6853
8022
|
async function readCache(platformDir) {
|
|
6854
8023
|
try {
|
|
@@ -6872,7 +8041,7 @@ async function readCache(platformDir) {
|
|
|
6872
8041
|
async function writeCache(platformDir, cache) {
|
|
6873
8042
|
try {
|
|
6874
8043
|
const path = sidecarPath(platformDir);
|
|
6875
|
-
|
|
8044
|
+
mkdirSync6(dirname9(path), { recursive: true });
|
|
6876
8045
|
await Bun.write(path, `${JSON.stringify(cache, null, 2)}
|
|
6877
8046
|
`);
|
|
6878
8047
|
return true;
|
|
@@ -7178,8 +8347,6 @@ async function mintFromFlags(platformDir, key, textFlag, args, rawArgs) {
|
|
|
7178
8347
|
await mintAndReport(platformDir, key, {
|
|
7179
8348
|
statement,
|
|
7180
8349
|
why: (args.why ?? "").trim(),
|
|
7181
|
-
livesIn: listFlag(rawArgs, "lives", args.lives) ?? [],
|
|
7182
|
-
verifiedIn: listFlag(rawArgs, "tests", args.tests) ?? [],
|
|
7183
8350
|
relates: listFlag(rawArgs, "relates", args.relates) ?? [],
|
|
7184
8351
|
issue: (args.issue ?? "").trim() || undefined,
|
|
7185
8352
|
status: statusOf(args),
|
|
@@ -7199,11 +8366,9 @@ async function mintInteractively(platformDir, key, id, status) {
|
|
|
7199
8366
|
return;
|
|
7200
8367
|
}
|
|
7201
8368
|
const why = (await askLine("Why it matters: ")).trim();
|
|
7202
|
-
const livesIn = listFlag([], "lives", await askLine("Lives in: ")) ?? [];
|
|
7203
8369
|
await mintAndReport(platformDir, key, {
|
|
7204
8370
|
statement: requirement,
|
|
7205
8371
|
why,
|
|
7206
|
-
livesIn,
|
|
7207
8372
|
issue: undefined,
|
|
7208
8373
|
status,
|
|
7209
8374
|
json: false
|
|
@@ -7258,8 +8423,6 @@ var init_req = __esm(() => {
|
|
|
7258
8423
|
description: "Non-interactive authoring: the Requirement field. When set, the entry appends with zero prompts."
|
|
7259
8424
|
},
|
|
7260
8425
|
why: { type: "string", description: "Why it matters field (only with --text)" },
|
|
7261
|
-
lives: livesArg,
|
|
7262
|
-
tests: testsArg,
|
|
7263
8426
|
relates: relatesArg,
|
|
7264
8427
|
issue: {
|
|
7265
8428
|
type: "string",
|
|
@@ -7293,9 +8456,9 @@ var init_req = __esm(() => {
|
|
|
7293
8456
|
await mintFromFlags(platformDir, key, textFlag, args, rawArgs);
|
|
7294
8457
|
return;
|
|
7295
8458
|
}
|
|
7296
|
-
const hasFieldFlags = typeof args.why === "string" || typeof args.
|
|
8459
|
+
const hasFieldFlags = typeof args.why === "string" || typeof args.relates === "string" || typeof args.issue === "string";
|
|
7297
8460
|
if (hasFieldFlags) {
|
|
7298
|
-
console.error("spec req: --why/--
|
|
8461
|
+
console.error("spec req: --why/--relates/--issue require --text (the Requirement field is mandatory)");
|
|
7299
8462
|
process.exit(EXIT.USAGE);
|
|
7300
8463
|
return;
|
|
7301
8464
|
}
|
|
@@ -7383,8 +8546,8 @@ async function listTermsCommand(platformDirRaw, json) {
|
|
|
7383
8546
|
console.log(JSON.stringify(result.rows));
|
|
7384
8547
|
return;
|
|
7385
8548
|
}
|
|
7386
|
-
for (const
|
|
7387
|
-
console.log(`${
|
|
8549
|
+
for (const row of result.rows) {
|
|
8550
|
+
console.log(`${row.id} ${row.term} ${row.status}`);
|
|
7388
8551
|
}
|
|
7389
8552
|
}
|
|
7390
8553
|
async function reviseTermCommand(opts) {
|
|
@@ -7431,8 +8594,8 @@ async function confirmTermCommand(opts) {
|
|
|
7431
8594
|
exitOnFailure("spec term confirm", result);
|
|
7432
8595
|
await withIndex({ platformDir, build: "fresh" }, () => {
|
|
7433
8596
|
return;
|
|
7434
|
-
}).catch((e) => {
|
|
7435
|
-
handleInvalidDomainFiles(e, false, EXIT.USAGE);
|
|
8597
|
+
}).catch(async (e) => {
|
|
8598
|
+
await handleInvalidDomainFiles(e, false, EXIT.USAGE);
|
|
7436
8599
|
throw e;
|
|
7437
8600
|
});
|
|
7438
8601
|
const { pinned, file } = result;
|
|
@@ -7615,7 +8778,7 @@ function resolvePlatform2(platformDirRaw) {
|
|
|
7615
8778
|
}
|
|
7616
8779
|
return platformDir;
|
|
7617
8780
|
}
|
|
7618
|
-
async function
|
|
8781
|
+
async function migrate2(platformDir, json) {
|
|
7619
8782
|
const result = await migrateGlossary(platformDir);
|
|
7620
8783
|
if (!result.ok)
|
|
7621
8784
|
exitOnFailure("spec glossary", result);
|
|
@@ -7639,7 +8802,7 @@ async function generate(platformDir, json) {
|
|
|
7639
8802
|
else
|
|
7640
8803
|
console.log(`generated GLOSSARY.md from ${result.generated} terms`);
|
|
7641
8804
|
}
|
|
7642
|
-
function check2(platformDir, json) {
|
|
8805
|
+
async function check2(platformDir, json) {
|
|
7643
8806
|
if (checkGlossary(platformDir).clean) {
|
|
7644
8807
|
if (json)
|
|
7645
8808
|
console.log(JSON.stringify({ ok: true }));
|
|
@@ -7651,13 +8814,14 @@ function check2(platformDir, json) {
|
|
|
7651
8814
|
console.error("run `spec glossary .` to regenerate the human view from the store");
|
|
7652
8815
|
if (json)
|
|
7653
8816
|
console.log(JSON.stringify({ ok: false }));
|
|
7654
|
-
|
|
8817
|
+
await exit(EXIT.FAILURE);
|
|
7655
8818
|
}
|
|
7656
8819
|
var glossaryCommand;
|
|
7657
8820
|
var init_glossary2 = __esm(() => {
|
|
7658
8821
|
init_constants();
|
|
7659
8822
|
init_discover();
|
|
7660
8823
|
init_glossary();
|
|
8824
|
+
init_stdout();
|
|
7661
8825
|
init__args();
|
|
7662
8826
|
init__shared();
|
|
7663
8827
|
glossaryCommand = defineCommand14({
|
|
@@ -7681,15 +8845,69 @@ var init_glossary2 = __esm(() => {
|
|
|
7681
8845
|
const platformDir = resolvePlatform2(args.platformDir);
|
|
7682
8846
|
const json = Boolean(args.json);
|
|
7683
8847
|
if (args.migrate)
|
|
7684
|
-
await
|
|
8848
|
+
await migrate2(platformDir, json);
|
|
7685
8849
|
else if (args.check)
|
|
7686
|
-
check2(platformDir, json);
|
|
8850
|
+
await check2(platformDir, json);
|
|
7687
8851
|
else
|
|
7688
8852
|
await generate(platformDir, json);
|
|
7689
8853
|
}
|
|
7690
8854
|
});
|
|
7691
8855
|
});
|
|
7692
8856
|
|
|
8857
|
+
// src/operations/_retag.ts
|
|
8858
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
8859
|
+
import { join as join16 } from "path";
|
|
8860
|
+
function retagLine(line, oldId, newId, doc) {
|
|
8861
|
+
const cr = line.endsWith("\r") ? "\r" : "";
|
|
8862
|
+
const body = cr ? line.slice(0, -1) : line;
|
|
8863
|
+
const re = doc ? DOC_BINDING_RE : SPEC_TAG_RE;
|
|
8864
|
+
if (newId !== null) {
|
|
8865
|
+
return body.replace(re, (m, id) => id === oldId ? m.replace(id, newId) : m) + cr;
|
|
8866
|
+
}
|
|
8867
|
+
const stripped = body.replace(new RegExp(`\\s*(?:${re.source})`, "g"), (m, id, level) => {
|
|
8868
|
+
if (id !== oldId)
|
|
8869
|
+
return m;
|
|
8870
|
+
return typeof level === "string" && !LEVELS.has(level) ? m.slice(m.search(TRAILING_WORD)) : "";
|
|
8871
|
+
});
|
|
8872
|
+
if (stripped === body)
|
|
8873
|
+
return line;
|
|
8874
|
+
const rest = stripped.replace(EMPTY_COMMENT_TAIL, "");
|
|
8875
|
+
const residue = rest.trim();
|
|
8876
|
+
return residue === "" || residue === "*" ? null : rest + cr;
|
|
8877
|
+
}
|
|
8878
|
+
async function applyRetag(platformDir, oldId, newId, freshTags) {
|
|
8879
|
+
const sites = toReqTagRows(await freshTags(oldId));
|
|
8880
|
+
const byFile = new Map;
|
|
8881
|
+
for (const site of sites) {
|
|
8882
|
+
const entry = byFile.get(site.file) ?? { doc: site.kind === "documents", lines: new Set };
|
|
8883
|
+
entry.lines.add(site.line);
|
|
8884
|
+
byFile.set(site.file, entry);
|
|
8885
|
+
}
|
|
8886
|
+
for (const [file, { doc, lines }] of byFile) {
|
|
8887
|
+
const abs = join16(platformDir, file);
|
|
8888
|
+
const out = [];
|
|
8889
|
+
readFileSync5(abs, "utf8").split(`
|
|
8890
|
+
`).forEach((line, i) => {
|
|
8891
|
+
const next = lines.has(i + 1) ? retagLine(line, oldId, newId, doc) : line;
|
|
8892
|
+
if (next !== null)
|
|
8893
|
+
out.push(next);
|
|
8894
|
+
});
|
|
8895
|
+
writeFileSync2(abs, out.join(`
|
|
8896
|
+
`));
|
|
8897
|
+
}
|
|
8898
|
+
if (sites.length > 0)
|
|
8899
|
+
await freshTags(oldId);
|
|
8900
|
+
return sites;
|
|
8901
|
+
}
|
|
8902
|
+
var EMPTY_COMMENT_TAIL, TRAILING_WORD;
|
|
8903
|
+
var init__retag = __esm(() => {
|
|
8904
|
+
init_docs();
|
|
8905
|
+
init_tags();
|
|
8906
|
+
init_reads2();
|
|
8907
|
+
EMPTY_COMMENT_TAIL = /\s*(?:\/\/|#|--|\/\*+\s*\*+\/|<!--\s*-->)\s*$/;
|
|
8908
|
+
TRAILING_WORD = /\s+[a-z][a-z0-9]*$/;
|
|
8909
|
+
});
|
|
8910
|
+
|
|
7693
8911
|
// src/operations/supersede.ts
|
|
7694
8912
|
async function supersedeTarget(platformDir, id) {
|
|
7695
8913
|
const located = await locateEntry(platformDir, id);
|
|
@@ -7708,8 +8926,6 @@ async function supersedeTarget(platformDir, id) {
|
|
|
7708
8926
|
function successorFields(input, req) {
|
|
7709
8927
|
return {
|
|
7710
8928
|
why: (input.why ?? req.why ?? "").trim(),
|
|
7711
|
-
livesIn: input.livesIn ?? [...req.livesIn],
|
|
7712
|
-
verifiedIn: input.verifiedIn ?? [...req.verifiedIn],
|
|
7713
8929
|
relates: input.relates === undefined ? [...req.relates] : splitList(input.relates)
|
|
7714
8930
|
};
|
|
7715
8931
|
}
|
|
@@ -7754,15 +8970,17 @@ async function supersede(input, freshTags) {
|
|
|
7754
8970
|
}
|
|
7755
8971
|
warnings.push({ kind: "grammar", text: grammarWarningText("", verdict).slice(2) });
|
|
7756
8972
|
}
|
|
7757
|
-
const
|
|
7758
|
-
|
|
8973
|
+
const rules = statementRulesVerdict(platformDir, key, input.statement);
|
|
8974
|
+
if (rules.refusal !== null)
|
|
8975
|
+
return fail("usage", rules.refusal);
|
|
8976
|
+
warnings.push(...rules.warnings);
|
|
8977
|
+
const { why, relates } = successorFields(input, req);
|
|
8978
|
+
warnings.push(...unresolvableRefWarnings(platformDir, [input.statement, why]));
|
|
7759
8979
|
const newId = await nextRequirementId(platformDir, key);
|
|
7760
8980
|
const issue = input.issue?.trim() || undefined;
|
|
7761
8981
|
const successor = successorEntry(newId, req, {
|
|
7762
8982
|
statement: input.statement,
|
|
7763
8983
|
why,
|
|
7764
|
-
livesIn,
|
|
7765
|
-
verifiedIn,
|
|
7766
8984
|
relates,
|
|
7767
8985
|
issue
|
|
7768
8986
|
});
|
|
@@ -7779,18 +8997,19 @@ async function supersede(input, freshTags) {
|
|
|
7779
8997
|
warnings
|
|
7780
8998
|
});
|
|
7781
8999
|
}
|
|
7782
|
-
const
|
|
7783
|
-
return { ok: true, oldId: id, newId, file: relFile, specVersion,
|
|
9000
|
+
const retagged = await applyRetag(platformDir, id, newId, freshTags);
|
|
9001
|
+
return { ok: true, oldId: id, newId, file: relFile, specVersion, retagged, warnings };
|
|
7784
9002
|
}
|
|
7785
9003
|
var init_supersede = __esm(() => {
|
|
7786
9004
|
init_src();
|
|
7787
9005
|
init_domains();
|
|
7788
9006
|
init_grammar2();
|
|
9007
|
+
init_statementRules3();
|
|
7789
9008
|
init_domainJson();
|
|
7790
9009
|
init__envelope();
|
|
7791
9010
|
init__result();
|
|
9011
|
+
init__retag();
|
|
7792
9012
|
init_mint();
|
|
7793
|
-
init_reads2();
|
|
7794
9013
|
});
|
|
7795
9014
|
|
|
7796
9015
|
// src/resolve/format.ts
|
|
@@ -7806,14 +9025,14 @@ function renderResolve(rows, mode) {
|
|
|
7806
9025
|
return "";
|
|
7807
9026
|
const header = ["REQ_ID", "STATUS", "TEXT"];
|
|
7808
9027
|
const lines = [header];
|
|
7809
|
-
for (const
|
|
7810
|
-
const raw =
|
|
9028
|
+
for (const row of sorted) {
|
|
9029
|
+
const raw = row.text ?? "";
|
|
7811
9030
|
const truncated = raw.length > TEXT_MAX ? raw.slice(0, TEXT_MAX) + ELLIPSIS2 : raw;
|
|
7812
9031
|
const text = truncated.replace(/\s+/g, " ").trim();
|
|
7813
|
-
lines.push([
|
|
9032
|
+
lines.push([row.id, row.status, text || EMPTY_CELL4]);
|
|
7814
9033
|
}
|
|
7815
|
-
const widths = header.map((_, col) => Math.max(...lines.map((
|
|
7816
|
-
return lines.map((
|
|
9034
|
+
const widths = header.map((_, col) => Math.max(...lines.map((row) => row[col]?.length ?? 0)));
|
|
9035
|
+
return lines.map((row) => row.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ")).join(`
|
|
7817
9036
|
`);
|
|
7818
9037
|
}
|
|
7819
9038
|
function sortReqTags(rows) {
|
|
@@ -7828,8 +9047,8 @@ function renderReqTags(rows, mode) {
|
|
|
7828
9047
|
return "";
|
|
7829
9048
|
const header = ["REPO", "FILE", "LINE", "KIND", "LEVEL"];
|
|
7830
9049
|
const lines = [header];
|
|
7831
|
-
for (const
|
|
7832
|
-
lines.push([
|
|
9050
|
+
for (const row of sorted) {
|
|
9051
|
+
lines.push([row.repo, row.file, String(row.line), row.kind, row.level ?? EMPTY_CELL4]);
|
|
7833
9052
|
}
|
|
7834
9053
|
const widths = header.map((_, col) => Math.max(...lines.map((l) => (l[col] ?? "").length)));
|
|
7835
9054
|
return lines.map((l) => l.map((cell, col) => cell.padEnd(widths[col])).join(" ").trimEnd()).join(`
|
|
@@ -7873,12 +9092,6 @@ function inputFromArgs(args, rawArgs, platformDir, id, statement) {
|
|
|
7873
9092
|
const input = { platformDir, id, statement, noBump: Boolean(args.noBump) };
|
|
7874
9093
|
if (typeof args.why === "string")
|
|
7875
9094
|
input.why = args.why;
|
|
7876
|
-
const livesIn = listFlag(rawArgs, "lives", args.lives);
|
|
7877
|
-
if (livesIn !== undefined)
|
|
7878
|
-
input.livesIn = livesIn;
|
|
7879
|
-
const verifiedIn = listFlag(rawArgs, "tests", args.tests);
|
|
7880
|
-
if (verifiedIn !== undefined)
|
|
7881
|
-
input.verifiedIn = verifiedIn;
|
|
7882
9095
|
const relates = listFlag(rawArgs, "relates", args.relates);
|
|
7883
9096
|
if (relates !== undefined)
|
|
7884
9097
|
input.relates = relates;
|
|
@@ -7915,14 +9128,6 @@ var init_supersede2 = __esm(() => {
|
|
|
7915
9128
|
type: "string",
|
|
7916
9129
|
description: "Successor's Why it matters (default: copied from the old entry)"
|
|
7917
9130
|
},
|
|
7918
|
-
lives: {
|
|
7919
|
-
type: "string",
|
|
7920
|
-
description: "Successor's Lives in: repeat or comma-separate; empty clears (default: copied from the old entry)"
|
|
7921
|
-
},
|
|
7922
|
-
tests: {
|
|
7923
|
-
type: "string",
|
|
7924
|
-
description: "Successor's Verified in (verifiedIn): repeat or comma-separate; empty clears (default: copied from the old entry)"
|
|
7925
|
-
},
|
|
7926
9131
|
relates: {
|
|
7927
9132
|
type: "string",
|
|
7928
9133
|
description: "Successor's related ids: repeat or comma-separate; empty clears (default: copied from the old entry)"
|
|
@@ -7945,13 +9150,13 @@ var init_supersede2 = __esm(() => {
|
|
|
7945
9150
|
},
|
|
7946
9151
|
json: {
|
|
7947
9152
|
type: "boolean",
|
|
7948
|
-
description: "Emit { old_id, new_id, file, spec_version,
|
|
9153
|
+
description: "Emit { old_id, new_id, file, spec_version, retagged } as JSON instead of the text summary"
|
|
7949
9154
|
}
|
|
7950
9155
|
};
|
|
7951
9156
|
supersedeCommand = defineCommand15({
|
|
7952
9157
|
meta: {
|
|
7953
9158
|
name: "supersede",
|
|
7954
|
-
description: "Supersede a shipped requirement: flip it to superseded, mint the successor, bump specVersion, and
|
|
9159
|
+
description: "Supersede a shipped requirement: flip it to superseded, mint the successor, bump specVersion, and retag every site of it to the successor."
|
|
7955
9160
|
},
|
|
7956
9161
|
args: supersedeArgs,
|
|
7957
9162
|
async run({ args, rawArgs }) {
|
|
@@ -7971,21 +9176,18 @@ var init_supersede2 = __esm(() => {
|
|
|
7971
9176
|
if (!result.ok)
|
|
7972
9177
|
exitOnFailure("spec supersede", result);
|
|
7973
9178
|
printWarnings("spec supersede", result.warnings);
|
|
7974
|
-
const { newId, file, specVersion,
|
|
9179
|
+
const { newId, file, specVersion, retagged } = result;
|
|
7975
9180
|
if (args.json) {
|
|
7976
|
-
console.log(JSON.stringify({ old_id: id, new_id: newId, file, spec_version: specVersion,
|
|
9181
|
+
console.log(JSON.stringify({ old_id: id, new_id: newId, file, spec_version: specVersion, retagged }));
|
|
7977
9182
|
} else {
|
|
7978
9183
|
console.log(`superseded ${id} \u2192 ${newId} in ${file}`);
|
|
7979
9184
|
if (specVersion !== null)
|
|
7980
9185
|
console.log(`specVersion bumped to ${specVersion}`);
|
|
7981
|
-
if (
|
|
7982
|
-
console.log(`
|
|
7983
|
-
console.log(renderReqTags(
|
|
9186
|
+
if (retagged.length > 0) {
|
|
9187
|
+
console.log(`retagged ${retagged.length} site(s) from ${id} to ${newId}:`);
|
|
9188
|
+
console.log(renderReqTags(retagged, "text"));
|
|
7984
9189
|
}
|
|
7985
9190
|
}
|
|
7986
|
-
if (retag.length > 0) {
|
|
7987
|
-
console.error(`note: spec check will report SUPERSEDED_REFERENCED at each remaining ${id} tag until the sites are retagged to ${newId}`);
|
|
7988
|
-
}
|
|
7989
9191
|
}
|
|
7990
9192
|
});
|
|
7991
9193
|
});
|
|
@@ -8014,14 +9216,14 @@ async function deprecate(input, freshTags) {
|
|
|
8014
9216
|
diagnostics: res.diagnostics
|
|
8015
9217
|
});
|
|
8016
9218
|
}
|
|
8017
|
-
const
|
|
8018
|
-
return { ok: true, id, file: relFile, reason,
|
|
9219
|
+
const removed = await applyRetag(platformDir, id, null, freshTags);
|
|
9220
|
+
return { ok: true, id, file: relFile, reason, removed };
|
|
8019
9221
|
}
|
|
8020
9222
|
var init_deprecate = __esm(() => {
|
|
8021
9223
|
init_src();
|
|
8022
9224
|
init__envelope();
|
|
8023
9225
|
init__result();
|
|
8024
|
-
|
|
9226
|
+
init__retag();
|
|
8025
9227
|
});
|
|
8026
9228
|
|
|
8027
9229
|
// src/commands/deprecate.ts
|
|
@@ -8056,7 +9258,7 @@ var init_deprecate2 = __esm(() => {
|
|
|
8056
9258
|
},
|
|
8057
9259
|
json: {
|
|
8058
9260
|
type: "boolean",
|
|
8059
|
-
description: "Print { id, file, reason,
|
|
9261
|
+
description: "Print { id, file, reason, removed } instead of text"
|
|
8060
9262
|
}
|
|
8061
9263
|
},
|
|
8062
9264
|
async run({ args }) {
|
|
@@ -8079,15 +9281,15 @@ var init_deprecate2 = __esm(() => {
|
|
|
8079
9281
|
const result = await deprecate({ platformDir, id, reason }, cliFreshTags(platformDir));
|
|
8080
9282
|
if (!result.ok)
|
|
8081
9283
|
exitOnFailure("spec deprecate", result);
|
|
8082
|
-
const { file,
|
|
9284
|
+
const { file, removed } = result;
|
|
8083
9285
|
if (args.json) {
|
|
8084
|
-
console.log(JSON.stringify({ id, file, reason,
|
|
9286
|
+
console.log(JSON.stringify({ id, file, reason, removed }));
|
|
8085
9287
|
return;
|
|
8086
9288
|
}
|
|
8087
9289
|
console.log(`deprecated ${id} in ${file} \u2014 reason recorded`);
|
|
8088
|
-
if (
|
|
8089
|
-
console.
|
|
8090
|
-
console.
|
|
9290
|
+
if (removed.length > 0) {
|
|
9291
|
+
console.log(`removed ${removed.length} tag(s) of ${id}:`);
|
|
9292
|
+
console.log(renderReqTags(removed, "text"));
|
|
8091
9293
|
}
|
|
8092
9294
|
}
|
|
8093
9295
|
});
|
|
@@ -8115,8 +9317,6 @@ function successorFields2(input, req) {
|
|
|
8115
9317
|
return {
|
|
8116
9318
|
statement: (input.statement ?? req.statement).trim(),
|
|
8117
9319
|
why: (input.why ?? req.why ?? "").trim(),
|
|
8118
|
-
livesIn: input.livesIn ?? [...req.livesIn],
|
|
8119
|
-
verifiedIn: input.verifiedIn ?? [...req.verifiedIn],
|
|
8120
9320
|
relates: [...req.relates]
|
|
8121
9321
|
};
|
|
8122
9322
|
}
|
|
@@ -8160,13 +9360,12 @@ async function move(input, freshTags) {
|
|
|
8160
9360
|
}
|
|
8161
9361
|
warnings.push({ kind: "grammar", text: grammarWarningText("", verdict).slice(2) });
|
|
8162
9362
|
}
|
|
9363
|
+
const rules = statementRulesVerdict(platformDir, targetKey, fields.statement);
|
|
9364
|
+
if (rules.refusal !== null)
|
|
9365
|
+
return fail("usage", rules.refusal);
|
|
9366
|
+
warnings.push(...rules.warnings);
|
|
8163
9367
|
}
|
|
8164
|
-
warnings.push(...unresolvableRefWarnings(platformDir, [
|
|
8165
|
-
fields.statement,
|
|
8166
|
-
fields.why,
|
|
8167
|
-
...fields.livesIn,
|
|
8168
|
-
...fields.verifiedIn
|
|
8169
|
-
]));
|
|
9368
|
+
warnings.push(...unresolvableRefWarnings(platformDir, [fields.statement, fields.why]));
|
|
8170
9369
|
const newId = await nextRequirementId(platformDir, targetKey);
|
|
8171
9370
|
const sourceCurrent = source.domain.specVersion ?? 1;
|
|
8172
9371
|
req.status = "superseded";
|
|
@@ -8186,7 +9385,7 @@ async function move(input, freshTags) {
|
|
|
8186
9385
|
}
|
|
8187
9386
|
await validateAndWrite(target.specPath, target.domain, target.relFile);
|
|
8188
9387
|
await validateAndWrite(source.specPath, source.domain, source.relFile);
|
|
8189
|
-
const
|
|
9388
|
+
const retagged = await applyRetag(platformDir, id, newId, freshTags);
|
|
8190
9389
|
return {
|
|
8191
9390
|
ok: true,
|
|
8192
9391
|
oldId: id,
|
|
@@ -8195,7 +9394,7 @@ async function move(input, freshTags) {
|
|
|
8195
9394
|
toFile: target.relFile,
|
|
8196
9395
|
sourceSpecVersion,
|
|
8197
9396
|
targetSpecVersion,
|
|
8198
|
-
|
|
9397
|
+
retagged,
|
|
8199
9398
|
warnings
|
|
8200
9399
|
};
|
|
8201
9400
|
}
|
|
@@ -8203,11 +9402,12 @@ var init_move = __esm(() => {
|
|
|
8203
9402
|
init_src();
|
|
8204
9403
|
init_domains();
|
|
8205
9404
|
init_grammar2();
|
|
9405
|
+
init_statementRules3();
|
|
8206
9406
|
init_domainJson();
|
|
8207
9407
|
init__envelope();
|
|
8208
9408
|
init__result();
|
|
9409
|
+
init__retag();
|
|
8209
9410
|
init_mint();
|
|
8210
|
-
init_reads2();
|
|
8211
9411
|
});
|
|
8212
9412
|
|
|
8213
9413
|
// src/commands/move.ts
|
|
@@ -8216,18 +9416,12 @@ __export(exports_move, {
|
|
|
8216
9416
|
moveCommand: () => moveCommand
|
|
8217
9417
|
});
|
|
8218
9418
|
import { defineCommand as defineCommand17 } from "citty";
|
|
8219
|
-
function inputFromArgs2(args,
|
|
9419
|
+
function inputFromArgs2(args, platformDir, id, targetKey) {
|
|
8220
9420
|
const input = { platformDir, id, targetKey, noBump: Boolean(args.noBump) };
|
|
8221
9421
|
if (typeof args.text === "string")
|
|
8222
9422
|
input.statement = args.text;
|
|
8223
9423
|
if (typeof args.why === "string")
|
|
8224
9424
|
input.why = args.why;
|
|
8225
|
-
const livesIn = listFlag(rawArgs, "lives", args.lives);
|
|
8226
|
-
if (livesIn !== undefined)
|
|
8227
|
-
input.livesIn = livesIn;
|
|
8228
|
-
const verifiedIn = listFlag(rawArgs, "tests", args.tests);
|
|
8229
|
-
if (verifiedIn !== undefined)
|
|
8230
|
-
input.verifiedIn = verifiedIn;
|
|
8231
9425
|
return input;
|
|
8232
9426
|
}
|
|
8233
9427
|
var moveArgs, moveCommand;
|
|
@@ -8259,30 +9453,22 @@ var init_move2 = __esm(() => {
|
|
|
8259
9453
|
type: "string",
|
|
8260
9454
|
description: "Rewrite the successor's Why (default: copied from the source)"
|
|
8261
9455
|
},
|
|
8262
|
-
lives: {
|
|
8263
|
-
type: "string",
|
|
8264
|
-
description: "Rewrite the successor's Lives in: repeat or comma-separate; empty clears (default: copied from the source)"
|
|
8265
|
-
},
|
|
8266
|
-
tests: {
|
|
8267
|
-
type: "string",
|
|
8268
|
-
description: "Rewrite the successor's Verified in (verifiedIn): repeat or comma-separate; empty clears (default: copied from the source)"
|
|
8269
|
-
},
|
|
8270
9456
|
noBump: {
|
|
8271
9457
|
type: "boolean",
|
|
8272
9458
|
description: "Do not bump either envelope's specVersion"
|
|
8273
9459
|
},
|
|
8274
9460
|
json: {
|
|
8275
9461
|
type: "boolean",
|
|
8276
|
-
description: "Emit { old_id, new_id, from_file, to_file, source_spec_version, target_spec_version,
|
|
9462
|
+
description: "Emit { old_id, new_id, from_file, to_file, source_spec_version, target_spec_version, retagged } as JSON"
|
|
8277
9463
|
}
|
|
8278
9464
|
};
|
|
8279
9465
|
moveCommand = defineCommand17({
|
|
8280
9466
|
meta: {
|
|
8281
9467
|
name: "move",
|
|
8282
|
-
description: "Move a requirement to another domain: mint the successor in <NEW-DOMAIN> carrying the source's fields, mark the source superseded, bump both specVersions, and
|
|
9468
|
+
description: "Move a requirement to another domain: mint the successor in <NEW-DOMAIN> carrying the source's fields, mark the source superseded, bump both specVersions, and retag every site of it to the successor."
|
|
8283
9469
|
},
|
|
8284
9470
|
args: moveArgs,
|
|
8285
|
-
async run({ args
|
|
9471
|
+
async run({ args }) {
|
|
8286
9472
|
const id = args.id;
|
|
8287
9473
|
const rawTargetKey = args.newDomain;
|
|
8288
9474
|
const platformDir = resolvePlatformDir(args);
|
|
@@ -8300,11 +9486,11 @@ var init_move2 = __esm(() => {
|
|
|
8300
9486
|
} catch (e) {
|
|
8301
9487
|
handleNotAPlatform(e);
|
|
8302
9488
|
}
|
|
8303
|
-
const result = await move(inputFromArgs2(args,
|
|
9489
|
+
const result = await move(inputFromArgs2(args, platformDir, id, targetKey), cliFreshTags(platformDir));
|
|
8304
9490
|
if (!result.ok)
|
|
8305
9491
|
exitOnFailure("spec move", result);
|
|
8306
9492
|
printWarnings("spec move", result.warnings);
|
|
8307
|
-
const { newId, fromFile, toFile, sourceSpecVersion, targetSpecVersion,
|
|
9493
|
+
const { newId, fromFile, toFile, sourceSpecVersion, targetSpecVersion, retagged } = result;
|
|
8308
9494
|
const sourceKey = id.slice(0, id.indexOf("-"));
|
|
8309
9495
|
if (args.json) {
|
|
8310
9496
|
console.log(JSON.stringify({
|
|
@@ -8314,21 +9500,18 @@ var init_move2 = __esm(() => {
|
|
|
8314
9500
|
to_file: toFile,
|
|
8315
9501
|
source_spec_version: sourceSpecVersion,
|
|
8316
9502
|
target_spec_version: targetSpecVersion,
|
|
8317
|
-
|
|
9503
|
+
retagged
|
|
8318
9504
|
}));
|
|
8319
9505
|
} else {
|
|
8320
9506
|
console.log(`moved ${id} \u2192 ${newId} (${sourceKey} \u2192 ${targetKey})`);
|
|
8321
9507
|
if (sourceSpecVersion !== null || targetSpecVersion !== null) {
|
|
8322
9508
|
console.log(`specVersion: ${sourceKey}\u2192${sourceSpecVersion}, ${targetKey}\u2192${targetSpecVersion}`);
|
|
8323
9509
|
}
|
|
8324
|
-
if (
|
|
8325
|
-
console.log(`
|
|
8326
|
-
console.log(renderReqTags(
|
|
9510
|
+
if (retagged.length > 0) {
|
|
9511
|
+
console.log(`retagged ${retagged.length} site(s) from ${id} to ${newId}:`);
|
|
9512
|
+
console.log(renderReqTags(retagged, "text"));
|
|
8327
9513
|
}
|
|
8328
9514
|
}
|
|
8329
|
-
if (retag.length > 0) {
|
|
8330
|
-
console.error(`note: spec check will report SUPERSEDED_REFERENCED at each remaining ${id} tag until the sites are retagged to ${newId}`);
|
|
8331
|
-
}
|
|
8332
9515
|
}
|
|
8333
9516
|
});
|
|
8334
9517
|
});
|
|
@@ -8357,7 +9540,7 @@ function gateRejection(platformDir, relFile, req, fields) {
|
|
|
8357
9540
|
const tag = releaseHolding(platformDir, relFile, id);
|
|
8358
9541
|
if (tag === null)
|
|
8359
9542
|
return null;
|
|
8360
|
-
return fail("conflict", `${id} is released in ${tag} \u2014 its promise is immutable. ` + `Run \`spec supersede ${id}\` to change the statement or why; ` + "pass only --
|
|
9543
|
+
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 --relates or --issue to amend its metadata in place.");
|
|
8361
9544
|
}
|
|
8362
9545
|
function applyFields(req, fields) {
|
|
8363
9546
|
const fieldsChanged = [];
|
|
@@ -8372,16 +9555,6 @@ function applyFields(req, fields) {
|
|
|
8372
9555
|
fieldsChanged.push("why");
|
|
8373
9556
|
refValues.push(fields.why ?? "");
|
|
8374
9557
|
}
|
|
8375
|
-
if (fields.livesIn !== undefined) {
|
|
8376
|
-
req.livesIn = fields.livesIn;
|
|
8377
|
-
fieldsChanged.push("lives");
|
|
8378
|
-
refValues.push(...fields.livesIn);
|
|
8379
|
-
}
|
|
8380
|
-
if (fields.verifiedIn !== undefined) {
|
|
8381
|
-
req.verifiedIn = fields.verifiedIn;
|
|
8382
|
-
fieldsChanged.push("tests");
|
|
8383
|
-
refValues.push(...fields.verifiedIn);
|
|
8384
|
-
}
|
|
8385
9558
|
if (fields.relates !== undefined) {
|
|
8386
9559
|
req.relates = splitList(fields.relates);
|
|
8387
9560
|
fieldsChanged.push("relates");
|
|
@@ -8419,6 +9592,10 @@ async function amend(input) {
|
|
|
8419
9592
|
}
|
|
8420
9593
|
warnings.push({ kind: "grammar", text: grammarWarningText("", verdict).slice(2) });
|
|
8421
9594
|
}
|
|
9595
|
+
const rules = statementRulesVerdict(platformDir, key, fields.statement);
|
|
9596
|
+
if (rules.refusal !== null)
|
|
9597
|
+
return fail("usage", rules.refusal);
|
|
9598
|
+
warnings.push(...rules.warnings);
|
|
8422
9599
|
}
|
|
8423
9600
|
const { fieldsChanged, refValues } = applyFields(req, fields);
|
|
8424
9601
|
warnings.push(...unresolvableRefWarnings(platformDir, refValues));
|
|
@@ -8437,6 +9614,7 @@ var PROMISE_FIELDS;
|
|
|
8437
9614
|
var init_amend = __esm(() => {
|
|
8438
9615
|
init_src();
|
|
8439
9616
|
init_grammar2();
|
|
9617
|
+
init_statementRules3();
|
|
8440
9618
|
init__envelope();
|
|
8441
9619
|
init__result();
|
|
8442
9620
|
init_mint();
|
|
@@ -8451,9 +9629,9 @@ __export(exports_amend, {
|
|
|
8451
9629
|
import { defineCommand as defineCommand18 } from "citty";
|
|
8452
9630
|
function assertAmendFlags(args) {
|
|
8453
9631
|
const hasIssue = args.issue !== undefined && args.issue.trim() !== "";
|
|
8454
|
-
const hasField = args.text !== undefined || args.why !== undefined || args.
|
|
9632
|
+
const hasField = args.text !== undefined || args.why !== undefined || args.relates !== undefined || args.term !== undefined || args.aliases !== undefined;
|
|
8455
9633
|
if (!hasField && !hasIssue) {
|
|
8456
|
-
console.error("spec amend: nothing to amend \u2014 pass at least one of --text / --why / --
|
|
9634
|
+
console.error("spec amend: nothing to amend \u2014 pass at least one of --text / --why / --relates / --term / --aliases");
|
|
8457
9635
|
process.exit(EXIT.USAGE);
|
|
8458
9636
|
}
|
|
8459
9637
|
if (args.text !== undefined && args.text.trim() === "") {
|
|
@@ -8477,12 +9655,6 @@ function fieldsFromArgs(args, rawArgs) {
|
|
|
8477
9655
|
const v = args.why.trim();
|
|
8478
9656
|
fields.why = v === "" ? null : v;
|
|
8479
9657
|
}
|
|
8480
|
-
const livesIn = listFlag(rawArgs, "lives", args.lives);
|
|
8481
|
-
if (livesIn !== undefined)
|
|
8482
|
-
fields.livesIn = livesIn;
|
|
8483
|
-
const verifiedIn = listFlag(rawArgs, "tests", args.tests);
|
|
8484
|
-
if (verifiedIn !== undefined)
|
|
8485
|
-
fields.verifiedIn = verifiedIn;
|
|
8486
9658
|
const relates = listFlag(rawArgs, "relates", args.relates);
|
|
8487
9659
|
if (relates !== undefined)
|
|
8488
9660
|
fields.relates = relates;
|
|
@@ -8511,8 +9683,6 @@ var init_amend2 = __esm(() => {
|
|
|
8511
9683
|
platformDir: platformDirArg,
|
|
8512
9684
|
text: { type: "string", description: "New Requirement (statement) field value" },
|
|
8513
9685
|
why: { type: "string", description: "New Why it matters field value" },
|
|
8514
|
-
lives: livesArg,
|
|
8515
|
-
tests: testsArg,
|
|
8516
9686
|
relates: relatesArg,
|
|
8517
9687
|
issue: {
|
|
8518
9688
|
type: "string",
|
|
@@ -8528,7 +9698,7 @@ var init_amend2 = __esm(() => {
|
|
|
8528
9698
|
amendCommand = defineCommand18({
|
|
8529
9699
|
meta: {
|
|
8530
9700
|
name: "amend",
|
|
8531
|
-
description: "Revise a requirement's fields in place (same id, no version bump). A released requirement's statement and why are superseded instead; --
|
|
9701
|
+
description: "Revise a requirement's fields in place (same id, no version bump). A released requirement's statement and why are superseded instead; --relates and --issue still amend."
|
|
8532
9702
|
},
|
|
8533
9703
|
args: amendArgs,
|
|
8534
9704
|
async run({ args, rawArgs }) {
|
|
@@ -8568,6 +9738,9 @@ async function accept(input) {
|
|
|
8568
9738
|
if (req.status.toLowerCase() !== "draft") {
|
|
8569
9739
|
return fail("conflict", `${id} is ${displayStatus(req.status)} \u2014 only a Draft entry accepts`);
|
|
8570
9740
|
}
|
|
9741
|
+
const rules = statementRulesVerdict(platformDir, located.key, req.statement);
|
|
9742
|
+
if (rules.refusal !== null)
|
|
9743
|
+
return fail("usage", rules.refusal);
|
|
8571
9744
|
req.status = "active";
|
|
8572
9745
|
domain2.updated = localToday();
|
|
8573
9746
|
const res = await validateAndWrite(specPath, domain2, relFile);
|
|
@@ -8577,10 +9750,11 @@ async function accept(input) {
|
|
|
8577
9750
|
diagnostics: res.diagnostics
|
|
8578
9751
|
});
|
|
8579
9752
|
}
|
|
8580
|
-
return { ok: true, id, file: relFile };
|
|
9753
|
+
return { ok: true, id, file: relFile, warnings: rules.warnings };
|
|
8581
9754
|
}
|
|
8582
9755
|
var init_accept = __esm(() => {
|
|
8583
9756
|
init_src();
|
|
9757
|
+
init_statementRules3();
|
|
8584
9758
|
init__envelope();
|
|
8585
9759
|
init__result();
|
|
8586
9760
|
});
|
|
@@ -8629,6 +9803,7 @@ var init_accept2 = __esm(() => {
|
|
|
8629
9803
|
const result = await accept({ platformDir, id });
|
|
8630
9804
|
if (!result.ok)
|
|
8631
9805
|
exitOnFailure("spec accept", result);
|
|
9806
|
+
printWarnings("spec accept", result.warnings);
|
|
8632
9807
|
if (args.json) {
|
|
8633
9808
|
console.log(JSON.stringify({ id, file: result.file }));
|
|
8634
9809
|
return;
|
|
@@ -8638,6 +9813,158 @@ var init_accept2 = __esm(() => {
|
|
|
8638
9813
|
});
|
|
8639
9814
|
});
|
|
8640
9815
|
|
|
9816
|
+
// src/operations/lint.ts
|
|
9817
|
+
import { existsSync as existsSync14 } from "fs";
|
|
9818
|
+
async function lint2(input) {
|
|
9819
|
+
const { platformDir, target } = input;
|
|
9820
|
+
const config2 = readCheckConfig(platformDir);
|
|
9821
|
+
if (!config2.ok)
|
|
9822
|
+
return fail("usage", config2.detail);
|
|
9823
|
+
const statuses = new Set(input.all ? ["active", "draft"] : ["active"]);
|
|
9824
|
+
let keys;
|
|
9825
|
+
let id;
|
|
9826
|
+
if (target !== undefined && ID_RE.test(target)) {
|
|
9827
|
+
const located = await locateEntry(platformDir, target);
|
|
9828
|
+
if (!located.ok)
|
|
9829
|
+
return located;
|
|
9830
|
+
keys = [located.key];
|
|
9831
|
+
id = target;
|
|
9832
|
+
} else if (target !== undefined && KEY_RE.test(target)) {
|
|
9833
|
+
const { abs, rel } = specPaths(platformDir, target);
|
|
9834
|
+
if (!existsSync14(abs))
|
|
9835
|
+
return fail("not_found", `no domain ${target} (expected ${rel})`);
|
|
9836
|
+
keys = [target];
|
|
9837
|
+
} else if (target !== undefined) {
|
|
9838
|
+
return fail("usage", `${target} is neither a domain key nor a requirement id`);
|
|
9839
|
+
}
|
|
9840
|
+
const findings = await statementRuleRows(platformDir, config2.config.statementRules, {
|
|
9841
|
+
keys,
|
|
9842
|
+
id,
|
|
9843
|
+
statuses
|
|
9844
|
+
});
|
|
9845
|
+
return { ok: true, findings };
|
|
9846
|
+
}
|
|
9847
|
+
var init_lint = __esm(() => {
|
|
9848
|
+
init_src();
|
|
9849
|
+
init_severity();
|
|
9850
|
+
init_statementRules2();
|
|
9851
|
+
init_constants();
|
|
9852
|
+
init__envelope();
|
|
9853
|
+
init__result();
|
|
9854
|
+
});
|
|
9855
|
+
|
|
9856
|
+
// src/commands/lint.ts
|
|
9857
|
+
var exports_lint = {};
|
|
9858
|
+
__export(exports_lint, {
|
|
9859
|
+
lintCommand: () => lintCommand
|
|
9860
|
+
});
|
|
9861
|
+
import { existsSync as existsSync15 } from "fs";
|
|
9862
|
+
import { join as join17, resolve as resolve13 } from "path";
|
|
9863
|
+
import { defineCommand as defineCommand20 } from "citty";
|
|
9864
|
+
function splitPositionals(args) {
|
|
9865
|
+
const { target, platformDir } = args;
|
|
9866
|
+
if (target !== undefined && platformDir === undefined && existsSync15(join17(resolve13(target), CANONICAL_SPECS_DIR))) {
|
|
9867
|
+
return { target: undefined, platformDir: resolve13(target) };
|
|
9868
|
+
}
|
|
9869
|
+
return { target, platformDir: resolvePlatformDir({ platformDir }) };
|
|
9870
|
+
}
|
|
9871
|
+
var lintCommand;
|
|
9872
|
+
var init_lint2 = __esm(() => {
|
|
9873
|
+
init_src();
|
|
9874
|
+
init_constants();
|
|
9875
|
+
init_discover();
|
|
9876
|
+
init_lint();
|
|
9877
|
+
init_stdout();
|
|
9878
|
+
init__args();
|
|
9879
|
+
init__shared();
|
|
9880
|
+
lintCommand = defineCommand20({
|
|
9881
|
+
meta: {
|
|
9882
|
+
name: "lint",
|
|
9883
|
+
description: "Print every statement-rule finding for the platform, one domain (KEY), or one requirement (KEY-NNN); exit 1 on any"
|
|
9884
|
+
},
|
|
9885
|
+
args: {
|
|
9886
|
+
target: {
|
|
9887
|
+
type: "positional",
|
|
9888
|
+
required: false,
|
|
9889
|
+
description: "Domain key or requirement id (default: the whole platform)"
|
|
9890
|
+
},
|
|
9891
|
+
platformDir: platformDirArg,
|
|
9892
|
+
all: { type: "boolean", description: "Judge Draft requirements as well as Active ones" },
|
|
9893
|
+
json: jsonArg
|
|
9894
|
+
},
|
|
9895
|
+
async run({ args }) {
|
|
9896
|
+
const { target, platformDir } = splitPositionals(args);
|
|
9897
|
+
try {
|
|
9898
|
+
assertSpecPlatform(platformDir);
|
|
9899
|
+
} catch (e) {
|
|
9900
|
+
handleNotAPlatform(e);
|
|
9901
|
+
}
|
|
9902
|
+
const result = await lint2({ platformDir, target, all: !!args.all });
|
|
9903
|
+
if (!result.ok)
|
|
9904
|
+
exitOnFailure("spec lint", result);
|
|
9905
|
+
if (args.json) {
|
|
9906
|
+
console.log(JSON.stringify(result.findings.map(({ req_id, rule, detail, reason, source_file, line }) => ({
|
|
9907
|
+
req_id,
|
|
9908
|
+
rule,
|
|
9909
|
+
detail,
|
|
9910
|
+
reason,
|
|
9911
|
+
source_file,
|
|
9912
|
+
line
|
|
9913
|
+
}))));
|
|
9914
|
+
} else {
|
|
9915
|
+
for (const f of result.findings)
|
|
9916
|
+
console.log(`${f.req_id} ${formatStatementFinding(f)}`);
|
|
9917
|
+
if (result.findings.length > 0)
|
|
9918
|
+
console.error(RULE_ZERO);
|
|
9919
|
+
}
|
|
9920
|
+
await exit(result.findings.length > 0 ? EXIT.FAILURE : EXIT.OK);
|
|
9921
|
+
}
|
|
9922
|
+
});
|
|
9923
|
+
});
|
|
9924
|
+
|
|
9925
|
+
// src/commands/migrate.ts
|
|
9926
|
+
var exports_migrate = {};
|
|
9927
|
+
__export(exports_migrate, {
|
|
9928
|
+
migrateCommand: () => migrateCommand
|
|
9929
|
+
});
|
|
9930
|
+
import { defineCommand as defineCommand21 } from "citty";
|
|
9931
|
+
var migrateCommand;
|
|
9932
|
+
var init_migrate2 = __esm(() => {
|
|
9933
|
+
init_discover();
|
|
9934
|
+
init_migrate();
|
|
9935
|
+
init__args();
|
|
9936
|
+
init__shared();
|
|
9937
|
+
migrateCommand = defineCommand21({
|
|
9938
|
+
meta: {
|
|
9939
|
+
name: "migrate",
|
|
9940
|
+
description: "Remove the retired livesIn and verifiedIn fields from every SPEC.json, rewriting only the files that carry one"
|
|
9941
|
+
},
|
|
9942
|
+
args: {
|
|
9943
|
+
platformDir: platformDirArg,
|
|
9944
|
+
json: jsonArg
|
|
9945
|
+
},
|
|
9946
|
+
async run({ args }) {
|
|
9947
|
+
const platformDir = resolvePlatformDir(args);
|
|
9948
|
+
try {
|
|
9949
|
+
assertSpecPlatform(platformDir);
|
|
9950
|
+
} catch (e) {
|
|
9951
|
+
handleNotAPlatform(e);
|
|
9952
|
+
}
|
|
9953
|
+
const result = await migrate(platformDir);
|
|
9954
|
+
if (!result.ok)
|
|
9955
|
+
exitOnFailure("spec migrate", result);
|
|
9956
|
+
if (args.json) {
|
|
9957
|
+
console.log(JSON.stringify({ files: result.files }));
|
|
9958
|
+
return;
|
|
9959
|
+
}
|
|
9960
|
+
if (result.files.length === 0)
|
|
9961
|
+
console.error("spec migrate: nothing to migrate");
|
|
9962
|
+
for (const f of result.files)
|
|
9963
|
+
console.log(`migrated ${f}`);
|
|
9964
|
+
}
|
|
9965
|
+
});
|
|
9966
|
+
});
|
|
9967
|
+
|
|
8641
9968
|
// src/server/authorPrompt.ts
|
|
8642
9969
|
function renderAuthorPrompt(opts) {
|
|
8643
9970
|
const charterBlock = opts.charter ? `
|
|
@@ -8647,6 +9974,8 @@ Domain charter (scope): ${opts.charter}
|
|
|
8647
9974
|
Target domain: ${opts.domain} (no charter/scope recorded \u2014 check placement carefully)
|
|
8648
9975
|
` : "\nNo target domain given \u2014 determine placement from `spec domain list`.\n";
|
|
8649
9976
|
return [
|
|
9977
|
+
RULE_ZERO,
|
|
9978
|
+
"",
|
|
8650
9979
|
"You are authoring Spec Engine requirements from a brief. Follow this playbook exactly.",
|
|
8651
9980
|
charterBlock,
|
|
8652
9981
|
"Brief:",
|
|
@@ -8656,14 +9985,17 @@ Target domain: ${opts.domain} (no charter/scope recorded \u2014 check placement
|
|
|
8656
9985
|
"1. One requirement per behavior \u2014 never one per ticket; a brief fans out into several.",
|
|
8657
9986
|
"2. Placement: the domain whose promise the requirement protects (see the charter above); run `spec query <text>` first \u2014 if a close requirement exists, amend or relate it instead of minting.",
|
|
8658
9987
|
'3. Draft each statement in one of the five EARS shapes ("shall" separates who acts from the observable result): `The <system> shall <result>` / `When <trigger>, the <system> shall <result>` / `While <state>, \u2026` / `If <failure>, then \u2026` / `Where <feature>, \u2026`. Example: "When an unknown id is passed, spec resolve shall print [] and exit 0."',
|
|
8659
|
-
"4. Self-check: the statement makes sense read alone; it describes what the system does, not how the code does it; no expiration dates ('currently', removed things); the why names what breaks if the promise stops holding \u2014 never a restatement.",
|
|
9988
|
+
"4. Self-check: the statement is one sentence of at most 280 characters that names no repository file or folder, test mechanics, CI or tooling layout, history, project jargon, or the tool in use (spec req refuses it otherwise; `spec lint` lists the findings); it makes sense read alone; it describes what the system does, not how the code does it; no expiration dates ('currently', removed things); the why names what breaks if the promise stops holding \u2014 never a restatement.",
|
|
8660
9989
|
"5. Tie each draft to its source: quote the exact phrase of the brief it derives from; label anything you inferred as inferred.",
|
|
8661
9990
|
"6. Present the full batch (statement, why, placement, source quote) for approval BEFORE writing.",
|
|
8662
|
-
'7. Mint each approved requirement: `spec req <domain> --text "\u2026" --why "\u2026"
|
|
9991
|
+
'7. Mint each approved requirement: `spec req <domain> --text "\u2026" --why "\u2026"`.',
|
|
8663
9992
|
"8. The originating ticket is provenance only \u2014 never a requirement id, never a code tag."
|
|
8664
9993
|
].join(`
|
|
8665
9994
|
`);
|
|
8666
9995
|
}
|
|
9996
|
+
var init_authorPrompt = __esm(() => {
|
|
9997
|
+
init_src();
|
|
9998
|
+
});
|
|
8667
9999
|
|
|
8668
10000
|
// src/server/mcp.ts
|
|
8669
10001
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -8680,10 +10012,6 @@ function amendFieldsOf(args) {
|
|
|
8680
10012
|
fields.statement = args.text;
|
|
8681
10013
|
if (args.why !== undefined)
|
|
8682
10014
|
fields.why = args.why || null;
|
|
8683
|
-
if (args.lives !== undefined)
|
|
8684
|
-
fields.livesIn = args.lives;
|
|
8685
|
-
if (args.verified_in !== undefined)
|
|
8686
|
-
fields.verifiedIn = args.verified_in;
|
|
8687
10015
|
if (args.relates !== undefined)
|
|
8688
10016
|
fields.relates = args.relates;
|
|
8689
10017
|
if (args.issue)
|
|
@@ -8713,7 +10041,7 @@ function buildMcpServer(platformDir) {
|
|
|
8713
10041
|
}, async ({ files }) => fresh((s) => jsonResult(resolveFiles(s, files).rows)));
|
|
8714
10042
|
server.registerTool("spec_req_tags", {
|
|
8715
10043
|
title: "Requirement \u2192 tag sites (reverse query)",
|
|
8716
|
-
description: "Every code/test/doc site tagging the given requirement, across all repos \u2014 impact analysis
|
|
10044
|
+
description: "Every code/test/doc site tagging the given requirement, across all repos \u2014 impact analysis.",
|
|
8717
10045
|
inputSchema: { req_id: z4.string().describe("Requirement id (KEY-NNN)") }
|
|
8718
10046
|
}, async ({ req_id }) => {
|
|
8719
10047
|
if (!ID_RE.test(req_id)) {
|
|
@@ -8754,15 +10082,15 @@ function buildMcpServer(platformDir) {
|
|
|
8754
10082
|
});
|
|
8755
10083
|
server.registerTool("spec_get", {
|
|
8756
10084
|
title: "One requirement's full record",
|
|
8757
|
-
description: "The full record for one requirement id: status, key, seq, statement, why, versions, and
|
|
10085
|
+
description: "The full record for one requirement id: status, key, seq, statement, why, versions, source location, and the implemented_in and verified_in tag sites. An unknown id returns [] (the `spec get --json` shape).",
|
|
8758
10086
|
inputSchema: { req_id: z4.string().describe("Requirement id (KEY-NNN)") }
|
|
8759
10087
|
}, async ({ req_id }) => {
|
|
8760
10088
|
if (!ID_RE.test(req_id)) {
|
|
8761
10089
|
return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
|
|
8762
10090
|
}
|
|
8763
10091
|
return fresh((s) => {
|
|
8764
|
-
const { row
|
|
8765
|
-
return jsonResult(
|
|
10092
|
+
const { row } = getRecord(s, req_id);
|
|
10093
|
+
return jsonResult(row ?? []);
|
|
8766
10094
|
});
|
|
8767
10095
|
});
|
|
8768
10096
|
server.registerTool("spec_list", {
|
|
@@ -8787,17 +10115,15 @@ function buildMcpServer(platformDir) {
|
|
|
8787
10115
|
});
|
|
8788
10116
|
server.registerTool("spec_supersede", {
|
|
8789
10117
|
title: "Supersede a shipped requirement",
|
|
8790
|
-
description: "Flip an Active requirement to superseded, mint its successor in the same domain,
|
|
10118
|
+
description: "Flip an Active requirement to superseded, mint its successor in the same domain, rewrite every tag of the predecessor to the successor, and return the rewritten sites. The same JSON as `spec supersede --json`.",
|
|
8791
10119
|
inputSchema: {
|
|
8792
10120
|
req_id: z4.string().describe("The Active requirement id to supersede (KEY-NNN)"),
|
|
8793
10121
|
statement: z4.string().min(1).describe("The successor's statement"),
|
|
8794
10122
|
why: z4.string().optional().describe("Successor's why (default: copied from the predecessor)"),
|
|
8795
|
-
lives_in: z4.array(z4.string()).optional().describe("Successor's livesIn paths (default: copied from the predecessor)"),
|
|
8796
|
-
verified_in: z4.array(z4.string()).optional().describe("Successor's verifiedIn paths (default: copied from the predecessor)"),
|
|
8797
10123
|
relates: z4.array(z4.string()).optional().describe("Successor's related ids, each comma-split; an empty list clears (default: copied from the predecessor)"),
|
|
8798
10124
|
issue: z4.string().optional().describe("Ticket recorded as supersedes-via on the predecessor and created on the successor")
|
|
8799
10125
|
}
|
|
8800
|
-
}, async ({ req_id, statement, why,
|
|
10126
|
+
}, async ({ req_id, statement, why, relates, issue }) => {
|
|
8801
10127
|
if (!ID_RE.test(req_id)) {
|
|
8802
10128
|
return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
|
|
8803
10129
|
}
|
|
@@ -8806,8 +10132,6 @@ function buildMcpServer(platformDir) {
|
|
|
8806
10132
|
id: req_id,
|
|
8807
10133
|
statement,
|
|
8808
10134
|
why,
|
|
8809
|
-
livesIn: lives_in,
|
|
8810
|
-
verifiedIn: verified_in,
|
|
8811
10135
|
relates,
|
|
8812
10136
|
issue
|
|
8813
10137
|
}, coldFreshTags(platformDir));
|
|
@@ -8820,12 +10144,12 @@ function buildMcpServer(platformDir) {
|
|
|
8820
10144
|
new_id: r.newId,
|
|
8821
10145
|
file: r.file,
|
|
8822
10146
|
spec_version: r.specVersion,
|
|
8823
|
-
|
|
10147
|
+
retagged: r.retagged
|
|
8824
10148
|
});
|
|
8825
10149
|
});
|
|
8826
10150
|
server.registerTool("spec_deprecate", {
|
|
8827
10151
|
title: "Deprecate a requirement",
|
|
8828
|
-
description: "Mark an Active or Draft requirement deprecated with a recorded reason
|
|
10152
|
+
description: "Mark an Active or Draft requirement deprecated with a recorded reason, remove every tag of it from the code, and return the removed sites. The same JSON as `spec deprecate --json`.",
|
|
8829
10153
|
inputSchema: {
|
|
8830
10154
|
req_id: z4.string().describe("The requirement id to deprecate (KEY-NNN)"),
|
|
8831
10155
|
reason: z4.string().min(1).describe("Why the requirement ended; the durable record")
|
|
@@ -8838,7 +10162,7 @@ function buildMcpServer(platformDir) {
|
|
|
8838
10162
|
if (trimmed === "")
|
|
8839
10163
|
return errorResult("reason must be non-empty");
|
|
8840
10164
|
const r = await deprecate({ platformDir, id: req_id, reason: trimmed }, coldFreshTags(platformDir));
|
|
8841
|
-
return r.ok ? jsonResult({ id: r.id, file: r.file, reason: r.reason,
|
|
10165
|
+
return r.ok ? jsonResult({ id: r.id, file: r.file, reason: r.reason, removed: r.removed }) : errorResult(r.detail);
|
|
8842
10166
|
});
|
|
8843
10167
|
server.registerTool("spec_accept", {
|
|
8844
10168
|
title: "Accept a draft requirement",
|
|
@@ -8851,7 +10175,11 @@ function buildMcpServer(platformDir) {
|
|
|
8851
10175
|
return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
|
|
8852
10176
|
}
|
|
8853
10177
|
const r = await accept({ platformDir, id: req_id });
|
|
8854
|
-
|
|
10178
|
+
if (!r.ok)
|
|
10179
|
+
return errorResult(r.detail);
|
|
10180
|
+
for (const w of r.warnings)
|
|
10181
|
+
console.error(`spec_accept: ${w.text}`);
|
|
10182
|
+
return jsonResult({ id: r.id, file: r.file });
|
|
8855
10183
|
});
|
|
8856
10184
|
server.registerTool("spec_req", {
|
|
8857
10185
|
title: "Author a requirement",
|
|
@@ -8860,13 +10188,11 @@ function buildMcpServer(platformDir) {
|
|
|
8860
10188
|
domain: z4.string().min(1).describe("Domain key or unique case-insensitive prefix"),
|
|
8861
10189
|
text: z4.string().trim().min(1).describe("The requirement statement"),
|
|
8862
10190
|
why: z4.string().trim().default("").describe("What breaks without it"),
|
|
8863
|
-
lives: z4.array(z4.string()).default([]).describe("livesIn paths"),
|
|
8864
|
-
verified_in: z4.array(z4.string()).default([]).describe("verifiedIn test paths"),
|
|
8865
10191
|
relates: z4.array(z4.string()).default([]).describe("Related requirement ids, each comma-split"),
|
|
8866
10192
|
issue: z4.string().trim().optional().describe("Originating ticket, recorded as created"),
|
|
8867
10193
|
draft: z4.boolean().default(false).describe("Author as Draft instead of Active")
|
|
8868
10194
|
}
|
|
8869
|
-
}, async ({ domain: domain2, text, why,
|
|
10195
|
+
}, async ({ domain: domain2, text, why, relates, issue, draft }) => {
|
|
8870
10196
|
const allocated = await nextId(platformDir, domain2);
|
|
8871
10197
|
if (!allocated.ok)
|
|
8872
10198
|
return errorResult(allocated.detail);
|
|
@@ -8875,8 +10201,6 @@ function buildMcpServer(platformDir) {
|
|
|
8875
10201
|
key: allocated.key,
|
|
8876
10202
|
statement: text,
|
|
8877
10203
|
why,
|
|
8878
|
-
livesIn: lives,
|
|
8879
|
-
verifiedIn: verified_in,
|
|
8880
10204
|
relates,
|
|
8881
10205
|
issue: issue || undefined,
|
|
8882
10206
|
status: draft ? "draft" : "active"
|
|
@@ -8894,17 +10218,15 @@ function buildMcpServer(platformDir) {
|
|
|
8894
10218
|
id: z4.string().describe("The requirement id to amend (KEY-NNN)"),
|
|
8895
10219
|
text: z4.string().trim().min(1).optional().describe("New statement"),
|
|
8896
10220
|
why: z4.string().trim().optional().describe("New why; empty clears it"),
|
|
8897
|
-
lives: z4.array(z4.string()).optional().describe("Replacement livesIn list; empty clears"),
|
|
8898
|
-
verified_in: z4.array(z4.string()).optional().describe("Replacement verifiedIn list; empty clears"),
|
|
8899
10221
|
relates: z4.array(z4.string()).optional().describe("Replacement related ids, each comma-split; empty clears"),
|
|
8900
10222
|
issue: z4.string().trim().optional().describe("Ticket recorded as amends-via")
|
|
8901
10223
|
}
|
|
8902
|
-
}, async ({ id, text, why,
|
|
10224
|
+
}, async ({ id, text, why, relates, issue }) => {
|
|
8903
10225
|
if (!ID_RE.test(id))
|
|
8904
10226
|
return errorResult(`id must be a requirement id (KEY-NNN); got ${id}`);
|
|
8905
|
-
const fields = amendFieldsOf({ text, why,
|
|
10227
|
+
const fields = amendFieldsOf({ text, why, relates, issue });
|
|
8906
10228
|
if (Object.keys(fields).length === 0) {
|
|
8907
|
-
return errorResult("nothing to amend \u2014 pass at least one of text, why,
|
|
10229
|
+
return errorResult("nothing to amend \u2014 pass at least one of text, why, relates, issue");
|
|
8908
10230
|
}
|
|
8909
10231
|
const r = await amend({ platformDir, id, fields });
|
|
8910
10232
|
if (!r.ok)
|
|
@@ -8915,16 +10237,14 @@ function buildMcpServer(platformDir) {
|
|
|
8915
10237
|
});
|
|
8916
10238
|
server.registerTool("spec_move", {
|
|
8917
10239
|
title: "Move a requirement to another domain",
|
|
8918
|
-
description: "Mint the successor in newDomain carrying the source's fields, mark the source superseded, and return the
|
|
10240
|
+
description: "Mint the successor in newDomain carrying the source's fields, mark the source superseded, rewrite every tag of the source to the successor, and return the rewritten sites. The same JSON as `spec move --json`.",
|
|
8919
10241
|
inputSchema: {
|
|
8920
10242
|
id: z4.string().describe("The Active requirement id to move (KEY-NNN)"),
|
|
8921
10243
|
newDomain: z4.string().min(1).describe("The existing target domain key"),
|
|
8922
10244
|
text: z4.string().optional().describe("Successor statement (default: copied)"),
|
|
8923
|
-
why: z4.string().optional().describe("Successor why (default: copied)")
|
|
8924
|
-
lives: z4.array(z4.string()).optional().describe("Successor livesIn list; empty clears (default: copied)"),
|
|
8925
|
-
verified_in: z4.array(z4.string()).optional().describe("Successor verifiedIn list; empty clears (default: copied)")
|
|
10245
|
+
why: z4.string().optional().describe("Successor why (default: copied)")
|
|
8926
10246
|
}
|
|
8927
|
-
}, async ({ id, newDomain: newDomain2, text, why
|
|
10247
|
+
}, async ({ id, newDomain: newDomain2, text, why }) => {
|
|
8928
10248
|
if (!ID_RE.test(id))
|
|
8929
10249
|
return errorResult(`id must be a requirement id (KEY-NNN); got ${id}`);
|
|
8930
10250
|
const targetKey = normalizeDomainKey(newDomain2);
|
|
@@ -8935,9 +10255,7 @@ function buildMcpServer(platformDir) {
|
|
|
8935
10255
|
id,
|
|
8936
10256
|
targetKey,
|
|
8937
10257
|
statement: text,
|
|
8938
|
-
why
|
|
8939
|
-
livesIn: lives,
|
|
8940
|
-
verifiedIn: verified_in
|
|
10258
|
+
why
|
|
8941
10259
|
}, coldFreshTags(platformDir));
|
|
8942
10260
|
if (!r.ok)
|
|
8943
10261
|
return errorResult(r.detail);
|
|
@@ -8950,7 +10268,7 @@ function buildMcpServer(platformDir) {
|
|
|
8950
10268
|
to_file: r.toFile,
|
|
8951
10269
|
source_spec_version: r.sourceSpecVersion,
|
|
8952
10270
|
target_spec_version: r.targetSpecVersion,
|
|
8953
|
-
|
|
10271
|
+
retagged: r.retagged
|
|
8954
10272
|
});
|
|
8955
10273
|
});
|
|
8956
10274
|
server.registerPrompt("author_requirements", {
|
|
@@ -8999,6 +10317,7 @@ var init_mcp = __esm(() => {
|
|
|
8999
10317
|
init_records();
|
|
9000
10318
|
init_supersede();
|
|
9001
10319
|
init_grammar3();
|
|
10320
|
+
init_authorPrompt();
|
|
9002
10321
|
});
|
|
9003
10322
|
|
|
9004
10323
|
// src/commands/mcp.ts
|
|
@@ -9007,14 +10326,14 @@ __export(exports_mcp, {
|
|
|
9007
10326
|
mcpCommand: () => mcpCommand
|
|
9008
10327
|
});
|
|
9009
10328
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9010
|
-
import { defineCommand as
|
|
10329
|
+
import { defineCommand as defineCommand22 } from "citty";
|
|
9011
10330
|
var mcpCommand;
|
|
9012
10331
|
var init_mcp2 = __esm(() => {
|
|
9013
10332
|
init_discover();
|
|
9014
10333
|
init_mcp();
|
|
9015
10334
|
init__args();
|
|
9016
10335
|
init__shared();
|
|
9017
|
-
mcpCommand =
|
|
10336
|
+
mcpCommand = defineCommand22({
|
|
9018
10337
|
meta: {
|
|
9019
10338
|
name: "mcp",
|
|
9020
10339
|
description: "Serve Spec Engine as an MCP (Model Context Protocol) server over stdio \u2014 agent-native query/resolve/check/report tools."
|
|
@@ -9042,8 +10361,8 @@ __export(exports_resolve, {
|
|
|
9042
10361
|
resolveCommand: () => resolveCommand
|
|
9043
10362
|
});
|
|
9044
10363
|
import { statSync as statSync5 } from "fs";
|
|
9045
|
-
import { join as
|
|
9046
|
-
import { defineCommand as
|
|
10364
|
+
import { join as join18, relative as relative4, resolve as resolve14, sep as sep4 } from "path";
|
|
10365
|
+
import { defineCommand as defineCommand23 } from "citty";
|
|
9047
10366
|
function classifyArg(a) {
|
|
9048
10367
|
if (a.startsWith("--")) {
|
|
9049
10368
|
const eq = a.indexOf("=");
|
|
@@ -9078,10 +10397,10 @@ function splitFilesAndPlatformDir(positionals, fallbackPlatformDir) {
|
|
|
9078
10397
|
let lastIsPlatformDir = false;
|
|
9079
10398
|
let lastIsDirWithoutSpecEngine = false;
|
|
9080
10399
|
try {
|
|
9081
|
-
const resolved =
|
|
10400
|
+
const resolved = resolve14(last);
|
|
9082
10401
|
if (statSync5(resolved).isDirectory()) {
|
|
9083
10402
|
try {
|
|
9084
|
-
if (statSync5(
|
|
10403
|
+
if (statSync5(join18(resolved, "spec-engine")).isDirectory()) {
|
|
9085
10404
|
lastIsPlatformDir = true;
|
|
9086
10405
|
} else {
|
|
9087
10406
|
lastIsDirWithoutSpecEngine = true;
|
|
@@ -9168,7 +10487,7 @@ async function dispatchReverseQuery(reqArg, resolved, args) {
|
|
|
9168
10487
|
}
|
|
9169
10488
|
await runReverseQuery({
|
|
9170
10489
|
reqId: reqArg,
|
|
9171
|
-
platformDir:
|
|
10490
|
+
platformDir: resolve14(lone ?? process.cwd()),
|
|
9172
10491
|
outArg: args.out,
|
|
9173
10492
|
json: Boolean(args.json),
|
|
9174
10493
|
fresh: Boolean(args.fresh),
|
|
@@ -9178,7 +10497,7 @@ async function dispatchReverseQuery(reqArg, resolved, args) {
|
|
|
9178
10497
|
function normalizeFileInputs(files, platformDir) {
|
|
9179
10498
|
const normalized = [];
|
|
9180
10499
|
for (const input of files) {
|
|
9181
|
-
const rel =
|
|
10500
|
+
const rel = relative4(platformDir, resolve14(platformDir, input));
|
|
9182
10501
|
const segments = rel.split(sep4);
|
|
9183
10502
|
if (rel === "" || rel === "." || segments[0] === "..") {
|
|
9184
10503
|
console.error(`spec resolve: file path must be inside platformDir: ${input}`);
|
|
@@ -9229,7 +10548,7 @@ var init_resolve2 = __esm(() => {
|
|
|
9229
10548
|
description: "Suppress interactive onboarding prompt for siblings missing spec-engine.member.json (defaults to NO_SPEC_CONFIG warning)"
|
|
9230
10549
|
}
|
|
9231
10550
|
};
|
|
9232
|
-
resolveCommand =
|
|
10551
|
+
resolveCommand = defineCommand23({
|
|
9233
10552
|
meta: {
|
|
9234
10553
|
name: "resolve",
|
|
9235
10554
|
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."
|
|
@@ -9253,7 +10572,7 @@ var init_resolve2 = __esm(() => {
|
|
|
9253
10572
|
process.exit(EXIT.USAGE);
|
|
9254
10573
|
return;
|
|
9255
10574
|
}
|
|
9256
|
-
const platformDir =
|
|
10575
|
+
const platformDir = resolve14(resolved.platformDir ?? process.cwd());
|
|
9257
10576
|
const outArg2 = args.out;
|
|
9258
10577
|
const dbPath = resolveDbPath(platformDir, outArg2);
|
|
9259
10578
|
if (outArg2)
|
|
@@ -9392,10 +10711,10 @@ var exports_gate = {};
|
|
|
9392
10711
|
__export(exports_gate, {
|
|
9393
10712
|
gateCommand: () => gateCommand
|
|
9394
10713
|
});
|
|
9395
|
-
import { existsSync as
|
|
9396
|
-
import { join as
|
|
9397
|
-
import { defineCommand as
|
|
9398
|
-
function
|
|
10714
|
+
import { existsSync as existsSync16 } from "fs";
|
|
10715
|
+
import { join as join19 } from "path";
|
|
10716
|
+
import { defineCommand as defineCommand24 } from "citty";
|
|
10717
|
+
function isEpipe2(e) {
|
|
9399
10718
|
if (e === null || typeof e !== "object" || !("code" in e))
|
|
9400
10719
|
return false;
|
|
9401
10720
|
const code = e.code;
|
|
@@ -9405,7 +10724,7 @@ function writeStdoutTolerant(line) {
|
|
|
9405
10724
|
try {
|
|
9406
10725
|
console.log(line);
|
|
9407
10726
|
} catch (writeErr) {
|
|
9408
|
-
if (!
|
|
10727
|
+
if (!isEpipe2(writeErr))
|
|
9409
10728
|
throw writeErr;
|
|
9410
10729
|
}
|
|
9411
10730
|
}
|
|
@@ -9417,7 +10736,7 @@ function resolveGateArgs(args) {
|
|
|
9417
10736
|
process.exit(EXIT.USAGE);
|
|
9418
10737
|
}
|
|
9419
10738
|
const platformDir = resolvePlatformDir(args);
|
|
9420
|
-
if (!
|
|
10739
|
+
if (!existsSync16(join19(platformDir, "spec-engine"))) {
|
|
9421
10740
|
console.error(formatNotASpecPlatform(platformDir));
|
|
9422
10741
|
process.exit(EXIT.USAGE);
|
|
9423
10742
|
}
|
|
@@ -9430,6 +10749,7 @@ var init_gate2 = __esm(() => {
|
|
|
9430
10749
|
init_prompt();
|
|
9431
10750
|
init__index();
|
|
9432
10751
|
init_gate();
|
|
10752
|
+
init_stdout();
|
|
9433
10753
|
init__args();
|
|
9434
10754
|
init__shared();
|
|
9435
10755
|
gateArgs = {
|
|
@@ -9457,7 +10777,7 @@ var init_gate2 = __esm(() => {
|
|
|
9457
10777
|
description: "Suppress interactive onboarding prompt for siblings missing spec-engine.member.json (defaults to NO_SPEC_CONFIG warning)"
|
|
9458
10778
|
}
|
|
9459
10779
|
};
|
|
9460
|
-
gateCommand =
|
|
10780
|
+
gateCommand = defineCommand24({
|
|
9461
10781
|
meta: {
|
|
9462
10782
|
name: "gate",
|
|
9463
10783
|
description: "Rung-3 approval primitive: passes iff <reqId> is Active and <repo>'s pinned spec_version covers its changed_at_version. Exits 0 PASS / 1 any gate failure / 2 crash or bad args."
|
|
@@ -9490,7 +10810,7 @@ var init_gate2 = __esm(() => {
|
|
|
9490
10810
|
writeStdoutTolerant(`build_id: ${gated.buildId}`);
|
|
9491
10811
|
}
|
|
9492
10812
|
} catch (e) {
|
|
9493
|
-
handleInvalidDomainFiles(e, Boolean(args.json));
|
|
10813
|
+
await handleInvalidDomainFiles(e, Boolean(args.json));
|
|
9494
10814
|
const msg = e instanceof Error ? e.message : String(e);
|
|
9495
10815
|
console.error(`spec gate: crashed: ${msg}`);
|
|
9496
10816
|
process.exit(EXIT.USAGE);
|
|
@@ -9500,7 +10820,7 @@ var init_gate2 = __esm(() => {
|
|
|
9500
10820
|
process.exit(EXIT.USAGE);
|
|
9501
10821
|
return;
|
|
9502
10822
|
}
|
|
9503
|
-
|
|
10823
|
+
await exit(gated.outcome.pass ? EXIT.OK : EXIT.FAILURE);
|
|
9504
10824
|
}
|
|
9505
10825
|
});
|
|
9506
10826
|
});
|
|
@@ -9581,14 +10901,15 @@ var exports_guard = {};
|
|
|
9581
10901
|
__export(exports_guard, {
|
|
9582
10902
|
guardCommand: () => guardCommand
|
|
9583
10903
|
});
|
|
9584
|
-
import { defineCommand as
|
|
10904
|
+
import { defineCommand as defineCommand25 } from "citty";
|
|
9585
10905
|
var guardCommand;
|
|
9586
10906
|
var init_guard2 = __esm(() => {
|
|
9587
10907
|
init_constants();
|
|
9588
10908
|
init_guard();
|
|
10909
|
+
init_stdout();
|
|
9589
10910
|
init__args();
|
|
9590
10911
|
init__shared();
|
|
9591
|
-
guardCommand =
|
|
10912
|
+
guardCommand = defineCommand25({
|
|
9592
10913
|
meta: {
|
|
9593
10914
|
name: "guard",
|
|
9594
10915
|
description: "Loss detection: diff the requirement derivation at a git ref (default HEAD) against the working tree and block requirements about to be steamrolled."
|
|
@@ -9611,14 +10932,14 @@ var init_guard2 = __esm(() => {
|
|
|
9611
10932
|
if (!gitRefResolves(platformDir, ref)) {
|
|
9612
10933
|
console.error(`spec guard: NOT_A_GIT_REPO \u2014 ref '${ref}' does not resolve in ${platformDir} ` + "(not a git repo, a fresh repo with no commit, or an unfetched/misspelled ref); " + "skipping loss detection");
|
|
9613
10934
|
console.log(jsonMode ? "[]" : `\u2713 spec guard: no requirements about to be lost (non-git context)`);
|
|
9614
|
-
|
|
10935
|
+
await exit(EXIT.OK);
|
|
9615
10936
|
}
|
|
9616
10937
|
let losses = [];
|
|
9617
10938
|
await withReadStorage({ platformDir, dbPath: defaultIndexPath(platformDir), fresh: true, json: jsonMode }, async (storage2) => {
|
|
9618
10939
|
losses = (await guard(storage2, platformDir, ref)).losses;
|
|
9619
10940
|
});
|
|
9620
10941
|
console.log(renderGuard(losses, jsonMode ? "json" : "text", ref));
|
|
9621
|
-
|
|
10942
|
+
await exit(losses.length > 0 ? EXIT.FAILURE : EXIT.OK);
|
|
9622
10943
|
}
|
|
9623
10944
|
});
|
|
9624
10945
|
});
|
|
@@ -11220,9 +12541,9 @@ function requirementStatusBadge(status) {
|
|
|
11220
12541
|
const variant = status === "Active" ? "active" : status === "Superseded" ? "superseded" : status === "Draft" ? "draft" : "retired";
|
|
11221
12542
|
return badge(variant, STATUS_LABEL[status]);
|
|
11222
12543
|
}
|
|
11223
|
-
function propagationStateBadge(
|
|
12544
|
+
function propagationStateBadge(row) {
|
|
11224
12545
|
let variant;
|
|
11225
|
-
switch (
|
|
12546
|
+
switch (row.state) {
|
|
11226
12547
|
case "MIGRATED_VERIFIED":
|
|
11227
12548
|
variant = "verified";
|
|
11228
12549
|
break;
|
|
@@ -11235,7 +12556,7 @@ function propagationStateBadge(row2) {
|
|
|
11235
12556
|
default:
|
|
11236
12557
|
variant = "unbound";
|
|
11237
12558
|
}
|
|
11238
|
-
return badge(variant,
|
|
12559
|
+
return badge(variant, row.state);
|
|
11239
12560
|
}
|
|
11240
12561
|
function coverageGlyph(impl, verif) {
|
|
11241
12562
|
const [variant, char, title] = impl && verif ? ["both", "\u2713", "src + test"] : impl ? ["impl", "S", "src only"] : verif ? ["test", "T", "test only"] : ["none", "", "none"];
|
|
@@ -11369,18 +12690,18 @@ import { html as html4, raw as raw3 } from "hono/html";
|
|
|
11369
12690
|
function pivot(rows) {
|
|
11370
12691
|
const repos = [...new Set(rows.map((r) => r.repo))].sort();
|
|
11371
12692
|
const byDomain = new Map;
|
|
11372
|
-
for (const
|
|
11373
|
-
let dom = byDomain.get(
|
|
12693
|
+
for (const row of rows) {
|
|
12694
|
+
let dom = byDomain.get(row.domain_key);
|
|
11374
12695
|
if (!dom) {
|
|
11375
12696
|
dom = new Map;
|
|
11376
|
-
byDomain.set(
|
|
12697
|
+
byDomain.set(row.domain_key, dom);
|
|
11377
12698
|
}
|
|
11378
|
-
let req = dom.get(
|
|
12699
|
+
let req = dom.get(row.req_id);
|
|
11379
12700
|
if (!req) {
|
|
11380
|
-
req = { id:
|
|
11381
|
-
dom.set(
|
|
12701
|
+
req = { id: row.req_id, status: row.req_status, cells: new Map };
|
|
12702
|
+
dom.set(row.req_id, req);
|
|
11382
12703
|
}
|
|
11383
|
-
req.cells.set(
|
|
12704
|
+
req.cells.set(row.repo, { impl: row.implemented, verif: row.verified });
|
|
11384
12705
|
}
|
|
11385
12706
|
const domains = [...byDomain.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([key, reqMap]) => ({
|
|
11386
12707
|
key,
|
|
@@ -11627,14 +12948,6 @@ function mountEditor(app, bindHost = DEFAULT_BIND_HOST) {
|
|
|
11627
12948
|
Why (optional)
|
|
11628
12949
|
<textarea name="why" rows="2"></textarea>
|
|
11629
12950
|
</label>
|
|
11630
|
-
<label>
|
|
11631
|
-
Lives in (optional, single path)
|
|
11632
|
-
<input type="text" name="livesIn" placeholder="path/to/impl.ts" />
|
|
11633
|
-
</label>
|
|
11634
|
-
<label>
|
|
11635
|
-
Verified in (optional, single path)
|
|
11636
|
-
<input type="text" name="verifiedIn" placeholder="path/to/impl.test.ts" />
|
|
11637
|
-
</label>
|
|
11638
12951
|
<label>
|
|
11639
12952
|
Relates to (optional, comma-separated ids)
|
|
11640
12953
|
<input type="text" name="relates" placeholder="BILLING-001, BILLING-002" />
|
|
@@ -11672,8 +12985,6 @@ function mountEditor(app, bindHost = DEFAULT_BIND_HOST) {
|
|
|
11672
12985
|
const form = await c.req.parseBody();
|
|
11673
12986
|
const statement = field(form, "statement");
|
|
11674
12987
|
const why = field(form, "why");
|
|
11675
|
-
const livesIn = field(form, "livesIn");
|
|
11676
|
-
const verifiedIn = field(form, "verifiedIn");
|
|
11677
12988
|
const relates = field(form, "relates");
|
|
11678
12989
|
const payload = {
|
|
11679
12990
|
key: field(form, "key"),
|
|
@@ -11681,10 +12992,6 @@ function mountEditor(app, bindHost = DEFAULT_BIND_HOST) {
|
|
|
11681
12992
|
};
|
|
11682
12993
|
if (why.length > 0)
|
|
11683
12994
|
payload.why = why;
|
|
11684
|
-
if (livesIn.length > 0)
|
|
11685
|
-
payload.livesIn = livesIn;
|
|
11686
|
-
if (verifiedIn.length > 0)
|
|
11687
|
-
payload.verifiedIn = verifiedIn;
|
|
11688
12995
|
if (relates.length > 0)
|
|
11689
12996
|
payload.relates = relates;
|
|
11690
12997
|
const res = await app.request("/api/requirements", {
|
|
@@ -12001,15 +13308,15 @@ var init_relations2 = __esm(() => {
|
|
|
12001
13308
|
import { html as html10, raw as raw9 } from "hono/html";
|
|
12002
13309
|
function coverageRollups(coverage) {
|
|
12003
13310
|
const cov = new Map;
|
|
12004
|
-
for (const
|
|
12005
|
-
if (
|
|
13311
|
+
for (const row of coverage) {
|
|
13312
|
+
if (row.req_status !== "Active")
|
|
12006
13313
|
continue;
|
|
12007
|
-
const c = cov.get(
|
|
12008
|
-
if (
|
|
13314
|
+
const c = cov.get(row.req_id) ?? { impl: 0, verif: 0 };
|
|
13315
|
+
if (row.implemented)
|
|
12009
13316
|
c.impl += 1;
|
|
12010
|
-
if (
|
|
13317
|
+
if (row.verified)
|
|
12011
13318
|
c.verif += 1;
|
|
12012
|
-
cov.set(
|
|
13319
|
+
cov.set(row.req_id, c);
|
|
12013
13320
|
}
|
|
12014
13321
|
return cov;
|
|
12015
13322
|
}
|
|
@@ -12253,6 +13560,9 @@ function mountRequirements(app) {
|
|
|
12253
13560
|
</html>`);
|
|
12254
13561
|
}
|
|
12255
13562
|
const r = await res.json();
|
|
13563
|
+
const sites = (list) => list.length === 0 ? "\u2014" : html10`<ul>
|
|
13564
|
+
${list.map((s) => html10`<li><code>${s.file}:${s.line}</code></li>`)}
|
|
13565
|
+
</ul>`;
|
|
12256
13566
|
const body = html10`
|
|
12257
13567
|
${navBar("requirements")}
|
|
12258
13568
|
<div class="eyebrow">/ Requirement</div>
|
|
@@ -12272,6 +13582,8 @@ function mountRequirements(app) {
|
|
|
12272
13582
|
<dt>line</dt><dd>${r.line}</dd>
|
|
12273
13583
|
<dt>text</dt><dd>${r.text}</dd>
|
|
12274
13584
|
<dt>why</dt><dd>${r.why ?? "\u2014"}</dd>
|
|
13585
|
+
<dt>implemented_in</dt><dd>${sites(r.implemented_in)}</dd>
|
|
13586
|
+
<dt>verified_in</dt><dd>${sites(r.verified_in)}</dd>
|
|
12275
13587
|
</dl>
|
|
12276
13588
|
`;
|
|
12277
13589
|
return c.html(html10`<!doctype html>
|
|
@@ -12356,15 +13668,15 @@ function mountSetup(app) {
|
|
|
12356
13668
|
const memberCount = members.length;
|
|
12357
13669
|
const note = readingNote(mode, memberCount);
|
|
12358
13670
|
const boundByRepo = new Map;
|
|
12359
|
-
for (const
|
|
12360
|
-
if (!
|
|
13671
|
+
for (const row of cov) {
|
|
13672
|
+
if (!row.implemented)
|
|
12361
13673
|
continue;
|
|
12362
|
-
let set = boundByRepo.get(
|
|
13674
|
+
let set = boundByRepo.get(row.repo);
|
|
12363
13675
|
if (!set) {
|
|
12364
13676
|
set = new Set;
|
|
12365
|
-
boundByRepo.set(
|
|
13677
|
+
boundByRepo.set(row.repo, set);
|
|
12366
13678
|
}
|
|
12367
|
-
set.add(
|
|
13679
|
+
set.add(row.req_id);
|
|
12368
13680
|
}
|
|
12369
13681
|
const body = html11`
|
|
12370
13682
|
${navBar("setup")}
|
|
@@ -12601,8 +13913,6 @@ function mintFieldsFromBody(body) {
|
|
|
12601
13913
|
return {
|
|
12602
13914
|
statement: typeof body.statement === "string" ? body.statement : "",
|
|
12603
13915
|
why: typeof body.why === "string" ? body.why : "",
|
|
12604
|
-
livesIn: toStringList(body.livesIn),
|
|
12605
|
-
verifiedIn: toStringList(body.verifiedIn),
|
|
12606
13916
|
relates: toStringList(body.relates),
|
|
12607
13917
|
issue: typeof body.issue === "string" && body.issue.trim() !== "" ? body.issue.trim() : undefined,
|
|
12608
13918
|
status: body.status === "draft" ? "draft" : "active"
|
|
@@ -12611,8 +13921,6 @@ function mintFieldsFromBody(body) {
|
|
|
12611
13921
|
function successorFieldsFromBody(body) {
|
|
12612
13922
|
return {
|
|
12613
13923
|
why: typeof body.why === "string" ? body.why : undefined,
|
|
12614
|
-
livesIn: "livesIn" in body ? toStringList(body.livesIn) : undefined,
|
|
12615
|
-
verifiedIn: "verifiedIn" in body ? toStringList(body.verifiedIn) : undefined,
|
|
12616
13924
|
relates: "relates" in body ? toStringList(body.relates) : undefined,
|
|
12617
13925
|
issue: typeof body.issue === "string" && body.issue.trim() !== "" ? body.issue.trim() : undefined
|
|
12618
13926
|
};
|
|
@@ -12623,10 +13931,6 @@ function amendFieldsFromBody(body) {
|
|
|
12623
13931
|
fields.statement = body.statement;
|
|
12624
13932
|
if ("why" in body)
|
|
12625
13933
|
fields.why = typeof body.why === "string" ? body.why : null;
|
|
12626
|
-
if ("livesIn" in body)
|
|
12627
|
-
fields.livesIn = toStringList(body.livesIn);
|
|
12628
|
-
if ("verifiedIn" in body)
|
|
12629
|
-
fields.verifiedIn = toStringList(body.verifiedIn);
|
|
12630
13934
|
if ("relates" in body)
|
|
12631
13935
|
fields.relates = toStringList(body.relates);
|
|
12632
13936
|
return Object.keys(fields).length === 0 ? null : fields;
|
|
@@ -12688,8 +13992,8 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
|
|
|
12688
13992
|
}));
|
|
12689
13993
|
app.get("/api/requirements/:id", guarded((c) => {
|
|
12690
13994
|
const id = c.req.param("id") ?? "";
|
|
12691
|
-
const { row
|
|
12692
|
-
return
|
|
13995
|
+
const { row } = getRecord(storage2, id);
|
|
13996
|
+
return row ? c.json(row) : c.json({ error: "not found" }, 404);
|
|
12693
13997
|
}));
|
|
12694
13998
|
app.post("/api/requirements", guarded(async function createRequirement(c) {
|
|
12695
13999
|
if (!featureEnabled("editor")) {
|
|
@@ -12735,7 +14039,7 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
|
|
|
12735
14039
|
return withWriteLock(async () => {
|
|
12736
14040
|
const fields = amendFieldsFromBody(body);
|
|
12737
14041
|
if (fields === null) {
|
|
12738
|
-
return c.json({ error: "nothing to amend \u2014 provide statement, why,
|
|
14042
|
+
return c.json({ error: "nothing to amend \u2014 provide statement, why, or relates" }, 400);
|
|
12739
14043
|
}
|
|
12740
14044
|
const r = await amend({ platformDir, id, fields });
|
|
12741
14045
|
if (!r.ok)
|
|
@@ -12777,7 +14081,7 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
|
|
|
12777
14081
|
new_id: r.newId,
|
|
12778
14082
|
file: r.file,
|
|
12779
14083
|
spec_version: r.specVersion,
|
|
12780
|
-
|
|
14084
|
+
retagged: r.retagged
|
|
12781
14085
|
}, 201);
|
|
12782
14086
|
});
|
|
12783
14087
|
}));
|
|
@@ -12807,9 +14111,7 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
|
|
|
12807
14111
|
id,
|
|
12808
14112
|
targetKey,
|
|
12809
14113
|
statement: typeof body.text === "string" ? body.text : undefined,
|
|
12810
|
-
why: typeof body.why === "string" ? body.why : undefined
|
|
12811
|
-
livesIn: "lives" in body ? toStringList(body.lives) : undefined,
|
|
12812
|
-
verifiedIn: "verifiedIn" in body ? toStringList(body.verifiedIn) : undefined
|
|
14114
|
+
why: typeof body.why === "string" ? body.why : undefined
|
|
12813
14115
|
}, async (reqId) => {
|
|
12814
14116
|
await runIndex({ platformDir, storage: storage2 });
|
|
12815
14117
|
return storage2.listTags({ req_id: reqId });
|
|
@@ -12824,7 +14126,7 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
|
|
|
12824
14126
|
to_file: r.toFile,
|
|
12825
14127
|
source_spec_version: r.sourceSpecVersion,
|
|
12826
14128
|
target_spec_version: r.targetSpecVersion,
|
|
12827
|
-
|
|
14129
|
+
retagged: r.retagged
|
|
12828
14130
|
}, 201);
|
|
12829
14131
|
});
|
|
12830
14132
|
}));
|
|
@@ -12855,7 +14157,7 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
|
|
|
12855
14157
|
});
|
|
12856
14158
|
if (!r.ok)
|
|
12857
14159
|
return failureResponse(c, r);
|
|
12858
|
-
return c.json({ ok: true, id: r.id, file: r.file, reason: r.reason,
|
|
14160
|
+
return c.json({ ok: true, id: r.id, file: r.file, reason: r.reason, removed: r.removed }, 200);
|
|
12859
14161
|
});
|
|
12860
14162
|
}));
|
|
12861
14163
|
app.post("/api/requirements/:id/accept", guarded(async function acceptRequirement(c) {
|
|
@@ -12901,12 +14203,12 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
|
|
|
12901
14203
|
if (!featureEnabled("relations")) {
|
|
12902
14204
|
return c.json({ error: featureDisabledMessage("relations") }, 404);
|
|
12903
14205
|
}
|
|
12904
|
-
const
|
|
12905
|
-
if (
|
|
14206
|
+
const format2 = c.req.query("format");
|
|
14207
|
+
if (format2 !== undefined && format2 !== "mermaid") {
|
|
12906
14208
|
return c.json({ error: 'format must be "mermaid" when provided' }, 400);
|
|
12907
14209
|
}
|
|
12908
14210
|
const rows = relations(storage2).rows;
|
|
12909
|
-
if (
|
|
14211
|
+
if (format2 === "mermaid") {
|
|
12910
14212
|
return c.text(renderRelations(rows, "mermaid"));
|
|
12911
14213
|
}
|
|
12912
14214
|
return c.json(sortRelations(rows));
|
|
@@ -13097,9 +14399,9 @@ __export(exports_serve, {
|
|
|
13097
14399
|
resolveBind: () => resolveBind,
|
|
13098
14400
|
composeServeApp: () => composeServeApp
|
|
13099
14401
|
});
|
|
13100
|
-
import { existsSync as
|
|
13101
|
-
import { dirname as
|
|
13102
|
-
import { defineCommand as
|
|
14402
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync7 } from "fs";
|
|
14403
|
+
import { dirname as dirname10 } from "path";
|
|
14404
|
+
import { defineCommand as defineCommand26 } from "citty";
|
|
13103
14405
|
import { Hono as Hono2 } from "hono";
|
|
13104
14406
|
function composeServeApp(storage2, platformDir = process.cwd(), bindHost = DEFAULT_BIND_HOST) {
|
|
13105
14407
|
const app = new Hono2;
|
|
@@ -13161,7 +14463,7 @@ async function runProbe() {
|
|
|
13161
14463
|
}
|
|
13162
14464
|
console.log("serve --probe OK");
|
|
13163
14465
|
server.stop();
|
|
13164
|
-
|
|
14466
|
+
await exit(EXIT.OK);
|
|
13165
14467
|
}
|
|
13166
14468
|
var serveCommand;
|
|
13167
14469
|
var init_serve = __esm(() => {
|
|
@@ -13173,10 +14475,11 @@ var init_serve = __esm(() => {
|
|
|
13173
14475
|
init_prompt();
|
|
13174
14476
|
init_api();
|
|
13175
14477
|
init_lan();
|
|
14478
|
+
init_stdout();
|
|
13176
14479
|
init_sqlite();
|
|
13177
14480
|
init__args();
|
|
13178
14481
|
init__shared();
|
|
13179
|
-
serveCommand =
|
|
14482
|
+
serveCommand = defineCommand26({
|
|
13180
14483
|
meta: {
|
|
13181
14484
|
name: "serve",
|
|
13182
14485
|
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."
|
|
@@ -13248,8 +14551,8 @@ var init_serve = __esm(() => {
|
|
|
13248
14551
|
noPrompt: args.noPrompt
|
|
13249
14552
|
}
|
|
13250
14553
|
});
|
|
13251
|
-
|
|
13252
|
-
const needsIndex = !
|
|
14554
|
+
mkdirSync7(dirname10(dbPath), { recursive: true });
|
|
14555
|
+
const needsIndex = !existsSync17(dbPath);
|
|
13253
14556
|
let storage2;
|
|
13254
14557
|
try {
|
|
13255
14558
|
storage2 = openStorage(dbPath);
|
|
@@ -13270,7 +14573,7 @@ var init_serve = __esm(() => {
|
|
|
13270
14573
|
});
|
|
13271
14574
|
} catch (err) {
|
|
13272
14575
|
storage2.close();
|
|
13273
|
-
handleInvalidDomainFiles(err, false);
|
|
14576
|
+
await handleInvalidDomainFiles(err, false);
|
|
13274
14577
|
handleStorageUnavailable(err, dbPath);
|
|
13275
14578
|
console.error(`spec serve: failed to start on ${urlHost(host, port)}:`, err);
|
|
13276
14579
|
process.exit(EXIT.FAILURE);
|
|
@@ -13297,13 +14600,13 @@ __export(exports_docs, {
|
|
|
13297
14600
|
docsCommand: () => docsCommand,
|
|
13298
14601
|
createDocsFetchHandler: () => createDocsFetchHandler
|
|
13299
14602
|
});
|
|
13300
|
-
import { existsSync as
|
|
13301
|
-
import { join as
|
|
14603
|
+
import { existsSync as existsSync18, statSync as statSync6 } from "fs";
|
|
14604
|
+
import { join as join20, normalize, sep as sep5 } from "path";
|
|
13302
14605
|
import { fileURLToPath } from "url";
|
|
13303
|
-
import { defineCommand as
|
|
14606
|
+
import { defineCommand as defineCommand27 } from "citty";
|
|
13304
14607
|
function resolveDocsRootFrom(candidates) {
|
|
13305
14608
|
for (const candidate of candidates) {
|
|
13306
|
-
if (
|
|
14609
|
+
if (existsSync18(join20(candidate, "index.html")))
|
|
13307
14610
|
return candidate;
|
|
13308
14611
|
}
|
|
13309
14612
|
return null;
|
|
@@ -13327,17 +14630,17 @@ function createDocsFetchHandler(root) {
|
|
|
13327
14630
|
if (pathname.includes("\x00")) {
|
|
13328
14631
|
return new Response("Bad request", { status: 400 });
|
|
13329
14632
|
}
|
|
13330
|
-
const target = normalize(
|
|
14633
|
+
const target = normalize(join20(normalRoot, pathname));
|
|
13331
14634
|
if (target !== normalRoot && !target.startsWith(rootPrefix)) {
|
|
13332
14635
|
return new Response("Not found", { status: 404 });
|
|
13333
14636
|
}
|
|
13334
|
-
for (const candidate of [target,
|
|
13335
|
-
if (
|
|
14637
|
+
for (const candidate of [target, join20(target, "index.html"), `${target}.html`]) {
|
|
14638
|
+
if (existsSync18(candidate) && statSync6(candidate).isFile()) {
|
|
13336
14639
|
return new Response(Bun.file(candidate));
|
|
13337
14640
|
}
|
|
13338
14641
|
}
|
|
13339
|
-
const notFoundPage =
|
|
13340
|
-
if (
|
|
14642
|
+
const notFoundPage = join20(normalRoot, "404.html");
|
|
14643
|
+
if (existsSync18(notFoundPage)) {
|
|
13341
14644
|
return new Response(Bun.file(notFoundPage), { status: 404 });
|
|
13342
14645
|
}
|
|
13343
14646
|
return new Response("Not found", { status: 404 });
|
|
@@ -13369,7 +14672,7 @@ async function runProbe2(root) {
|
|
|
13369
14672
|
}
|
|
13370
14673
|
console.log("docs --probe OK");
|
|
13371
14674
|
server.stop();
|
|
13372
|
-
|
|
14675
|
+
await exit(EXIT.OK);
|
|
13373
14676
|
}
|
|
13374
14677
|
function exitNoDocsRoot() {
|
|
13375
14678
|
console.error("spec docs: no built documentation found. From a checkout run `bun run build:site` " + "(writes packages/site/dist); the published npm package ships it at dist/docs.");
|
|
@@ -13378,7 +14681,8 @@ function exitNoDocsRoot() {
|
|
|
13378
14681
|
var docsCommand;
|
|
13379
14682
|
var init_docs2 = __esm(() => {
|
|
13380
14683
|
init_constants();
|
|
13381
|
-
|
|
14684
|
+
init_stdout();
|
|
14685
|
+
docsCommand = defineCommand27({
|
|
13382
14686
|
meta: {
|
|
13383
14687
|
name: "docs",
|
|
13384
14688
|
description: "Serve the bundled documentation site offline. Binds 127.0.0.1 on --port (loopback only \u2014 no --host). --probe is the boot-and-fetch smoke."
|
|
@@ -13439,10 +14743,10 @@ __export(exports___smoke, {
|
|
|
13439
14743
|
schemaSmokeCommand: () => schemaSmokeCommand,
|
|
13440
14744
|
schemaMismatchSmokeCommand: () => schemaMismatchSmokeCommand
|
|
13441
14745
|
});
|
|
13442
|
-
import { defineCommand as
|
|
14746
|
+
import { defineCommand as defineCommand28 } from "citty";
|
|
13443
14747
|
var schemaSmokeCommand, schemaMismatchSmokeCommand;
|
|
13444
14748
|
var init___smoke = __esm(() => {
|
|
13445
|
-
schemaSmokeCommand =
|
|
14749
|
+
schemaSmokeCommand = defineCommand28({
|
|
13446
14750
|
meta: {
|
|
13447
14751
|
name: "__schema-smoke",
|
|
13448
14752
|
description: "(hidden CI) open a fresh DB, exec DDL, assert the schema shape",
|
|
@@ -13485,7 +14789,7 @@ var init___smoke = __esm(() => {
|
|
|
13485
14789
|
console.log("__schema-smoke OK");
|
|
13486
14790
|
}
|
|
13487
14791
|
});
|
|
13488
|
-
schemaMismatchSmokeCommand =
|
|
14792
|
+
schemaMismatchSmokeCommand = defineCommand28({
|
|
13489
14793
|
meta: {
|
|
13490
14794
|
name: "__schema-mismatch-smoke",
|
|
13491
14795
|
description: "(hidden CI) corrupt _schema_version, re-open, assert silent rebuild",
|
|
@@ -13511,8 +14815,10 @@ var init___smoke = __esm(() => {
|
|
|
13511
14815
|
|
|
13512
14816
|
// src/cli.ts
|
|
13513
14817
|
init_package();
|
|
13514
|
-
|
|
13515
|
-
|
|
14818
|
+
init_stdout();
|
|
14819
|
+
import { defineCommand as defineCommand29, runMain, showUsage } from "citty";
|
|
14820
|
+
routeStdout();
|
|
14821
|
+
var main = defineCommand29({
|
|
13516
14822
|
meta: {
|
|
13517
14823
|
name: "spec",
|
|
13518
14824
|
version: package_default.version,
|
|
@@ -13538,6 +14844,8 @@ var main = defineCommand27({
|
|
|
13538
14844
|
move: () => Promise.resolve().then(() => (init_move2(), exports_move)).then((m) => m.moveCommand),
|
|
13539
14845
|
amend: () => Promise.resolve().then(() => (init_amend2(), exports_amend)).then((m) => m.amendCommand),
|
|
13540
14846
|
accept: () => Promise.resolve().then(() => (init_accept2(), exports_accept)).then((m) => m.acceptCommand),
|
|
14847
|
+
lint: () => Promise.resolve().then(() => (init_lint2(), exports_lint)).then((m) => m.lintCommand),
|
|
14848
|
+
migrate: () => Promise.resolve().then(() => (init_migrate2(), exports_migrate)).then((m) => m.migrateCommand),
|
|
13541
14849
|
mcp: () => Promise.resolve().then(() => (init_mcp2(), exports_mcp)).then((m) => m.mcpCommand),
|
|
13542
14850
|
resolve: () => Promise.resolve().then(() => (init_resolve2(), exports_resolve)).then((m) => m.resolveCommand),
|
|
13543
14851
|
gate: () => Promise.resolve().then(() => (init_gate2(), exports_gate)).then((m) => m.gateCommand),
|