@spec-engine/spec-engine 0.8.1 → 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 +1948 -653
- 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_2ee687a.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_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_59c3abf.pf_index +0 -0
- package/dist/docs/pagefind/index/en_a630d23.pf_index +0 -0
- package/dist/docs/pagefind/pagefind.en_ced49c317e.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",
|
|
@@ -109,15 +109,13 @@ var init_diagnostics = __esm(() => {
|
|
|
109
109
|
SUPERSEDED_REFERENCED: "SUPERSEDED_REFERENCED",
|
|
110
110
|
DEPRECATED_REFERENCED: "DEPRECATED_REFERENCED",
|
|
111
111
|
STATEMENT_GRAMMAR: "STATEMENT_GRAMMAR",
|
|
112
|
+
STATEMENT_RULES: "STATEMENT_RULES",
|
|
112
113
|
DRAFT_REFERENCED: "DRAFT_REFERENCED",
|
|
113
114
|
GLOSSARY_DRIFT: "GLOSSARY_DRIFT",
|
|
114
115
|
ORPHAN_REQ: "ORPHAN_REQ",
|
|
115
116
|
UNVERIFIED_REQ: "UNVERIFIED_REQ",
|
|
116
117
|
DRIFT: "DRIFT",
|
|
117
118
|
NO_SPEC_CONFIG: "NO_SPEC_CONFIG",
|
|
118
|
-
BROKEN_FILE_REF: "BROKEN_FILE_REF",
|
|
119
|
-
UNIMPLEMENTED_IN_FILE: "UNIMPLEMENTED_IN_FILE",
|
|
120
|
-
UNVERIFIED_IN_FILE: "UNVERIFIED_IN_FILE",
|
|
121
119
|
BROKEN_RELATES: "BROKEN_RELATES",
|
|
122
120
|
RELATES_SUPERSEDED: "RELATES_SUPERSEDED",
|
|
123
121
|
CYCLIC_SUPERSEDE: "CYCLIC_SUPERSEDE",
|
|
@@ -125,6 +123,7 @@ var init_diagnostics = __esm(() => {
|
|
|
125
123
|
UNKNOWN_ROLE: "UNKNOWN_ROLE",
|
|
126
124
|
UNSOURCED_CHANGE: "UNSOURCED_CHANGE",
|
|
127
125
|
INVALID_DOMAIN_FILE: "INVALID_DOMAIN_FILE",
|
|
126
|
+
MIGRATION_PENDING: "MIGRATION_PENDING",
|
|
128
127
|
UNPROVEN_REQ: "UNPROVEN_REQ",
|
|
129
128
|
PROOFS_UNCONFIRMED: "PROOFS_UNCONFIRMED",
|
|
130
129
|
REQUIREMENT_REMOVED: "REQUIREMENT_REMOVED",
|
|
@@ -239,8 +238,6 @@ function orderDomain(d) {
|
|
|
239
238
|
supersedes: r.supersedes ?? null,
|
|
240
239
|
supersededBy: r.supersededBy ?? null,
|
|
241
240
|
relates: r.relates,
|
|
242
|
-
livesIn: r.livesIn,
|
|
243
|
-
verifiedIn: r.verifiedIn,
|
|
244
241
|
issues: r.issues.map((iss) => ({ role: iss.role, id: iss.id })),
|
|
245
242
|
changedAtVersion: r.changedAtVersion,
|
|
246
243
|
supersededAtVersion: r.supersededAtVersion,
|
|
@@ -287,6 +284,10 @@ function preserveShape(raw, rawDomain, domain) {
|
|
|
287
284
|
const next = orderDomain(domain);
|
|
288
285
|
patchFields(raw, before, next);
|
|
289
286
|
const rawEntries = raw.requirements;
|
|
287
|
+
for (const entry of rawEntries) {
|
|
288
|
+
for (const key of LEGACY_ENTRY_KEYS)
|
|
289
|
+
delete entry[key];
|
|
290
|
+
}
|
|
290
291
|
const byId = new Map;
|
|
291
292
|
rawDomain.requirements.forEach((r, i) => {
|
|
292
293
|
byId.set(r.id, [...byId.get(r.id) ?? [], i]);
|
|
@@ -357,7 +358,7 @@ async function validateAndWrite(path, domain, sourceFile) {
|
|
|
357
358
|
await Bun.write(path, serialized);
|
|
358
359
|
return { ok: true };
|
|
359
360
|
}
|
|
360
|
-
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);
|
|
361
362
|
var init_domain = __esm(() => {
|
|
362
363
|
init_diagnostics();
|
|
363
364
|
ID_RE = /^[A-Z][A-Z0-9]*-\d+$/;
|
|
@@ -370,6 +371,7 @@ var init_domain = __esm(() => {
|
|
|
370
371
|
term: z.string(),
|
|
371
372
|
pinned: z.number().int()
|
|
372
373
|
}).strict();
|
|
374
|
+
LEGACY_ENTRY_KEYS = ["livesIn", "verifiedIn"];
|
|
373
375
|
SpecRequirementSchema = z.object({
|
|
374
376
|
id: z.string().regex(ID_RE),
|
|
375
377
|
status: z.string().min(1),
|
|
@@ -378,8 +380,8 @@ var init_domain = __esm(() => {
|
|
|
378
380
|
supersedes: z.string().nullable().optional(),
|
|
379
381
|
supersededBy: z.string().nullable().optional(),
|
|
380
382
|
relates: z.array(z.string()).optional().default([]),
|
|
381
|
-
livesIn: z.
|
|
382
|
-
verifiedIn: z.
|
|
383
|
+
livesIn: z.unknown().optional(),
|
|
384
|
+
verifiedIn: z.unknown().optional(),
|
|
383
385
|
issues: z.array(SpecIssueSchema).optional().default([]),
|
|
384
386
|
changedAtVersion: z.number().int().optional(),
|
|
385
387
|
supersededAtVersion: z.number().int().optional(),
|
|
@@ -388,7 +390,7 @@ var init_domain = __esm(() => {
|
|
|
388
390
|
aliases: z.array(z.string()).optional().default([]),
|
|
389
391
|
cites: z.array(SpecCiteSchema).optional().default([]),
|
|
390
392
|
section: z.string().nullable().optional()
|
|
391
|
-
}).strict();
|
|
393
|
+
}).strict().transform(({ livesIn: _l, verifiedIn: _v, ...entry }) => entry);
|
|
392
394
|
SpecDomainSchema = z.object({
|
|
393
395
|
key: z.string().regex(KEY_RE),
|
|
394
396
|
owner: z.string().nullable().optional(),
|
|
@@ -401,6 +403,159 @@ var init_domain = __esm(() => {
|
|
|
401
403
|
}).strict();
|
|
402
404
|
});
|
|
403
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
|
+
|
|
404
559
|
// ../shared/src/checkConfig.ts
|
|
405
560
|
import { z as z2 } from "zod";
|
|
406
561
|
function parseCheckConfigText(text, sourceFile) {
|
|
@@ -418,10 +573,11 @@ function parseCheckConfigText(text, sourceFile) {
|
|
|
418
573
|
const path = issue && issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
419
574
|
return { ok: false, detail: `${sourceFile}: ${path}: ${issue?.message ?? "invalid"}` };
|
|
420
575
|
}
|
|
421
|
-
var CODES, matchEntry, SeverityRuleSchema, CheckConfigSchema;
|
|
576
|
+
var CODES, matchEntry, SeverityRuleSchema, StatementRulesConfigSchema, CheckConfigSchema;
|
|
422
577
|
var init_checkConfig = __esm(() => {
|
|
423
578
|
init_diagnostics();
|
|
424
579
|
init_domain();
|
|
580
|
+
init_statementRules();
|
|
425
581
|
CODES = Object.values(DiagnosticCode);
|
|
426
582
|
matchEntry = z2.string().refine((s) => KEY_RE.test(s) || ID_RE.test(s), "must be a domain key or a requirement id");
|
|
427
583
|
SeverityRuleSchema = z2.object({
|
|
@@ -429,7 +585,16 @@ var init_checkConfig = __esm(() => {
|
|
|
429
585
|
severity: z2.enum(["error", "warning", "off"]),
|
|
430
586
|
match: z2.array(matchEntry).optional()
|
|
431
587
|
}).strict();
|
|
432
|
-
|
|
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();
|
|
433
598
|
});
|
|
434
599
|
|
|
435
600
|
// ../shared/src/config.ts
|
|
@@ -869,6 +1034,7 @@ var init_src = __esm(() => {
|
|
|
869
1034
|
init_net();
|
|
870
1035
|
init_propagation();
|
|
871
1036
|
init_schema();
|
|
1037
|
+
init_statementRules();
|
|
872
1038
|
init_storage();
|
|
873
1039
|
});
|
|
874
1040
|
|
|
@@ -1138,9 +1304,6 @@ function isPathIgnored(path, extra = []) {
|
|
|
1138
1304
|
function isHiddenEntry(name) {
|
|
1139
1305
|
return name.startsWith(".") && name !== ".github";
|
|
1140
1306
|
}
|
|
1141
|
-
function isHiddenPath(rel) {
|
|
1142
|
-
return rel.split("/").some(isHiddenEntry);
|
|
1143
|
-
}
|
|
1144
1307
|
async function walkInto(rootDir, relDir, keep, extraIgnore, out) {
|
|
1145
1308
|
const absDir = relDir ? join2(rootDir, relDir) : rootDir;
|
|
1146
1309
|
let entries;
|
|
@@ -2686,25 +2849,6 @@ function repoExtraIgnore(member) {
|
|
|
2686
2849
|
const repoIgnore = (member.ignore ?? []).map((e) => e.endsWith("/") ? e : `${e}/`);
|
|
2687
2850
|
return [...member.selfMember ? ["spec-engine/"] : [], ...repoIgnore];
|
|
2688
2851
|
}
|
|
2689
|
-
function scannedTagFile(members, platformDir, platformRel) {
|
|
2690
|
-
const abs = resolve3(platformDir, platformRel);
|
|
2691
|
-
let owner = null;
|
|
2692
|
-
for (const member2 of members) {
|
|
2693
|
-
const rel2 = relative(member2.path, abs).split(sep2).join("/");
|
|
2694
|
-
if (rel2 === "" || rel2 === ".." || rel2.startsWith("../") || isAbsolute(rel2))
|
|
2695
|
-
continue;
|
|
2696
|
-
if (owner === null || member2.path.length > owner.member.path.length)
|
|
2697
|
-
owner = { member: member2, rel: rel2 };
|
|
2698
|
-
}
|
|
2699
|
-
if (owner === null)
|
|
2700
|
-
return null;
|
|
2701
|
-
const { member, rel } = owner;
|
|
2702
|
-
if (isHiddenPath(rel) || isPathIgnored(rel, repoExtraIgnore(member)))
|
|
2703
|
-
return null;
|
|
2704
|
-
if (!isTagSource(rel) && !isDocSource(rel))
|
|
2705
|
-
return null;
|
|
2706
|
-
return tagFile(member, rel);
|
|
2707
|
-
}
|
|
2708
2852
|
function tagFile(member, rel) {
|
|
2709
2853
|
return member.selfMember ? rel : `${member.name}/${rel}`;
|
|
2710
2854
|
}
|
|
@@ -2919,6 +3063,944 @@ var init_pipeline = __esm(() => {
|
|
|
2919
3063
|
init_discover();
|
|
2920
3064
|
});
|
|
2921
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
|
+
|
|
2922
4004
|
// src/operations/_result.ts
|
|
2923
4005
|
function fail(reason, detail, extra = {}) {
|
|
2924
4006
|
const out = { ok: false, reason, detail };
|
|
@@ -2940,8 +4022,8 @@ var init__result = __esm(() => {
|
|
|
2940
4022
|
});
|
|
2941
4023
|
|
|
2942
4024
|
// src/operations/init.ts
|
|
2943
|
-
import { existsSync as
|
|
2944
|
-
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";
|
|
2945
4027
|
import {
|
|
2946
4028
|
applyInit,
|
|
2947
4029
|
applyLink,
|
|
@@ -2975,7 +4057,7 @@ function canonicalize(repoDir) {
|
|
|
2975
4057
|
function judgeSpecTree(repoDir, canonical, root) {
|
|
2976
4058
|
if (!hasSpecEngineSegment(canonical))
|
|
2977
4059
|
return { ok: true };
|
|
2978
|
-
const judged = root !== null && isEnginePlatform(root, ENGINE_PACKAGE_NAME) ?
|
|
4060
|
+
const judged = root !== null && isEnginePlatform(root, ENGINE_PACKAGE_NAME) ? relative3(root, canonical) : canonical;
|
|
2979
4061
|
if (hasSpecEngineSegment(judged)) {
|
|
2980
4062
|
return fail("usage", `${repoDir} resolves to ${canonical}, which is inside a spec-engine/ tree \u2014 refusing to scaffold there.`);
|
|
2981
4063
|
}
|
|
@@ -3023,7 +4105,7 @@ function place(repoDir, canonical) {
|
|
|
3023
4105
|
return fail("usage", `${canonical} is neither a declared member nor a workspace package of the platform at ${root}`);
|
|
3024
4106
|
}
|
|
3025
4107
|
function link(canonical, platformRoot) {
|
|
3026
|
-
if (!
|
|
4108
|
+
if (!existsSync4(join6(canonical, PLATFORM_MAP_FILENAME))) {
|
|
3027
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>\``);
|
|
3028
4110
|
}
|
|
3029
4111
|
const plan = planLink(canonical, { root: resolve4(platformRoot) });
|
|
@@ -3038,7 +4120,7 @@ async function resolveMemberPin(input) {
|
|
|
3038
4120
|
if (!canon.ok)
|
|
3039
4121
|
return canon;
|
|
3040
4122
|
const { canonical } = canon;
|
|
3041
|
-
if (isExistingDir(
|
|
4123
|
+
if (isExistingDir(join6(canonical, CANONICAL_SPECS_DIR))) {
|
|
3042
4124
|
return fail("usage", `${canonical} is a platform dir (contains spec-engine/) \u2014 pass a member subdir or cd into one`);
|
|
3043
4125
|
}
|
|
3044
4126
|
let linked = null;
|
|
@@ -3088,7 +4170,7 @@ function planDeclare(platformDir, name) {
|
|
|
3088
4170
|
}
|
|
3089
4171
|
const candidate = plan.candidates.find((c) => c.name === name);
|
|
3090
4172
|
if (candidate === undefined) {
|
|
3091
|
-
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`);
|
|
3092
4174
|
}
|
|
3093
4175
|
if (candidate.marker !== undefined && candidate.marker !== plan.platformName) {
|
|
3094
4176
|
return fail("conflict", `${name} carries a platform-map.json marker for platform "${candidate.marker}", not "${plan.platformName}"`);
|
|
@@ -3131,8 +4213,8 @@ async function write(configPath, specs, ignore) {
|
|
|
3131
4213
|
}
|
|
3132
4214
|
async function writeMemberConfig(input) {
|
|
3133
4215
|
const { canonical, pin } = input;
|
|
3134
|
-
const configPath =
|
|
3135
|
-
if (!
|
|
4216
|
+
const configPath = join6(canonical, MEMBER_CONFIG_FILENAME);
|
|
4217
|
+
if (!existsSync4(configPath)) {
|
|
3136
4218
|
await write(configPath, pin, input.ignore);
|
|
3137
4219
|
return { ok: true, action: "wrote", path: configPath, pin };
|
|
3138
4220
|
}
|
|
@@ -3255,16 +4337,16 @@ function renderDiagnostics(rows, mode) {
|
|
|
3255
4337
|
}
|
|
3256
4338
|
|
|
3257
4339
|
// src/operations/_index.ts
|
|
3258
|
-
import { existsSync as
|
|
3259
|
-
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";
|
|
3260
4342
|
async function withIndex(opts, fn) {
|
|
3261
4343
|
const { platformDir, build } = opts;
|
|
3262
4344
|
const dbPath = opts.dbPath ?? defaultIndexPath(platformDir);
|
|
3263
4345
|
assertSpecPlatform(platformDir);
|
|
3264
|
-
|
|
4346
|
+
mkdirSync3(dirname5(dbPath), { recursive: true });
|
|
3265
4347
|
if (build === "fresh" || build === "reset")
|
|
3266
4348
|
coldResetDb(dbPath);
|
|
3267
|
-
const wasMissing = !
|
|
4349
|
+
const wasMissing = !existsSync5(dbPath);
|
|
3268
4350
|
const storage2 = openStorage(dbPath);
|
|
3269
4351
|
try {
|
|
3270
4352
|
const warnings = [];
|
|
@@ -3285,12 +4367,12 @@ async function withIndex(opts, fn) {
|
|
|
3285
4367
|
function staleIndexWarning(platformDir, dbPath) {
|
|
3286
4368
|
try {
|
|
3287
4369
|
const dbMtime = statSync3(dbPath).mtimeMs;
|
|
3288
|
-
const specsDir =
|
|
4370
|
+
const specsDir = join7(platformDir, CANONICAL_SPECS_DIR);
|
|
3289
4371
|
for (const entry of readdirSync(specsDir, { withFileTypes: true })) {
|
|
3290
4372
|
if (!entry.isDirectory())
|
|
3291
4373
|
continue;
|
|
3292
|
-
const specPath =
|
|
3293
|
-
if (!
|
|
4374
|
+
const specPath = join7(specsDir, entry.name, SPEC_FILENAME);
|
|
4375
|
+
if (!existsSync5(specPath))
|
|
3294
4376
|
continue;
|
|
3295
4377
|
if (statSync3(specPath).mtimeMs > dbMtime) {
|
|
3296
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.`;
|
|
@@ -3383,7 +4465,7 @@ function printWarnings(cmdName, warnings) {
|
|
|
3383
4465
|
for (const w of warnings ?? []) {
|
|
3384
4466
|
if (w.kind === "ref")
|
|
3385
4467
|
console.error(`spec req: warning \u2014 ${w.text}`);
|
|
3386
|
-
else if (w.kind === "grammar")
|
|
4468
|
+
else if (w.kind === "grammar" || w.kind === "statement")
|
|
3387
4469
|
console.error(`${cmdName}: ${w.text}`);
|
|
3388
4470
|
else
|
|
3389
4471
|
console.error(w.text);
|
|
@@ -3429,7 +4511,7 @@ __export(exports_init, {
|
|
|
3429
4511
|
runInit: () => runInit,
|
|
3430
4512
|
initCommand: () => initCommand
|
|
3431
4513
|
});
|
|
3432
|
-
import { resolve as resolve5 } from "path";
|
|
4514
|
+
import { dirname as dirname6, join as join8, resolve as resolve5 } from "path";
|
|
3433
4515
|
import { defineCommand } from "citty";
|
|
3434
4516
|
function errMessage2(err) {
|
|
3435
4517
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -3461,19 +4543,54 @@ function sourceText(result, json) {
|
|
|
3461
4543
|
}
|
|
3462
4544
|
return "fallback @1";
|
|
3463
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
|
+
}
|
|
3464
4578
|
async function runInit(args) {
|
|
3465
4579
|
const json = Boolean(args.json);
|
|
4580
|
+
const force = Boolean(args.force);
|
|
3466
4581
|
const override = typeof args.specs === "string" ? validateOverride(args.specs) : undefined;
|
|
3467
4582
|
const result = await initMember({
|
|
3468
4583
|
repoDir: resolveRepoArg(args.repo),
|
|
3469
4584
|
override,
|
|
3470
4585
|
platformRoot: typeof args.platform === "string" ? args.platform : undefined,
|
|
3471
|
-
force
|
|
4586
|
+
force
|
|
3472
4587
|
});
|
|
3473
4588
|
if (!result.ok)
|
|
3474
4589
|
exitOnFailure("spec init", result);
|
|
3475
4590
|
const source = sourceText(result, json);
|
|
3476
4591
|
const { declared, linked } = result;
|
|
4592
|
+
const surface = surfaceFor(result, force);
|
|
4593
|
+
const agent_surface = surface.written;
|
|
3477
4594
|
if (result.action === "already-configured") {
|
|
3478
4595
|
if (json) {
|
|
3479
4596
|
console.log(JSON.stringify({
|
|
@@ -3482,8 +4599,10 @@ async function runInit(args) {
|
|
|
3482
4599
|
pin: result.pin,
|
|
3483
4600
|
extra_fields: result.extraFields,
|
|
3484
4601
|
declared,
|
|
3485
|
-
linked
|
|
4602
|
+
linked,
|
|
4603
|
+
agent_surface
|
|
3486
4604
|
}));
|
|
4605
|
+
printSurface(surface, json);
|
|
3487
4606
|
return;
|
|
3488
4607
|
}
|
|
3489
4608
|
console.log("spec init: already configured");
|
|
@@ -3496,6 +4615,7 @@ async function runInit(args) {
|
|
|
3496
4615
|
console.log(` declared: ${file}`);
|
|
3497
4616
|
if (linked !== null)
|
|
3498
4617
|
console.log(` linked: ${linked}`);
|
|
4618
|
+
printSurface(surface, json);
|
|
3499
4619
|
return;
|
|
3500
4620
|
}
|
|
3501
4621
|
if (json) {
|
|
@@ -3505,8 +4625,10 @@ async function runInit(args) {
|
|
|
3505
4625
|
pin: result.pin,
|
|
3506
4626
|
source,
|
|
3507
4627
|
declared,
|
|
3508
|
-
linked
|
|
4628
|
+
linked,
|
|
4629
|
+
agent_surface
|
|
3509
4630
|
}));
|
|
4631
|
+
printSurface(surface, json);
|
|
3510
4632
|
return;
|
|
3511
4633
|
}
|
|
3512
4634
|
console.log(`spec init: wrote ${result.path}`);
|
|
@@ -3516,17 +4638,19 @@ async function runInit(args) {
|
|
|
3516
4638
|
console.log(` declared: ${file}`);
|
|
3517
4639
|
if (linked !== null)
|
|
3518
4640
|
console.log(` linked: ${linked}`);
|
|
4641
|
+
printSurface(surface, json);
|
|
3519
4642
|
}
|
|
3520
4643
|
var initCommand;
|
|
3521
4644
|
var init_init2 = __esm(() => {
|
|
3522
4645
|
init_src();
|
|
3523
4646
|
init_constants();
|
|
4647
|
+
init_agentSurface();
|
|
3524
4648
|
init_init();
|
|
3525
4649
|
init__shared();
|
|
3526
4650
|
initCommand = defineCommand({
|
|
3527
4651
|
meta: {
|
|
3528
4652
|
name: "init",
|
|
3529
|
-
description: "Declare a member repo
|
|
4653
|
+
description: "Declare a member repo, scaffold its spec-engine.member.json, and install the agent surface"
|
|
3530
4654
|
},
|
|
3531
4655
|
args: {
|
|
3532
4656
|
repo: {
|
|
@@ -3544,7 +4668,7 @@ var init_init2 = __esm(() => {
|
|
|
3544
4668
|
},
|
|
3545
4669
|
force: {
|
|
3546
4670
|
type: "boolean",
|
|
3547
|
-
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."
|
|
3548
4672
|
},
|
|
3549
4673
|
json: {
|
|
3550
4674
|
type: "boolean",
|
|
@@ -3552,7 +4676,11 @@ var init_init2 = __esm(() => {
|
|
|
3552
4676
|
}
|
|
3553
4677
|
},
|
|
3554
4678
|
async run({ args }) {
|
|
3555
|
-
|
|
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);
|
|
3556
4684
|
}
|
|
3557
4685
|
});
|
|
3558
4686
|
});
|
|
@@ -3617,7 +4745,7 @@ function listFlag(rawArgs, name, parsed) {
|
|
|
3617
4745
|
}
|
|
3618
4746
|
return raw.flatMap((v) => v.split(",")).map((v) => v.trim()).filter((v) => v !== "");
|
|
3619
4747
|
}
|
|
3620
|
-
var platformDirArg, outArg, jsonArg, freshArg, noPromptArg,
|
|
4748
|
+
var platformDirArg, outArg, jsonArg, freshArg, noPromptArg, relatesArg;
|
|
3621
4749
|
var init__args = __esm(() => {
|
|
3622
4750
|
init_constants();
|
|
3623
4751
|
platformDirArg = {
|
|
@@ -3638,14 +4766,6 @@ var init__args = __esm(() => {
|
|
|
3638
4766
|
type: "boolean",
|
|
3639
4767
|
description: "Suppress the interactive onboarding prompt for siblings missing spec-engine.member.json (they surface as NO_SPEC_CONFIG warnings)"
|
|
3640
4768
|
};
|
|
3641
|
-
livesArg = {
|
|
3642
|
-
type: "string",
|
|
3643
|
-
description: "Lives in (livesIn) paths: repeat the flag or comma-separate; an empty value clears the list"
|
|
3644
|
-
};
|
|
3645
|
-
testsArg = {
|
|
3646
|
-
type: "string",
|
|
3647
|
-
description: "Verified in (verifiedIn) test paths: repeat the flag or comma-separate; an empty value clears the list"
|
|
3648
|
-
};
|
|
3649
4769
|
relatesArg = {
|
|
3650
4770
|
type: "string",
|
|
3651
4771
|
description: "Related requirement ids (relates): repeat the flag or comma-separate; an empty value clears the list"
|
|
@@ -3657,8 +4777,8 @@ var exports_commands = {};
|
|
|
3657
4777
|
__export(exports_commands, {
|
|
3658
4778
|
indexCommand: () => indexCommand
|
|
3659
4779
|
});
|
|
3660
|
-
import { mkdirSync as
|
|
3661
|
-
import { dirname as
|
|
4780
|
+
import { mkdirSync as mkdirSync4 } from "fs";
|
|
4781
|
+
import { dirname as dirname7 } from "path";
|
|
3662
4782
|
import { defineCommand as defineCommand2 } from "citty";
|
|
3663
4783
|
var indexCommand;
|
|
3664
4784
|
var init_commands = __esm(() => {
|
|
@@ -3705,7 +4825,7 @@ var init_commands = __esm(() => {
|
|
|
3705
4825
|
let storage2;
|
|
3706
4826
|
try {
|
|
3707
4827
|
assertSpecPlatform(platformDir);
|
|
3708
|
-
|
|
4828
|
+
mkdirSync4(dirname7(dbPath), { recursive: true });
|
|
3709
4829
|
storage2 = openStorage(dbPath);
|
|
3710
4830
|
const result = await runIndex({ platformDir, storage: storage2 });
|
|
3711
4831
|
if (args.json) {
|
|
@@ -3734,6 +4854,87 @@ var init_commands = __esm(() => {
|
|
|
3734
4854
|
});
|
|
3735
4855
|
});
|
|
3736
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
|
+
|
|
3737
4938
|
// src/check/proven.ts
|
|
3738
4939
|
function normalizeToSegments(path) {
|
|
3739
4940
|
const out = [];
|
|
@@ -3857,74 +5058,6 @@ var init_proven = __esm(() => {
|
|
|
3857
5058
|
init_src();
|
|
3858
5059
|
});
|
|
3859
5060
|
|
|
3860
|
-
// src/base/gitBase.ts
|
|
3861
|
-
function isSafeRef(ref) {
|
|
3862
|
-
return /^[A-Za-z0-9._/][A-Za-z0-9._/-]*$/.test(ref);
|
|
3863
|
-
}
|
|
3864
|
-
function gitRepoPrefix(platformDir) {
|
|
3865
|
-
const proc = Bun.spawnSync(["git", "rev-parse", "--show-prefix"], { cwd: platformDir });
|
|
3866
|
-
if (proc.exitCode !== 0)
|
|
3867
|
-
return "";
|
|
3868
|
-
return proc.stdout.toString().trim();
|
|
3869
|
-
}
|
|
3870
|
-
function gitRefResolves(platformDir, ref) {
|
|
3871
|
-
if (!isSafeRef(ref))
|
|
3872
|
-
return false;
|
|
3873
|
-
const proc = Bun.spawnSync(["git", "rev-parse", "--verify", "--quiet", `${ref}^{commit}`], {
|
|
3874
|
-
cwd: platformDir
|
|
3875
|
-
});
|
|
3876
|
-
return proc.exitCode === 0;
|
|
3877
|
-
}
|
|
3878
|
-
function gitShow(platformDir, ref, relPath) {
|
|
3879
|
-
if (!isSafeRef(ref))
|
|
3880
|
-
return null;
|
|
3881
|
-
const full = `${gitRepoPrefix(platformDir)}${relPath}`;
|
|
3882
|
-
const proc = Bun.spawnSync(["git", "show", `${ref}:${full}`], { cwd: platformDir });
|
|
3883
|
-
if (proc.exitCode !== 0)
|
|
3884
|
-
return null;
|
|
3885
|
-
return proc.stdout.toString();
|
|
3886
|
-
}
|
|
3887
|
-
function gitLsTree(platformDir, ref, dir) {
|
|
3888
|
-
if (!isSafeRef(ref))
|
|
3889
|
-
return [];
|
|
3890
|
-
const prefix = gitRepoPrefix(platformDir);
|
|
3891
|
-
const proc = Bun.spawnSync(["git", "ls-tree", "-r", "--name-only", ref, "--", `${prefix}${dir}`], { cwd: platformDir });
|
|
3892
|
-
if (proc.exitCode !== 0)
|
|
3893
|
-
return [];
|
|
3894
|
-
return proc.stdout.toString().split(`
|
|
3895
|
-
`).map((l) => l.trim()).filter((l) => l.length > 0).map((l) => prefix.length > 0 && l.startsWith(prefix) ? l.slice(prefix.length) : l);
|
|
3896
|
-
}
|
|
3897
|
-
function gitDiffNameStatus(platformDir, ref) {
|
|
3898
|
-
if (!isSafeRef(ref))
|
|
3899
|
-
return [];
|
|
3900
|
-
const proc = Bun.spawnSync(["git", "diff", "--name-status", "--no-renames", "--relative", ref, "--"], {
|
|
3901
|
-
cwd: platformDir
|
|
3902
|
-
});
|
|
3903
|
-
if (proc.exitCode !== 0)
|
|
3904
|
-
return [];
|
|
3905
|
-
const rows = [];
|
|
3906
|
-
for (const line of proc.stdout.toString().split(`
|
|
3907
|
-
`)) {
|
|
3908
|
-
if (line.length === 0)
|
|
3909
|
-
continue;
|
|
3910
|
-
const parts = line.split("\t");
|
|
3911
|
-
const status = (parts[0] ?? "").trim().toUpperCase();
|
|
3912
|
-
const path = (parts[parts.length - 1] ?? "").trim();
|
|
3913
|
-
if (status.length === 0 || path.length === 0)
|
|
3914
|
-
continue;
|
|
3915
|
-
rows.push({ status, path });
|
|
3916
|
-
}
|
|
3917
|
-
return rows;
|
|
3918
|
-
}
|
|
3919
|
-
function newestReleaseTag(platformDir) {
|
|
3920
|
-
const proc = Bun.spawnSync(["git", "tag", "--list", "v*", "--merged", "HEAD", "--sort=-v:refname"], { cwd: platformDir });
|
|
3921
|
-
if (proc.exitCode !== 0)
|
|
3922
|
-
return null;
|
|
3923
|
-
const tag = proc.stdout.toString().split(`
|
|
3924
|
-
`)[0]?.trim() ?? "";
|
|
3925
|
-
return tag !== "" && isSafeRef(tag) ? tag : null;
|
|
3926
|
-
}
|
|
3927
|
-
|
|
3928
5061
|
// src/check/codeowners.ts
|
|
3929
5062
|
function parseCodeowners(text) {
|
|
3930
5063
|
const rules = [];
|
|
@@ -4002,18 +5135,18 @@ function matchesSegment(pat, seg) {
|
|
|
4002
5135
|
}
|
|
4003
5136
|
|
|
4004
5137
|
// src/authoring/domains.ts
|
|
4005
|
-
import { existsSync as
|
|
4006
|
-
import { join as
|
|
5138
|
+
import { existsSync as existsSync6, readdirSync as readdirSync2 } from "fs";
|
|
5139
|
+
import { join as join9 } from "path";
|
|
4007
5140
|
function normalizeDomainKey(raw) {
|
|
4008
5141
|
return raw.toUpperCase().replace(/\s+/g, "");
|
|
4009
5142
|
}
|
|
4010
5143
|
function listDomainKeys(platformDir) {
|
|
4011
|
-
const specsDir =
|
|
4012
|
-
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();
|
|
4013
5146
|
}
|
|
4014
5147
|
async function domainScope(platformDir, key) {
|
|
4015
5148
|
const { abs: jsonPath, rel: relFile } = specPaths(platformDir, key);
|
|
4016
|
-
if (!
|
|
5149
|
+
if (!existsSync6(jsonPath))
|
|
4017
5150
|
return null;
|
|
4018
5151
|
let text;
|
|
4019
5152
|
try {
|
|
@@ -4054,166 +5187,28 @@ function requirementsOf(text, sourceFile) {
|
|
|
4054
5187
|
}
|
|
4055
5188
|
async function headMaxSeq(platformDir, key) {
|
|
4056
5189
|
try {
|
|
4057
|
-
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() });
|
|
4058
5191
|
const text = await new Response(proc.stdout).text();
|
|
4059
|
-
if (await proc.exited !== 0)
|
|
4060
|
-
return 0;
|
|
4061
|
-
return maxSeqOf(requirementsOf(text, specPaths(platformDir, key).rel));
|
|
4062
|
-
} catch {
|
|
4063
|
-
return 0;
|
|
4064
|
-
}
|
|
4065
|
-
}
|
|
4066
|
-
async function nextRequirementId(platformDir, key) {
|
|
4067
|
-
const { abs: jsonPath, rel: relFile } = specPaths(platformDir, key);
|
|
4068
|
-
if (!existsSync5(jsonPath))
|
|
4069
|
-
return `${key}-001`;
|
|
4070
|
-
const requirements = requirementsOf(await Bun.file(jsonPath).text(), relFile);
|
|
4071
|
-
const maxSeq = Math.max(maxSeqOf(requirements), await headMaxSeq(platformDir, key));
|
|
4072
|
-
const next = String(maxSeq + 1).padStart(3, "0");
|
|
4073
|
-
return `${key}-${next}`;
|
|
4074
|
-
}
|
|
4075
|
-
var init_domains = __esm(() => {
|
|
4076
|
-
init_src();
|
|
4077
|
-
init_constants();
|
|
4078
|
-
init_src();
|
|
4079
|
-
});
|
|
4080
|
-
|
|
4081
|
-
// src/authoring/filerefs.ts
|
|
4082
|
-
import { existsSync as existsSync6 } from "fs";
|
|
4083
|
-
import { resolve as resolve7 } from "path";
|
|
4084
|
-
function extractRefsFromText(s) {
|
|
4085
|
-
const out = [];
|
|
4086
|
-
for (const m of s.matchAll(FILE_REF_RE)) {
|
|
4087
|
-
const candidate = (m[1] ?? "").replace(/\.+$/, "");
|
|
4088
|
-
if (candidate.includes("/"))
|
|
4089
|
-
out.push(candidate);
|
|
4090
|
-
}
|
|
4091
|
-
return out;
|
|
4092
|
-
}
|
|
4093
|
-
function resolveFileRef(platformDir, ref) {
|
|
4094
|
-
const root = resolve7(platformDir);
|
|
4095
|
-
const abs = resolve7(root, ref);
|
|
4096
|
-
if (!(abs === root || abs.startsWith(`${root}/`)))
|
|
4097
|
-
return false;
|
|
4098
|
-
return existsSync6(abs);
|
|
4099
|
-
}
|
|
4100
|
-
var FILE_REF_RE;
|
|
4101
|
-
var init_filerefs = __esm(() => {
|
|
4102
|
-
FILE_REF_RE = /(?<![^\s])@([A-Za-z0-9_][A-Za-z0-9_\-./]*)/g;
|
|
4103
|
-
});
|
|
4104
|
-
|
|
4105
|
-
// src/check/filerefs.ts
|
|
4106
|
-
function locateInDomain(raw, sourceFile, requirements, statuses) {
|
|
4107
|
-
const out = [];
|
|
4108
|
-
const rawLines = raw.split(`
|
|
4109
|
-
`);
|
|
4110
|
-
let lineCursor = 0;
|
|
4111
|
-
for (const req of requirements) {
|
|
4112
|
-
const found = findRequirementIdLine(rawLines, req.id, lineCursor);
|
|
4113
|
-
if (found >= 0)
|
|
4114
|
-
lineCursor = found + 1;
|
|
4115
|
-
if (statuses.has(req.status.toLowerCase())) {
|
|
4116
|
-
out.push({ sourceFile, line: found >= 0 ? found + 1 : 0, req });
|
|
4117
|
-
}
|
|
4118
|
-
}
|
|
4119
|
-
return out;
|
|
4120
|
-
}
|
|
4121
|
-
async function locatedEntries(platformDir, statuses) {
|
|
4122
|
-
const out = [];
|
|
4123
|
-
for (const key of listDomainKeys(platformDir)) {
|
|
4124
|
-
const { abs, rel } = specPaths(platformDir, key);
|
|
4125
|
-
let raw;
|
|
4126
|
-
try {
|
|
4127
|
-
raw = await Bun.file(abs).text();
|
|
4128
|
-
} catch {
|
|
4129
|
-
continue;
|
|
4130
|
-
}
|
|
4131
|
-
const parsed = parseDomainText(raw, rel);
|
|
4132
|
-
if (parsed.ok)
|
|
4133
|
-
out.push(...locateInDomain(raw, rel, parsed.data.requirements, statuses));
|
|
4134
|
-
}
|
|
4135
|
-
return out;
|
|
4136
|
-
}
|
|
4137
|
-
function sortRows(rows) {
|
|
4138
|
-
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));
|
|
4139
|
-
}
|
|
4140
|
-
function row(at, code, severity, detail) {
|
|
4141
|
-
return {
|
|
4142
|
-
code,
|
|
4143
|
-
severity,
|
|
4144
|
-
repo: null,
|
|
4145
|
-
source_file: at.sourceFile,
|
|
4146
|
-
line: at.line,
|
|
4147
|
-
req_id: at.req.id,
|
|
4148
|
-
detail
|
|
4149
|
-
};
|
|
4150
|
-
}
|
|
4151
|
-
async function brokenFileRefDiagnostics(platformDir) {
|
|
4152
|
-
const rows = [];
|
|
4153
|
-
for (const at of await locatedEntries(platformDir, FILE_REF_STATUSES)) {
|
|
4154
|
-
for (const [field, entries] of [
|
|
4155
|
-
["livesIn", at.req.livesIn],
|
|
4156
|
-
["verifiedIn", at.req.verifiedIn]
|
|
4157
|
-
]) {
|
|
4158
|
-
for (const entry of entries) {
|
|
4159
|
-
const ref = refOf(entry);
|
|
4160
|
-
if (ref.length === 0 || resolveFileRef(platformDir, ref))
|
|
4161
|
-
continue;
|
|
4162
|
-
rows.push(row(at, DiagnosticCode.BROKEN_FILE_REF, "error", `${field} entry ${entry} does not resolve to a file under the platform root`));
|
|
4163
|
-
}
|
|
4164
|
-
}
|
|
4165
|
-
}
|
|
4166
|
-
return sortRows(rows);
|
|
4167
|
-
}
|
|
4168
|
-
function tagKeys(tags, kinds) {
|
|
4169
|
-
return new Set(tags.filter((t) => kinds.includes(t.kind)).map((t) => `${t.req_id} ${t.file}`));
|
|
4170
|
-
}
|
|
4171
|
-
function inFileRows(platformDir, members, at, check) {
|
|
4172
|
-
const rows = [];
|
|
4173
|
-
for (const entry of at.req[check.field]) {
|
|
4174
|
-
const ref = refOf(entry);
|
|
4175
|
-
if (ref.length === 0 || !resolveFileRef(platformDir, ref))
|
|
4176
|
-
continue;
|
|
4177
|
-
const file = scannedTagFile(members, platformDir, ref);
|
|
4178
|
-
if (file === null || check.keys.has(`${at.req.id} ${file}`))
|
|
4179
|
-
continue;
|
|
4180
|
-
rows.push(row(at, check.code, "warning", `${check.field} entry ${entry} holds no ${check.missing} tag for ${at.req.id}`));
|
|
5192
|
+
if (await proc.exited !== 0)
|
|
5193
|
+
return 0;
|
|
5194
|
+
return maxSeqOf(requirementsOf(text, specPaths(platformDir, key).rel));
|
|
5195
|
+
} catch {
|
|
5196
|
+
return 0;
|
|
4181
5197
|
}
|
|
4182
|
-
return rows;
|
|
4183
5198
|
}
|
|
4184
|
-
async function
|
|
4185
|
-
const {
|
|
4186
|
-
|
|
4187
|
-
{
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
},
|
|
4193
|
-
{
|
|
4194
|
-
field: "verifiedIn",
|
|
4195
|
-
code: DiagnosticCode.UNVERIFIED_IN_FILE,
|
|
4196
|
-
keys: tagKeys(tags, ["verifies"]),
|
|
4197
|
-
missing: "verifying"
|
|
4198
|
-
}
|
|
4199
|
-
];
|
|
4200
|
-
const rows = [];
|
|
4201
|
-
for (const at of await locatedEntries(platformDir, IN_FILE_STATUSES)) {
|
|
4202
|
-
for (const check of checks)
|
|
4203
|
-
rows.push(...inFileRows(platformDir, members, at, check));
|
|
4204
|
-
}
|
|
4205
|
-
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}`;
|
|
4206
5207
|
}
|
|
4207
|
-
var
|
|
4208
|
-
var init_filerefs2 = __esm(() => {
|
|
5208
|
+
var init_domains = __esm(() => {
|
|
4209
5209
|
init_src();
|
|
4210
|
-
init_domains();
|
|
4211
|
-
init_filerefs();
|
|
4212
5210
|
init_constants();
|
|
4213
|
-
|
|
4214
|
-
init_pipeline();
|
|
4215
|
-
FILE_REF_STATUSES = new Set(["active", "draft"]);
|
|
4216
|
-
IN_FILE_STATUSES = new Set(["active"]);
|
|
5211
|
+
init_src();
|
|
4217
5212
|
});
|
|
4218
5213
|
|
|
4219
5214
|
// src/check/grammar.ts
|
|
@@ -4346,20 +5341,20 @@ var init_removed = __esm(() => {
|
|
|
4346
5341
|
});
|
|
4347
5342
|
|
|
4348
5343
|
// src/check/severity.ts
|
|
4349
|
-
import { existsSync as existsSync7, readFileSync } from "fs";
|
|
4350
|
-
import { join as
|
|
5344
|
+
import { existsSync as existsSync7, readFileSync as readFileSync2 } from "fs";
|
|
5345
|
+
import { join as join10 } from "path";
|
|
4351
5346
|
function readCheckConfig(platformDir) {
|
|
4352
|
-
const path =
|
|
5347
|
+
const path = join10(platformDir, CHECK_CONFIG_REL);
|
|
4353
5348
|
if (!existsSync7(path))
|
|
4354
5349
|
return { ok: true, config: { severity: [] } };
|
|
4355
|
-
return parseCheckConfigText(
|
|
5350
|
+
return parseCheckConfigText(readFileSync2(path, "utf-8"), CHECK_CONFIG_REL);
|
|
4356
5351
|
}
|
|
4357
|
-
function specificity(rule,
|
|
4358
|
-
if (rule.code !==
|
|
5352
|
+
function specificity(rule, row) {
|
|
5353
|
+
if (rule.code !== row.code)
|
|
4359
5354
|
return Specificity.none;
|
|
4360
5355
|
if (rule.match === undefined)
|
|
4361
5356
|
return Specificity.code;
|
|
4362
|
-
const id =
|
|
5357
|
+
const id = row.req_id;
|
|
4363
5358
|
if (id === null)
|
|
4364
5359
|
return Specificity.none;
|
|
4365
5360
|
if (rule.match.includes(id))
|
|
@@ -4369,20 +5364,20 @@ function specificity(rule, row2) {
|
|
|
4369
5364
|
}
|
|
4370
5365
|
function applySeverity(rows, rules) {
|
|
4371
5366
|
const out = [];
|
|
4372
|
-
for (const
|
|
5367
|
+
for (const row of rows) {
|
|
4373
5368
|
let best;
|
|
4374
5369
|
let bestScore = Specificity.none;
|
|
4375
5370
|
for (const rule of rules) {
|
|
4376
|
-
const score = specificity(rule,
|
|
5371
|
+
const score = specificity(rule, row);
|
|
4377
5372
|
if (score !== Specificity.none && score >= bestScore) {
|
|
4378
5373
|
best = rule;
|
|
4379
5374
|
bestScore = score;
|
|
4380
5375
|
}
|
|
4381
5376
|
}
|
|
4382
5377
|
if (best === undefined)
|
|
4383
|
-
out.push(
|
|
5378
|
+
out.push(row);
|
|
4384
5379
|
else if (best.severity !== "off")
|
|
4385
|
-
out.push({ ...
|
|
5380
|
+
out.push({ ...row, severity: best.severity });
|
|
4386
5381
|
}
|
|
4387
5382
|
return out;
|
|
4388
5383
|
}
|
|
@@ -4429,6 +5424,60 @@ var init_sqlDiagnostics = __esm(() => {
|
|
|
4429
5424
|
]);
|
|
4430
5425
|
});
|
|
4431
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
|
+
|
|
4432
5481
|
// src/check/statusflip.ts
|
|
4433
5482
|
function normalizeHandle(h) {
|
|
4434
5483
|
return (h.startsWith("@") ? h.slice(1) : h).toLowerCase();
|
|
@@ -4900,8 +5949,6 @@ function successorEntry(id, pred, fields) {
|
|
|
4900
5949
|
supersedes: null,
|
|
4901
5950
|
supersededBy: null,
|
|
4902
5951
|
relates: fields.relates,
|
|
4903
|
-
livesIn: fields.livesIn,
|
|
4904
|
-
verifiedIn: fields.verifiedIn,
|
|
4905
5952
|
issues: fields.issue ? [{ role: "created", id: fields.issue }] : [],
|
|
4906
5953
|
aliases: [...pred.aliases],
|
|
4907
5954
|
cites: pred.cites.map((c) => ({ ...c }))
|
|
@@ -4919,6 +5966,30 @@ var init__envelope = __esm(() => {
|
|
|
4919
5966
|
init__result();
|
|
4920
5967
|
});
|
|
4921
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
|
+
|
|
4922
5993
|
// src/authoring/grammar.ts
|
|
4923
5994
|
async function grammarConfig(platformDir, key) {
|
|
4924
5995
|
const freeform = { grammar: "freeform", severity: "warning" };
|
|
@@ -4967,8 +6038,35 @@ var init_grammar2 = __esm(() => {
|
|
|
4967
6038
|
init_constants();
|
|
4968
6039
|
});
|
|
4969
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
|
+
|
|
4970
6068
|
// src/operations/mint.ts
|
|
4971
|
-
import { existsSync as
|
|
6069
|
+
import { existsSync as existsSync10 } from "fs";
|
|
4972
6070
|
function unresolvableRefWarnings(platformDir, fieldValues) {
|
|
4973
6071
|
const out = [];
|
|
4974
6072
|
for (const value of fieldValues) {
|
|
@@ -4983,15 +6081,10 @@ function unresolvableRefWarnings(platformDir, fieldValues) {
|
|
|
4983
6081
|
async function mint(input) {
|
|
4984
6082
|
const { platformDir, key } = input;
|
|
4985
6083
|
const { abs: specPath, rel: relFile } = specPaths(platformDir, key);
|
|
4986
|
-
if (!
|
|
6084
|
+
if (!existsSync10(specPath)) {
|
|
4987
6085
|
return fail("not_found", `no domain ${key} (expected ${relFile} under ${platformDir})`);
|
|
4988
6086
|
}
|
|
4989
|
-
const warnings = unresolvableRefWarnings(platformDir, [
|
|
4990
|
-
input.statement,
|
|
4991
|
-
input.why,
|
|
4992
|
-
...input.livesIn,
|
|
4993
|
-
...input.verifiedIn ?? []
|
|
4994
|
-
]);
|
|
6087
|
+
const warnings = unresolvableRefWarnings(platformDir, [input.statement, input.why]);
|
|
4995
6088
|
const verdict = await judgeStatementGrammar(platformDir, key, input.statement);
|
|
4996
6089
|
if (!verdict.ok) {
|
|
4997
6090
|
if (verdict.severity === "error") {
|
|
@@ -4999,6 +6092,10 @@ async function mint(input) {
|
|
|
4999
6092
|
}
|
|
5000
6093
|
warnings.push({ kind: "grammar", text: grammarWarningText("", verdict).slice(2) });
|
|
5001
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);
|
|
5002
6099
|
const file = await readEnvelope(platformDir, key);
|
|
5003
6100
|
if (!file.ok) {
|
|
5004
6101
|
return fail(file.reason, file.detail, { diagnostics: file.diagnostics, warnings });
|
|
@@ -5013,8 +6110,6 @@ async function mint(input) {
|
|
|
5013
6110
|
supersedes: null,
|
|
5014
6111
|
supersededBy: null,
|
|
5015
6112
|
relates: splitList(input.relates ?? []),
|
|
5016
|
-
livesIn: input.livesIn,
|
|
5017
|
-
verifiedIn: input.verifiedIn ?? [],
|
|
5018
6113
|
issues: input.issue ? [{ role: "created", id: input.issue }] : [],
|
|
5019
6114
|
aliases: [],
|
|
5020
6115
|
cites: input.cites ?? []
|
|
@@ -5036,6 +6131,7 @@ var init_mint = __esm(() => {
|
|
|
5036
6131
|
init_domains();
|
|
5037
6132
|
init_filerefs();
|
|
5038
6133
|
init_grammar2();
|
|
6134
|
+
init_statementRules3();
|
|
5039
6135
|
init_constants();
|
|
5040
6136
|
init__envelope();
|
|
5041
6137
|
init__result();
|
|
@@ -5071,8 +6167,6 @@ async function mintTerm(input) {
|
|
|
5071
6167
|
supersedes: null,
|
|
5072
6168
|
supersededBy: null,
|
|
5073
6169
|
relates: [],
|
|
5074
|
-
livesIn: [],
|
|
5075
|
-
verifiedIn: [],
|
|
5076
6170
|
issues: [],
|
|
5077
6171
|
aliases: input.aliases,
|
|
5078
6172
|
cites: [],
|
|
@@ -5173,8 +6267,8 @@ var init_term = __esm(() => {
|
|
|
5173
6267
|
});
|
|
5174
6268
|
|
|
5175
6269
|
// src/operations/glossary.ts
|
|
5176
|
-
import { existsSync as
|
|
5177
|
-
import { join as
|
|
6270
|
+
import { existsSync as existsSync11, readFileSync as readFileSync3 } from "fs";
|
|
6271
|
+
import { join as join11 } from "path";
|
|
5178
6272
|
function parseGlossary(md) {
|
|
5179
6273
|
const terms = [];
|
|
5180
6274
|
let section = null;
|
|
@@ -5226,15 +6320,15 @@ function generateGlossary(terms) {
|
|
|
5226
6320
|
}
|
|
5227
6321
|
function readStoreTerms(platformDir) {
|
|
5228
6322
|
const { abs: specPath, rel: relFile } = specPaths(platformDir, TERM_KEY);
|
|
5229
|
-
if (!
|
|
6323
|
+
if (!existsSync11(specPath))
|
|
5230
6324
|
return [];
|
|
5231
|
-
const parsed = parseDomainText(
|
|
6325
|
+
const parsed = parseDomainText(readFileSync3(specPath, "utf8"), relFile);
|
|
5232
6326
|
if (!parsed.ok)
|
|
5233
6327
|
return [];
|
|
5234
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 }));
|
|
5235
6329
|
}
|
|
5236
6330
|
function glossaryPath(platformDir) {
|
|
5237
|
-
return
|
|
6331
|
+
return join11(platformDir, "GLOSSARY.md");
|
|
5238
6332
|
}
|
|
5239
6333
|
async function migrateGlossary(platformDir) {
|
|
5240
6334
|
const store = await termStore(platformDir);
|
|
@@ -5245,10 +6339,10 @@ async function migrateGlossary(platformDir) {
|
|
|
5245
6339
|
return { ok: true, skipped: true, existing: domain2.requirements.length };
|
|
5246
6340
|
}
|
|
5247
6341
|
const gPath = glossaryPath(platformDir);
|
|
5248
|
-
if (!
|
|
6342
|
+
if (!existsSync11(gPath)) {
|
|
5249
6343
|
return fail("not_found", `no GLOSSARY.md to migrate (expected ${gPath})`);
|
|
5250
6344
|
}
|
|
5251
|
-
const parsed = parseGlossary(
|
|
6345
|
+
const parsed = parseGlossary(readFileSync3(gPath, "utf8"));
|
|
5252
6346
|
domain2.requirements = parsed.map((t, i) => ({
|
|
5253
6347
|
id: `${TERM_KEY}-${String(i + 1).padStart(3, "0")}`,
|
|
5254
6348
|
status: "active",
|
|
@@ -5258,8 +6352,6 @@ async function migrateGlossary(platformDir) {
|
|
|
5258
6352
|
supersedes: null,
|
|
5259
6353
|
supersededBy: null,
|
|
5260
6354
|
relates: [],
|
|
5261
|
-
livesIn: [],
|
|
5262
|
-
verifiedIn: [],
|
|
5263
6355
|
issues: [],
|
|
5264
6356
|
aliases: [],
|
|
5265
6357
|
cites: [],
|
|
@@ -5286,10 +6378,10 @@ function glossaryDriftDiagnostic(platformDir) {
|
|
|
5286
6378
|
if (terms.length === 0)
|
|
5287
6379
|
return null;
|
|
5288
6380
|
const gPath = glossaryPath(platformDir);
|
|
5289
|
-
if (!
|
|
6381
|
+
if (!existsSync11(gPath))
|
|
5290
6382
|
return null;
|
|
5291
6383
|
const generated = generateGlossary(terms);
|
|
5292
|
-
const committed =
|
|
6384
|
+
const committed = readFileSync3(gPath, "utf8");
|
|
5293
6385
|
if (generated === committed)
|
|
5294
6386
|
return null;
|
|
5295
6387
|
return {
|
|
@@ -5305,7 +6397,7 @@ function glossaryDriftDiagnostic(platformDir) {
|
|
|
5305
6397
|
function checkGlossary(platformDir) {
|
|
5306
6398
|
const generated = generateGlossary(readStoreTerms(platformDir));
|
|
5307
6399
|
const gPath = glossaryPath(platformDir);
|
|
5308
|
-
const committed =
|
|
6400
|
+
const committed = existsSync11(gPath) ? readFileSync3(gPath, "utf8") : "";
|
|
5309
6401
|
return { ok: true, clean: generated === committed };
|
|
5310
6402
|
}
|
|
5311
6403
|
var GLOSSARY_INTRO = `Canonical names for Spec Engine concepts. When prose, code comments, diagnostics, or
|
|
@@ -5319,7 +6411,7 @@ var init_glossary = __esm(() => {
|
|
|
5319
6411
|
});
|
|
5320
6412
|
|
|
5321
6413
|
// src/guard/collect.ts
|
|
5322
|
-
import { join as
|
|
6414
|
+
import { join as join12 } from "path";
|
|
5323
6415
|
function isIgnored(path) {
|
|
5324
6416
|
return isPathIgnored(path);
|
|
5325
6417
|
}
|
|
@@ -5367,7 +6459,7 @@ async function collectWorktreeReqs(canonicalDir) {
|
|
|
5367
6459
|
const activeIds = new Set;
|
|
5368
6460
|
const supersedesTargets = new Set;
|
|
5369
6461
|
for (const rel of await findDomainJsonFiles(canonicalDir)) {
|
|
5370
|
-
const text = await Bun.file(
|
|
6462
|
+
const text = await Bun.file(join12(canonicalDir, rel)).text();
|
|
5371
6463
|
for (const r of parseReqs(text)) {
|
|
5372
6464
|
ids.add(r.id);
|
|
5373
6465
|
if (r.status.toLowerCase() === "active")
|
|
@@ -5395,7 +6487,7 @@ async function collectFacts(platformDir, ref, storage2) {
|
|
|
5395
6487
|
const deletedSpecFiles = changed.filter((e) => e.status === "D" && isSpecFile(e.path)).map((e) => e.path);
|
|
5396
6488
|
const base = collectBaseReqs(platformDir, ref, specPaths2);
|
|
5397
6489
|
const baseTags = collectBaseTags(platformDir, ref, codePaths);
|
|
5398
|
-
const worktree = await collectWorktreeReqs(
|
|
6490
|
+
const worktree = await collectWorktreeReqs(join12(platformDir, CANONICAL_SPECS_DIR));
|
|
5399
6491
|
const counts = tallyWorktreeTags(storage2);
|
|
5400
6492
|
return {
|
|
5401
6493
|
baseReqs: base.reqs,
|
|
@@ -5512,14 +6604,63 @@ var init_guard = __esm(() => {
|
|
|
5512
6604
|
init_collect();
|
|
5513
6605
|
});
|
|
5514
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
|
+
|
|
5515
6656
|
// src/operations/check.ts
|
|
5516
|
-
import { existsSync as
|
|
5517
|
-
import { join as
|
|
6657
|
+
import { existsSync as existsSync12, readFileSync as readFileSync4 } from "fs";
|
|
6658
|
+
import { join as join13 } from "path";
|
|
5518
6659
|
function readCodeownersText(platformDir) {
|
|
5519
|
-
const path =
|
|
5520
|
-
if (!
|
|
6660
|
+
const path = join13(platformDir, ".github", "CODEOWNERS");
|
|
6661
|
+
if (!existsSync12(path))
|
|
5521
6662
|
return null;
|
|
5522
|
-
return
|
|
6663
|
+
return readFileSync4(path, "utf-8");
|
|
5523
6664
|
}
|
|
5524
6665
|
function domainReqsFromText(text, platformRel) {
|
|
5525
6666
|
const validated = parseDomainText(text, platformRel);
|
|
@@ -5532,7 +6673,7 @@ async function collectChangeReqs(canonicalDir) {
|
|
|
5532
6673
|
const relPathById = new Map;
|
|
5533
6674
|
for (const rel of await findDomainJsonFiles(canonicalDir)) {
|
|
5534
6675
|
const platformRel = `${CANONICAL_SPECS_DIR}/${rel}`;
|
|
5535
|
-
const text = await Bun.file(
|
|
6676
|
+
const text = await Bun.file(join13(canonicalDir, rel)).text();
|
|
5536
6677
|
const parsed = domainReqsFromText(text, platformRel);
|
|
5537
6678
|
for (const r of parsed.reqs) {
|
|
5538
6679
|
reqs.push(r);
|
|
@@ -5573,7 +6714,7 @@ async function ingestResults(storage2, resultsPath, diagnostics2) {
|
|
|
5573
6714
|
}
|
|
5574
6715
|
async function governanceGate(input, ref, diagnostics2, results) {
|
|
5575
6716
|
const { platformDir } = input;
|
|
5576
|
-
const change = await collectChangeReqs(
|
|
6717
|
+
const change = await collectChangeReqs(join13(platformDir, CANONICAL_SPECS_DIR));
|
|
5577
6718
|
const base = collectBaseReqs2(platformDir, ref, diagnostics2);
|
|
5578
6719
|
diagnostics2.push(...requirementRemoved(base.reqs, change.reqs, (id) => base.relPathById.get(id) ?? null));
|
|
5579
6720
|
const codeowners = parseCodeowners(readCodeownersText(platformDir) ?? "");
|
|
@@ -5616,9 +6757,9 @@ async function check(input, storage2) {
|
|
|
5616
6757
|
diagnostics2.push(...unsourcedChanges(superseded, storage2.listProvenance()));
|
|
5617
6758
|
}
|
|
5618
6759
|
diagnostics2.push(...await statementGrammarDiagnostics(platformDir));
|
|
5619
|
-
diagnostics2.push(...await
|
|
5620
|
-
diagnostics2.push(...await inFileTagDiagnostics(platformDir, storage2.listTags()));
|
|
6760
|
+
diagnostics2.push(...await statementRulesDiagnostics(platformDir, checkConfig2.config.statementRules));
|
|
5621
6761
|
diagnostics2.push(...await supersedesPointerDiagnostics(platformDir));
|
|
6762
|
+
diagnostics2.push(...await migrationPendingDiagnostics(platformDir));
|
|
5622
6763
|
const glossaryDrift = glossaryDriftDiagnostic(platformDir);
|
|
5623
6764
|
if (glossaryDrift !== null)
|
|
5624
6765
|
diagnostics2.push(glossaryDrift);
|
|
@@ -5627,7 +6768,7 @@ async function check(input, storage2) {
|
|
|
5627
6768
|
await governanceGate(input, input.base, diagnostics2, results);
|
|
5628
6769
|
diagnostics2.push(...await lossDiagnostics(storage2, platformDir, input.base));
|
|
5629
6770
|
} else if (gitRefResolves(platformDir, "HEAD")) {
|
|
5630
|
-
const change = await collectChangeReqs(
|
|
6771
|
+
const change = await collectChangeReqs(join13(platformDir, CANONICAL_SPECS_DIR));
|
|
5631
6772
|
const baseParseSink = [];
|
|
5632
6773
|
const base = collectBaseReqs2(platformDir, "HEAD", baseParseSink);
|
|
5633
6774
|
diagnostics2.push(...requirementRemoved(base.reqs, change.reqs, (id) => base.relPathById.get(id) ?? null));
|
|
@@ -5645,13 +6786,13 @@ async function check(input, storage2) {
|
|
|
5645
6786
|
var LOSS_SEVERITY;
|
|
5646
6787
|
var init_check = __esm(() => {
|
|
5647
6788
|
init_src();
|
|
5648
|
-
init_filerefs2();
|
|
5649
6789
|
init_grammar();
|
|
5650
6790
|
init_propagation_teeth();
|
|
5651
6791
|
init_proven();
|
|
5652
6792
|
init_removed();
|
|
5653
6793
|
init_severity();
|
|
5654
6794
|
init_sqlDiagnostics();
|
|
6795
|
+
init_statementRules2();
|
|
5655
6796
|
init_statusflip();
|
|
5656
6797
|
init_supersedes();
|
|
5657
6798
|
init_unsourced();
|
|
@@ -5662,6 +6803,7 @@ var init_check = __esm(() => {
|
|
|
5662
6803
|
init__result();
|
|
5663
6804
|
init_glossary();
|
|
5664
6805
|
init_guard();
|
|
6806
|
+
init_migrate();
|
|
5665
6807
|
LOSS_SEVERITY = {
|
|
5666
6808
|
IMPL_LOST: "error",
|
|
5667
6809
|
SPEC_FILE_DELETED: "error",
|
|
@@ -5672,7 +6814,8 @@ var init_check = __esm(() => {
|
|
|
5672
6814
|
// src/commands/check.ts
|
|
5673
6815
|
var exports_check = {};
|
|
5674
6816
|
__export(exports_check, {
|
|
5675
|
-
checkCommand: () => checkCommand
|
|
6817
|
+
checkCommand: () => checkCommand,
|
|
6818
|
+
CI_BASE_WARNING: () => CI_BASE_WARNING
|
|
5676
6819
|
});
|
|
5677
6820
|
import { resolve as resolve8 } from "path";
|
|
5678
6821
|
import { defineCommand as defineCommand3 } from "citty";
|
|
@@ -5695,7 +6838,7 @@ function exitAfterCrash(e) {
|
|
|
5695
6838
|
console.error(`spec check: crashed: ${msg}`);
|
|
5696
6839
|
process.exit(EXIT.USAGE);
|
|
5697
6840
|
}
|
|
5698
|
-
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;
|
|
5699
6842
|
var init_check2 = __esm(() => {
|
|
5700
6843
|
init_src();
|
|
5701
6844
|
init_proven();
|
|
@@ -5785,6 +6928,9 @@ var init_check2 = __esm(() => {
|
|
|
5785
6928
|
process.exit(EXIT.USAGE);
|
|
5786
6929
|
return;
|
|
5787
6930
|
}
|
|
6931
|
+
if (args.ci && !args.base && gitTreeMatchesHead(platformDir)) {
|
|
6932
|
+
console.error(CI_BASE_WARNING);
|
|
6933
|
+
}
|
|
5788
6934
|
const diagnostics2 = [...outcome.diagnostics];
|
|
5789
6935
|
if (outcome.proofsUnconfirmed) {
|
|
5790
6936
|
if (jsonMode) {
|
|
@@ -5802,8 +6948,8 @@ var init_check2 = __esm(() => {
|
|
|
5802
6948
|
});
|
|
5803
6949
|
|
|
5804
6950
|
// src/operations/domain.ts
|
|
5805
|
-
import { existsSync as
|
|
5806
|
-
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";
|
|
5807
6953
|
function definedFields(options) {
|
|
5808
6954
|
const out = {};
|
|
5809
6955
|
if (options.scope !== undefined)
|
|
@@ -5823,12 +6969,12 @@ async function newDomain(platformDir, key, options = {}) {
|
|
|
5823
6969
|
if (!(resolvedDest === resolvedRoot || resolvedDest.startsWith(`${resolvedRoot}/`))) {
|
|
5824
6970
|
return fail("usage", `refusing to write outside platformDir (${resolvedDest})`);
|
|
5825
6971
|
}
|
|
5826
|
-
if (
|
|
6972
|
+
if (existsSync13(dest))
|
|
5827
6973
|
return fail("conflict", `refusing to overwrite ${dest}`);
|
|
5828
|
-
if (
|
|
6974
|
+
if (existsSync13(join14(dirname8(dest), "SPEC.md"))) {
|
|
5829
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)");
|
|
5830
6976
|
}
|
|
5831
|
-
|
|
6977
|
+
mkdirSync5(dirname8(dest), { recursive: true });
|
|
5832
6978
|
const envelope = { ...scaffoldDomainObject(key, localToday()), ...definedFields(options) };
|
|
5833
6979
|
const res = await validateAndWrite(dest, envelope, relFile);
|
|
5834
6980
|
if (!res.ok) {
|
|
@@ -6046,12 +7192,12 @@ function sortMatrix(rows) {
|
|
|
6046
7192
|
return a.repo.localeCompare(b.repo);
|
|
6047
7193
|
});
|
|
6048
7194
|
}
|
|
6049
|
-
function cellStatus(
|
|
6050
|
-
if (
|
|
7195
|
+
function cellStatus(row) {
|
|
7196
|
+
if (row.implemented === 1 && row.verified === 1)
|
|
6051
7197
|
return "src+test";
|
|
6052
|
-
if (
|
|
7198
|
+
if (row.implemented === 1)
|
|
6053
7199
|
return "src";
|
|
6054
|
-
if (
|
|
7200
|
+
if (row.verified === 1)
|
|
6055
7201
|
return "test";
|
|
6056
7202
|
return EMPTY_CELL;
|
|
6057
7203
|
}
|
|
@@ -6064,18 +7210,18 @@ function renderMatrix(rows, mode) {
|
|
|
6064
7210
|
return "";
|
|
6065
7211
|
const repos = Array.from(new Set(sorted.map((r) => r.repo))).sort((a, b) => a.localeCompare(b));
|
|
6066
7212
|
const byReq = new Map;
|
|
6067
|
-
for (const
|
|
6068
|
-
let entry = byReq.get(
|
|
7213
|
+
for (const row of sorted) {
|
|
7214
|
+
let entry = byReq.get(row.req_id);
|
|
6069
7215
|
if (!entry) {
|
|
6070
7216
|
entry = {
|
|
6071
|
-
domain_key:
|
|
6072
|
-
req_id:
|
|
6073
|
-
req_status:
|
|
7217
|
+
domain_key: row.domain_key,
|
|
7218
|
+
req_id: row.req_id,
|
|
7219
|
+
req_status: row.req_status,
|
|
6074
7220
|
cells: new Map
|
|
6075
7221
|
};
|
|
6076
|
-
byReq.set(
|
|
7222
|
+
byReq.set(row.req_id, entry);
|
|
6077
7223
|
}
|
|
6078
|
-
entry.cells.set(
|
|
7224
|
+
entry.cells.set(row.repo, cellStatus(row));
|
|
6079
7225
|
}
|
|
6080
7226
|
const header = ["DOMAIN", "REQUIREMENT", "STATUS", ...repos];
|
|
6081
7227
|
const lines = [header];
|
|
@@ -6083,8 +7229,8 @@ function renderMatrix(rows, mode) {
|
|
|
6083
7229
|
const cells = repos.map((r) => entry.cells.get(r) ?? EMPTY_CELL);
|
|
6084
7230
|
lines.push([entry.domain_key, entry.req_id, entry.req_status, ...cells]);
|
|
6085
7231
|
}
|
|
6086
|
-
const widths = header.map((_, col) => Math.max(...lines.map((
|
|
6087
|
-
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(`
|
|
6088
7234
|
`);
|
|
6089
7235
|
}
|
|
6090
7236
|
var EMPTY_CELL = "\u2014";
|
|
@@ -6220,12 +7366,19 @@ var init_map = __esm(() => {
|
|
|
6220
7366
|
|
|
6221
7367
|
// src/operations/records.ts
|
|
6222
7368
|
function getRecord(storage2, id) {
|
|
6223
|
-
const
|
|
6224
|
-
|
|
6225
|
-
ok: true,
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
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 };
|
|
6229
7382
|
}
|
|
6230
7383
|
function listRecords(storage2, filter = {}) {
|
|
6231
7384
|
const rows = storage2.listRequirements(filter);
|
|
@@ -6261,8 +7414,18 @@ function renderRecords(rows, mode) {
|
|
|
6261
7414
|
lines.push([r.id, r.status, r.key, String(r.seq), String(r.changed_at_version), r.text]);
|
|
6262
7415
|
}
|
|
6263
7416
|
const fixed = HEADER.length - 1;
|
|
6264
|
-
const widths = HEADER.slice(0, fixed).map((_, col) => Math.max(...lines.map((
|
|
6265
|
-
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(`
|
|
6266
7429
|
`);
|
|
6267
7430
|
}
|
|
6268
7431
|
var HEADER;
|
|
@@ -6289,7 +7452,7 @@ var init_get = __esm(() => {
|
|
|
6289
7452
|
getCommand = defineCommand6({
|
|
6290
7453
|
meta: {
|
|
6291
7454
|
name: "get",
|
|
6292
|
-
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."
|
|
6293
7456
|
},
|
|
6294
7457
|
args: {
|
|
6295
7458
|
reqId: {
|
|
@@ -6326,7 +7489,7 @@ var init_get = __esm(() => {
|
|
|
6326
7489
|
console.log("[]");
|
|
6327
7490
|
return;
|
|
6328
7491
|
}
|
|
6329
|
-
console.log(args.json ? JSON.stringify(result.row) :
|
|
7492
|
+
console.log(args.json ? JSON.stringify(result.row) : renderRecord(result.row));
|
|
6330
7493
|
});
|
|
6331
7494
|
}
|
|
6332
7495
|
});
|
|
@@ -6414,11 +7577,11 @@ function renderPropagation(rows, mode) {
|
|
|
6414
7577
|
return "";
|
|
6415
7578
|
const header = ["REPO", "STATE", "VIA", "DRIFT?"];
|
|
6416
7579
|
const lines = [header];
|
|
6417
|
-
for (const
|
|
6418
|
-
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"]);
|
|
6419
7582
|
}
|
|
6420
|
-
const widths = header.map((_, col) => Math.max(...lines.map((
|
|
6421
|
-
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(`
|
|
6422
7585
|
`);
|
|
6423
7586
|
}
|
|
6424
7587
|
var EMPTY_CELL2 = "\u2014";
|
|
@@ -6492,8 +7655,8 @@ function excerptOf(raw) {
|
|
|
6492
7655
|
}
|
|
6493
7656
|
function renderTable(header, rows) {
|
|
6494
7657
|
const lines = [header, ...rows];
|
|
6495
|
-
const widths = header.map((_, col) => Math.max(...lines.map((
|
|
6496
|
-
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(`
|
|
6497
7660
|
`);
|
|
6498
7661
|
}
|
|
6499
7662
|
function renderQuery(hits, mode) {
|
|
@@ -6748,17 +7911,17 @@ function compareReqIds2(a, b) {
|
|
|
6748
7911
|
function sortProvenance(rows) {
|
|
6749
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);
|
|
6750
7913
|
}
|
|
6751
|
-
function backingTests(
|
|
7914
|
+
function backingTests(row) {
|
|
6752
7915
|
let base;
|
|
6753
|
-
if (
|
|
7916
|
+
if (row.implemented === 1 && row.verified === 1)
|
|
6754
7917
|
base = "src+test";
|
|
6755
|
-
else if (
|
|
7918
|
+
else if (row.implemented === 1)
|
|
6756
7919
|
base = "src";
|
|
6757
|
-
else if (
|
|
7920
|
+
else if (row.verified === 1)
|
|
6758
7921
|
base = "test";
|
|
6759
7922
|
else
|
|
6760
7923
|
base = "\u2014";
|
|
6761
|
-
return
|
|
7924
|
+
return row.test_levels ? `${base} (${row.test_levels})` : base;
|
|
6762
7925
|
}
|
|
6763
7926
|
function renderProvenance(rows, mode) {
|
|
6764
7927
|
const sorted = sortProvenance(rows);
|
|
@@ -6768,12 +7931,12 @@ function renderProvenance(rows, mode) {
|
|
|
6768
7931
|
if (sorted.length === 0)
|
|
6769
7932
|
return "";
|
|
6770
7933
|
const byReq = new Map;
|
|
6771
|
-
for (const
|
|
6772
|
-
const bucket = byReq.get(
|
|
7934
|
+
for (const row of sorted) {
|
|
7935
|
+
const bucket = byReq.get(row.req_id);
|
|
6773
7936
|
if (bucket)
|
|
6774
|
-
bucket.push(
|
|
7937
|
+
bucket.push(row);
|
|
6775
7938
|
else
|
|
6776
|
-
byReq.set(
|
|
7939
|
+
byReq.set(row.req_id, [row]);
|
|
6777
7940
|
}
|
|
6778
7941
|
const lines = [];
|
|
6779
7942
|
for (const [reqId, links] of byReq) {
|
|
@@ -6788,8 +7951,8 @@ function renderProvenance(rows, mode) {
|
|
|
6788
7951
|
return lines.join(`
|
|
6789
7952
|
`);
|
|
6790
7953
|
}
|
|
6791
|
-
function decorateRow(
|
|
6792
|
-
const prefix = ` ${
|
|
7954
|
+
function decorateRow(row, resolved) {
|
|
7955
|
+
const prefix = ` ${row.role} ${row.issue_id} ${row.source_file}:${row.line}`;
|
|
6793
7956
|
if (resolved?.ok && resolved.title && resolved.url) {
|
|
6794
7957
|
const staleMark = resolved.stale ? " (cached \u2014 refresh failed)" : "";
|
|
6795
7958
|
return `${prefix} ${resolved.title} [${resolved.status ?? ""}] ${resolved.url}${staleMark}`;
|
|
@@ -6813,12 +7976,12 @@ function renderProvenanceDecorated(rows, resolved, mode) {
|
|
|
6813
7976
|
if (sorted.length === 0)
|
|
6814
7977
|
return "";
|
|
6815
7978
|
const byReq = new Map;
|
|
6816
|
-
for (const
|
|
6817
|
-
const bucket = byReq.get(
|
|
7979
|
+
for (const row of sorted) {
|
|
7980
|
+
const bucket = byReq.get(row.req_id);
|
|
6818
7981
|
if (bucket)
|
|
6819
|
-
bucket.push(
|
|
7982
|
+
bucket.push(row);
|
|
6820
7983
|
else
|
|
6821
|
-
byReq.set(
|
|
7984
|
+
byReq.set(row.req_id, [row]);
|
|
6822
7985
|
}
|
|
6823
7986
|
const lines = [];
|
|
6824
7987
|
for (const [reqId, links] of byReq) {
|
|
@@ -6839,8 +8002,8 @@ var TOKEN_HINT = "set SPEC_TRACKER_TOKEN to resolve issue titles";
|
|
|
6839
8002
|
var init_adapter = () => {};
|
|
6840
8003
|
|
|
6841
8004
|
// ../tracker/src/cache.ts
|
|
6842
|
-
import { mkdirSync as
|
|
6843
|
-
import { dirname as
|
|
8005
|
+
import { mkdirSync as mkdirSync6 } from "fs";
|
|
8006
|
+
import { dirname as dirname9, join as join15 } from "path";
|
|
6844
8007
|
function isFresh(entry, now, ttlMs = CACHE_TTL_MS) {
|
|
6845
8008
|
const t = Date.parse(entry.fetched_at);
|
|
6846
8009
|
if (Number.isNaN(t))
|
|
@@ -6854,7 +8017,7 @@ function isValidEntry(e) {
|
|
|
6854
8017
|
return typeof r.title === "string" && typeof r.status === "string" && typeof r.url === "string" && typeof r.fetched_at === "string";
|
|
6855
8018
|
}
|
|
6856
8019
|
function sidecarPath(platformDir) {
|
|
6857
|
-
return
|
|
8020
|
+
return join15(platformDir, ".spec-engine", SIDECAR_FILE);
|
|
6858
8021
|
}
|
|
6859
8022
|
async function readCache(platformDir) {
|
|
6860
8023
|
try {
|
|
@@ -6878,7 +8041,7 @@ async function readCache(platformDir) {
|
|
|
6878
8041
|
async function writeCache(platformDir, cache) {
|
|
6879
8042
|
try {
|
|
6880
8043
|
const path = sidecarPath(platformDir);
|
|
6881
|
-
|
|
8044
|
+
mkdirSync6(dirname9(path), { recursive: true });
|
|
6882
8045
|
await Bun.write(path, `${JSON.stringify(cache, null, 2)}
|
|
6883
8046
|
`);
|
|
6884
8047
|
return true;
|
|
@@ -7184,8 +8347,6 @@ async function mintFromFlags(platformDir, key, textFlag, args, rawArgs) {
|
|
|
7184
8347
|
await mintAndReport(platformDir, key, {
|
|
7185
8348
|
statement,
|
|
7186
8349
|
why: (args.why ?? "").trim(),
|
|
7187
|
-
livesIn: listFlag(rawArgs, "lives", args.lives) ?? [],
|
|
7188
|
-
verifiedIn: listFlag(rawArgs, "tests", args.tests) ?? [],
|
|
7189
8350
|
relates: listFlag(rawArgs, "relates", args.relates) ?? [],
|
|
7190
8351
|
issue: (args.issue ?? "").trim() || undefined,
|
|
7191
8352
|
status: statusOf(args),
|
|
@@ -7205,11 +8366,9 @@ async function mintInteractively(platformDir, key, id, status) {
|
|
|
7205
8366
|
return;
|
|
7206
8367
|
}
|
|
7207
8368
|
const why = (await askLine("Why it matters: ")).trim();
|
|
7208
|
-
const livesIn = listFlag([], "lives", await askLine("Lives in: ")) ?? [];
|
|
7209
8369
|
await mintAndReport(platformDir, key, {
|
|
7210
8370
|
statement: requirement,
|
|
7211
8371
|
why,
|
|
7212
|
-
livesIn,
|
|
7213
8372
|
issue: undefined,
|
|
7214
8373
|
status,
|
|
7215
8374
|
json: false
|
|
@@ -7264,8 +8423,6 @@ var init_req = __esm(() => {
|
|
|
7264
8423
|
description: "Non-interactive authoring: the Requirement field. When set, the entry appends with zero prompts."
|
|
7265
8424
|
},
|
|
7266
8425
|
why: { type: "string", description: "Why it matters field (only with --text)" },
|
|
7267
|
-
lives: livesArg,
|
|
7268
|
-
tests: testsArg,
|
|
7269
8426
|
relates: relatesArg,
|
|
7270
8427
|
issue: {
|
|
7271
8428
|
type: "string",
|
|
@@ -7299,9 +8456,9 @@ var init_req = __esm(() => {
|
|
|
7299
8456
|
await mintFromFlags(platformDir, key, textFlag, args, rawArgs);
|
|
7300
8457
|
return;
|
|
7301
8458
|
}
|
|
7302
|
-
const hasFieldFlags = typeof args.why === "string" || typeof args.
|
|
8459
|
+
const hasFieldFlags = typeof args.why === "string" || typeof args.relates === "string" || typeof args.issue === "string";
|
|
7303
8460
|
if (hasFieldFlags) {
|
|
7304
|
-
console.error("spec req: --why/--
|
|
8461
|
+
console.error("spec req: --why/--relates/--issue require --text (the Requirement field is mandatory)");
|
|
7305
8462
|
process.exit(EXIT.USAGE);
|
|
7306
8463
|
return;
|
|
7307
8464
|
}
|
|
@@ -7389,8 +8546,8 @@ async function listTermsCommand(platformDirRaw, json) {
|
|
|
7389
8546
|
console.log(JSON.stringify(result.rows));
|
|
7390
8547
|
return;
|
|
7391
8548
|
}
|
|
7392
|
-
for (const
|
|
7393
|
-
console.log(`${
|
|
8549
|
+
for (const row of result.rows) {
|
|
8550
|
+
console.log(`${row.id} ${row.term} ${row.status}`);
|
|
7394
8551
|
}
|
|
7395
8552
|
}
|
|
7396
8553
|
async function reviseTermCommand(opts) {
|
|
@@ -7621,7 +8778,7 @@ function resolvePlatform2(platformDirRaw) {
|
|
|
7621
8778
|
}
|
|
7622
8779
|
return platformDir;
|
|
7623
8780
|
}
|
|
7624
|
-
async function
|
|
8781
|
+
async function migrate2(platformDir, json) {
|
|
7625
8782
|
const result = await migrateGlossary(platformDir);
|
|
7626
8783
|
if (!result.ok)
|
|
7627
8784
|
exitOnFailure("spec glossary", result);
|
|
@@ -7688,7 +8845,7 @@ var init_glossary2 = __esm(() => {
|
|
|
7688
8845
|
const platformDir = resolvePlatform2(args.platformDir);
|
|
7689
8846
|
const json = Boolean(args.json);
|
|
7690
8847
|
if (args.migrate)
|
|
7691
|
-
await
|
|
8848
|
+
await migrate2(platformDir, json);
|
|
7692
8849
|
else if (args.check)
|
|
7693
8850
|
await check2(platformDir, json);
|
|
7694
8851
|
else
|
|
@@ -7697,6 +8854,60 @@ var init_glossary2 = __esm(() => {
|
|
|
7697
8854
|
});
|
|
7698
8855
|
});
|
|
7699
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
|
+
|
|
7700
8911
|
// src/operations/supersede.ts
|
|
7701
8912
|
async function supersedeTarget(platformDir, id) {
|
|
7702
8913
|
const located = await locateEntry(platformDir, id);
|
|
@@ -7715,8 +8926,6 @@ async function supersedeTarget(platformDir, id) {
|
|
|
7715
8926
|
function successorFields(input, req) {
|
|
7716
8927
|
return {
|
|
7717
8928
|
why: (input.why ?? req.why ?? "").trim(),
|
|
7718
|
-
livesIn: input.livesIn ?? [...req.livesIn],
|
|
7719
|
-
verifiedIn: input.verifiedIn ?? [...req.verifiedIn],
|
|
7720
8929
|
relates: input.relates === undefined ? [...req.relates] : splitList(input.relates)
|
|
7721
8930
|
};
|
|
7722
8931
|
}
|
|
@@ -7761,15 +8970,17 @@ async function supersede(input, freshTags) {
|
|
|
7761
8970
|
}
|
|
7762
8971
|
warnings.push({ kind: "grammar", text: grammarWarningText("", verdict).slice(2) });
|
|
7763
8972
|
}
|
|
7764
|
-
const
|
|
7765
|
-
|
|
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]));
|
|
7766
8979
|
const newId = await nextRequirementId(platformDir, key);
|
|
7767
8980
|
const issue = input.issue?.trim() || undefined;
|
|
7768
8981
|
const successor = successorEntry(newId, req, {
|
|
7769
8982
|
statement: input.statement,
|
|
7770
8983
|
why,
|
|
7771
|
-
livesIn,
|
|
7772
|
-
verifiedIn,
|
|
7773
8984
|
relates,
|
|
7774
8985
|
issue
|
|
7775
8986
|
});
|
|
@@ -7786,18 +8997,19 @@ async function supersede(input, freshTags) {
|
|
|
7786
8997
|
warnings
|
|
7787
8998
|
});
|
|
7788
8999
|
}
|
|
7789
|
-
const
|
|
7790
|
-
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 };
|
|
7791
9002
|
}
|
|
7792
9003
|
var init_supersede = __esm(() => {
|
|
7793
9004
|
init_src();
|
|
7794
9005
|
init_domains();
|
|
7795
9006
|
init_grammar2();
|
|
9007
|
+
init_statementRules3();
|
|
7796
9008
|
init_domainJson();
|
|
7797
9009
|
init__envelope();
|
|
7798
9010
|
init__result();
|
|
9011
|
+
init__retag();
|
|
7799
9012
|
init_mint();
|
|
7800
|
-
init_reads2();
|
|
7801
9013
|
});
|
|
7802
9014
|
|
|
7803
9015
|
// src/resolve/format.ts
|
|
@@ -7813,14 +9025,14 @@ function renderResolve(rows, mode) {
|
|
|
7813
9025
|
return "";
|
|
7814
9026
|
const header = ["REQ_ID", "STATUS", "TEXT"];
|
|
7815
9027
|
const lines = [header];
|
|
7816
|
-
for (const
|
|
7817
|
-
const raw =
|
|
9028
|
+
for (const row of sorted) {
|
|
9029
|
+
const raw = row.text ?? "";
|
|
7818
9030
|
const truncated = raw.length > TEXT_MAX ? raw.slice(0, TEXT_MAX) + ELLIPSIS2 : raw;
|
|
7819
9031
|
const text = truncated.replace(/\s+/g, " ").trim();
|
|
7820
|
-
lines.push([
|
|
9032
|
+
lines.push([row.id, row.status, text || EMPTY_CELL4]);
|
|
7821
9033
|
}
|
|
7822
|
-
const widths = header.map((_, col) => Math.max(...lines.map((
|
|
7823
|
-
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(`
|
|
7824
9036
|
`);
|
|
7825
9037
|
}
|
|
7826
9038
|
function sortReqTags(rows) {
|
|
@@ -7835,8 +9047,8 @@ function renderReqTags(rows, mode) {
|
|
|
7835
9047
|
return "";
|
|
7836
9048
|
const header = ["REPO", "FILE", "LINE", "KIND", "LEVEL"];
|
|
7837
9049
|
const lines = [header];
|
|
7838
|
-
for (const
|
|
7839
|
-
lines.push([
|
|
9050
|
+
for (const row of sorted) {
|
|
9051
|
+
lines.push([row.repo, row.file, String(row.line), row.kind, row.level ?? EMPTY_CELL4]);
|
|
7840
9052
|
}
|
|
7841
9053
|
const widths = header.map((_, col) => Math.max(...lines.map((l) => (l[col] ?? "").length)));
|
|
7842
9054
|
return lines.map((l) => l.map((cell, col) => cell.padEnd(widths[col])).join(" ").trimEnd()).join(`
|
|
@@ -7880,12 +9092,6 @@ function inputFromArgs(args, rawArgs, platformDir, id, statement) {
|
|
|
7880
9092
|
const input = { platformDir, id, statement, noBump: Boolean(args.noBump) };
|
|
7881
9093
|
if (typeof args.why === "string")
|
|
7882
9094
|
input.why = args.why;
|
|
7883
|
-
const livesIn = listFlag(rawArgs, "lives", args.lives);
|
|
7884
|
-
if (livesIn !== undefined)
|
|
7885
|
-
input.livesIn = livesIn;
|
|
7886
|
-
const verifiedIn = listFlag(rawArgs, "tests", args.tests);
|
|
7887
|
-
if (verifiedIn !== undefined)
|
|
7888
|
-
input.verifiedIn = verifiedIn;
|
|
7889
9095
|
const relates = listFlag(rawArgs, "relates", args.relates);
|
|
7890
9096
|
if (relates !== undefined)
|
|
7891
9097
|
input.relates = relates;
|
|
@@ -7922,14 +9128,6 @@ var init_supersede2 = __esm(() => {
|
|
|
7922
9128
|
type: "string",
|
|
7923
9129
|
description: "Successor's Why it matters (default: copied from the old entry)"
|
|
7924
9130
|
},
|
|
7925
|
-
lives: {
|
|
7926
|
-
type: "string",
|
|
7927
|
-
description: "Successor's Lives in: repeat or comma-separate; empty clears (default: copied from the old entry)"
|
|
7928
|
-
},
|
|
7929
|
-
tests: {
|
|
7930
|
-
type: "string",
|
|
7931
|
-
description: "Successor's Verified in (verifiedIn): repeat or comma-separate; empty clears (default: copied from the old entry)"
|
|
7932
|
-
},
|
|
7933
9131
|
relates: {
|
|
7934
9132
|
type: "string",
|
|
7935
9133
|
description: "Successor's related ids: repeat or comma-separate; empty clears (default: copied from the old entry)"
|
|
@@ -7952,13 +9150,13 @@ var init_supersede2 = __esm(() => {
|
|
|
7952
9150
|
},
|
|
7953
9151
|
json: {
|
|
7954
9152
|
type: "boolean",
|
|
7955
|
-
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"
|
|
7956
9154
|
}
|
|
7957
9155
|
};
|
|
7958
9156
|
supersedeCommand = defineCommand15({
|
|
7959
9157
|
meta: {
|
|
7960
9158
|
name: "supersede",
|
|
7961
|
-
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."
|
|
7962
9160
|
},
|
|
7963
9161
|
args: supersedeArgs,
|
|
7964
9162
|
async run({ args, rawArgs }) {
|
|
@@ -7978,21 +9176,18 @@ var init_supersede2 = __esm(() => {
|
|
|
7978
9176
|
if (!result.ok)
|
|
7979
9177
|
exitOnFailure("spec supersede", result);
|
|
7980
9178
|
printWarnings("spec supersede", result.warnings);
|
|
7981
|
-
const { newId, file, specVersion,
|
|
9179
|
+
const { newId, file, specVersion, retagged } = result;
|
|
7982
9180
|
if (args.json) {
|
|
7983
|
-
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 }));
|
|
7984
9182
|
} else {
|
|
7985
9183
|
console.log(`superseded ${id} \u2192 ${newId} in ${file}`);
|
|
7986
9184
|
if (specVersion !== null)
|
|
7987
9185
|
console.log(`specVersion bumped to ${specVersion}`);
|
|
7988
|
-
if (
|
|
7989
|
-
console.log(`
|
|
7990
|
-
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"));
|
|
7991
9189
|
}
|
|
7992
9190
|
}
|
|
7993
|
-
if (retag.length > 0) {
|
|
7994
|
-
console.error(`note: spec check will report SUPERSEDED_REFERENCED at each remaining ${id} tag until the sites are retagged to ${newId}`);
|
|
7995
|
-
}
|
|
7996
9191
|
}
|
|
7997
9192
|
});
|
|
7998
9193
|
});
|
|
@@ -8021,14 +9216,14 @@ async function deprecate(input, freshTags) {
|
|
|
8021
9216
|
diagnostics: res.diagnostics
|
|
8022
9217
|
});
|
|
8023
9218
|
}
|
|
8024
|
-
const
|
|
8025
|
-
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 };
|
|
8026
9221
|
}
|
|
8027
9222
|
var init_deprecate = __esm(() => {
|
|
8028
9223
|
init_src();
|
|
8029
9224
|
init__envelope();
|
|
8030
9225
|
init__result();
|
|
8031
|
-
|
|
9226
|
+
init__retag();
|
|
8032
9227
|
});
|
|
8033
9228
|
|
|
8034
9229
|
// src/commands/deprecate.ts
|
|
@@ -8063,7 +9258,7 @@ var init_deprecate2 = __esm(() => {
|
|
|
8063
9258
|
},
|
|
8064
9259
|
json: {
|
|
8065
9260
|
type: "boolean",
|
|
8066
|
-
description: "Print { id, file, reason,
|
|
9261
|
+
description: "Print { id, file, reason, removed } instead of text"
|
|
8067
9262
|
}
|
|
8068
9263
|
},
|
|
8069
9264
|
async run({ args }) {
|
|
@@ -8086,15 +9281,15 @@ var init_deprecate2 = __esm(() => {
|
|
|
8086
9281
|
const result = await deprecate({ platformDir, id, reason }, cliFreshTags(platformDir));
|
|
8087
9282
|
if (!result.ok)
|
|
8088
9283
|
exitOnFailure("spec deprecate", result);
|
|
8089
|
-
const { file,
|
|
9284
|
+
const { file, removed } = result;
|
|
8090
9285
|
if (args.json) {
|
|
8091
|
-
console.log(JSON.stringify({ id, file, reason,
|
|
9286
|
+
console.log(JSON.stringify({ id, file, reason, removed }));
|
|
8092
9287
|
return;
|
|
8093
9288
|
}
|
|
8094
9289
|
console.log(`deprecated ${id} in ${file} \u2014 reason recorded`);
|
|
8095
|
-
if (
|
|
8096
|
-
console.
|
|
8097
|
-
console.
|
|
9290
|
+
if (removed.length > 0) {
|
|
9291
|
+
console.log(`removed ${removed.length} tag(s) of ${id}:`);
|
|
9292
|
+
console.log(renderReqTags(removed, "text"));
|
|
8098
9293
|
}
|
|
8099
9294
|
}
|
|
8100
9295
|
});
|
|
@@ -8122,8 +9317,6 @@ function successorFields2(input, req) {
|
|
|
8122
9317
|
return {
|
|
8123
9318
|
statement: (input.statement ?? req.statement).trim(),
|
|
8124
9319
|
why: (input.why ?? req.why ?? "").trim(),
|
|
8125
|
-
livesIn: input.livesIn ?? [...req.livesIn],
|
|
8126
|
-
verifiedIn: input.verifiedIn ?? [...req.verifiedIn],
|
|
8127
9320
|
relates: [...req.relates]
|
|
8128
9321
|
};
|
|
8129
9322
|
}
|
|
@@ -8167,13 +9360,12 @@ async function move(input, freshTags) {
|
|
|
8167
9360
|
}
|
|
8168
9361
|
warnings.push({ kind: "grammar", text: grammarWarningText("", verdict).slice(2) });
|
|
8169
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);
|
|
8170
9367
|
}
|
|
8171
|
-
warnings.push(...unresolvableRefWarnings(platformDir, [
|
|
8172
|
-
fields.statement,
|
|
8173
|
-
fields.why,
|
|
8174
|
-
...fields.livesIn,
|
|
8175
|
-
...fields.verifiedIn
|
|
8176
|
-
]));
|
|
9368
|
+
warnings.push(...unresolvableRefWarnings(platformDir, [fields.statement, fields.why]));
|
|
8177
9369
|
const newId = await nextRequirementId(platformDir, targetKey);
|
|
8178
9370
|
const sourceCurrent = source.domain.specVersion ?? 1;
|
|
8179
9371
|
req.status = "superseded";
|
|
@@ -8193,7 +9385,7 @@ async function move(input, freshTags) {
|
|
|
8193
9385
|
}
|
|
8194
9386
|
await validateAndWrite(target.specPath, target.domain, target.relFile);
|
|
8195
9387
|
await validateAndWrite(source.specPath, source.domain, source.relFile);
|
|
8196
|
-
const
|
|
9388
|
+
const retagged = await applyRetag(platformDir, id, newId, freshTags);
|
|
8197
9389
|
return {
|
|
8198
9390
|
ok: true,
|
|
8199
9391
|
oldId: id,
|
|
@@ -8202,7 +9394,7 @@ async function move(input, freshTags) {
|
|
|
8202
9394
|
toFile: target.relFile,
|
|
8203
9395
|
sourceSpecVersion,
|
|
8204
9396
|
targetSpecVersion,
|
|
8205
|
-
|
|
9397
|
+
retagged,
|
|
8206
9398
|
warnings
|
|
8207
9399
|
};
|
|
8208
9400
|
}
|
|
@@ -8210,11 +9402,12 @@ var init_move = __esm(() => {
|
|
|
8210
9402
|
init_src();
|
|
8211
9403
|
init_domains();
|
|
8212
9404
|
init_grammar2();
|
|
9405
|
+
init_statementRules3();
|
|
8213
9406
|
init_domainJson();
|
|
8214
9407
|
init__envelope();
|
|
8215
9408
|
init__result();
|
|
9409
|
+
init__retag();
|
|
8216
9410
|
init_mint();
|
|
8217
|
-
init_reads2();
|
|
8218
9411
|
});
|
|
8219
9412
|
|
|
8220
9413
|
// src/commands/move.ts
|
|
@@ -8223,18 +9416,12 @@ __export(exports_move, {
|
|
|
8223
9416
|
moveCommand: () => moveCommand
|
|
8224
9417
|
});
|
|
8225
9418
|
import { defineCommand as defineCommand17 } from "citty";
|
|
8226
|
-
function inputFromArgs2(args,
|
|
9419
|
+
function inputFromArgs2(args, platformDir, id, targetKey) {
|
|
8227
9420
|
const input = { platformDir, id, targetKey, noBump: Boolean(args.noBump) };
|
|
8228
9421
|
if (typeof args.text === "string")
|
|
8229
9422
|
input.statement = args.text;
|
|
8230
9423
|
if (typeof args.why === "string")
|
|
8231
9424
|
input.why = args.why;
|
|
8232
|
-
const livesIn = listFlag(rawArgs, "lives", args.lives);
|
|
8233
|
-
if (livesIn !== undefined)
|
|
8234
|
-
input.livesIn = livesIn;
|
|
8235
|
-
const verifiedIn = listFlag(rawArgs, "tests", args.tests);
|
|
8236
|
-
if (verifiedIn !== undefined)
|
|
8237
|
-
input.verifiedIn = verifiedIn;
|
|
8238
9425
|
return input;
|
|
8239
9426
|
}
|
|
8240
9427
|
var moveArgs, moveCommand;
|
|
@@ -8266,30 +9453,22 @@ var init_move2 = __esm(() => {
|
|
|
8266
9453
|
type: "string",
|
|
8267
9454
|
description: "Rewrite the successor's Why (default: copied from the source)"
|
|
8268
9455
|
},
|
|
8269
|
-
lives: {
|
|
8270
|
-
type: "string",
|
|
8271
|
-
description: "Rewrite the successor's Lives in: repeat or comma-separate; empty clears (default: copied from the source)"
|
|
8272
|
-
},
|
|
8273
|
-
tests: {
|
|
8274
|
-
type: "string",
|
|
8275
|
-
description: "Rewrite the successor's Verified in (verifiedIn): repeat or comma-separate; empty clears (default: copied from the source)"
|
|
8276
|
-
},
|
|
8277
9456
|
noBump: {
|
|
8278
9457
|
type: "boolean",
|
|
8279
9458
|
description: "Do not bump either envelope's specVersion"
|
|
8280
9459
|
},
|
|
8281
9460
|
json: {
|
|
8282
9461
|
type: "boolean",
|
|
8283
|
-
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"
|
|
8284
9463
|
}
|
|
8285
9464
|
};
|
|
8286
9465
|
moveCommand = defineCommand17({
|
|
8287
9466
|
meta: {
|
|
8288
9467
|
name: "move",
|
|
8289
|
-
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."
|
|
8290
9469
|
},
|
|
8291
9470
|
args: moveArgs,
|
|
8292
|
-
async run({ args
|
|
9471
|
+
async run({ args }) {
|
|
8293
9472
|
const id = args.id;
|
|
8294
9473
|
const rawTargetKey = args.newDomain;
|
|
8295
9474
|
const platformDir = resolvePlatformDir(args);
|
|
@@ -8307,11 +9486,11 @@ var init_move2 = __esm(() => {
|
|
|
8307
9486
|
} catch (e) {
|
|
8308
9487
|
handleNotAPlatform(e);
|
|
8309
9488
|
}
|
|
8310
|
-
const result = await move(inputFromArgs2(args,
|
|
9489
|
+
const result = await move(inputFromArgs2(args, platformDir, id, targetKey), cliFreshTags(platformDir));
|
|
8311
9490
|
if (!result.ok)
|
|
8312
9491
|
exitOnFailure("spec move", result);
|
|
8313
9492
|
printWarnings("spec move", result.warnings);
|
|
8314
|
-
const { newId, fromFile, toFile, sourceSpecVersion, targetSpecVersion,
|
|
9493
|
+
const { newId, fromFile, toFile, sourceSpecVersion, targetSpecVersion, retagged } = result;
|
|
8315
9494
|
const sourceKey = id.slice(0, id.indexOf("-"));
|
|
8316
9495
|
if (args.json) {
|
|
8317
9496
|
console.log(JSON.stringify({
|
|
@@ -8321,21 +9500,18 @@ var init_move2 = __esm(() => {
|
|
|
8321
9500
|
to_file: toFile,
|
|
8322
9501
|
source_spec_version: sourceSpecVersion,
|
|
8323
9502
|
target_spec_version: targetSpecVersion,
|
|
8324
|
-
|
|
9503
|
+
retagged
|
|
8325
9504
|
}));
|
|
8326
9505
|
} else {
|
|
8327
9506
|
console.log(`moved ${id} \u2192 ${newId} (${sourceKey} \u2192 ${targetKey})`);
|
|
8328
9507
|
if (sourceSpecVersion !== null || targetSpecVersion !== null) {
|
|
8329
9508
|
console.log(`specVersion: ${sourceKey}\u2192${sourceSpecVersion}, ${targetKey}\u2192${targetSpecVersion}`);
|
|
8330
9509
|
}
|
|
8331
|
-
if (
|
|
8332
|
-
console.log(`
|
|
8333
|
-
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"));
|
|
8334
9513
|
}
|
|
8335
9514
|
}
|
|
8336
|
-
if (retag.length > 0) {
|
|
8337
|
-
console.error(`note: spec check will report SUPERSEDED_REFERENCED at each remaining ${id} tag until the sites are retagged to ${newId}`);
|
|
8338
|
-
}
|
|
8339
9515
|
}
|
|
8340
9516
|
});
|
|
8341
9517
|
});
|
|
@@ -8364,7 +9540,7 @@ function gateRejection(platformDir, relFile, req, fields) {
|
|
|
8364
9540
|
const tag = releaseHolding(platformDir, relFile, id);
|
|
8365
9541
|
if (tag === null)
|
|
8366
9542
|
return null;
|
|
8367
|
-
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.");
|
|
8368
9544
|
}
|
|
8369
9545
|
function applyFields(req, fields) {
|
|
8370
9546
|
const fieldsChanged = [];
|
|
@@ -8379,16 +9555,6 @@ function applyFields(req, fields) {
|
|
|
8379
9555
|
fieldsChanged.push("why");
|
|
8380
9556
|
refValues.push(fields.why ?? "");
|
|
8381
9557
|
}
|
|
8382
|
-
if (fields.livesIn !== undefined) {
|
|
8383
|
-
req.livesIn = fields.livesIn;
|
|
8384
|
-
fieldsChanged.push("lives");
|
|
8385
|
-
refValues.push(...fields.livesIn);
|
|
8386
|
-
}
|
|
8387
|
-
if (fields.verifiedIn !== undefined) {
|
|
8388
|
-
req.verifiedIn = fields.verifiedIn;
|
|
8389
|
-
fieldsChanged.push("tests");
|
|
8390
|
-
refValues.push(...fields.verifiedIn);
|
|
8391
|
-
}
|
|
8392
9558
|
if (fields.relates !== undefined) {
|
|
8393
9559
|
req.relates = splitList(fields.relates);
|
|
8394
9560
|
fieldsChanged.push("relates");
|
|
@@ -8426,6 +9592,10 @@ async function amend(input) {
|
|
|
8426
9592
|
}
|
|
8427
9593
|
warnings.push({ kind: "grammar", text: grammarWarningText("", verdict).slice(2) });
|
|
8428
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);
|
|
8429
9599
|
}
|
|
8430
9600
|
const { fieldsChanged, refValues } = applyFields(req, fields);
|
|
8431
9601
|
warnings.push(...unresolvableRefWarnings(platformDir, refValues));
|
|
@@ -8444,6 +9614,7 @@ var PROMISE_FIELDS;
|
|
|
8444
9614
|
var init_amend = __esm(() => {
|
|
8445
9615
|
init_src();
|
|
8446
9616
|
init_grammar2();
|
|
9617
|
+
init_statementRules3();
|
|
8447
9618
|
init__envelope();
|
|
8448
9619
|
init__result();
|
|
8449
9620
|
init_mint();
|
|
@@ -8458,9 +9629,9 @@ __export(exports_amend, {
|
|
|
8458
9629
|
import { defineCommand as defineCommand18 } from "citty";
|
|
8459
9630
|
function assertAmendFlags(args) {
|
|
8460
9631
|
const hasIssue = args.issue !== undefined && args.issue.trim() !== "";
|
|
8461
|
-
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;
|
|
8462
9633
|
if (!hasField && !hasIssue) {
|
|
8463
|
-
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");
|
|
8464
9635
|
process.exit(EXIT.USAGE);
|
|
8465
9636
|
}
|
|
8466
9637
|
if (args.text !== undefined && args.text.trim() === "") {
|
|
@@ -8484,12 +9655,6 @@ function fieldsFromArgs(args, rawArgs) {
|
|
|
8484
9655
|
const v = args.why.trim();
|
|
8485
9656
|
fields.why = v === "" ? null : v;
|
|
8486
9657
|
}
|
|
8487
|
-
const livesIn = listFlag(rawArgs, "lives", args.lives);
|
|
8488
|
-
if (livesIn !== undefined)
|
|
8489
|
-
fields.livesIn = livesIn;
|
|
8490
|
-
const verifiedIn = listFlag(rawArgs, "tests", args.tests);
|
|
8491
|
-
if (verifiedIn !== undefined)
|
|
8492
|
-
fields.verifiedIn = verifiedIn;
|
|
8493
9658
|
const relates = listFlag(rawArgs, "relates", args.relates);
|
|
8494
9659
|
if (relates !== undefined)
|
|
8495
9660
|
fields.relates = relates;
|
|
@@ -8518,8 +9683,6 @@ var init_amend2 = __esm(() => {
|
|
|
8518
9683
|
platformDir: platformDirArg,
|
|
8519
9684
|
text: { type: "string", description: "New Requirement (statement) field value" },
|
|
8520
9685
|
why: { type: "string", description: "New Why it matters field value" },
|
|
8521
|
-
lives: livesArg,
|
|
8522
|
-
tests: testsArg,
|
|
8523
9686
|
relates: relatesArg,
|
|
8524
9687
|
issue: {
|
|
8525
9688
|
type: "string",
|
|
@@ -8535,7 +9698,7 @@ var init_amend2 = __esm(() => {
|
|
|
8535
9698
|
amendCommand = defineCommand18({
|
|
8536
9699
|
meta: {
|
|
8537
9700
|
name: "amend",
|
|
8538
|
-
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."
|
|
8539
9702
|
},
|
|
8540
9703
|
args: amendArgs,
|
|
8541
9704
|
async run({ args, rawArgs }) {
|
|
@@ -8575,6 +9738,9 @@ async function accept(input) {
|
|
|
8575
9738
|
if (req.status.toLowerCase() !== "draft") {
|
|
8576
9739
|
return fail("conflict", `${id} is ${displayStatus(req.status)} \u2014 only a Draft entry accepts`);
|
|
8577
9740
|
}
|
|
9741
|
+
const rules = statementRulesVerdict(platformDir, located.key, req.statement);
|
|
9742
|
+
if (rules.refusal !== null)
|
|
9743
|
+
return fail("usage", rules.refusal);
|
|
8578
9744
|
req.status = "active";
|
|
8579
9745
|
domain2.updated = localToday();
|
|
8580
9746
|
const res = await validateAndWrite(specPath, domain2, relFile);
|
|
@@ -8584,10 +9750,11 @@ async function accept(input) {
|
|
|
8584
9750
|
diagnostics: res.diagnostics
|
|
8585
9751
|
});
|
|
8586
9752
|
}
|
|
8587
|
-
return { ok: true, id, file: relFile };
|
|
9753
|
+
return { ok: true, id, file: relFile, warnings: rules.warnings };
|
|
8588
9754
|
}
|
|
8589
9755
|
var init_accept = __esm(() => {
|
|
8590
9756
|
init_src();
|
|
9757
|
+
init_statementRules3();
|
|
8591
9758
|
init__envelope();
|
|
8592
9759
|
init__result();
|
|
8593
9760
|
});
|
|
@@ -8636,6 +9803,7 @@ var init_accept2 = __esm(() => {
|
|
|
8636
9803
|
const result = await accept({ platformDir, id });
|
|
8637
9804
|
if (!result.ok)
|
|
8638
9805
|
exitOnFailure("spec accept", result);
|
|
9806
|
+
printWarnings("spec accept", result.warnings);
|
|
8639
9807
|
if (args.json) {
|
|
8640
9808
|
console.log(JSON.stringify({ id, file: result.file }));
|
|
8641
9809
|
return;
|
|
@@ -8645,6 +9813,158 @@ var init_accept2 = __esm(() => {
|
|
|
8645
9813
|
});
|
|
8646
9814
|
});
|
|
8647
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
|
+
|
|
8648
9968
|
// src/server/authorPrompt.ts
|
|
8649
9969
|
function renderAuthorPrompt(opts) {
|
|
8650
9970
|
const charterBlock = opts.charter ? `
|
|
@@ -8654,6 +9974,8 @@ Domain charter (scope): ${opts.charter}
|
|
|
8654
9974
|
Target domain: ${opts.domain} (no charter/scope recorded \u2014 check placement carefully)
|
|
8655
9975
|
` : "\nNo target domain given \u2014 determine placement from `spec domain list`.\n";
|
|
8656
9976
|
return [
|
|
9977
|
+
RULE_ZERO,
|
|
9978
|
+
"",
|
|
8657
9979
|
"You are authoring Spec Engine requirements from a brief. Follow this playbook exactly.",
|
|
8658
9980
|
charterBlock,
|
|
8659
9981
|
"Brief:",
|
|
@@ -8663,14 +9985,17 @@ Target domain: ${opts.domain} (no charter/scope recorded \u2014 check placement
|
|
|
8663
9985
|
"1. One requirement per behavior \u2014 never one per ticket; a brief fans out into several.",
|
|
8664
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.",
|
|
8665
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."',
|
|
8666
|
-
"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.",
|
|
8667
9989
|
"5. Tie each draft to its source: quote the exact phrase of the brief it derives from; label anything you inferred as inferred.",
|
|
8668
9990
|
"6. Present the full batch (statement, why, placement, source quote) for approval BEFORE writing.",
|
|
8669
|
-
'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"`.',
|
|
8670
9992
|
"8. The originating ticket is provenance only \u2014 never a requirement id, never a code tag."
|
|
8671
9993
|
].join(`
|
|
8672
9994
|
`);
|
|
8673
9995
|
}
|
|
9996
|
+
var init_authorPrompt = __esm(() => {
|
|
9997
|
+
init_src();
|
|
9998
|
+
});
|
|
8674
9999
|
|
|
8675
10000
|
// src/server/mcp.ts
|
|
8676
10001
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -8687,10 +10012,6 @@ function amendFieldsOf(args) {
|
|
|
8687
10012
|
fields.statement = args.text;
|
|
8688
10013
|
if (args.why !== undefined)
|
|
8689
10014
|
fields.why = args.why || null;
|
|
8690
|
-
if (args.lives !== undefined)
|
|
8691
|
-
fields.livesIn = args.lives;
|
|
8692
|
-
if (args.verified_in !== undefined)
|
|
8693
|
-
fields.verifiedIn = args.verified_in;
|
|
8694
10015
|
if (args.relates !== undefined)
|
|
8695
10016
|
fields.relates = args.relates;
|
|
8696
10017
|
if (args.issue)
|
|
@@ -8720,7 +10041,7 @@ function buildMcpServer(platformDir) {
|
|
|
8720
10041
|
}, async ({ files }) => fresh((s) => jsonResult(resolveFiles(s, files).rows)));
|
|
8721
10042
|
server.registerTool("spec_req_tags", {
|
|
8722
10043
|
title: "Requirement \u2192 tag sites (reverse query)",
|
|
8723
|
-
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.",
|
|
8724
10045
|
inputSchema: { req_id: z4.string().describe("Requirement id (KEY-NNN)") }
|
|
8725
10046
|
}, async ({ req_id }) => {
|
|
8726
10047
|
if (!ID_RE.test(req_id)) {
|
|
@@ -8761,15 +10082,15 @@ function buildMcpServer(platformDir) {
|
|
|
8761
10082
|
});
|
|
8762
10083
|
server.registerTool("spec_get", {
|
|
8763
10084
|
title: "One requirement's full record",
|
|
8764
|
-
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).",
|
|
8765
10086
|
inputSchema: { req_id: z4.string().describe("Requirement id (KEY-NNN)") }
|
|
8766
10087
|
}, async ({ req_id }) => {
|
|
8767
10088
|
if (!ID_RE.test(req_id)) {
|
|
8768
10089
|
return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
|
|
8769
10090
|
}
|
|
8770
10091
|
return fresh((s) => {
|
|
8771
|
-
const { row
|
|
8772
|
-
return jsonResult(
|
|
10092
|
+
const { row } = getRecord(s, req_id);
|
|
10093
|
+
return jsonResult(row ?? []);
|
|
8773
10094
|
});
|
|
8774
10095
|
});
|
|
8775
10096
|
server.registerTool("spec_list", {
|
|
@@ -8794,17 +10115,15 @@ function buildMcpServer(platformDir) {
|
|
|
8794
10115
|
});
|
|
8795
10116
|
server.registerTool("spec_supersede", {
|
|
8796
10117
|
title: "Supersede a shipped requirement",
|
|
8797
|
-
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`.",
|
|
8798
10119
|
inputSchema: {
|
|
8799
10120
|
req_id: z4.string().describe("The Active requirement id to supersede (KEY-NNN)"),
|
|
8800
10121
|
statement: z4.string().min(1).describe("The successor's statement"),
|
|
8801
10122
|
why: z4.string().optional().describe("Successor's why (default: copied from the predecessor)"),
|
|
8802
|
-
lives_in: z4.array(z4.string()).optional().describe("Successor's livesIn paths (default: copied from the predecessor)"),
|
|
8803
|
-
verified_in: z4.array(z4.string()).optional().describe("Successor's verifiedIn paths (default: copied from the predecessor)"),
|
|
8804
10123
|
relates: z4.array(z4.string()).optional().describe("Successor's related ids, each comma-split; an empty list clears (default: copied from the predecessor)"),
|
|
8805
10124
|
issue: z4.string().optional().describe("Ticket recorded as supersedes-via on the predecessor and created on the successor")
|
|
8806
10125
|
}
|
|
8807
|
-
}, async ({ req_id, statement, why,
|
|
10126
|
+
}, async ({ req_id, statement, why, relates, issue }) => {
|
|
8808
10127
|
if (!ID_RE.test(req_id)) {
|
|
8809
10128
|
return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
|
|
8810
10129
|
}
|
|
@@ -8813,8 +10132,6 @@ function buildMcpServer(platformDir) {
|
|
|
8813
10132
|
id: req_id,
|
|
8814
10133
|
statement,
|
|
8815
10134
|
why,
|
|
8816
|
-
livesIn: lives_in,
|
|
8817
|
-
verifiedIn: verified_in,
|
|
8818
10135
|
relates,
|
|
8819
10136
|
issue
|
|
8820
10137
|
}, coldFreshTags(platformDir));
|
|
@@ -8827,12 +10144,12 @@ function buildMcpServer(platformDir) {
|
|
|
8827
10144
|
new_id: r.newId,
|
|
8828
10145
|
file: r.file,
|
|
8829
10146
|
spec_version: r.specVersion,
|
|
8830
|
-
|
|
10147
|
+
retagged: r.retagged
|
|
8831
10148
|
});
|
|
8832
10149
|
});
|
|
8833
10150
|
server.registerTool("spec_deprecate", {
|
|
8834
10151
|
title: "Deprecate a requirement",
|
|
8835
|
-
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`.",
|
|
8836
10153
|
inputSchema: {
|
|
8837
10154
|
req_id: z4.string().describe("The requirement id to deprecate (KEY-NNN)"),
|
|
8838
10155
|
reason: z4.string().min(1).describe("Why the requirement ended; the durable record")
|
|
@@ -8845,7 +10162,7 @@ function buildMcpServer(platformDir) {
|
|
|
8845
10162
|
if (trimmed === "")
|
|
8846
10163
|
return errorResult("reason must be non-empty");
|
|
8847
10164
|
const r = await deprecate({ platformDir, id: req_id, reason: trimmed }, coldFreshTags(platformDir));
|
|
8848
|
-
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);
|
|
8849
10166
|
});
|
|
8850
10167
|
server.registerTool("spec_accept", {
|
|
8851
10168
|
title: "Accept a draft requirement",
|
|
@@ -8858,7 +10175,11 @@ function buildMcpServer(platformDir) {
|
|
|
8858
10175
|
return errorResult(`req_id must be a requirement id (KEY-NNN); got ${req_id}`);
|
|
8859
10176
|
}
|
|
8860
10177
|
const r = await accept({ platformDir, id: req_id });
|
|
8861
|
-
|
|
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 });
|
|
8862
10183
|
});
|
|
8863
10184
|
server.registerTool("spec_req", {
|
|
8864
10185
|
title: "Author a requirement",
|
|
@@ -8867,13 +10188,11 @@ function buildMcpServer(platformDir) {
|
|
|
8867
10188
|
domain: z4.string().min(1).describe("Domain key or unique case-insensitive prefix"),
|
|
8868
10189
|
text: z4.string().trim().min(1).describe("The requirement statement"),
|
|
8869
10190
|
why: z4.string().trim().default("").describe("What breaks without it"),
|
|
8870
|
-
lives: z4.array(z4.string()).default([]).describe("livesIn paths"),
|
|
8871
|
-
verified_in: z4.array(z4.string()).default([]).describe("verifiedIn test paths"),
|
|
8872
10191
|
relates: z4.array(z4.string()).default([]).describe("Related requirement ids, each comma-split"),
|
|
8873
10192
|
issue: z4.string().trim().optional().describe("Originating ticket, recorded as created"),
|
|
8874
10193
|
draft: z4.boolean().default(false).describe("Author as Draft instead of Active")
|
|
8875
10194
|
}
|
|
8876
|
-
}, async ({ domain: domain2, text, why,
|
|
10195
|
+
}, async ({ domain: domain2, text, why, relates, issue, draft }) => {
|
|
8877
10196
|
const allocated = await nextId(platformDir, domain2);
|
|
8878
10197
|
if (!allocated.ok)
|
|
8879
10198
|
return errorResult(allocated.detail);
|
|
@@ -8882,8 +10201,6 @@ function buildMcpServer(platformDir) {
|
|
|
8882
10201
|
key: allocated.key,
|
|
8883
10202
|
statement: text,
|
|
8884
10203
|
why,
|
|
8885
|
-
livesIn: lives,
|
|
8886
|
-
verifiedIn: verified_in,
|
|
8887
10204
|
relates,
|
|
8888
10205
|
issue: issue || undefined,
|
|
8889
10206
|
status: draft ? "draft" : "active"
|
|
@@ -8901,17 +10218,15 @@ function buildMcpServer(platformDir) {
|
|
|
8901
10218
|
id: z4.string().describe("The requirement id to amend (KEY-NNN)"),
|
|
8902
10219
|
text: z4.string().trim().min(1).optional().describe("New statement"),
|
|
8903
10220
|
why: z4.string().trim().optional().describe("New why; empty clears it"),
|
|
8904
|
-
lives: z4.array(z4.string()).optional().describe("Replacement livesIn list; empty clears"),
|
|
8905
|
-
verified_in: z4.array(z4.string()).optional().describe("Replacement verifiedIn list; empty clears"),
|
|
8906
10221
|
relates: z4.array(z4.string()).optional().describe("Replacement related ids, each comma-split; empty clears"),
|
|
8907
10222
|
issue: z4.string().trim().optional().describe("Ticket recorded as amends-via")
|
|
8908
10223
|
}
|
|
8909
|
-
}, async ({ id, text, why,
|
|
10224
|
+
}, async ({ id, text, why, relates, issue }) => {
|
|
8910
10225
|
if (!ID_RE.test(id))
|
|
8911
10226
|
return errorResult(`id must be a requirement id (KEY-NNN); got ${id}`);
|
|
8912
|
-
const fields = amendFieldsOf({ text, why,
|
|
10227
|
+
const fields = amendFieldsOf({ text, why, relates, issue });
|
|
8913
10228
|
if (Object.keys(fields).length === 0) {
|
|
8914
|
-
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");
|
|
8915
10230
|
}
|
|
8916
10231
|
const r = await amend({ platformDir, id, fields });
|
|
8917
10232
|
if (!r.ok)
|
|
@@ -8922,16 +10237,14 @@ function buildMcpServer(platformDir) {
|
|
|
8922
10237
|
});
|
|
8923
10238
|
server.registerTool("spec_move", {
|
|
8924
10239
|
title: "Move a requirement to another domain",
|
|
8925
|
-
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`.",
|
|
8926
10241
|
inputSchema: {
|
|
8927
10242
|
id: z4.string().describe("The Active requirement id to move (KEY-NNN)"),
|
|
8928
10243
|
newDomain: z4.string().min(1).describe("The existing target domain key"),
|
|
8929
10244
|
text: z4.string().optional().describe("Successor statement (default: copied)"),
|
|
8930
|
-
why: z4.string().optional().describe("Successor why (default: copied)")
|
|
8931
|
-
lives: z4.array(z4.string()).optional().describe("Successor livesIn list; empty clears (default: copied)"),
|
|
8932
|
-
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)")
|
|
8933
10246
|
}
|
|
8934
|
-
}, async ({ id, newDomain: newDomain2, text, why
|
|
10247
|
+
}, async ({ id, newDomain: newDomain2, text, why }) => {
|
|
8935
10248
|
if (!ID_RE.test(id))
|
|
8936
10249
|
return errorResult(`id must be a requirement id (KEY-NNN); got ${id}`);
|
|
8937
10250
|
const targetKey = normalizeDomainKey(newDomain2);
|
|
@@ -8942,9 +10255,7 @@ function buildMcpServer(platformDir) {
|
|
|
8942
10255
|
id,
|
|
8943
10256
|
targetKey,
|
|
8944
10257
|
statement: text,
|
|
8945
|
-
why
|
|
8946
|
-
livesIn: lives,
|
|
8947
|
-
verifiedIn: verified_in
|
|
10258
|
+
why
|
|
8948
10259
|
}, coldFreshTags(platformDir));
|
|
8949
10260
|
if (!r.ok)
|
|
8950
10261
|
return errorResult(r.detail);
|
|
@@ -8957,7 +10268,7 @@ function buildMcpServer(platformDir) {
|
|
|
8957
10268
|
to_file: r.toFile,
|
|
8958
10269
|
source_spec_version: r.sourceSpecVersion,
|
|
8959
10270
|
target_spec_version: r.targetSpecVersion,
|
|
8960
|
-
|
|
10271
|
+
retagged: r.retagged
|
|
8961
10272
|
});
|
|
8962
10273
|
});
|
|
8963
10274
|
server.registerPrompt("author_requirements", {
|
|
@@ -9006,6 +10317,7 @@ var init_mcp = __esm(() => {
|
|
|
9006
10317
|
init_records();
|
|
9007
10318
|
init_supersede();
|
|
9008
10319
|
init_grammar3();
|
|
10320
|
+
init_authorPrompt();
|
|
9009
10321
|
});
|
|
9010
10322
|
|
|
9011
10323
|
// src/commands/mcp.ts
|
|
@@ -9014,14 +10326,14 @@ __export(exports_mcp, {
|
|
|
9014
10326
|
mcpCommand: () => mcpCommand
|
|
9015
10327
|
});
|
|
9016
10328
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9017
|
-
import { defineCommand as
|
|
10329
|
+
import { defineCommand as defineCommand22 } from "citty";
|
|
9018
10330
|
var mcpCommand;
|
|
9019
10331
|
var init_mcp2 = __esm(() => {
|
|
9020
10332
|
init_discover();
|
|
9021
10333
|
init_mcp();
|
|
9022
10334
|
init__args();
|
|
9023
10335
|
init__shared();
|
|
9024
|
-
mcpCommand =
|
|
10336
|
+
mcpCommand = defineCommand22({
|
|
9025
10337
|
meta: {
|
|
9026
10338
|
name: "mcp",
|
|
9027
10339
|
description: "Serve Spec Engine as an MCP (Model Context Protocol) server over stdio \u2014 agent-native query/resolve/check/report tools."
|
|
@@ -9049,8 +10361,8 @@ __export(exports_resolve, {
|
|
|
9049
10361
|
resolveCommand: () => resolveCommand
|
|
9050
10362
|
});
|
|
9051
10363
|
import { statSync as statSync5 } from "fs";
|
|
9052
|
-
import { join as
|
|
9053
|
-
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";
|
|
9054
10366
|
function classifyArg(a) {
|
|
9055
10367
|
if (a.startsWith("--")) {
|
|
9056
10368
|
const eq = a.indexOf("=");
|
|
@@ -9085,10 +10397,10 @@ function splitFilesAndPlatformDir(positionals, fallbackPlatformDir) {
|
|
|
9085
10397
|
let lastIsPlatformDir = false;
|
|
9086
10398
|
let lastIsDirWithoutSpecEngine = false;
|
|
9087
10399
|
try {
|
|
9088
|
-
const resolved =
|
|
10400
|
+
const resolved = resolve14(last);
|
|
9089
10401
|
if (statSync5(resolved).isDirectory()) {
|
|
9090
10402
|
try {
|
|
9091
|
-
if (statSync5(
|
|
10403
|
+
if (statSync5(join18(resolved, "spec-engine")).isDirectory()) {
|
|
9092
10404
|
lastIsPlatformDir = true;
|
|
9093
10405
|
} else {
|
|
9094
10406
|
lastIsDirWithoutSpecEngine = true;
|
|
@@ -9175,7 +10487,7 @@ async function dispatchReverseQuery(reqArg, resolved, args) {
|
|
|
9175
10487
|
}
|
|
9176
10488
|
await runReverseQuery({
|
|
9177
10489
|
reqId: reqArg,
|
|
9178
|
-
platformDir:
|
|
10490
|
+
platformDir: resolve14(lone ?? process.cwd()),
|
|
9179
10491
|
outArg: args.out,
|
|
9180
10492
|
json: Boolean(args.json),
|
|
9181
10493
|
fresh: Boolean(args.fresh),
|
|
@@ -9185,7 +10497,7 @@ async function dispatchReverseQuery(reqArg, resolved, args) {
|
|
|
9185
10497
|
function normalizeFileInputs(files, platformDir) {
|
|
9186
10498
|
const normalized = [];
|
|
9187
10499
|
for (const input of files) {
|
|
9188
|
-
const rel =
|
|
10500
|
+
const rel = relative4(platformDir, resolve14(platformDir, input));
|
|
9189
10501
|
const segments = rel.split(sep4);
|
|
9190
10502
|
if (rel === "" || rel === "." || segments[0] === "..") {
|
|
9191
10503
|
console.error(`spec resolve: file path must be inside platformDir: ${input}`);
|
|
@@ -9236,7 +10548,7 @@ var init_resolve2 = __esm(() => {
|
|
|
9236
10548
|
description: "Suppress interactive onboarding prompt for siblings missing spec-engine.member.json (defaults to NO_SPEC_CONFIG warning)"
|
|
9237
10549
|
}
|
|
9238
10550
|
};
|
|
9239
|
-
resolveCommand =
|
|
10551
|
+
resolveCommand = defineCommand23({
|
|
9240
10552
|
meta: {
|
|
9241
10553
|
name: "resolve",
|
|
9242
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."
|
|
@@ -9260,7 +10572,7 @@ var init_resolve2 = __esm(() => {
|
|
|
9260
10572
|
process.exit(EXIT.USAGE);
|
|
9261
10573
|
return;
|
|
9262
10574
|
}
|
|
9263
|
-
const platformDir =
|
|
10575
|
+
const platformDir = resolve14(resolved.platformDir ?? process.cwd());
|
|
9264
10576
|
const outArg2 = args.out;
|
|
9265
10577
|
const dbPath = resolveDbPath(platformDir, outArg2);
|
|
9266
10578
|
if (outArg2)
|
|
@@ -9399,9 +10711,9 @@ var exports_gate = {};
|
|
|
9399
10711
|
__export(exports_gate, {
|
|
9400
10712
|
gateCommand: () => gateCommand
|
|
9401
10713
|
});
|
|
9402
|
-
import { existsSync as
|
|
9403
|
-
import { join as
|
|
9404
|
-
import { defineCommand as
|
|
10714
|
+
import { existsSync as existsSync16 } from "fs";
|
|
10715
|
+
import { join as join19 } from "path";
|
|
10716
|
+
import { defineCommand as defineCommand24 } from "citty";
|
|
9405
10717
|
function isEpipe2(e) {
|
|
9406
10718
|
if (e === null || typeof e !== "object" || !("code" in e))
|
|
9407
10719
|
return false;
|
|
@@ -9424,7 +10736,7 @@ function resolveGateArgs(args) {
|
|
|
9424
10736
|
process.exit(EXIT.USAGE);
|
|
9425
10737
|
}
|
|
9426
10738
|
const platformDir = resolvePlatformDir(args);
|
|
9427
|
-
if (!
|
|
10739
|
+
if (!existsSync16(join19(platformDir, "spec-engine"))) {
|
|
9428
10740
|
console.error(formatNotASpecPlatform(platformDir));
|
|
9429
10741
|
process.exit(EXIT.USAGE);
|
|
9430
10742
|
}
|
|
@@ -9465,7 +10777,7 @@ var init_gate2 = __esm(() => {
|
|
|
9465
10777
|
description: "Suppress interactive onboarding prompt for siblings missing spec-engine.member.json (defaults to NO_SPEC_CONFIG warning)"
|
|
9466
10778
|
}
|
|
9467
10779
|
};
|
|
9468
|
-
gateCommand =
|
|
10780
|
+
gateCommand = defineCommand24({
|
|
9469
10781
|
meta: {
|
|
9470
10782
|
name: "gate",
|
|
9471
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."
|
|
@@ -9589,7 +10901,7 @@ var exports_guard = {};
|
|
|
9589
10901
|
__export(exports_guard, {
|
|
9590
10902
|
guardCommand: () => guardCommand
|
|
9591
10903
|
});
|
|
9592
|
-
import { defineCommand as
|
|
10904
|
+
import { defineCommand as defineCommand25 } from "citty";
|
|
9593
10905
|
var guardCommand;
|
|
9594
10906
|
var init_guard2 = __esm(() => {
|
|
9595
10907
|
init_constants();
|
|
@@ -9597,7 +10909,7 @@ var init_guard2 = __esm(() => {
|
|
|
9597
10909
|
init_stdout();
|
|
9598
10910
|
init__args();
|
|
9599
10911
|
init__shared();
|
|
9600
|
-
guardCommand =
|
|
10912
|
+
guardCommand = defineCommand25({
|
|
9601
10913
|
meta: {
|
|
9602
10914
|
name: "guard",
|
|
9603
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."
|
|
@@ -11229,9 +12541,9 @@ function requirementStatusBadge(status) {
|
|
|
11229
12541
|
const variant = status === "Active" ? "active" : status === "Superseded" ? "superseded" : status === "Draft" ? "draft" : "retired";
|
|
11230
12542
|
return badge(variant, STATUS_LABEL[status]);
|
|
11231
12543
|
}
|
|
11232
|
-
function propagationStateBadge(
|
|
12544
|
+
function propagationStateBadge(row) {
|
|
11233
12545
|
let variant;
|
|
11234
|
-
switch (
|
|
12546
|
+
switch (row.state) {
|
|
11235
12547
|
case "MIGRATED_VERIFIED":
|
|
11236
12548
|
variant = "verified";
|
|
11237
12549
|
break;
|
|
@@ -11244,7 +12556,7 @@ function propagationStateBadge(row2) {
|
|
|
11244
12556
|
default:
|
|
11245
12557
|
variant = "unbound";
|
|
11246
12558
|
}
|
|
11247
|
-
return badge(variant,
|
|
12559
|
+
return badge(variant, row.state);
|
|
11248
12560
|
}
|
|
11249
12561
|
function coverageGlyph(impl, verif) {
|
|
11250
12562
|
const [variant, char, title] = impl && verif ? ["both", "\u2713", "src + test"] : impl ? ["impl", "S", "src only"] : verif ? ["test", "T", "test only"] : ["none", "", "none"];
|
|
@@ -11378,18 +12690,18 @@ import { html as html4, raw as raw3 } from "hono/html";
|
|
|
11378
12690
|
function pivot(rows) {
|
|
11379
12691
|
const repos = [...new Set(rows.map((r) => r.repo))].sort();
|
|
11380
12692
|
const byDomain = new Map;
|
|
11381
|
-
for (const
|
|
11382
|
-
let dom = byDomain.get(
|
|
12693
|
+
for (const row of rows) {
|
|
12694
|
+
let dom = byDomain.get(row.domain_key);
|
|
11383
12695
|
if (!dom) {
|
|
11384
12696
|
dom = new Map;
|
|
11385
|
-
byDomain.set(
|
|
12697
|
+
byDomain.set(row.domain_key, dom);
|
|
11386
12698
|
}
|
|
11387
|
-
let req = dom.get(
|
|
12699
|
+
let req = dom.get(row.req_id);
|
|
11388
12700
|
if (!req) {
|
|
11389
|
-
req = { id:
|
|
11390
|
-
dom.set(
|
|
12701
|
+
req = { id: row.req_id, status: row.req_status, cells: new Map };
|
|
12702
|
+
dom.set(row.req_id, req);
|
|
11391
12703
|
}
|
|
11392
|
-
req.cells.set(
|
|
12704
|
+
req.cells.set(row.repo, { impl: row.implemented, verif: row.verified });
|
|
11393
12705
|
}
|
|
11394
12706
|
const domains = [...byDomain.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([key, reqMap]) => ({
|
|
11395
12707
|
key,
|
|
@@ -11636,14 +12948,6 @@ function mountEditor(app, bindHost = DEFAULT_BIND_HOST) {
|
|
|
11636
12948
|
Why (optional)
|
|
11637
12949
|
<textarea name="why" rows="2"></textarea>
|
|
11638
12950
|
</label>
|
|
11639
|
-
<label>
|
|
11640
|
-
Lives in (optional, single path)
|
|
11641
|
-
<input type="text" name="livesIn" placeholder="path/to/impl.ts" />
|
|
11642
|
-
</label>
|
|
11643
|
-
<label>
|
|
11644
|
-
Verified in (optional, single path)
|
|
11645
|
-
<input type="text" name="verifiedIn" placeholder="path/to/impl.test.ts" />
|
|
11646
|
-
</label>
|
|
11647
12951
|
<label>
|
|
11648
12952
|
Relates to (optional, comma-separated ids)
|
|
11649
12953
|
<input type="text" name="relates" placeholder="BILLING-001, BILLING-002" />
|
|
@@ -11681,8 +12985,6 @@ function mountEditor(app, bindHost = DEFAULT_BIND_HOST) {
|
|
|
11681
12985
|
const form = await c.req.parseBody();
|
|
11682
12986
|
const statement = field(form, "statement");
|
|
11683
12987
|
const why = field(form, "why");
|
|
11684
|
-
const livesIn = field(form, "livesIn");
|
|
11685
|
-
const verifiedIn = field(form, "verifiedIn");
|
|
11686
12988
|
const relates = field(form, "relates");
|
|
11687
12989
|
const payload = {
|
|
11688
12990
|
key: field(form, "key"),
|
|
@@ -11690,10 +12992,6 @@ function mountEditor(app, bindHost = DEFAULT_BIND_HOST) {
|
|
|
11690
12992
|
};
|
|
11691
12993
|
if (why.length > 0)
|
|
11692
12994
|
payload.why = why;
|
|
11693
|
-
if (livesIn.length > 0)
|
|
11694
|
-
payload.livesIn = livesIn;
|
|
11695
|
-
if (verifiedIn.length > 0)
|
|
11696
|
-
payload.verifiedIn = verifiedIn;
|
|
11697
12995
|
if (relates.length > 0)
|
|
11698
12996
|
payload.relates = relates;
|
|
11699
12997
|
const res = await app.request("/api/requirements", {
|
|
@@ -12010,15 +13308,15 @@ var init_relations2 = __esm(() => {
|
|
|
12010
13308
|
import { html as html10, raw as raw9 } from "hono/html";
|
|
12011
13309
|
function coverageRollups(coverage) {
|
|
12012
13310
|
const cov = new Map;
|
|
12013
|
-
for (const
|
|
12014
|
-
if (
|
|
13311
|
+
for (const row of coverage) {
|
|
13312
|
+
if (row.req_status !== "Active")
|
|
12015
13313
|
continue;
|
|
12016
|
-
const c = cov.get(
|
|
12017
|
-
if (
|
|
13314
|
+
const c = cov.get(row.req_id) ?? { impl: 0, verif: 0 };
|
|
13315
|
+
if (row.implemented)
|
|
12018
13316
|
c.impl += 1;
|
|
12019
|
-
if (
|
|
13317
|
+
if (row.verified)
|
|
12020
13318
|
c.verif += 1;
|
|
12021
|
-
cov.set(
|
|
13319
|
+
cov.set(row.req_id, c);
|
|
12022
13320
|
}
|
|
12023
13321
|
return cov;
|
|
12024
13322
|
}
|
|
@@ -12262,6 +13560,9 @@ function mountRequirements(app) {
|
|
|
12262
13560
|
</html>`);
|
|
12263
13561
|
}
|
|
12264
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>`;
|
|
12265
13566
|
const body = html10`
|
|
12266
13567
|
${navBar("requirements")}
|
|
12267
13568
|
<div class="eyebrow">/ Requirement</div>
|
|
@@ -12281,6 +13582,8 @@ function mountRequirements(app) {
|
|
|
12281
13582
|
<dt>line</dt><dd>${r.line}</dd>
|
|
12282
13583
|
<dt>text</dt><dd>${r.text}</dd>
|
|
12283
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>
|
|
12284
13587
|
</dl>
|
|
12285
13588
|
`;
|
|
12286
13589
|
return c.html(html10`<!doctype html>
|
|
@@ -12365,15 +13668,15 @@ function mountSetup(app) {
|
|
|
12365
13668
|
const memberCount = members.length;
|
|
12366
13669
|
const note = readingNote(mode, memberCount);
|
|
12367
13670
|
const boundByRepo = new Map;
|
|
12368
|
-
for (const
|
|
12369
|
-
if (!
|
|
13671
|
+
for (const row of cov) {
|
|
13672
|
+
if (!row.implemented)
|
|
12370
13673
|
continue;
|
|
12371
|
-
let set = boundByRepo.get(
|
|
13674
|
+
let set = boundByRepo.get(row.repo);
|
|
12372
13675
|
if (!set) {
|
|
12373
13676
|
set = new Set;
|
|
12374
|
-
boundByRepo.set(
|
|
13677
|
+
boundByRepo.set(row.repo, set);
|
|
12375
13678
|
}
|
|
12376
|
-
set.add(
|
|
13679
|
+
set.add(row.req_id);
|
|
12377
13680
|
}
|
|
12378
13681
|
const body = html11`
|
|
12379
13682
|
${navBar("setup")}
|
|
@@ -12610,8 +13913,6 @@ function mintFieldsFromBody(body) {
|
|
|
12610
13913
|
return {
|
|
12611
13914
|
statement: typeof body.statement === "string" ? body.statement : "",
|
|
12612
13915
|
why: typeof body.why === "string" ? body.why : "",
|
|
12613
|
-
livesIn: toStringList(body.livesIn),
|
|
12614
|
-
verifiedIn: toStringList(body.verifiedIn),
|
|
12615
13916
|
relates: toStringList(body.relates),
|
|
12616
13917
|
issue: typeof body.issue === "string" && body.issue.trim() !== "" ? body.issue.trim() : undefined,
|
|
12617
13918
|
status: body.status === "draft" ? "draft" : "active"
|
|
@@ -12620,8 +13921,6 @@ function mintFieldsFromBody(body) {
|
|
|
12620
13921
|
function successorFieldsFromBody(body) {
|
|
12621
13922
|
return {
|
|
12622
13923
|
why: typeof body.why === "string" ? body.why : undefined,
|
|
12623
|
-
livesIn: "livesIn" in body ? toStringList(body.livesIn) : undefined,
|
|
12624
|
-
verifiedIn: "verifiedIn" in body ? toStringList(body.verifiedIn) : undefined,
|
|
12625
13924
|
relates: "relates" in body ? toStringList(body.relates) : undefined,
|
|
12626
13925
|
issue: typeof body.issue === "string" && body.issue.trim() !== "" ? body.issue.trim() : undefined
|
|
12627
13926
|
};
|
|
@@ -12632,10 +13931,6 @@ function amendFieldsFromBody(body) {
|
|
|
12632
13931
|
fields.statement = body.statement;
|
|
12633
13932
|
if ("why" in body)
|
|
12634
13933
|
fields.why = typeof body.why === "string" ? body.why : null;
|
|
12635
|
-
if ("livesIn" in body)
|
|
12636
|
-
fields.livesIn = toStringList(body.livesIn);
|
|
12637
|
-
if ("verifiedIn" in body)
|
|
12638
|
-
fields.verifiedIn = toStringList(body.verifiedIn);
|
|
12639
13934
|
if ("relates" in body)
|
|
12640
13935
|
fields.relates = toStringList(body.relates);
|
|
12641
13936
|
return Object.keys(fields).length === 0 ? null : fields;
|
|
@@ -12697,8 +13992,8 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
|
|
|
12697
13992
|
}));
|
|
12698
13993
|
app.get("/api/requirements/:id", guarded((c) => {
|
|
12699
13994
|
const id = c.req.param("id") ?? "";
|
|
12700
|
-
const { row
|
|
12701
|
-
return
|
|
13995
|
+
const { row } = getRecord(storage2, id);
|
|
13996
|
+
return row ? c.json(row) : c.json({ error: "not found" }, 404);
|
|
12702
13997
|
}));
|
|
12703
13998
|
app.post("/api/requirements", guarded(async function createRequirement(c) {
|
|
12704
13999
|
if (!featureEnabled("editor")) {
|
|
@@ -12744,7 +14039,7 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
|
|
|
12744
14039
|
return withWriteLock(async () => {
|
|
12745
14040
|
const fields = amendFieldsFromBody(body);
|
|
12746
14041
|
if (fields === null) {
|
|
12747
|
-
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);
|
|
12748
14043
|
}
|
|
12749
14044
|
const r = await amend({ platformDir, id, fields });
|
|
12750
14045
|
if (!r.ok)
|
|
@@ -12786,7 +14081,7 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
|
|
|
12786
14081
|
new_id: r.newId,
|
|
12787
14082
|
file: r.file,
|
|
12788
14083
|
spec_version: r.specVersion,
|
|
12789
|
-
|
|
14084
|
+
retagged: r.retagged
|
|
12790
14085
|
}, 201);
|
|
12791
14086
|
});
|
|
12792
14087
|
}));
|
|
@@ -12816,9 +14111,7 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
|
|
|
12816
14111
|
id,
|
|
12817
14112
|
targetKey,
|
|
12818
14113
|
statement: typeof body.text === "string" ? body.text : undefined,
|
|
12819
|
-
why: typeof body.why === "string" ? body.why : undefined
|
|
12820
|
-
livesIn: "lives" in body ? toStringList(body.lives) : undefined,
|
|
12821
|
-
verifiedIn: "verifiedIn" in body ? toStringList(body.verifiedIn) : undefined
|
|
14114
|
+
why: typeof body.why === "string" ? body.why : undefined
|
|
12822
14115
|
}, async (reqId) => {
|
|
12823
14116
|
await runIndex({ platformDir, storage: storage2 });
|
|
12824
14117
|
return storage2.listTags({ req_id: reqId });
|
|
@@ -12833,7 +14126,7 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
|
|
|
12833
14126
|
to_file: r.toFile,
|
|
12834
14127
|
source_spec_version: r.sourceSpecVersion,
|
|
12835
14128
|
target_spec_version: r.targetSpecVersion,
|
|
12836
|
-
|
|
14129
|
+
retagged: r.retagged
|
|
12837
14130
|
}, 201);
|
|
12838
14131
|
});
|
|
12839
14132
|
}));
|
|
@@ -12864,7 +14157,7 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
|
|
|
12864
14157
|
});
|
|
12865
14158
|
if (!r.ok)
|
|
12866
14159
|
return failureResponse(c, r);
|
|
12867
|
-
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);
|
|
12868
14161
|
});
|
|
12869
14162
|
}));
|
|
12870
14163
|
app.post("/api/requirements/:id/accept", guarded(async function acceptRequirement(c) {
|
|
@@ -13106,9 +14399,9 @@ __export(exports_serve, {
|
|
|
13106
14399
|
resolveBind: () => resolveBind,
|
|
13107
14400
|
composeServeApp: () => composeServeApp
|
|
13108
14401
|
});
|
|
13109
|
-
import { existsSync as
|
|
13110
|
-
import { dirname as
|
|
13111
|
-
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";
|
|
13112
14405
|
import { Hono as Hono2 } from "hono";
|
|
13113
14406
|
function composeServeApp(storage2, platformDir = process.cwd(), bindHost = DEFAULT_BIND_HOST) {
|
|
13114
14407
|
const app = new Hono2;
|
|
@@ -13186,7 +14479,7 @@ var init_serve = __esm(() => {
|
|
|
13186
14479
|
init_sqlite();
|
|
13187
14480
|
init__args();
|
|
13188
14481
|
init__shared();
|
|
13189
|
-
serveCommand =
|
|
14482
|
+
serveCommand = defineCommand26({
|
|
13190
14483
|
meta: {
|
|
13191
14484
|
name: "serve",
|
|
13192
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."
|
|
@@ -13258,8 +14551,8 @@ var init_serve = __esm(() => {
|
|
|
13258
14551
|
noPrompt: args.noPrompt
|
|
13259
14552
|
}
|
|
13260
14553
|
});
|
|
13261
|
-
|
|
13262
|
-
const needsIndex = !
|
|
14554
|
+
mkdirSync7(dirname10(dbPath), { recursive: true });
|
|
14555
|
+
const needsIndex = !existsSync17(dbPath);
|
|
13263
14556
|
let storage2;
|
|
13264
14557
|
try {
|
|
13265
14558
|
storage2 = openStorage(dbPath);
|
|
@@ -13307,13 +14600,13 @@ __export(exports_docs, {
|
|
|
13307
14600
|
docsCommand: () => docsCommand,
|
|
13308
14601
|
createDocsFetchHandler: () => createDocsFetchHandler
|
|
13309
14602
|
});
|
|
13310
|
-
import { existsSync as
|
|
13311
|
-
import { join as
|
|
14603
|
+
import { existsSync as existsSync18, statSync as statSync6 } from "fs";
|
|
14604
|
+
import { join as join20, normalize, sep as sep5 } from "path";
|
|
13312
14605
|
import { fileURLToPath } from "url";
|
|
13313
|
-
import { defineCommand as
|
|
14606
|
+
import { defineCommand as defineCommand27 } from "citty";
|
|
13314
14607
|
function resolveDocsRootFrom(candidates) {
|
|
13315
14608
|
for (const candidate of candidates) {
|
|
13316
|
-
if (
|
|
14609
|
+
if (existsSync18(join20(candidate, "index.html")))
|
|
13317
14610
|
return candidate;
|
|
13318
14611
|
}
|
|
13319
14612
|
return null;
|
|
@@ -13337,17 +14630,17 @@ function createDocsFetchHandler(root) {
|
|
|
13337
14630
|
if (pathname.includes("\x00")) {
|
|
13338
14631
|
return new Response("Bad request", { status: 400 });
|
|
13339
14632
|
}
|
|
13340
|
-
const target = normalize(
|
|
14633
|
+
const target = normalize(join20(normalRoot, pathname));
|
|
13341
14634
|
if (target !== normalRoot && !target.startsWith(rootPrefix)) {
|
|
13342
14635
|
return new Response("Not found", { status: 404 });
|
|
13343
14636
|
}
|
|
13344
|
-
for (const candidate of [target,
|
|
13345
|
-
if (
|
|
14637
|
+
for (const candidate of [target, join20(target, "index.html"), `${target}.html`]) {
|
|
14638
|
+
if (existsSync18(candidate) && statSync6(candidate).isFile()) {
|
|
13346
14639
|
return new Response(Bun.file(candidate));
|
|
13347
14640
|
}
|
|
13348
14641
|
}
|
|
13349
|
-
const notFoundPage =
|
|
13350
|
-
if (
|
|
14642
|
+
const notFoundPage = join20(normalRoot, "404.html");
|
|
14643
|
+
if (existsSync18(notFoundPage)) {
|
|
13351
14644
|
return new Response(Bun.file(notFoundPage), { status: 404 });
|
|
13352
14645
|
}
|
|
13353
14646
|
return new Response("Not found", { status: 404 });
|
|
@@ -13389,7 +14682,7 @@ var docsCommand;
|
|
|
13389
14682
|
var init_docs2 = __esm(() => {
|
|
13390
14683
|
init_constants();
|
|
13391
14684
|
init_stdout();
|
|
13392
|
-
docsCommand =
|
|
14685
|
+
docsCommand = defineCommand27({
|
|
13393
14686
|
meta: {
|
|
13394
14687
|
name: "docs",
|
|
13395
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."
|
|
@@ -13450,10 +14743,10 @@ __export(exports___smoke, {
|
|
|
13450
14743
|
schemaSmokeCommand: () => schemaSmokeCommand,
|
|
13451
14744
|
schemaMismatchSmokeCommand: () => schemaMismatchSmokeCommand
|
|
13452
14745
|
});
|
|
13453
|
-
import { defineCommand as
|
|
14746
|
+
import { defineCommand as defineCommand28 } from "citty";
|
|
13454
14747
|
var schemaSmokeCommand, schemaMismatchSmokeCommand;
|
|
13455
14748
|
var init___smoke = __esm(() => {
|
|
13456
|
-
schemaSmokeCommand =
|
|
14749
|
+
schemaSmokeCommand = defineCommand28({
|
|
13457
14750
|
meta: {
|
|
13458
14751
|
name: "__schema-smoke",
|
|
13459
14752
|
description: "(hidden CI) open a fresh DB, exec DDL, assert the schema shape",
|
|
@@ -13496,7 +14789,7 @@ var init___smoke = __esm(() => {
|
|
|
13496
14789
|
console.log("__schema-smoke OK");
|
|
13497
14790
|
}
|
|
13498
14791
|
});
|
|
13499
|
-
schemaMismatchSmokeCommand =
|
|
14792
|
+
schemaMismatchSmokeCommand = defineCommand28({
|
|
13500
14793
|
meta: {
|
|
13501
14794
|
name: "__schema-mismatch-smoke",
|
|
13502
14795
|
description: "(hidden CI) corrupt _schema_version, re-open, assert silent rebuild",
|
|
@@ -13523,9 +14816,9 @@ var init___smoke = __esm(() => {
|
|
|
13523
14816
|
// src/cli.ts
|
|
13524
14817
|
init_package();
|
|
13525
14818
|
init_stdout();
|
|
13526
|
-
import { defineCommand as
|
|
14819
|
+
import { defineCommand as defineCommand29, runMain, showUsage } from "citty";
|
|
13527
14820
|
routeStdout();
|
|
13528
|
-
var main =
|
|
14821
|
+
var main = defineCommand29({
|
|
13529
14822
|
meta: {
|
|
13530
14823
|
name: "spec",
|
|
13531
14824
|
version: package_default.version,
|
|
@@ -13551,6 +14844,8 @@ var main = defineCommand27({
|
|
|
13551
14844
|
move: () => Promise.resolve().then(() => (init_move2(), exports_move)).then((m) => m.moveCommand),
|
|
13552
14845
|
amend: () => Promise.resolve().then(() => (init_amend2(), exports_amend)).then((m) => m.amendCommand),
|
|
13553
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),
|
|
13554
14849
|
mcp: () => Promise.resolve().then(() => (init_mcp2(), exports_mcp)).then((m) => m.mcpCommand),
|
|
13555
14850
|
resolve: () => Promise.resolve().then(() => (init_resolve2(), exports_resolve)).then((m) => m.resolveCommand),
|
|
13556
14851
|
gate: () => Promise.resolve().then(() => (init_gate2(), exports_gate)).then((m) => m.gateCommand),
|