@spec-engine/spec-engine 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/docs/404.html +4 -4
- package/dist/docs/_astro/{Code.DfIJP77p.css → Code.CAimF0uL.css} +1 -1
- package/dist/docs/_astro/{common.xlUlWolS.css → common.DslAiFsT.css} +1 -1
- package/dist/docs/agent-reference/index.html +8 -8
- package/dist/docs/architecture/index.html +8 -8
- package/dist/docs/checks/index.html +10 -10
- package/dist/docs/commands/index.html +14 -13
- package/dist/docs/format/index.html +10 -10
- package/dist/docs/getting-started/index.html +8 -8
- package/dist/docs/guides/adoption/index.html +9 -9
- package/dist/docs/guides/platform-setup/index.html +8 -8
- package/dist/docs/guides/single-repo/index.html +8 -8
- package/dist/docs/index.html +1 -1
- package/dist/docs/overview/index.html +28 -43
- package/dist/docs/pagefind/fragment/en_10bec64.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_435dca1.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_4bdd17f.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_6c144c1.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_8d65739.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_93176f1.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_adb4528.pf_fragment +0 -0
- package/dist/docs/pagefind/index/en_90d86c1.pf_index +0 -0
- package/dist/docs/pagefind/pagefind-entry.json +1 -1
- package/dist/docs/pagefind/pagefind.en_4cf42761c7.pf_meta +0 -0
- package/dist/docs/pagefind/wasm.en.pagefind +0 -0
- package/dist/docs/pagefind/wasm.unknown.pagefind +0 -0
- package/dist/docs/tags/index.html +8 -8
- package/dist/impl.js +1355 -653
- package/package.json +5 -1
- package/dist/docs/pagefind/fragment/en_591940a.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_5f79492.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_629887a.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_6339f38.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_9f5f7d8.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_a4de4f3.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_ae342c4.pf_fragment +0 -0
- package/dist/docs/pagefind/index/en_c844839.pf_index +0 -0
- package/dist/docs/pagefind/pagefind.en_f3a4ba28d6.pf_meta +0 -0
package/dist/impl.js
CHANGED
|
@@ -35,6 +35,10 @@ var init_diagnostics = __esm(() => {
|
|
|
35
35
|
BAD_STATUS: "BAD_STATUS",
|
|
36
36
|
DANGLING_TAG: "DANGLING_TAG",
|
|
37
37
|
SUPERSEDED_REFERENCED: "SUPERSEDED_REFERENCED",
|
|
38
|
+
DEPRECATED_REFERENCED: "DEPRECATED_REFERENCED",
|
|
39
|
+
STATEMENT_GRAMMAR: "STATEMENT_GRAMMAR",
|
|
40
|
+
DRAFT_REFERENCED: "DRAFT_REFERENCED",
|
|
41
|
+
GLOSSARY_DRIFT: "GLOSSARY_DRIFT",
|
|
38
42
|
ORPHAN_REQ: "ORPHAN_REQ",
|
|
39
43
|
UNVERIFIED_REQ: "UNVERIFIED_REQ",
|
|
40
44
|
DRIFT: "DRIFT",
|
|
@@ -103,6 +107,8 @@ function orderDomain(d) {
|
|
|
103
107
|
specVersion: d.specVersion,
|
|
104
108
|
updated: d.updated,
|
|
105
109
|
scope: d.scope ?? null,
|
|
110
|
+
grammar: d.grammar,
|
|
111
|
+
grammarSeverity: d.grammarSeverity,
|
|
106
112
|
requirements: d.requirements.map((r) => ({
|
|
107
113
|
id: r.id,
|
|
108
114
|
status: r.status,
|
|
@@ -115,6 +121,7 @@ function orderDomain(d) {
|
|
|
115
121
|
issues: r.issues.map((iss) => ({ role: iss.role, id: iss.id })),
|
|
116
122
|
changedAtVersion: r.changedAtVersion,
|
|
117
123
|
supersededAtVersion: r.supersededAtVersion,
|
|
124
|
+
deprecatedReason: r.deprecatedReason,
|
|
118
125
|
term: r.term,
|
|
119
126
|
aliases: r.aliases,
|
|
120
127
|
cites: r.cites.map((c) => ({ term: c.term, pinned: c.pinned })),
|
|
@@ -157,6 +164,7 @@ var init_domain = __esm(() => {
|
|
|
157
164
|
issues: z2.array(SpecIssueSchema).optional().default([]),
|
|
158
165
|
changedAtVersion: z2.number().int().optional(),
|
|
159
166
|
supersededAtVersion: z2.number().int().optional(),
|
|
167
|
+
deprecatedReason: z2.string().optional(),
|
|
160
168
|
term: z2.string().optional(),
|
|
161
169
|
aliases: z2.array(z2.string()).optional().default([]),
|
|
162
170
|
cites: z2.array(SpecCiteSchema).optional().default([]),
|
|
@@ -168,9 +176,109 @@ var init_domain = __esm(() => {
|
|
|
168
176
|
specVersion: z2.number().int().positive().optional(),
|
|
169
177
|
updated: z2.string(),
|
|
170
178
|
scope: z2.string().nullable().optional(),
|
|
179
|
+
grammar: z2.enum(["ears", "freeform"]).optional(),
|
|
180
|
+
grammarSeverity: z2.enum(["warning", "error"]).optional(),
|
|
171
181
|
requirements: z2.array(SpecRequirementSchema)
|
|
172
182
|
}).strict();
|
|
173
183
|
});
|
|
184
|
+
|
|
185
|
+
// ../shared/src/ears.ts
|
|
186
|
+
function reject(problem) {
|
|
187
|
+
return { ok: false, problem, expected: EARS_EXPECTED };
|
|
188
|
+
}
|
|
189
|
+
function parseEarsStatement(raw) {
|
|
190
|
+
const statement = raw.trim();
|
|
191
|
+
if (statement === "")
|
|
192
|
+
return reject("the statement is empty");
|
|
193
|
+
if (!/\bshall\b/.test(statement)) {
|
|
194
|
+
return reject('missing the word "shall" \u2014 it separates who acts from the observable result (e.g. "spec check shall exit 1")');
|
|
195
|
+
}
|
|
196
|
+
for (const { keyword, pattern, re } of CONDITIONAL) {
|
|
197
|
+
if (!statement.startsWith(`${keyword} `))
|
|
198
|
+
continue;
|
|
199
|
+
const m2 = statement.match(re);
|
|
200
|
+
if (m2) {
|
|
201
|
+
const [, condition, system, response] = m2;
|
|
202
|
+
return {
|
|
203
|
+
ok: true,
|
|
204
|
+
clauses: {
|
|
205
|
+
pattern,
|
|
206
|
+
condition: condition.trim(),
|
|
207
|
+
system: system.trim().replace(/^the\s+/i, ""),
|
|
208
|
+
response: response.trim()
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
if (keyword === "If" && !/,\s*then\s/.test(statement)) {
|
|
213
|
+
return reject('starts with "If" but has no ", then" \u2014 the failure clause never hands over to the system');
|
|
214
|
+
}
|
|
215
|
+
return reject(`starts with "${keyword}" but the condition clause never ends \u2014 add a comma before naming the system that acts`);
|
|
216
|
+
}
|
|
217
|
+
const m = statement.match(ALWAYS_RE);
|
|
218
|
+
if (m) {
|
|
219
|
+
const [, system, response] = m;
|
|
220
|
+
if (/,/.test(system)) {
|
|
221
|
+
return reject('the clause before "shall" contains a comma but starts with no condition keyword (When/While/If/Where) \u2014 either drop the comma or lead with the condition');
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
ok: true,
|
|
225
|
+
clauses: {
|
|
226
|
+
pattern: "always",
|
|
227
|
+
condition: null,
|
|
228
|
+
system: system.trim().replace(/^the\s+/i, ""),
|
|
229
|
+
response: response.trim()
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return reject('nothing follows "shall" \u2014 the observable result is missing');
|
|
234
|
+
}
|
|
235
|
+
var EARS_EXPECTED, CONDITIONAL, ALWAYS_RE;
|
|
236
|
+
var init_ears = __esm(() => {
|
|
237
|
+
EARS_EXPECTED = [
|
|
238
|
+
'When <trigger>, the <system> shall <result> \u2014 e.g. "When an unknown id is passed, spec resolve shall print [] and exit 0."',
|
|
239
|
+
"While <state>, the <system> shall <result>",
|
|
240
|
+
"If <failure>, then the <system> shall <result>",
|
|
241
|
+
"Where <feature>, the <system> shall <result>",
|
|
242
|
+
"The <system> shall <result>"
|
|
243
|
+
].join(`
|
|
244
|
+
`);
|
|
245
|
+
CONDITIONAL = [
|
|
246
|
+
{ keyword: "When", pattern: "when", re: /^When\s+(.+?),\s*(.+?)\s+shall\s+(.+)$/s },
|
|
247
|
+
{ keyword: "While", pattern: "while", re: /^While\s+(.+?),\s*(.+?)\s+shall\s+(.+)$/s },
|
|
248
|
+
{ keyword: "If", pattern: "if", re: /^If\s+(.+?),\s*then\s+(.+?)\s+shall\s+(.+)$/s },
|
|
249
|
+
{ keyword: "Where", pattern: "where", re: /^Where\s+(.+?),\s*(.+?)\s+shall\s+(.+)$/s }
|
|
250
|
+
];
|
|
251
|
+
ALWAYS_RE = /^(.+?)\s+shall\s+(.+)$/s;
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// ../shared/src/flags.ts
|
|
255
|
+
function parseFlags(value) {
|
|
256
|
+
const enabled = new Set;
|
|
257
|
+
for (const rawKey of (value ?? "").split(",")) {
|
|
258
|
+
const key = rawKey.trim().toLowerCase();
|
|
259
|
+
if (FEATURE_KEYS.includes(key))
|
|
260
|
+
enabled.add(key);
|
|
261
|
+
}
|
|
262
|
+
return enabled;
|
|
263
|
+
}
|
|
264
|
+
function featureEnabled(key) {
|
|
265
|
+
return parseFlags(process.env.SPEC_FLAGS).has(key);
|
|
266
|
+
}
|
|
267
|
+
function featureDisabledMessage(key) {
|
|
268
|
+
return `${FEATURES[key].label} is not enabled (coming soon). To try it locally, run with SPEC_FLAGS=${key}.`;
|
|
269
|
+
}
|
|
270
|
+
var FEATURES, FEATURE_KEYS;
|
|
271
|
+
var init_flags = __esm(() => {
|
|
272
|
+
FEATURES = {
|
|
273
|
+
glossary: { label: "Glossary" },
|
|
274
|
+
relations: { label: "Relations" },
|
|
275
|
+
provenance: { label: "Provenance" },
|
|
276
|
+
query: { label: "Query" },
|
|
277
|
+
editor: { label: "Editor" },
|
|
278
|
+
logs: { label: "Logs" }
|
|
279
|
+
};
|
|
280
|
+
FEATURE_KEYS = Object.keys(FEATURES);
|
|
281
|
+
});
|
|
174
282
|
// ../shared/src/net.ts
|
|
175
283
|
function isLoopbackHostname(hostname) {
|
|
176
284
|
return LOOPBACK_HOSTNAMES.has(hostname);
|
|
@@ -340,7 +448,7 @@ CREATE INDEX IF NOT EXISTS idx_term_citations_term ON term_citations(term_id);
|
|
|
340
448
|
-- Invariant #4). source_file + line locate the authored **Issues:** field line.
|
|
341
449
|
-- Exactly ONE index, on req_id (SC3) \u2014 never on issue_id, which would read as
|
|
342
450
|
-- treating the opaque payload as a lookup key (PROV-02, grep-fenced in Plan 04).
|
|
343
|
-
-- @spec PROV-
|
|
451
|
+
-- @spec PROV-004
|
|
344
452
|
CREATE TABLE IF NOT EXISTS provenance (
|
|
345
453
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
346
454
|
req_id TEXT NOT NULL,
|
|
@@ -398,6 +506,7 @@ LEFT JOIN tags t
|
|
|
398
506
|
-- cell. The Q4 ORPHAN_REQ / Q5 UNVERIFIED_REQ queries (sqlite.ts) carry the
|
|
399
507
|
-- SAME literal exclusion so a term never fires a coverage ERROR. The match is
|
|
400
508
|
-- the exact literal 'TERM' \u2014 never a real domain key.
|
|
509
|
+
-- @spec SCHM-017
|
|
401
510
|
WHERE r.key != 'TERM'
|
|
402
511
|
GROUP BY r.id, repos.name;
|
|
403
512
|
|
|
@@ -424,7 +533,7 @@ WHERE r.changed_at_version > repos.pinned_spec_version;
|
|
|
424
533
|
-- the Wave-D diagnostics. term.key='TERM' keeps the VIEW scoped to the reserved
|
|
425
534
|
-- glossary domain. Present-and-empty until Wave C populates term_citations; the
|
|
426
535
|
-- diagnostic (TERM_DRIFT) reads it in Wave E.
|
|
427
|
-
-- @spec CHCK-
|
|
536
|
+
-- @spec CHCK-018
|
|
428
537
|
CREATE VIEW IF NOT EXISTS term_drift AS
|
|
429
538
|
SELECT
|
|
430
539
|
tc.req_id AS req_id,
|
|
@@ -510,7 +619,7 @@ var init_schema = __esm(() => {
|
|
|
510
619
|
// ../shared/src/storage.ts
|
|
511
620
|
var REQUIREMENT_STATUSES;
|
|
512
621
|
var init_storage = __esm(() => {
|
|
513
|
-
REQUIREMENT_STATUSES = ["Active", "Superseded", "Draft", "
|
|
622
|
+
REQUIREMENT_STATUSES = ["Active", "Superseded", "Draft", "Deprecated"];
|
|
514
623
|
});
|
|
515
624
|
|
|
516
625
|
// ../shared/src/index.ts
|
|
@@ -518,6 +627,8 @@ var init_src = __esm(() => {
|
|
|
518
627
|
init_config();
|
|
519
628
|
init_diagnostics();
|
|
520
629
|
init_domain();
|
|
630
|
+
init_ears();
|
|
631
|
+
init_flags();
|
|
521
632
|
init_net();
|
|
522
633
|
init_propagation();
|
|
523
634
|
init_schema();
|
|
@@ -550,6 +661,23 @@ var init_constants = __esm(() => {
|
|
|
550
661
|
OUT_HELP = `DB path override (default: <platformDir>/${INDEX_DIR}/${INDEX_DB_FILENAME})`;
|
|
551
662
|
});
|
|
552
663
|
|
|
664
|
+
// src/parser/requirementLine.ts
|
|
665
|
+
function findRequirementIdLine(rawLines, id, from = 0) {
|
|
666
|
+
const needle = `"id": "${id}"`;
|
|
667
|
+
const exact = `${SEAM_INDENT}${needle}`;
|
|
668
|
+
const start = from > 0 ? from : 0;
|
|
669
|
+
let loose = -1;
|
|
670
|
+
for (let i = start;i < rawLines.length; i++) {
|
|
671
|
+
const line = rawLines[i];
|
|
672
|
+
if (line === exact || line === `${exact},`)
|
|
673
|
+
return i;
|
|
674
|
+
if (loose < 0 && line.includes(needle))
|
|
675
|
+
loose = i;
|
|
676
|
+
}
|
|
677
|
+
return loose;
|
|
678
|
+
}
|
|
679
|
+
var SEAM_INDENT = " ";
|
|
680
|
+
|
|
553
681
|
// src/parser/domainJson.ts
|
|
554
682
|
function parseDomainJsonFile(opts) {
|
|
555
683
|
let parsed;
|
|
@@ -587,6 +715,7 @@ function parseDomainJson(opts) {
|
|
|
587
715
|
const updated = data.updated ?? null;
|
|
588
716
|
const rawLines = text.split(`
|
|
589
717
|
`);
|
|
718
|
+
let lineCursor = 0;
|
|
590
719
|
const working = [];
|
|
591
720
|
const relatesAcc = {
|
|
592
721
|
relations: [],
|
|
@@ -607,7 +736,10 @@ function parseDomainJson(opts) {
|
|
|
607
736
|
};
|
|
608
737
|
for (let idx = 0;idx < data.requirements.length; idx++) {
|
|
609
738
|
const r = data.requirements[idx];
|
|
610
|
-
const
|
|
739
|
+
const found = findRequirementIdLine(rawLines, r.id, lineCursor);
|
|
740
|
+
const line = found >= 0 ? found + 1 : idx + 1;
|
|
741
|
+
if (found >= 0)
|
|
742
|
+
lineCursor = found + 1;
|
|
611
743
|
working.push(mapRequirementRow(r, key, sourceFile, spec_version, line));
|
|
612
744
|
flattenRelates(r, sourceFile, line, relatesAcc);
|
|
613
745
|
flattenIssues(r, sourceFile, line, issuesAcc);
|
|
@@ -631,11 +763,6 @@ function parseDomainJson(opts) {
|
|
|
631
763
|
term_citations: termAcc.citations
|
|
632
764
|
};
|
|
633
765
|
}
|
|
634
|
-
function deriveRequirementLine(rawLines, id, idx) {
|
|
635
|
-
const needle = `"id": "${id}"`;
|
|
636
|
-
const found = rawLines.findIndex((l) => l.includes(needle));
|
|
637
|
-
return found >= 0 ? found + 1 : idx + 1;
|
|
638
|
-
}
|
|
639
766
|
function mapRequirementRow(r, key, sourceFile, spec_version, line) {
|
|
640
767
|
const id = r.id;
|
|
641
768
|
const seq = Number(id.split("-")[1]);
|
|
@@ -772,7 +899,8 @@ var init_domainJson = __esm(() => {
|
|
|
772
899
|
active: "Active",
|
|
773
900
|
draft: "Draft",
|
|
774
901
|
superseded: "Superseded",
|
|
775
|
-
|
|
902
|
+
deprecated: "Deprecated",
|
|
903
|
+
retired: "Deprecated"
|
|
776
904
|
});
|
|
777
905
|
VALID_ROLES = new Set(["created", "supersedes-via", "amends-via"]);
|
|
778
906
|
});
|
|
@@ -1251,10 +1379,11 @@ class SqliteStorage {
|
|
|
1251
1379
|
return row ?? null;
|
|
1252
1380
|
}
|
|
1253
1381
|
listDomains() {
|
|
1254
|
-
return
|
|
1382
|
+
return this.#db.query(LIST_DOMAINS_SQL).all();
|
|
1255
1383
|
}
|
|
1256
|
-
getDomain(
|
|
1257
|
-
|
|
1384
|
+
getDomain(key) {
|
|
1385
|
+
const row = this.#db.query(GET_DOMAIN_SQL).get({ key });
|
|
1386
|
+
return row ?? null;
|
|
1258
1387
|
}
|
|
1259
1388
|
listRequirements(opts) {
|
|
1260
1389
|
const key = opts?.key;
|
|
@@ -1292,6 +1421,8 @@ class SqliteStorage {
|
|
|
1292
1421
|
for (const sql of [
|
|
1293
1422
|
Q1_DANGLING_TAG_SQL,
|
|
1294
1423
|
Q2_SUPERSEDED_REFERENCED_SQL,
|
|
1424
|
+
Q2B_DEPRECATED_REFERENCED_SQL,
|
|
1425
|
+
Q2D_DRAFT_REFERENCED_SQL,
|
|
1295
1426
|
Q3_DRIFT_SQL,
|
|
1296
1427
|
Q4_ORPHAN_REQ_SQL,
|
|
1297
1428
|
Q5_UNVERIFIED_REQ_SQL,
|
|
@@ -1651,6 +1782,34 @@ FROM tags t
|
|
|
1651
1782
|
JOIN requirements r ON r.id = t.req_id
|
|
1652
1783
|
WHERE r.status = 'Superseded'
|
|
1653
1784
|
ORDER BY t.repo, t.file, t.line
|
|
1785
|
+
`, Q2B_DEPRECATED_REFERENCED_SQL = `
|
|
1786
|
+
SELECT
|
|
1787
|
+
'DEPRECATED_REFERENCED' AS code,
|
|
1788
|
+
t.repo AS repo,
|
|
1789
|
+
t.file AS source_file,
|
|
1790
|
+
t.line AS line,
|
|
1791
|
+
t.req_id AS req_id,
|
|
1792
|
+
('Tag references deprecated requirement ' || t.req_id ||
|
|
1793
|
+
' \u2014 it is end-of-life with no successor') AS detail,
|
|
1794
|
+
'error' AS severity
|
|
1795
|
+
FROM tags t
|
|
1796
|
+
JOIN requirements r ON r.id = t.req_id
|
|
1797
|
+
WHERE r.status = 'Deprecated'
|
|
1798
|
+
ORDER BY t.repo, t.file, t.line
|
|
1799
|
+
`, Q2D_DRAFT_REFERENCED_SQL = `
|
|
1800
|
+
SELECT
|
|
1801
|
+
'DRAFT_REFERENCED' AS code,
|
|
1802
|
+
t.repo AS repo,
|
|
1803
|
+
t.file AS source_file,
|
|
1804
|
+
t.line AS line,
|
|
1805
|
+
t.req_id AS req_id,
|
|
1806
|
+
('Tag references Draft requirement ' || t.req_id ||
|
|
1807
|
+
' \u2014 code is bound to a promise nobody approved yet') AS detail,
|
|
1808
|
+
'warning' AS severity
|
|
1809
|
+
FROM tags t
|
|
1810
|
+
JOIN requirements r ON r.id = t.req_id
|
|
1811
|
+
WHERE r.status = 'Draft'
|
|
1812
|
+
ORDER BY t.repo, t.file, t.line
|
|
1654
1813
|
`, Q3_DRIFT_SQL = `
|
|
1655
1814
|
SELECT
|
|
1656
1815
|
'DRIFT' AS code,
|
|
@@ -1680,6 +1839,7 @@ WHERE r.status = 'Active'
|
|
|
1680
1839
|
-- code-coverage obligation \u2014 exclude the reserved TERM domain so a migrated
|
|
1681
1840
|
-- term never fires ORPHAN_REQ. Literal 'TERM' only, never a real domain key;
|
|
1682
1841
|
-- mirrors the coverage VIEW exclusion (WHERE r.key != 'TERM') in schema.ts.
|
|
1842
|
+
-- @spec SCHM-017
|
|
1683
1843
|
AND r.key != 'TERM'
|
|
1684
1844
|
AND NOT EXISTS (
|
|
1685
1845
|
SELECT 1 FROM tags t WHERE t.req_id = r.id AND t.kind != 'documents'
|
|
@@ -1853,7 +2013,7 @@ SELECT
|
|
|
1853
2013
|
FROM repos
|
|
1854
2014
|
WHERE repos.name != 'spec-engine'
|
|
1855
2015
|
ORDER BY repos.name
|
|
1856
|
-
`, FTS_SEARCH_SQL = `SELECT r.id AS req_id, r.key AS key, r.text AS text, r.why AS why, r.source_file AS source_file, r.line AS line, bm25(requirements_fts, 1.0, 0.5) AS rank FROM requirements_fts JOIN requirements r ON r.rowid = requirements_fts.rowid WHERE requirements_fts MATCH $query AND r.status
|
|
2016
|
+
`, FTS_SEARCH_SQL = `SELECT r.id AS req_id, r.key AS key, r.text AS text, r.why AS why, r.source_file AS source_file, r.line AS line, bm25(requirements_fts, 1.0, 0.5) AS rank FROM requirements_fts JOIN requirements r ON r.rowid = requirements_fts.rowid WHERE requirements_fts MATCH $query AND r.status NOT IN ('Superseded', 'Deprecated') ORDER BY rank ASC LIMIT $limit`, RESOLVE_BY_FILES_SQL_PREFIX = "SELECT DISTINCT r.id, r.key, r.seq, r.status, r.superseded_by, r.text, r.why, r.source_file, r.line, r.spec_version, r.changed_at_version, r.superseded_at_version FROM tags t JOIN requirements r ON r.id = t.req_id WHERE t.file IN (", RESOLVE_BY_FILES_SQL_SUFFIX = ") ORDER BY r.key, r.seq", LIST_REPOS_SQL = "SELECT name, path, pinned_spec_version FROM repos ORDER BY name", GET_REPO_SQL = "SELECT name, path, pinned_spec_version FROM repos WHERE name = $name", LIST_DOMAINS_SQL = "SELECT key, owner, schema, spec_version, source_repo FROM domains ORDER BY key", GET_DOMAIN_SQL = "SELECT key, owner, schema, spec_version, source_repo FROM domains WHERE key = $key", LIST_TAGS_SQL = `SELECT id, req_id, repo, file, line, kind, level FROM tags
|
|
1857
2017
|
WHERE ($repo IS NULL OR repo = $repo)
|
|
1858
2018
|
AND ($req_id IS NULL OR req_id = $req_id)
|
|
1859
2019
|
AND ($file IS NULL OR file = $file)
|
|
@@ -2659,8 +2819,8 @@ var init_errors = __esm(() => {
|
|
|
2659
2819
|
});
|
|
2660
2820
|
|
|
2661
2821
|
// src/commands/_shared.ts
|
|
2662
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
|
|
2663
|
-
import { dirname as dirname5 } from "path";
|
|
2822
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync, statSync as statSync4 } from "fs";
|
|
2823
|
+
import { dirname as dirname5, join as join7 } from "path";
|
|
2664
2824
|
function assertContainedPath(resolved, platformDir, subject) {
|
|
2665
2825
|
if (!isContainedPath(resolved, platformDir)) {
|
|
2666
2826
|
console.error(`${subject} path must be inside platformDir (resolved to ${resolved})`);
|
|
@@ -2692,6 +2852,23 @@ async function reindexAndListTags(platformDir, reqId) {
|
|
|
2692
2852
|
storage2.close();
|
|
2693
2853
|
}
|
|
2694
2854
|
}
|
|
2855
|
+
function warnIfIndexStale(platformDir, dbPath) {
|
|
2856
|
+
try {
|
|
2857
|
+
const dbMtime = statSync4(dbPath).mtimeMs;
|
|
2858
|
+
const specsDir = join7(platformDir, "spec-engine");
|
|
2859
|
+
for (const entry of readdirSync(specsDir, { withFileTypes: true })) {
|
|
2860
|
+
if (!entry.isDirectory())
|
|
2861
|
+
continue;
|
|
2862
|
+
const specPath = join7(specsDir, entry.name, "SPEC.json");
|
|
2863
|
+
if (!existsSync4(specPath))
|
|
2864
|
+
continue;
|
|
2865
|
+
if (statSync4(specPath).mtimeMs > dbMtime) {
|
|
2866
|
+
console.error(`spec: warning \u2014 spec-engine/${entry.name}/SPEC.json changed after the index was built; results may be stale. Pass --fresh to rebuild.`);
|
|
2867
|
+
return;
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
} catch {}
|
|
2871
|
+
}
|
|
2695
2872
|
async function withReadStorage(opts, fn) {
|
|
2696
2873
|
const { platformDir, dbPath, fresh } = opts;
|
|
2697
2874
|
try {
|
|
@@ -2704,6 +2881,8 @@ async function withReadStorage(opts, fn) {
|
|
|
2704
2881
|
try {
|
|
2705
2882
|
if (needsIndex || storage2.listRepos().length === 0) {
|
|
2706
2883
|
await runIndex({ platformDir, storage: storage2 });
|
|
2884
|
+
} else {
|
|
2885
|
+
warnIfIndexStale(platformDir, dbPath);
|
|
2707
2886
|
}
|
|
2708
2887
|
await fn(storage2);
|
|
2709
2888
|
} finally {
|
|
@@ -2944,6 +3123,173 @@ function matchesSegment(pat, seg) {
|
|
|
2944
3123
|
return p === pat.length;
|
|
2945
3124
|
}
|
|
2946
3125
|
|
|
3126
|
+
// src/authoring/domains.ts
|
|
3127
|
+
import { existsSync as existsSync5, readdirSync as readdirSync2 } from "fs";
|
|
3128
|
+
import { join as join8 } from "path";
|
|
3129
|
+
function normalizeDomainKey(raw) {
|
|
3130
|
+
return raw.toUpperCase().replace(/\s+/g, "");
|
|
3131
|
+
}
|
|
3132
|
+
function listDomainKeys(platformDir) {
|
|
3133
|
+
const specsDir = join8(platformDir, "spec-engine");
|
|
3134
|
+
return readdirSync2(specsDir, { withFileTypes: true }).filter((e) => e.isDirectory() && existsSync5(join8(specsDir, e.name, "SPEC.json"))).map((e) => e.name).sort();
|
|
3135
|
+
}
|
|
3136
|
+
async function domainScope(platformDir, key) {
|
|
3137
|
+
const jsonPath = join8(platformDir, "spec-engine", key, "SPEC.json");
|
|
3138
|
+
if (!existsSync5(jsonPath))
|
|
3139
|
+
return null;
|
|
3140
|
+
try {
|
|
3141
|
+
const env = JSON.parse(await Bun.file(jsonPath).text());
|
|
3142
|
+
if (typeof env.scope !== "string" || env.scope.trim() === "")
|
|
3143
|
+
return null;
|
|
3144
|
+
return env.scope;
|
|
3145
|
+
} catch (e) {
|
|
3146
|
+
if (e instanceof SyntaxError)
|
|
3147
|
+
return null;
|
|
3148
|
+
console.error(`spec: could not read scope for ${key}: ${e.message}`);
|
|
3149
|
+
return null;
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
async function domainsWithScope(platformDir) {
|
|
3153
|
+
const keys = listDomainKeys(platformDir);
|
|
3154
|
+
return Promise.all(keys.map(async (key) => ({ key, scope: await domainScope(platformDir, key) })));
|
|
3155
|
+
}
|
|
3156
|
+
function scaffoldDomainObject(key, today) {
|
|
3157
|
+
return {
|
|
3158
|
+
key,
|
|
3159
|
+
owner: null,
|
|
3160
|
+
...key === "TERM" ? { specVersion: 1 } : {},
|
|
3161
|
+
updated: today,
|
|
3162
|
+
requirements: []
|
|
3163
|
+
};
|
|
3164
|
+
}
|
|
3165
|
+
function maxSeqOf(domain2) {
|
|
3166
|
+
const reqs = Array.isArray(domain2.requirements) ? domain2.requirements : [];
|
|
3167
|
+
return reqs.reduce((m, r) => {
|
|
3168
|
+
const id = typeof r?.id === "string" ? r.id : "";
|
|
3169
|
+
const seq = Number(id.split("-")[1]);
|
|
3170
|
+
return Number.isFinite(seq) ? Math.max(m, seq) : m;
|
|
3171
|
+
}, 0);
|
|
3172
|
+
}
|
|
3173
|
+
async function headMaxSeq(platformDir, key) {
|
|
3174
|
+
try {
|
|
3175
|
+
const proc = Bun.spawn(["git", "-C", join8(platformDir, "spec-engine", key), "show", "HEAD:./SPEC.json"], { stdout: "pipe", stderr: "ignore" });
|
|
3176
|
+
const text = await new Response(proc.stdout).text();
|
|
3177
|
+
if (await proc.exited !== 0)
|
|
3178
|
+
return 0;
|
|
3179
|
+
return maxSeqOf(JSON.parse(text));
|
|
3180
|
+
} catch {
|
|
3181
|
+
return 0;
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
async function nextRequirementId(platformDir, key) {
|
|
3185
|
+
const jsonPath = join8(platformDir, "spec-engine", key, "SPEC.json");
|
|
3186
|
+
if (!existsSync5(jsonPath))
|
|
3187
|
+
return `${key}-001`;
|
|
3188
|
+
const domain2 = JSON.parse(await Bun.file(jsonPath).text());
|
|
3189
|
+
const maxSeq = Math.max(maxSeqOf(domain2), await headMaxSeq(platformDir, key));
|
|
3190
|
+
const next = String(maxSeq + 1).padStart(3, "0");
|
|
3191
|
+
return `${key}-${next}`;
|
|
3192
|
+
}
|
|
3193
|
+
var init_domains = __esm(() => {
|
|
3194
|
+
init_src();
|
|
3195
|
+
});
|
|
3196
|
+
|
|
3197
|
+
// src/authoring/filerefs.ts
|
|
3198
|
+
import { existsSync as existsSync6 } from "fs";
|
|
3199
|
+
import { resolve as resolve7 } from "path";
|
|
3200
|
+
function extractRefsFromText(s) {
|
|
3201
|
+
const out = [];
|
|
3202
|
+
for (const m of s.matchAll(FILE_REF_RE)) {
|
|
3203
|
+
const candidate = m[1].replace(/\.+$/, "");
|
|
3204
|
+
if (candidate.includes("/"))
|
|
3205
|
+
out.push(candidate);
|
|
3206
|
+
}
|
|
3207
|
+
return out;
|
|
3208
|
+
}
|
|
3209
|
+
function resolveFileRef(platformDir, ref) {
|
|
3210
|
+
const root = resolve7(platformDir);
|
|
3211
|
+
const abs = resolve7(root, ref);
|
|
3212
|
+
if (!(abs === root || abs.startsWith(`${root}/`)))
|
|
3213
|
+
return false;
|
|
3214
|
+
return existsSync6(abs);
|
|
3215
|
+
}
|
|
3216
|
+
var FILE_REF_RE;
|
|
3217
|
+
var init_filerefs = __esm(() => {
|
|
3218
|
+
FILE_REF_RE = /(?<![^\s])@([A-Za-z0-9_][A-Za-z0-9_\-./]*)/g;
|
|
3219
|
+
});
|
|
3220
|
+
|
|
3221
|
+
// src/check/filerefs.ts
|
|
3222
|
+
import { join as join9 } from "path";
|
|
3223
|
+
async function readDomain(platformDir, key) {
|
|
3224
|
+
try {
|
|
3225
|
+
const raw = await Bun.file(join9(platformDir, "spec-engine", key, "SPEC.json")).text();
|
|
3226
|
+
const doc = JSON.parse(raw);
|
|
3227
|
+
return {
|
|
3228
|
+
raw,
|
|
3229
|
+
requirements: Array.isArray(doc.requirements) ? doc.requirements : []
|
|
3230
|
+
};
|
|
3231
|
+
} catch {
|
|
3232
|
+
return null;
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
function rowsForRequirement(platformDir, key, reqId, livesIn, line) {
|
|
3236
|
+
if (!Array.isArray(livesIn))
|
|
3237
|
+
return [];
|
|
3238
|
+
const rows = [];
|
|
3239
|
+
for (const entry of livesIn) {
|
|
3240
|
+
if (typeof entry !== "string")
|
|
3241
|
+
continue;
|
|
3242
|
+
const ref = entry.startsWith("@") ? entry.slice(1) : entry;
|
|
3243
|
+
if (ref.length === 0 || resolveFileRef(platformDir, ref))
|
|
3244
|
+
continue;
|
|
3245
|
+
rows.push({
|
|
3246
|
+
code: DiagnosticCode.BROKEN_FILE_REF,
|
|
3247
|
+
severity: "error",
|
|
3248
|
+
repo: null,
|
|
3249
|
+
source_file: `spec-engine/${key}/SPEC.json`,
|
|
3250
|
+
line,
|
|
3251
|
+
req_id: reqId,
|
|
3252
|
+
detail: `livesIn entry ${entry} does not resolve to a file under the platform root`
|
|
3253
|
+
});
|
|
3254
|
+
}
|
|
3255
|
+
return rows;
|
|
3256
|
+
}
|
|
3257
|
+
function rowsForDomain(platformDir, key, domain2) {
|
|
3258
|
+
const rows = [];
|
|
3259
|
+
const rawLines = domain2.raw.split(`
|
|
3260
|
+
`);
|
|
3261
|
+
let lineCursor = 0;
|
|
3262
|
+
for (const req of domain2.requirements) {
|
|
3263
|
+
if (typeof req?.id !== "string")
|
|
3264
|
+
continue;
|
|
3265
|
+
const found = findRequirementIdLine(rawLines, req.id, lineCursor);
|
|
3266
|
+
if (found >= 0)
|
|
3267
|
+
lineCursor = found + 1;
|
|
3268
|
+
const status = typeof req.status === "string" ? req.status.toLowerCase() : "";
|
|
3269
|
+
if (!CHECKED_STATUSES.has(status))
|
|
3270
|
+
continue;
|
|
3271
|
+
rows.push(...rowsForRequirement(platformDir, key, req.id, req.livesIn, found >= 0 ? found + 1 : 0));
|
|
3272
|
+
}
|
|
3273
|
+
return rows;
|
|
3274
|
+
}
|
|
3275
|
+
async function brokenFileRefDiagnostics(platformDir) {
|
|
3276
|
+
const rows = [];
|
|
3277
|
+
for (const key of listDomainKeys(platformDir)) {
|
|
3278
|
+
const domain2 = await readDomain(platformDir, key);
|
|
3279
|
+
if (domain2 !== null)
|
|
3280
|
+
rows.push(...rowsForDomain(platformDir, key, domain2));
|
|
3281
|
+
}
|
|
3282
|
+
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.detail.localeCompare(b.detail));
|
|
3283
|
+
return rows;
|
|
3284
|
+
}
|
|
3285
|
+
var CHECKED_STATUSES;
|
|
3286
|
+
var init_filerefs2 = __esm(() => {
|
|
3287
|
+
init_src();
|
|
3288
|
+
init_domains();
|
|
3289
|
+
init_filerefs();
|
|
3290
|
+
CHECKED_STATUSES = new Set(["active", "draft"]);
|
|
3291
|
+
});
|
|
3292
|
+
|
|
2947
3293
|
// src/check/format.ts
|
|
2948
3294
|
function compareRepoNullsLast(a, b) {
|
|
2949
3295
|
if (a === b)
|
|
@@ -2987,6 +3333,74 @@ function renderDiagnostics(rows, mode) {
|
|
|
2987
3333
|
`);
|
|
2988
3334
|
}
|
|
2989
3335
|
|
|
3336
|
+
// src/check/grammar.ts
|
|
3337
|
+
import { join as join10 } from "path";
|
|
3338
|
+
async function readDeclaredDomain(platformDir, key) {
|
|
3339
|
+
try {
|
|
3340
|
+
const raw = await Bun.file(join10(platformDir, "spec-engine", key, "SPEC.json")).text();
|
|
3341
|
+
const doc = JSON.parse(raw);
|
|
3342
|
+
if (doc.grammar !== "ears")
|
|
3343
|
+
return null;
|
|
3344
|
+
return {
|
|
3345
|
+
raw,
|
|
3346
|
+
severity: doc.grammarSeverity === "error" ? "error" : "warning",
|
|
3347
|
+
requirements: Array.isArray(doc.requirements) ? doc.requirements : []
|
|
3348
|
+
};
|
|
3349
|
+
} catch {
|
|
3350
|
+
return null;
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
function rowsForDomain2(key, domain2) {
|
|
3354
|
+
const rows = [];
|
|
3355
|
+
const rawLines = domain2.raw.split(`
|
|
3356
|
+
`);
|
|
3357
|
+
let lineCursor = 0;
|
|
3358
|
+
for (const req of domain2.requirements) {
|
|
3359
|
+
if (typeof req?.id !== "string")
|
|
3360
|
+
continue;
|
|
3361
|
+
const found = findRequirementIdLine(rawLines, req.id, lineCursor);
|
|
3362
|
+
if (found >= 0)
|
|
3363
|
+
lineCursor = found + 1;
|
|
3364
|
+
if (typeof req.statement !== "string")
|
|
3365
|
+
continue;
|
|
3366
|
+
const status = typeof req.status === "string" ? req.status.toLowerCase() : "";
|
|
3367
|
+
if (!CHECKED_STATUSES2.has(status))
|
|
3368
|
+
continue;
|
|
3369
|
+
const result = parseEarsStatement(req.statement);
|
|
3370
|
+
if (result.ok)
|
|
3371
|
+
continue;
|
|
3372
|
+
rows.push({
|
|
3373
|
+
code: DiagnosticCode.STATEMENT_GRAMMAR,
|
|
3374
|
+
severity: domain2.severity,
|
|
3375
|
+
repo: null,
|
|
3376
|
+
source_file: `spec-engine/${key}/SPEC.json`,
|
|
3377
|
+
line: found >= 0 ? found + 1 : 0,
|
|
3378
|
+
req_id: req.id,
|
|
3379
|
+
detail: `${result.problem}. Expected one of:
|
|
3380
|
+
${result.expected}`
|
|
3381
|
+
});
|
|
3382
|
+
}
|
|
3383
|
+
return rows;
|
|
3384
|
+
}
|
|
3385
|
+
async function statementGrammarDiagnostics(platformDir) {
|
|
3386
|
+
const rows = [];
|
|
3387
|
+
for (const key of listDomainKeys(platformDir)) {
|
|
3388
|
+
if (key === "TERM")
|
|
3389
|
+
continue;
|
|
3390
|
+
const domain2 = await readDeclaredDomain(platformDir, key);
|
|
3391
|
+
if (domain2 !== null)
|
|
3392
|
+
rows.push(...rowsForDomain2(key, domain2));
|
|
3393
|
+
}
|
|
3394
|
+
rows.sort((a, b) => (a.source_file ?? "").localeCompare(b.source_file ?? "") || (a.line ?? 0) - (b.line ?? 0) || (a.req_id ?? "").localeCompare(b.req_id ?? ""));
|
|
3395
|
+
return rows;
|
|
3396
|
+
}
|
|
3397
|
+
var CHECKED_STATUSES2;
|
|
3398
|
+
var init_grammar = __esm(() => {
|
|
3399
|
+
init_src();
|
|
3400
|
+
init_domains();
|
|
3401
|
+
CHECKED_STATUSES2 = new Set(["active", "draft"]);
|
|
3402
|
+
});
|
|
3403
|
+
|
|
2990
3404
|
// src/check/proven.ts
|
|
2991
3405
|
function normalizeToSegments(path) {
|
|
2992
3406
|
const out = [];
|
|
@@ -3154,7 +3568,8 @@ function requirementRemoved(baseReqs, changeReqs, relPathById) {
|
|
|
3154
3568
|
for (const b of baseReqs) {
|
|
3155
3569
|
if (changeIds.has(b.id))
|
|
3156
3570
|
continue;
|
|
3157
|
-
const
|
|
3571
|
+
const activeAtBase = (b.status ?? "").toLowerCase() === "active";
|
|
3572
|
+
const exempt = activeAtBase && (b.supersededBy != null && changeIds.has(b.supersededBy) || changeReqs.some((y) => y.supersedes === b.id));
|
|
3158
3573
|
if (exempt)
|
|
3159
3574
|
continue;
|
|
3160
3575
|
out.push({
|
|
@@ -3163,7 +3578,7 @@ function requirementRemoved(baseReqs, changeReqs, relPathById) {
|
|
|
3163
3578
|
line: 0,
|
|
3164
3579
|
repo: null,
|
|
3165
3580
|
req_id: b.id,
|
|
3166
|
-
detail: `${b.id} was present in the base ref but is absent from the change with no approved supersession`,
|
|
3581
|
+
detail: activeAtBase ? `${b.id} was present in the base ref but is absent from the change with no approved supersession` : `${b.id} (${b.status}) was present in the base ref but is absent from the change \u2014 a requirement is never deleted, and its history is the record`,
|
|
3167
3582
|
severity: "error"
|
|
3168
3583
|
});
|
|
3169
3584
|
}
|
|
@@ -3193,11 +3608,11 @@ function normalizeHandle(h) {
|
|
|
3193
3608
|
return (h.startsWith("@") ? h.slice(1) : h).toLowerCase();
|
|
3194
3609
|
}
|
|
3195
3610
|
function isNewTerminalFlip(base, change) {
|
|
3196
|
-
if (change
|
|
3611
|
+
if (!TERMINAL_STATUSES.has(change))
|
|
3197
3612
|
return false;
|
|
3198
3613
|
if (base === undefined)
|
|
3199
3614
|
return false;
|
|
3200
|
-
if (base
|
|
3615
|
+
if (TERMINAL_STATUSES.has(base))
|
|
3201
3616
|
return false;
|
|
3202
3617
|
return base !== change;
|
|
3203
3618
|
}
|
|
@@ -3226,8 +3641,76 @@ function unapprovedStatusFlip(baseReqs, changeReqs, codeowners, approvedBy, relP
|
|
|
3226
3641
|
}
|
|
3227
3642
|
return out;
|
|
3228
3643
|
}
|
|
3644
|
+
var TERMINAL_STATUSES;
|
|
3229
3645
|
var init_statusflip = __esm(() => {
|
|
3230
3646
|
init_src();
|
|
3647
|
+
TERMINAL_STATUSES = new Set(["superseded", "deprecated", "retired"]);
|
|
3648
|
+
});
|
|
3649
|
+
|
|
3650
|
+
// src/check/supersedes.ts
|
|
3651
|
+
import { join as join11 } from "path";
|
|
3652
|
+
async function entriesForDomain(platformDir, key) {
|
|
3653
|
+
let raw;
|
|
3654
|
+
let reqs;
|
|
3655
|
+
try {
|
|
3656
|
+
raw = await Bun.file(join11(platformDir, "spec-engine", key, "SPEC.json")).text();
|
|
3657
|
+
const doc = JSON.parse(raw);
|
|
3658
|
+
reqs = Array.isArray(doc.requirements) ? doc.requirements : [];
|
|
3659
|
+
} catch {
|
|
3660
|
+
return [];
|
|
3661
|
+
}
|
|
3662
|
+
const out = [];
|
|
3663
|
+
const rawLines = raw.split(`
|
|
3664
|
+
`);
|
|
3665
|
+
let lineCursor = 0;
|
|
3666
|
+
for (const r of reqs) {
|
|
3667
|
+
if (typeof r?.id !== "string")
|
|
3668
|
+
continue;
|
|
3669
|
+
const found = findRequirementIdLine(rawLines, r.id, lineCursor);
|
|
3670
|
+
if (found >= 0)
|
|
3671
|
+
lineCursor = found + 1;
|
|
3672
|
+
out.push({
|
|
3673
|
+
id: r.id,
|
|
3674
|
+
supersedes: typeof r.supersedes === "string" ? r.supersedes : null,
|
|
3675
|
+
line: found >= 0 ? found + 1 : 0,
|
|
3676
|
+
sourceFile: `spec-engine/${key}/SPEC.json`
|
|
3677
|
+
});
|
|
3678
|
+
}
|
|
3679
|
+
return out;
|
|
3680
|
+
}
|
|
3681
|
+
async function collectEntries(platformDir) {
|
|
3682
|
+
const out = [];
|
|
3683
|
+
for (const key of listDomainKeys(platformDir)) {
|
|
3684
|
+
out.push(...await entriesForDomain(platformDir, key));
|
|
3685
|
+
}
|
|
3686
|
+
return out;
|
|
3687
|
+
}
|
|
3688
|
+
async function supersedesPointerDiagnostics(platformDir) {
|
|
3689
|
+
const entries = await collectEntries(platformDir);
|
|
3690
|
+
const ids = new Set(entries.map((e) => e.id));
|
|
3691
|
+
const rows = [];
|
|
3692
|
+
for (const e of entries) {
|
|
3693
|
+
if (e.supersedes === null)
|
|
3694
|
+
continue;
|
|
3695
|
+
const detail = e.supersedes === e.id ? `${e.id} declares supersedes on itself` : ids.has(e.supersedes) ? null : `${e.id} supersedes ${e.supersedes} which does not exist`;
|
|
3696
|
+
if (detail === null)
|
|
3697
|
+
continue;
|
|
3698
|
+
rows.push({
|
|
3699
|
+
code: DiagnosticCode.BROKEN_SUPERSEDE,
|
|
3700
|
+
severity: "error",
|
|
3701
|
+
repo: null,
|
|
3702
|
+
source_file: e.sourceFile,
|
|
3703
|
+
line: e.line,
|
|
3704
|
+
req_id: e.id,
|
|
3705
|
+
detail
|
|
3706
|
+
});
|
|
3707
|
+
}
|
|
3708
|
+
rows.sort((a, b) => (a.source_file ?? "").localeCompare(b.source_file ?? "") || (a.line ?? 0) - (b.line ?? 0) || (a.req_id ?? "").localeCompare(b.req_id ?? ""));
|
|
3709
|
+
return rows;
|
|
3710
|
+
}
|
|
3711
|
+
var init_supersedes = __esm(() => {
|
|
3712
|
+
init_src();
|
|
3713
|
+
init_domains();
|
|
3231
3714
|
});
|
|
3232
3715
|
|
|
3233
3716
|
// src/check/unsourced.ts
|
|
@@ -3521,24 +4004,255 @@ var init_junit = __esm(() => {
|
|
|
3521
4004
|
]);
|
|
3522
4005
|
});
|
|
3523
4006
|
|
|
3524
|
-
// src/
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
4007
|
+
// src/authoring/edit.ts
|
|
4008
|
+
function localToday() {
|
|
4009
|
+
const d = new Date;
|
|
4010
|
+
return `${d.getFullYear()}-` + `${String(d.getMonth() + 1).padStart(2, "0")}-` + `${String(d.getDate()).padStart(2, "0")}`;
|
|
4011
|
+
}
|
|
4012
|
+
|
|
4013
|
+
// src/commands/glossary.ts
|
|
4014
|
+
var exports_glossary = {};
|
|
4015
|
+
__export(exports_glossary, {
|
|
4016
|
+
parseGlossary: () => parseGlossary,
|
|
4017
|
+
glossaryDriftDiagnostic: () => glossaryDriftDiagnostic,
|
|
4018
|
+
glossaryCommand: () => glossaryCommand,
|
|
4019
|
+
generateGlossary: () => generateGlossary
|
|
3528
4020
|
});
|
|
3529
|
-
import { existsSync as
|
|
3530
|
-
import {
|
|
4021
|
+
import { existsSync as existsSync7, readFileSync } from "fs";
|
|
4022
|
+
import { join as join12, resolve as resolve8 } from "path";
|
|
3531
4023
|
import { defineCommand as defineCommand3 } from "citty";
|
|
3532
|
-
function
|
|
3533
|
-
const
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
4024
|
+
function parseGlossary(md) {
|
|
4025
|
+
const terms = [];
|
|
4026
|
+
let section = null;
|
|
4027
|
+
let buf = null;
|
|
4028
|
+
const flush = () => {
|
|
4029
|
+
if (buf === null)
|
|
4030
|
+
return;
|
|
4031
|
+
const m = buf.match(/^\*\*(.+?)\*\*/);
|
|
4032
|
+
if (m) {
|
|
4033
|
+
const remainder = buf.slice(m[0].length);
|
|
4034
|
+
const statement = remainder.replace(/^ \u2014 /, "").trim();
|
|
4035
|
+
terms.push({ term: m[1], statement, section });
|
|
4036
|
+
}
|
|
4037
|
+
buf = null;
|
|
4038
|
+
};
|
|
4039
|
+
for (const line of md.split(`
|
|
4040
|
+
`)) {
|
|
4041
|
+
if (line.startsWith("## ")) {
|
|
4042
|
+
flush();
|
|
4043
|
+
section = line.slice(3).trim();
|
|
4044
|
+
} else if (line.startsWith("- ")) {
|
|
4045
|
+
flush();
|
|
4046
|
+
buf = line.slice(2);
|
|
4047
|
+
} else if (buf !== null) {
|
|
4048
|
+
if (line.trim() === "")
|
|
4049
|
+
flush();
|
|
4050
|
+
else
|
|
4051
|
+
buf += ` ${line.trim()}`;
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
flush();
|
|
4055
|
+
return terms;
|
|
4056
|
+
}
|
|
4057
|
+
function generateGlossary(terms) {
|
|
4058
|
+
const parts = ["# Glossary", "", GLOSSARY_INTRO];
|
|
4059
|
+
let section;
|
|
4060
|
+
for (const t of terms) {
|
|
4061
|
+
if (t.section !== section) {
|
|
4062
|
+
section = t.section;
|
|
4063
|
+
if (section !== null && section !== undefined) {
|
|
4064
|
+
parts.push("", `## ${section}`, "");
|
|
4065
|
+
}
|
|
4066
|
+
}
|
|
4067
|
+
parts.push(`- **${t.term}** \u2014 ${t.statement}`);
|
|
4068
|
+
}
|
|
4069
|
+
return `${parts.join(`
|
|
4070
|
+
`)}
|
|
4071
|
+
`;
|
|
4072
|
+
}
|
|
4073
|
+
function resolvePlatform(platformDirRaw) {
|
|
4074
|
+
const platformDir = resolve8(platformDirRaw ?? process.cwd());
|
|
4075
|
+
try {
|
|
4076
|
+
assertSpecPlatform(platformDir);
|
|
4077
|
+
} catch (e) {
|
|
4078
|
+
handleNotAPlatform(e);
|
|
4079
|
+
}
|
|
4080
|
+
return platformDir;
|
|
4081
|
+
}
|
|
4082
|
+
function readStoreTerms(platformDir) {
|
|
4083
|
+
const specPath = join12(platformDir, "spec-engine", TERM_KEY, "SPEC.json");
|
|
4084
|
+
if (!existsSync7(specPath))
|
|
4085
|
+
return [];
|
|
4086
|
+
const domain2 = JSON.parse(readFileSync(specPath, "utf8"));
|
|
4087
|
+
const reqs = Array.isArray(domain2.requirements) ? domain2.requirements : [];
|
|
4088
|
+
return reqs.filter((r) => String(r.status).toLowerCase() === "active").sort((a, b) => String(a.id) < String(b.id) ? -1 : String(a.id) > String(b.id) ? 1 : 0).map((r) => ({
|
|
4089
|
+
term: String(r.term ?? ""),
|
|
4090
|
+
statement: String(r.statement ?? ""),
|
|
4091
|
+
section: r.section ?? null
|
|
4092
|
+
}));
|
|
4093
|
+
}
|
|
4094
|
+
function glossaryPath(platformDir) {
|
|
4095
|
+
return join12(platformDir, "GLOSSARY.md");
|
|
4096
|
+
}
|
|
4097
|
+
async function migrateGlossary(platformDir, json) {
|
|
4098
|
+
const relFile = `spec-engine/${TERM_KEY}/SPEC.json`;
|
|
4099
|
+
const specPath = join12(platformDir, "spec-engine", TERM_KEY, "SPEC.json");
|
|
4100
|
+
if (!existsSync7(specPath)) {
|
|
4101
|
+
console.error(`spec glossary: no TERM domain (expected ${relFile} under ${platformDir})`);
|
|
4102
|
+
process.exit(EXIT.USAGE);
|
|
4103
|
+
}
|
|
4104
|
+
const domain2 = JSON.parse(readFileSync(specPath, "utf8"));
|
|
4105
|
+
const existing = Array.isArray(domain2.requirements) ? domain2.requirements : [];
|
|
4106
|
+
if (existing.length > 0) {
|
|
4107
|
+
console.error(`spec glossary: TERM domain already holds ${existing.length} entries \u2014 migration skipped`);
|
|
4108
|
+
if (json)
|
|
4109
|
+
console.log(JSON.stringify({ migrated: 0, skipped: true }));
|
|
4110
|
+
else
|
|
4111
|
+
console.log("migration skipped (TERM domain not empty)");
|
|
4112
|
+
return;
|
|
4113
|
+
}
|
|
4114
|
+
const gPath = glossaryPath(platformDir);
|
|
4115
|
+
if (!existsSync7(gPath)) {
|
|
4116
|
+
console.error(`spec glossary: no GLOSSARY.md to migrate (expected ${gPath})`);
|
|
4117
|
+
process.exit(EXIT.USAGE);
|
|
4118
|
+
}
|
|
4119
|
+
const parsed = parseGlossary(readFileSync(gPath, "utf8"));
|
|
4120
|
+
domain2.requirements = parsed.map((t, i) => ({
|
|
4121
|
+
id: `${TERM_KEY}-${String(i + 1).padStart(3, "0")}`,
|
|
4122
|
+
status: "active",
|
|
4123
|
+
statement: t.statement,
|
|
4124
|
+
term: t.term,
|
|
4125
|
+
why: null,
|
|
4126
|
+
supersedes: null,
|
|
4127
|
+
supersededBy: null,
|
|
4128
|
+
relates: [],
|
|
4129
|
+
livesIn: [],
|
|
4130
|
+
issues: [],
|
|
4131
|
+
aliases: [],
|
|
4132
|
+
cites: [],
|
|
4133
|
+
changedAtVersion: 1,
|
|
4134
|
+
section: t.section
|
|
4135
|
+
}));
|
|
4136
|
+
domain2.updated = localToday();
|
|
4137
|
+
const res = await validateAndWrite(specPath, domain2, relFile);
|
|
4138
|
+
if (!res.ok) {
|
|
4139
|
+
for (const diag of res.diagnostics)
|
|
4140
|
+
console.error(`spec glossary: ${diag.detail}`);
|
|
4141
|
+
process.exit(EXIT.USAGE);
|
|
4142
|
+
}
|
|
4143
|
+
const count = parsed.length;
|
|
4144
|
+
if (json)
|
|
4145
|
+
console.log(JSON.stringify({ migrated: count, file: relFile }));
|
|
4146
|
+
else
|
|
4147
|
+
console.log(`migrated ${count} terms into ${relFile}`);
|
|
4148
|
+
}
|
|
4149
|
+
async function writeGlossary(platformDir, json) {
|
|
4150
|
+
const terms = readStoreTerms(platformDir);
|
|
4151
|
+
await Bun.write(glossaryPath(platformDir), generateGlossary(terms));
|
|
4152
|
+
if (json)
|
|
4153
|
+
console.log(JSON.stringify({ generated: terms.length, file: "GLOSSARY.md" }));
|
|
4154
|
+
else
|
|
4155
|
+
console.log(`generated GLOSSARY.md from ${terms.length} terms`);
|
|
4156
|
+
}
|
|
4157
|
+
function glossaryDriftDiagnostic(platformDir) {
|
|
4158
|
+
const terms = readStoreTerms(platformDir);
|
|
4159
|
+
if (terms.length === 0)
|
|
4160
|
+
return null;
|
|
4161
|
+
const gPath = glossaryPath(platformDir);
|
|
4162
|
+
if (!existsSync7(gPath))
|
|
4163
|
+
return null;
|
|
4164
|
+
const generated = generateGlossary(terms);
|
|
4165
|
+
const committed = readFileSync(gPath, "utf8");
|
|
4166
|
+
if (generated === committed)
|
|
4167
|
+
return null;
|
|
4168
|
+
return {
|
|
4169
|
+
code: DiagnosticCode.GLOSSARY_DRIFT,
|
|
4170
|
+
severity: "warning",
|
|
4171
|
+
repo: null,
|
|
4172
|
+
source_file: "GLOSSARY.md",
|
|
4173
|
+
line: 0,
|
|
4174
|
+
req_id: null,
|
|
4175
|
+
detail: "GLOSSARY.md does not match the TERM store \u2014 run `spec glossary .` to regenerate the human view"
|
|
4176
|
+
};
|
|
4177
|
+
}
|
|
4178
|
+
function checkGlossary(platformDir, json) {
|
|
4179
|
+
const generated = generateGlossary(readStoreTerms(platformDir));
|
|
4180
|
+
const gPath = glossaryPath(platformDir);
|
|
4181
|
+
const committed = existsSync7(gPath) ? readFileSync(gPath, "utf8") : "";
|
|
4182
|
+
if (generated === committed) {
|
|
4183
|
+
if (json)
|
|
4184
|
+
console.log(JSON.stringify({ ok: true }));
|
|
4185
|
+
else
|
|
4186
|
+
console.error("glossary round-trip: OK (committed GLOSSARY.md == generated)");
|
|
4187
|
+
return;
|
|
4188
|
+
}
|
|
4189
|
+
console.error("glossary drift: committed GLOSSARY.md != generated from the TERM store");
|
|
4190
|
+
console.error("run `spec glossary .` to regenerate the human view from the store");
|
|
4191
|
+
if (json)
|
|
4192
|
+
console.log(JSON.stringify({ ok: false }));
|
|
4193
|
+
process.exit(EXIT.FAILURE);
|
|
4194
|
+
}
|
|
4195
|
+
var TERM_KEY = "TERM", GLOSSARY_INTRO = `Canonical names for Spec Engine concepts. When prose, code comments, diagnostics, or
|
|
4196
|
+
docs need one of these ideas, use the term below \u2014 not a synonym. Terms are ordered
|
|
4197
|
+
by the data model, outside-in.`, glossaryCommand;
|
|
4198
|
+
var init_glossary = __esm(() => {
|
|
4199
|
+
init_src();
|
|
4200
|
+
init_constants();
|
|
4201
|
+
init_discover();
|
|
4202
|
+
init__shared();
|
|
4203
|
+
glossaryCommand = defineCommand3({
|
|
4204
|
+
meta: {
|
|
4205
|
+
name: "glossary",
|
|
4206
|
+
description: "Generate GLOSSARY.md from the TERM store (byte-stable); --migrate parses GLOSSARY.md into TERM-001..N; --check fails on drift (committed != generated)"
|
|
4207
|
+
},
|
|
4208
|
+
args: {
|
|
4209
|
+
platformDir: {
|
|
4210
|
+
type: "positional",
|
|
4211
|
+
required: false,
|
|
4212
|
+
description: "Platform directory containing spec-engine/ (default: cwd)"
|
|
4213
|
+
},
|
|
4214
|
+
migrate: {
|
|
4215
|
+
type: "boolean",
|
|
4216
|
+
description: "One-time: parse the committed GLOSSARY.md into TERM-001..N (idempotent-skip)"
|
|
4217
|
+
},
|
|
4218
|
+
check: {
|
|
4219
|
+
type: "boolean",
|
|
4220
|
+
description: "Fail (exit 1) if the committed GLOSSARY.md differs from the generated output"
|
|
4221
|
+
},
|
|
4222
|
+
json: { type: "boolean", description: "Emit a JSON result instead of the text summary" }
|
|
4223
|
+
},
|
|
4224
|
+
async run({ args }) {
|
|
4225
|
+
const platformDir = resolvePlatform(args.platformDir);
|
|
4226
|
+
const json = Boolean(args.json);
|
|
4227
|
+
if (args.migrate) {
|
|
4228
|
+
await migrateGlossary(platformDir, json);
|
|
4229
|
+
} else if (args.check) {
|
|
4230
|
+
checkGlossary(platformDir, json);
|
|
4231
|
+
} else {
|
|
4232
|
+
await writeGlossary(platformDir, json);
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4235
|
+
});
|
|
4236
|
+
});
|
|
4237
|
+
|
|
4238
|
+
// src/commands/check.ts
|
|
4239
|
+
var exports_check = {};
|
|
4240
|
+
__export(exports_check, {
|
|
4241
|
+
checkCommand: () => checkCommand
|
|
4242
|
+
});
|
|
4243
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync2 } from "fs";
|
|
4244
|
+
import { dirname as dirname7, join as join13, resolve as resolve9 } from "path";
|
|
4245
|
+
import { defineCommand as defineCommand4 } from "citty";
|
|
4246
|
+
function readCodeownersText(platformDir) {
|
|
4247
|
+
const path = join13(platformDir, ".github", "CODEOWNERS");
|
|
4248
|
+
if (!existsSync8(path))
|
|
4249
|
+
return null;
|
|
4250
|
+
return readFileSync2(path, "utf-8");
|
|
4251
|
+
}
|
|
4252
|
+
function domainReqsFromText(text, platformRel) {
|
|
4253
|
+
let parsed;
|
|
4254
|
+
try {
|
|
4255
|
+
parsed = JSON.parse(text);
|
|
3542
4256
|
} catch (err) {
|
|
3543
4257
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3544
4258
|
return {
|
|
@@ -3570,7 +4284,7 @@ async function ingestResults(storage2, platformDir, resultsArg, jsonMode, diagno
|
|
|
3570
4284
|
}
|
|
3571
4285
|
return { resultsParsed: undefined, verifyingTags: undefined };
|
|
3572
4286
|
}
|
|
3573
|
-
const resultsPath =
|
|
4287
|
+
const resultsPath = resolve9(platformDir, resultsArg);
|
|
3574
4288
|
if (!isContainedPath(resultsPath, platformDir)) {
|
|
3575
4289
|
console.error(`spec check: --results path must be inside platformDir (resolved to ${resultsPath})`);
|
|
3576
4290
|
try {
|
|
@@ -3590,7 +4304,7 @@ async function collectChangeReqs(canonicalDir) {
|
|
|
3590
4304
|
const relPathById = new Map;
|
|
3591
4305
|
for (const rel of await findDomainJsonFiles(canonicalDir)) {
|
|
3592
4306
|
const platformRel = `spec-engine/${rel}`;
|
|
3593
|
-
const text = await Bun.file(
|
|
4307
|
+
const text = await Bun.file(join13(canonicalDir, rel)).text();
|
|
3594
4308
|
const parsed = domainReqsFromText(text, platformRel);
|
|
3595
4309
|
for (const r of parsed.reqs) {
|
|
3596
4310
|
reqs.push(r);
|
|
@@ -3630,7 +4344,7 @@ async function runGovernanceGate(storage2, platformDir, args, diagnostics2, resu
|
|
|
3630
4344
|
} catch {}
|
|
3631
4345
|
process.exit(EXIT.USAGE);
|
|
3632
4346
|
}
|
|
3633
|
-
const canonicalDir =
|
|
4347
|
+
const canonicalDir = join13(platformDir, "spec-engine");
|
|
3634
4348
|
const change = await collectChangeReqs(canonicalDir);
|
|
3635
4349
|
const base = collectBaseReqs(platformDir, ref, diagnostics2);
|
|
3636
4350
|
diagnostics2.push(...requirementRemoved(base.reqs, change.reqs, (id) => base.relPathById.get(id) ?? null));
|
|
@@ -3656,10 +4370,13 @@ function exitAfterCrash(e, storage2) {
|
|
|
3656
4370
|
var checkCommand;
|
|
3657
4371
|
var init_check = __esm(() => {
|
|
3658
4372
|
init_src();
|
|
4373
|
+
init_filerefs2();
|
|
4374
|
+
init_grammar();
|
|
3659
4375
|
init_propagation_teeth();
|
|
3660
4376
|
init_proven();
|
|
3661
4377
|
init_removed();
|
|
3662
4378
|
init_statusflip();
|
|
4379
|
+
init_supersedes();
|
|
3663
4380
|
init_unsourced();
|
|
3664
4381
|
init_constants();
|
|
3665
4382
|
init_discover();
|
|
@@ -3669,7 +4386,8 @@ var init_check = __esm(() => {
|
|
|
3669
4386
|
init_fs();
|
|
3670
4387
|
init_sqlite();
|
|
3671
4388
|
init__shared();
|
|
3672
|
-
|
|
4389
|
+
init_glossary();
|
|
4390
|
+
checkCommand = defineCommand4({
|
|
3673
4391
|
meta: {
|
|
3674
4392
|
name: "check",
|
|
3675
4393
|
description: "Cross-repo integrity check. --ci rebuilds the index cold (rm db+wal+shm). Exits 1 on any error-severity diagnostic."
|
|
@@ -3718,7 +4436,7 @@ var init_check = __esm(() => {
|
|
|
3718
4436
|
}
|
|
3719
4437
|
},
|
|
3720
4438
|
async run({ args }) {
|
|
3721
|
-
const platformDir =
|
|
4439
|
+
const platformDir = resolve9(args.platformDir ?? process.cwd());
|
|
3722
4440
|
const outArg = args.out;
|
|
3723
4441
|
const dbPath = resolveDbPath(platformDir, outArg);
|
|
3724
4442
|
if (outArg)
|
|
@@ -3747,12 +4465,24 @@ var init_check = __esm(() => {
|
|
|
3747
4465
|
const provenance = storage2.listProvenance();
|
|
3748
4466
|
diagnostics2.push(...unsourcedChanges(superseded, provenance));
|
|
3749
4467
|
}
|
|
4468
|
+
diagnostics2.push(...await statementGrammarDiagnostics(platformDir));
|
|
4469
|
+
diagnostics2.push(...await brokenFileRefDiagnostics(platformDir));
|
|
4470
|
+
diagnostics2.push(...await supersedesPointerDiagnostics(platformDir));
|
|
4471
|
+
const glossaryDrift = glossaryDriftDiagnostic(platformDir);
|
|
4472
|
+
if (glossaryDrift !== null)
|
|
4473
|
+
diagnostics2.push(glossaryDrift);
|
|
3750
4474
|
const results = await ingestResults(storage2, platformDir, args.results, !!args.json, diagnostics2);
|
|
3751
4475
|
await runGovernanceGate(storage2, platformDir, {
|
|
3752
4476
|
base: args.base,
|
|
3753
4477
|
approvedBy: args.approvedBy,
|
|
3754
4478
|
requireOwnerApproval: args.requireOwnerApproval
|
|
3755
4479
|
}, diagnostics2, results);
|
|
4480
|
+
if (!args.base && gitRefResolves(platformDir, "HEAD")) {
|
|
4481
|
+
const change = await collectChangeReqs(join13(platformDir, "spec-engine"));
|
|
4482
|
+
const baseParseSink = [];
|
|
4483
|
+
const base = collectBaseReqs(platformDir, "HEAD", baseParseSink);
|
|
4484
|
+
diagnostics2.push(...requirementRemoved(base.reqs, change.reqs, (id) => base.relPathById.get(id) ?? null));
|
|
4485
|
+
}
|
|
3756
4486
|
const output = renderDiagnostics(diagnostics2, args.json ? "json" : "text");
|
|
3757
4487
|
console.log(output);
|
|
3758
4488
|
if (!args.json) {
|
|
@@ -3768,63 +4498,6 @@ var init_check = __esm(() => {
|
|
|
3768
4498
|
});
|
|
3769
4499
|
});
|
|
3770
4500
|
|
|
3771
|
-
// src/authoring/domains.ts
|
|
3772
|
-
import { existsSync as existsSync6, readdirSync } from "fs";
|
|
3773
|
-
import { join as join8 } from "path";
|
|
3774
|
-
function normalizeDomainKey(raw) {
|
|
3775
|
-
return raw.toUpperCase().replace(/\s+/g, "");
|
|
3776
|
-
}
|
|
3777
|
-
function listDomainKeys(platformDir) {
|
|
3778
|
-
const specsDir = join8(platformDir, "spec-engine");
|
|
3779
|
-
return readdirSync(specsDir, { withFileTypes: true }).filter((e) => e.isDirectory() && existsSync6(join8(specsDir, e.name, "SPEC.json"))).map((e) => e.name).sort();
|
|
3780
|
-
}
|
|
3781
|
-
async function domainScope(platformDir, key) {
|
|
3782
|
-
const jsonPath = join8(platformDir, "spec-engine", key, "SPEC.json");
|
|
3783
|
-
if (!existsSync6(jsonPath))
|
|
3784
|
-
return null;
|
|
3785
|
-
try {
|
|
3786
|
-
const env = JSON.parse(await Bun.file(jsonPath).text());
|
|
3787
|
-
if (typeof env.scope !== "string" || env.scope.trim() === "")
|
|
3788
|
-
return null;
|
|
3789
|
-
return env.scope;
|
|
3790
|
-
} catch (e) {
|
|
3791
|
-
if (e instanceof SyntaxError)
|
|
3792
|
-
return null;
|
|
3793
|
-
console.error(`spec: could not read scope for ${key}: ${e.message}`);
|
|
3794
|
-
return null;
|
|
3795
|
-
}
|
|
3796
|
-
}
|
|
3797
|
-
async function domainsWithScope(platformDir) {
|
|
3798
|
-
const keys = listDomainKeys(platformDir);
|
|
3799
|
-
return Promise.all(keys.map(async (key) => ({ key, scope: await domainScope(platformDir, key) })));
|
|
3800
|
-
}
|
|
3801
|
-
function scaffoldDomainObject(key, today) {
|
|
3802
|
-
return {
|
|
3803
|
-
key,
|
|
3804
|
-
owner: null,
|
|
3805
|
-
...key === "TERM" ? { specVersion: 1 } : {},
|
|
3806
|
-
updated: today,
|
|
3807
|
-
requirements: []
|
|
3808
|
-
};
|
|
3809
|
-
}
|
|
3810
|
-
async function nextRequirementId(platformDir, key) {
|
|
3811
|
-
const jsonPath = join8(platformDir, "spec-engine", key, "SPEC.json");
|
|
3812
|
-
if (!existsSync6(jsonPath))
|
|
3813
|
-
return `${key}-001`;
|
|
3814
|
-
const domain2 = JSON.parse(await Bun.file(jsonPath).text());
|
|
3815
|
-
const reqs = Array.isArray(domain2.requirements) ? domain2.requirements : [];
|
|
3816
|
-
const maxSeq = reqs.reduce((m, r) => {
|
|
3817
|
-
const id = typeof r?.id === "string" ? r.id : "";
|
|
3818
|
-
const seq = Number(id.split("-")[1]);
|
|
3819
|
-
return Number.isFinite(seq) ? Math.max(m, seq) : m;
|
|
3820
|
-
}, 0);
|
|
3821
|
-
const next = String(maxSeq + 1).padStart(3, "0");
|
|
3822
|
-
return `${key}-${next}`;
|
|
3823
|
-
}
|
|
3824
|
-
var init_domains = __esm(() => {
|
|
3825
|
-
init_src();
|
|
3826
|
-
});
|
|
3827
|
-
|
|
3828
4501
|
// src/commands/domain.ts
|
|
3829
4502
|
var exports_domain = {};
|
|
3830
4503
|
__export(exports_domain, {
|
|
@@ -3832,9 +4505,9 @@ __export(exports_domain, {
|
|
|
3832
4505
|
domainListCommand: () => domainListCommand,
|
|
3833
4506
|
domainCommand: () => domainCommand
|
|
3834
4507
|
});
|
|
3835
|
-
import { existsSync as
|
|
3836
|
-
import { dirname as dirname8, join as
|
|
3837
|
-
import { defineCommand as
|
|
4508
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync5 } from "fs";
|
|
4509
|
+
import { dirname as dirname8, join as join14, resolve as resolve10 } from "path";
|
|
4510
|
+
import { defineCommand as defineCommand5 } from "citty";
|
|
3838
4511
|
var domainNewCommand, domainListCommand, domainCommand;
|
|
3839
4512
|
var init_domain2 = __esm(() => {
|
|
3840
4513
|
init_src();
|
|
@@ -3842,7 +4515,7 @@ var init_domain2 = __esm(() => {
|
|
|
3842
4515
|
init_constants();
|
|
3843
4516
|
init_discover();
|
|
3844
4517
|
init__shared();
|
|
3845
|
-
domainNewCommand =
|
|
4518
|
+
domainNewCommand = defineCommand5({
|
|
3846
4519
|
meta: {
|
|
3847
4520
|
name: "new",
|
|
3848
4521
|
description: "Scaffold a fresh spec-engine/<KEY>/SPEC.json (input normalized)"
|
|
@@ -3861,7 +4534,7 @@ var init_domain2 = __esm(() => {
|
|
|
3861
4534
|
},
|
|
3862
4535
|
async run({ args }) {
|
|
3863
4536
|
const raw = args.name;
|
|
3864
|
-
const platformDir =
|
|
4537
|
+
const platformDir = resolve10(args.platformDir ?? process.cwd());
|
|
3865
4538
|
const key = normalizeDomainKey(raw);
|
|
3866
4539
|
if (key !== raw) {
|
|
3867
4540
|
console.log(`spec domain: normalized "${raw}" \u2192 ${key}`);
|
|
@@ -3871,21 +4544,21 @@ var init_domain2 = __esm(() => {
|
|
|
3871
4544
|
process.exit(EXIT.USAGE);
|
|
3872
4545
|
return;
|
|
3873
4546
|
}
|
|
3874
|
-
const dest =
|
|
3875
|
-
const resolvedDest =
|
|
3876
|
-
const resolvedRoot =
|
|
4547
|
+
const dest = join14(platformDir, "spec-engine", key, "SPEC.json");
|
|
4548
|
+
const resolvedDest = resolve10(dest);
|
|
4549
|
+
const resolvedRoot = resolve10(platformDir);
|
|
3877
4550
|
if (!(resolvedDest === resolvedRoot || resolvedDest.startsWith(`${resolvedRoot}/`))) {
|
|
3878
4551
|
console.error(`spec domain new: refusing to write outside platformDir (${resolvedDest})`);
|
|
3879
4552
|
process.exit(EXIT.USAGE);
|
|
3880
4553
|
return;
|
|
3881
4554
|
}
|
|
3882
|
-
if (
|
|
4555
|
+
if (existsSync9(dest)) {
|
|
3883
4556
|
console.error(`refusing to overwrite ${dest}`);
|
|
3884
4557
|
process.exit(EXIT.USAGE);
|
|
3885
4558
|
return;
|
|
3886
4559
|
}
|
|
3887
|
-
const mdSibling =
|
|
3888
|
-
if (
|
|
4560
|
+
const mdSibling = join14(dirname8(dest), "SPEC.md");
|
|
4561
|
+
if (existsSync9(mdSibling)) {
|
|
3889
4562
|
console.error(`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)");
|
|
3890
4563
|
process.exit(EXIT.USAGE);
|
|
3891
4564
|
return;
|
|
@@ -3905,7 +4578,7 @@ var init_domain2 = __esm(() => {
|
|
|
3905
4578
|
console.log(`created ${relFile}`);
|
|
3906
4579
|
}
|
|
3907
4580
|
});
|
|
3908
|
-
domainListCommand =
|
|
4581
|
+
domainListCommand = defineCommand5({
|
|
3909
4582
|
meta: {
|
|
3910
4583
|
name: "list",
|
|
3911
4584
|
description: "List domain keys (sorted, read from the filesystem)"
|
|
@@ -3922,7 +4595,7 @@ var init_domain2 = __esm(() => {
|
|
|
3922
4595
|
}
|
|
3923
4596
|
},
|
|
3924
4597
|
async run({ args }) {
|
|
3925
|
-
const platformDir =
|
|
4598
|
+
const platformDir = resolve10(args.platformDir ?? process.cwd());
|
|
3926
4599
|
try {
|
|
3927
4600
|
assertSpecPlatform(platformDir);
|
|
3928
4601
|
} catch (e) {
|
|
@@ -3937,7 +4610,7 @@ var init_domain2 = __esm(() => {
|
|
|
3937
4610
|
}
|
|
3938
4611
|
}
|
|
3939
4612
|
});
|
|
3940
|
-
domainCommand =
|
|
4613
|
+
domainCommand = defineCommand5({
|
|
3941
4614
|
meta: {
|
|
3942
4615
|
name: "domain",
|
|
3943
4616
|
description: "Manage spec domains (spec-engine/<KEY>/)"
|
|
@@ -4016,15 +4689,15 @@ var exports_map = {};
|
|
|
4016
4689
|
__export(exports_map, {
|
|
4017
4690
|
mapCommand: () => mapCommand
|
|
4018
4691
|
});
|
|
4019
|
-
import { resolve as
|
|
4020
|
-
import { defineCommand as
|
|
4692
|
+
import { resolve as resolve11 } from "path";
|
|
4693
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
4021
4694
|
var mapCommand;
|
|
4022
4695
|
var init_map = __esm(() => {
|
|
4023
4696
|
init_constants();
|
|
4024
4697
|
init_discover();
|
|
4025
4698
|
init_prompt();
|
|
4026
4699
|
init__shared();
|
|
4027
|
-
mapCommand =
|
|
4700
|
+
mapCommand = defineCommand6({
|
|
4028
4701
|
meta: {
|
|
4029
4702
|
name: "map",
|
|
4030
4703
|
description: "Render the cross-repo coverage matrix from the coverage VIEW. --json emits a deterministically sorted JSON array."
|
|
@@ -4053,7 +4726,7 @@ var init_map = __esm(() => {
|
|
|
4053
4726
|
}
|
|
4054
4727
|
},
|
|
4055
4728
|
async run({ args }) {
|
|
4056
|
-
const platformDir =
|
|
4729
|
+
const platformDir = resolve11(args.platformDir ?? process.cwd());
|
|
4057
4730
|
const outArg = args.out;
|
|
4058
4731
|
const dbPath = resolveDbPath(platformDir, outArg);
|
|
4059
4732
|
if (outArg)
|
|
@@ -4103,15 +4776,15 @@ var exports_propagation = {};
|
|
|
4103
4776
|
__export(exports_propagation, {
|
|
4104
4777
|
propagationCommand: () => propagationCommand
|
|
4105
4778
|
});
|
|
4106
|
-
import { resolve as
|
|
4107
|
-
import { defineCommand as
|
|
4779
|
+
import { resolve as resolve12 } from "path";
|
|
4780
|
+
import { defineCommand as defineCommand7 } from "citty";
|
|
4108
4781
|
var propagationCommand;
|
|
4109
4782
|
var init_propagation2 = __esm(() => {
|
|
4110
4783
|
init_constants();
|
|
4111
4784
|
init_discover();
|
|
4112
4785
|
init_prompt();
|
|
4113
4786
|
init__shared();
|
|
4114
|
-
propagationCommand =
|
|
4787
|
+
propagationCommand = defineCommand7({
|
|
4115
4788
|
meta: {
|
|
4116
4789
|
name: "propagation",
|
|
4117
4790
|
description: "Classify each member repo's relationship to a target requirement (PROP-01). States: MIGRATED_VERIFIED, MIGRATED_UNVERIFIED, ON_PREDECESSOR, ON_OTHER_DOMAIN_REQ, NO_DOMAIN_REFERENCE."
|
|
@@ -4151,7 +4824,7 @@ var init_propagation2 = __esm(() => {
|
|
|
4151
4824
|
process.exit(EXIT.USAGE);
|
|
4152
4825
|
return;
|
|
4153
4826
|
}
|
|
4154
|
-
const platformDir =
|
|
4827
|
+
const platformDir = resolve12(args.platformDir ?? process.cwd());
|
|
4155
4828
|
const outArg = args.out;
|
|
4156
4829
|
const dbPath = resolveDbPath(platformDir, outArg);
|
|
4157
4830
|
if (outArg)
|
|
@@ -4225,8 +4898,8 @@ var exports_query = {};
|
|
|
4225
4898
|
__export(exports_query, {
|
|
4226
4899
|
queryCommand: () => queryCommand
|
|
4227
4900
|
});
|
|
4228
|
-
import { resolve as
|
|
4229
|
-
import { defineCommand as
|
|
4901
|
+
import { resolve as resolve13 } from "path";
|
|
4902
|
+
import { defineCommand as defineCommand8 } from "citty";
|
|
4230
4903
|
var queryCommand;
|
|
4231
4904
|
var init_query = __esm(() => {
|
|
4232
4905
|
init_src();
|
|
@@ -4234,7 +4907,7 @@ var init_query = __esm(() => {
|
|
|
4234
4907
|
init_discover();
|
|
4235
4908
|
init_prompt();
|
|
4236
4909
|
init__shared();
|
|
4237
|
-
queryCommand =
|
|
4910
|
+
queryCommand = defineCommand8({
|
|
4238
4911
|
meta: {
|
|
4239
4912
|
name: "query",
|
|
4240
4913
|
description: "FTS5 retrieval over requirement text + why. Default LIMIT 10. Query syntax follows SQLite FTS5 MATCH (wrap phrases in double quotes; literal AND/OR/NOT are operators)."
|
|
@@ -4291,7 +4964,7 @@ var init_query = __esm(() => {
|
|
|
4291
4964
|
process.exit(EXIT.USAGE);
|
|
4292
4965
|
return;
|
|
4293
4966
|
}
|
|
4294
|
-
const platformDir =
|
|
4967
|
+
const platformDir = resolve13(args.platformDir ?? process.cwd());
|
|
4295
4968
|
const outArg = args.out;
|
|
4296
4969
|
const dbPath = resolveDbPath(platformDir, outArg);
|
|
4297
4970
|
if (outArg)
|
|
@@ -4380,8 +5053,8 @@ var exports_relations = {};
|
|
|
4380
5053
|
__export(exports_relations, {
|
|
4381
5054
|
relationsCommand: () => relationsCommand
|
|
4382
5055
|
});
|
|
4383
|
-
import { resolve as
|
|
4384
|
-
import { defineCommand as
|
|
5056
|
+
import { resolve as resolve14 } from "path";
|
|
5057
|
+
import { defineCommand as defineCommand9 } from "citty";
|
|
4385
5058
|
function formatNoRelations(platformDir) {
|
|
4386
5059
|
return [
|
|
4387
5060
|
`No Relates links indexed under ${platformDir}.`,
|
|
@@ -4394,7 +5067,7 @@ var init_relations = __esm(() => {
|
|
|
4394
5067
|
init_constants();
|
|
4395
5068
|
init_prompt();
|
|
4396
5069
|
init__shared();
|
|
4397
|
-
relationsCommand =
|
|
5070
|
+
relationsCommand = defineCommand9({
|
|
4398
5071
|
meta: {
|
|
4399
5072
|
name: "relations",
|
|
4400
5073
|
description: "Render the Relates links between requirements as a mermaid entity diagram. --json emits the sorted relation rows."
|
|
@@ -4423,7 +5096,7 @@ var init_relations = __esm(() => {
|
|
|
4423
5096
|
}
|
|
4424
5097
|
},
|
|
4425
5098
|
async run({ args }) {
|
|
4426
|
-
const platformDir =
|
|
5099
|
+
const platformDir = resolve14(args.platformDir ?? process.cwd());
|
|
4427
5100
|
const outArg = args.out;
|
|
4428
5101
|
const dbPath = resolveDbPath(platformDir, outArg);
|
|
4429
5102
|
if (outArg)
|
|
@@ -4510,7 +5183,8 @@ function renderProvenance(rows, mode) {
|
|
|
4510
5183
|
function decorateRow(row, resolved) {
|
|
4511
5184
|
const prefix = ` ${row.role} ${row.issue_id} ${row.source_file}:${row.line}`;
|
|
4512
5185
|
if (resolved?.ok && resolved.title && resolved.url) {
|
|
4513
|
-
|
|
5186
|
+
const staleMark = resolved.stale ? " (cached \u2014 refresh failed)" : "";
|
|
5187
|
+
return `${prefix} ${resolved.title} [${resolved.status ?? ""}] ${resolved.url}${staleMark}`;
|
|
4514
5188
|
}
|
|
4515
5189
|
return `${prefix} (${TOKEN_HINT})`;
|
|
4516
5190
|
}
|
|
@@ -4519,7 +5193,12 @@ function renderProvenanceDecorated(rows, resolved, mode) {
|
|
|
4519
5193
|
if (mode === "json") {
|
|
4520
5194
|
return JSON.stringify(sorted.map((r) => {
|
|
4521
5195
|
const hit = resolved.get(r.issue_id);
|
|
4522
|
-
const resolvedField = hit?.ok === true ? {
|
|
5196
|
+
const resolvedField = hit?.ok === true ? {
|
|
5197
|
+
title: hit.title ?? "",
|
|
5198
|
+
status: hit.status ?? "",
|
|
5199
|
+
url: hit.url ?? "",
|
|
5200
|
+
stale: hit.stale === true
|
|
5201
|
+
} : null;
|
|
4523
5202
|
return { ...r, resolved: resolvedField };
|
|
4524
5203
|
}));
|
|
4525
5204
|
}
|
|
@@ -4553,7 +5232,7 @@ var init_adapter = () => {};
|
|
|
4553
5232
|
|
|
4554
5233
|
// ../tracker/src/cache.ts
|
|
4555
5234
|
import { mkdirSync as mkdirSync6 } from "fs";
|
|
4556
|
-
import { dirname as dirname9, join as
|
|
5235
|
+
import { dirname as dirname9, join as join15 } from "path";
|
|
4557
5236
|
function isFresh(entry, now, ttlMs = CACHE_TTL_MS) {
|
|
4558
5237
|
const t = Date.parse(entry.fetched_at);
|
|
4559
5238
|
if (Number.isNaN(t))
|
|
@@ -4567,7 +5246,7 @@ function isValidEntry(e) {
|
|
|
4567
5246
|
return typeof r.title === "string" && typeof r.status === "string" && typeof r.url === "string" && typeof r.fetched_at === "string";
|
|
4568
5247
|
}
|
|
4569
5248
|
function sidecarPath(platformDir) {
|
|
4570
|
-
return
|
|
5249
|
+
return join15(platformDir, ".spec-engine", SIDECAR_FILE);
|
|
4571
5250
|
}
|
|
4572
5251
|
async function readCache(platformDir) {
|
|
4573
5252
|
try {
|
|
@@ -4735,7 +5414,13 @@ async function resolveAndCache(rows, platformDir, adapter = makeLinearAdapter(),
|
|
|
4735
5414
|
const out = new Map;
|
|
4736
5415
|
for (const id of uniqueIds) {
|
|
4737
5416
|
const entry = next[id];
|
|
4738
|
-
out.set(id, entry ? {
|
|
5417
|
+
out.set(id, entry ? {
|
|
5418
|
+
ok: true,
|
|
5419
|
+
title: entry.title,
|
|
5420
|
+
status: entry.status,
|
|
5421
|
+
url: entry.url,
|
|
5422
|
+
stale: !isFresh(entry, now)
|
|
5423
|
+
} : { ok: false });
|
|
4739
5424
|
}
|
|
4740
5425
|
return out;
|
|
4741
5426
|
}
|
|
@@ -4748,9 +5433,9 @@ var exports_provenance = {};
|
|
|
4748
5433
|
__export(exports_provenance, {
|
|
4749
5434
|
provenanceCommand: () => provenanceCommand
|
|
4750
5435
|
});
|
|
4751
|
-
import { statSync as
|
|
4752
|
-
import { resolve as
|
|
4753
|
-
import { defineCommand as
|
|
5436
|
+
import { statSync as statSync5 } from "fs";
|
|
5437
|
+
import { resolve as resolve15 } from "path";
|
|
5438
|
+
import { defineCommand as defineCommand10 } from "citty";
|
|
4754
5439
|
function formatNoProvenance(platformDir) {
|
|
4755
5440
|
return [
|
|
4756
5441
|
`No provenance links indexed under ${platformDir}.`,
|
|
@@ -4767,7 +5452,7 @@ var init_provenance = __esm(() => {
|
|
|
4767
5452
|
init_prompt();
|
|
4768
5453
|
init_resolve();
|
|
4769
5454
|
init__shared();
|
|
4770
|
-
provenanceCommand =
|
|
5455
|
+
provenanceCommand = defineCommand10({
|
|
4771
5456
|
meta: {
|
|
4772
5457
|
name: "provenance",
|
|
4773
5458
|
description: "Render the per-requirement provenance matrix (creating/revising issues + backing tests + git pointer). --json emits the deterministically-sorted matrix rows."
|
|
@@ -4810,7 +5495,7 @@ var init_provenance = __esm(() => {
|
|
|
4810
5495
|
if (issueId && platformArg === undefined) {
|
|
4811
5496
|
let isDir = false;
|
|
4812
5497
|
try {
|
|
4813
|
-
isDir =
|
|
5498
|
+
isDir = statSync5(resolve15(issueId)).isDirectory();
|
|
4814
5499
|
} catch {
|
|
4815
5500
|
isDir = false;
|
|
4816
5501
|
}
|
|
@@ -4819,7 +5504,7 @@ var init_provenance = __esm(() => {
|
|
|
4819
5504
|
issueId = "";
|
|
4820
5505
|
}
|
|
4821
5506
|
}
|
|
4822
|
-
const platformDir =
|
|
5507
|
+
const platformDir = resolve15(platformArg ?? process.cwd());
|
|
4823
5508
|
const outArg = args.out;
|
|
4824
5509
|
const dbPath = resolveDbPath(platformDir, outArg);
|
|
4825
5510
|
if (outArg)
|
|
@@ -4844,34 +5529,42 @@ var init_provenance = __esm(() => {
|
|
|
4844
5529
|
});
|
|
4845
5530
|
});
|
|
4846
5531
|
|
|
4847
|
-
// src/authoring/
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
for (const m of s.matchAll(FILE_REF_RE)) {
|
|
4859
|
-
const candidate = m[1].replace(/\.+$/, "");
|
|
4860
|
-
if (candidate.includes("/"))
|
|
4861
|
-
out.push(candidate);
|
|
5532
|
+
// src/authoring/grammar.ts
|
|
5533
|
+
import { join as join16 } from "path";
|
|
5534
|
+
async function grammarConfig(platformDir, key) {
|
|
5535
|
+
try {
|
|
5536
|
+
const doc = JSON.parse(await Bun.file(join16(platformDir, "spec-engine", key, "SPEC.json")).text());
|
|
5537
|
+
return {
|
|
5538
|
+
grammar: doc.grammar === "ears" ? "ears" : "freeform",
|
|
5539
|
+
severity: doc.grammarSeverity === "error" ? "error" : "warning"
|
|
5540
|
+
};
|
|
5541
|
+
} catch {
|
|
5542
|
+
return { grammar: "freeform", severity: "warning" };
|
|
4862
5543
|
}
|
|
4863
|
-
return out;
|
|
4864
5544
|
}
|
|
4865
|
-
function
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
5545
|
+
async function enforceStatementGrammar(platformDir, key, statement, cmdName) {
|
|
5546
|
+
if (key === "TERM")
|
|
5547
|
+
return;
|
|
5548
|
+
const cfg = await grammarConfig(platformDir, key);
|
|
5549
|
+
if (cfg.grammar !== "ears")
|
|
5550
|
+
return;
|
|
5551
|
+
const result = parseEarsStatement(statement);
|
|
5552
|
+
if (result.ok)
|
|
5553
|
+
return;
|
|
5554
|
+
const message = `${cmdName}: the statement does not match the domain's EARS shape \u2014 ${result.problem}.
|
|
5555
|
+
` + `Expected one of:
|
|
5556
|
+
${result.expected}`;
|
|
5557
|
+
if (cfg.severity === "error") {
|
|
5558
|
+
console.error(`${message}
|
|
5559
|
+
(${key} sets grammarSeverity: "error", so the write is refused)`);
|
|
5560
|
+
process.exit(EXIT.USAGE);
|
|
5561
|
+
}
|
|
5562
|
+
console.error(`${message}
|
|
5563
|
+
(warning \u2014 written anyway; ${key} sets grammar: "ears")`);
|
|
4871
5564
|
}
|
|
4872
|
-
var
|
|
4873
|
-
|
|
4874
|
-
|
|
5565
|
+
var init_grammar2 = __esm(() => {
|
|
5566
|
+
init_src();
|
|
5567
|
+
init_constants();
|
|
4875
5568
|
});
|
|
4876
5569
|
|
|
4877
5570
|
// src/commands/req.ts
|
|
@@ -4882,10 +5575,10 @@ __export(exports_req, {
|
|
|
4882
5575
|
askLine: () => askLine,
|
|
4883
5576
|
appendEntry: () => appendEntry
|
|
4884
5577
|
});
|
|
4885
|
-
import { existsSync as
|
|
4886
|
-
import { join as
|
|
5578
|
+
import { existsSync as existsSync10 } from "fs";
|
|
5579
|
+
import { join as join17, resolve as resolve16 } from "path";
|
|
4887
5580
|
import { createInterface as createInterface2 } from "readline";
|
|
4888
|
-
import { defineCommand as
|
|
5581
|
+
import { defineCommand as defineCommand11 } from "citty";
|
|
4889
5582
|
function resolveDomainKey(input, platformDir) {
|
|
4890
5583
|
const prefix = normalizeDomainKey(input);
|
|
4891
5584
|
const keys = listDomainKeys(platformDir);
|
|
@@ -4912,17 +5605,17 @@ async function authorFromFieldFlags(platformDir, key, nextId, textFlag, opts) {
|
|
|
4912
5605
|
}
|
|
4913
5606
|
await printResolvedCharter(platformDir, key);
|
|
4914
5607
|
const why = (opts.why ?? "").trim();
|
|
4915
|
-
const binds = (opts.binds ?? "").trim();
|
|
4916
5608
|
const lives = (opts.lives ?? "").trim();
|
|
4917
|
-
warnUnresolvableRefs(platformDir, [requirement, why,
|
|
5609
|
+
warnUnresolvableRefs(platformDir, [requirement, why, lives]);
|
|
4918
5610
|
const relFile = await appendEntry(platformDir, key, nextId, {
|
|
4919
5611
|
requirement,
|
|
4920
5612
|
why,
|
|
4921
|
-
|
|
4922
|
-
|
|
5613
|
+
lives,
|
|
5614
|
+
issue: (opts.issue ?? "").trim() || undefined
|
|
4923
5615
|
});
|
|
4924
5616
|
if (opts.json) {
|
|
4925
|
-
|
|
5617
|
+
const parsed = parseEarsStatement(requirement);
|
|
5618
|
+
console.log(JSON.stringify(parsed.ok ? { id: nextId, file: relFile, clauses: parsed.clauses } : { id: nextId, file: relFile }));
|
|
4926
5619
|
} else {
|
|
4927
5620
|
console.log(`appended ${nextId} to ${relFile}`);
|
|
4928
5621
|
}
|
|
@@ -4946,8 +5639,9 @@ function warnUnresolvableRefs(platformDir, fieldValues) {
|
|
|
4946
5639
|
}
|
|
4947
5640
|
async function appendEntry(platformDir, key, id, fields) {
|
|
4948
5641
|
const relFile = `spec-engine/${key}/SPEC.json`;
|
|
4949
|
-
const specPath =
|
|
4950
|
-
|
|
5642
|
+
const specPath = join17(platformDir, "spec-engine", key, "SPEC.json");
|
|
5643
|
+
await enforceStatementGrammar(platformDir, key, fields.requirement, "spec req");
|
|
5644
|
+
const domain2 = existsSync10(specPath) ? JSON.parse(await Bun.file(specPath).text()) : null;
|
|
4951
5645
|
if (domain2 === null) {
|
|
4952
5646
|
console.error(`spec req: no domain ${key} (expected ${relFile} under ${platformDir})`);
|
|
4953
5647
|
process.exit(EXIT.USAGE);
|
|
@@ -4962,7 +5656,7 @@ async function appendEntry(platformDir, key, id, fields) {
|
|
|
4962
5656
|
supersededBy: null,
|
|
4963
5657
|
relates: [],
|
|
4964
5658
|
livesIn: fields.lives ? [fields.lives] : [],
|
|
4965
|
-
issues: []
|
|
5659
|
+
issues: fields.issue ? [{ role: "created", id: fields.issue }] : []
|
|
4966
5660
|
});
|
|
4967
5661
|
domain2.requirements = requirements;
|
|
4968
5662
|
domain2.updated = localToday();
|
|
@@ -4988,10 +5682,11 @@ var init_req = __esm(() => {
|
|
|
4988
5682
|
init_src();
|
|
4989
5683
|
init_domains();
|
|
4990
5684
|
init_filerefs();
|
|
5685
|
+
init_grammar2();
|
|
4991
5686
|
init_constants();
|
|
4992
5687
|
init_discover();
|
|
4993
5688
|
init__shared();
|
|
4994
|
-
reqCommand =
|
|
5689
|
+
reqCommand = defineCommand11({
|
|
4995
5690
|
meta: {
|
|
4996
5691
|
name: "req",
|
|
4997
5692
|
description: "Author a new requirement interactively (TTY) or print the next unused id (non-TTY)"
|
|
@@ -5019,18 +5714,18 @@ var init_req = __esm(() => {
|
|
|
5019
5714
|
type: "string",
|
|
5020
5715
|
description: "Why it matters field (only with --text)"
|
|
5021
5716
|
},
|
|
5022
|
-
binds: {
|
|
5023
|
-
type: "string",
|
|
5024
|
-
description: "Binds field (only with --text)"
|
|
5025
|
-
},
|
|
5026
5717
|
lives: {
|
|
5027
5718
|
type: "string",
|
|
5028
5719
|
description: "Lives in field (only with --text)"
|
|
5720
|
+
},
|
|
5721
|
+
issue: {
|
|
5722
|
+
type: "string",
|
|
5723
|
+
description: "Originating ticket id, recorded as created-provenance on the entry (only with --text; opaque \u2014 never a requirement id)"
|
|
5029
5724
|
}
|
|
5030
5725
|
},
|
|
5031
5726
|
async run({ args }) {
|
|
5032
5727
|
const input = args.domainPrefix;
|
|
5033
|
-
const platformDir =
|
|
5728
|
+
const platformDir = resolve16(args.platformDir ?? process.cwd());
|
|
5034
5729
|
try {
|
|
5035
5730
|
assertSpecPlatform(platformDir);
|
|
5036
5731
|
} catch (e) {
|
|
@@ -5039,17 +5734,17 @@ var init_req = __esm(() => {
|
|
|
5039
5734
|
const key = resolveDomainKey(input, platformDir);
|
|
5040
5735
|
const nextId = await nextRequirementId(platformDir, key);
|
|
5041
5736
|
const textFlag = args.text;
|
|
5042
|
-
const hasFieldFlags = typeof args.why === "string" || typeof args.
|
|
5737
|
+
const hasFieldFlags = typeof args.why === "string" || typeof args.lives === "string" || typeof args.issue === "string";
|
|
5043
5738
|
if (textFlag === undefined && hasFieldFlags) {
|
|
5044
|
-
console.error("spec req: --why/--
|
|
5739
|
+
console.error("spec req: --why/--lives/--issue require --text (the Requirement field is mandatory)");
|
|
5045
5740
|
process.exit(EXIT.USAGE);
|
|
5046
5741
|
return;
|
|
5047
5742
|
}
|
|
5048
5743
|
if (textFlag !== undefined) {
|
|
5049
5744
|
await authorFromFieldFlags(platformDir, key, nextId, textFlag, {
|
|
5050
5745
|
why: args.why,
|
|
5051
|
-
binds: args.binds,
|
|
5052
5746
|
lives: args.lives,
|
|
5747
|
+
issue: args.issue,
|
|
5053
5748
|
json: Boolean(args.json)
|
|
5054
5749
|
});
|
|
5055
5750
|
return;
|
|
@@ -5071,13 +5766,11 @@ var init_req = __esm(() => {
|
|
|
5071
5766
|
return;
|
|
5072
5767
|
}
|
|
5073
5768
|
const why = (await askLine("Why it matters: ")).trim();
|
|
5074
|
-
const binds = (await askLine("Binds: ")).trim();
|
|
5075
5769
|
const lives = (await askLine("Lives in: ")).trim();
|
|
5076
|
-
warnUnresolvableRefs(platformDir, [requirement, why,
|
|
5770
|
+
warnUnresolvableRefs(platformDir, [requirement, why, lives]);
|
|
5077
5771
|
const relFile = await appendEntry(platformDir, key, nextId, {
|
|
5078
5772
|
requirement,
|
|
5079
5773
|
why,
|
|
5080
|
-
binds,
|
|
5081
5774
|
lives
|
|
5082
5775
|
});
|
|
5083
5776
|
console.log(`appended ${nextId} to ${relFile}`);
|
|
@@ -5086,7 +5779,7 @@ var init_req = __esm(() => {
|
|
|
5086
5779
|
});
|
|
5087
5780
|
|
|
5088
5781
|
// src/parser/grammar.ts
|
|
5089
|
-
var
|
|
5782
|
+
var init_grammar3 = __esm(() => {
|
|
5090
5783
|
init_src();
|
|
5091
5784
|
});
|
|
5092
5785
|
|
|
@@ -5098,16 +5791,16 @@ __export(exports_term, {
|
|
|
5098
5791
|
termConfirmCommand: () => termConfirmCommand,
|
|
5099
5792
|
termCommand: () => termCommand
|
|
5100
5793
|
});
|
|
5101
|
-
import { existsSync as
|
|
5102
|
-
import { join as
|
|
5103
|
-
import { defineCommand as
|
|
5794
|
+
import { existsSync as existsSync11 } from "fs";
|
|
5795
|
+
import { join as join18, resolve as resolve17 } from "path";
|
|
5796
|
+
import { defineCommand as defineCommand12 } from "citty";
|
|
5104
5797
|
function parseAliases(raw) {
|
|
5105
5798
|
if (typeof raw !== "string" || raw.trim() === "")
|
|
5106
5799
|
return [];
|
|
5107
5800
|
return raw.split(",").map((s) => s.trim()).filter((s) => s !== "");
|
|
5108
5801
|
}
|
|
5109
|
-
function
|
|
5110
|
-
const platformDir =
|
|
5802
|
+
function resolvePlatform2(platformDirRaw) {
|
|
5803
|
+
const platformDir = resolve17(platformDirRaw ?? process.cwd());
|
|
5111
5804
|
try {
|
|
5112
5805
|
assertSpecPlatform(platformDir);
|
|
5113
5806
|
} catch (e) {
|
|
@@ -5116,16 +5809,16 @@ function resolvePlatform(platformDirRaw) {
|
|
|
5116
5809
|
return platformDir;
|
|
5117
5810
|
}
|
|
5118
5811
|
async function readTermEnvelope(platformDir, cmd) {
|
|
5119
|
-
const specPath =
|
|
5120
|
-
if (!
|
|
5812
|
+
const specPath = join18(platformDir, "spec-engine", TERM_KEY2, "SPEC.json");
|
|
5813
|
+
if (!existsSync11(specPath)) {
|
|
5121
5814
|
console.error(`spec ${cmd}: no TERM domain (expected spec-engine/TERM/SPEC.json under ${platformDir})`);
|
|
5122
5815
|
process.exit(EXIT.USAGE);
|
|
5123
5816
|
}
|
|
5124
5817
|
return JSON.parse(await Bun.file(specPath).text());
|
|
5125
5818
|
}
|
|
5126
5819
|
async function appendTermEntry(platformDir, id, fields) {
|
|
5127
|
-
const relFile = `spec-engine/${
|
|
5128
|
-
const specPath =
|
|
5820
|
+
const relFile = `spec-engine/${TERM_KEY2}/SPEC.json`;
|
|
5821
|
+
const specPath = join18(platformDir, "spec-engine", TERM_KEY2, "SPEC.json");
|
|
5129
5822
|
const domain2 = await readTermEnvelope(platformDir, "term");
|
|
5130
5823
|
const requirements = Array.isArray(domain2.requirements) ? domain2.requirements : [];
|
|
5131
5824
|
const entry = {
|
|
@@ -5158,11 +5851,11 @@ async function appendTermEntry(platformDir, id, fields) {
|
|
|
5158
5851
|
return relFile;
|
|
5159
5852
|
}
|
|
5160
5853
|
async function authorOrQuery(opts) {
|
|
5161
|
-
const platformDir =
|
|
5162
|
-
const nextId = await nextRequirementId(platformDir,
|
|
5854
|
+
const platformDir = resolvePlatform2(opts.platformDirRaw);
|
|
5855
|
+
const nextId = await nextRequirementId(platformDir, TERM_KEY2);
|
|
5163
5856
|
if (opts.def === undefined) {
|
|
5164
5857
|
if (opts.json) {
|
|
5165
|
-
console.log(JSON.stringify({ domain:
|
|
5858
|
+
console.log(JSON.stringify({ domain: TERM_KEY2, next_id: nextId }));
|
|
5166
5859
|
} else {
|
|
5167
5860
|
console.log(nextId);
|
|
5168
5861
|
}
|
|
@@ -5188,9 +5881,9 @@ async function authorOrQuery(opts) {
|
|
|
5188
5881
|
}
|
|
5189
5882
|
}
|
|
5190
5883
|
async function listTerms(platformDirRaw, json) {
|
|
5191
|
-
const platformDir =
|
|
5192
|
-
const specPath =
|
|
5193
|
-
const domain2 =
|
|
5884
|
+
const platformDir = resolvePlatform2(platformDirRaw);
|
|
5885
|
+
const specPath = join18(platformDir, "spec-engine", TERM_KEY2, "SPEC.json");
|
|
5886
|
+
const domain2 = existsSync11(specPath) ? JSON.parse(await Bun.file(specPath).text()) : { requirements: [] };
|
|
5194
5887
|
const requirements = Array.isArray(domain2.requirements) ? domain2.requirements : [];
|
|
5195
5888
|
const rows = requirements.map((r) => ({
|
|
5196
5889
|
id: typeof r.id === "string" ? r.id : "",
|
|
@@ -5210,18 +5903,18 @@ async function resolveReviseTarget(id, platformDirRaw, defRaw) {
|
|
|
5210
5903
|
console.error(`spec term revise: id must be a requirement id (TERM-NNN); got ${id}`);
|
|
5211
5904
|
process.exit(EXIT.USAGE);
|
|
5212
5905
|
}
|
|
5213
|
-
if (id.slice(0, id.indexOf("-")) !==
|
|
5906
|
+
if (id.slice(0, id.indexOf("-")) !== TERM_KEY2) {
|
|
5214
5907
|
console.error(`spec term revise: ${id} is not a TERM id \u2014 revise operates on the TERM store`);
|
|
5215
5908
|
process.exit(EXIT.USAGE);
|
|
5216
5909
|
}
|
|
5217
|
-
const platformDir =
|
|
5910
|
+
const platformDir = resolvePlatform2(platformDirRaw);
|
|
5218
5911
|
const def = (defRaw ?? "").trim();
|
|
5219
5912
|
if (def === "") {
|
|
5220
5913
|
console.error("spec term revise: --def <definition> is required (a non-empty revision)");
|
|
5221
5914
|
process.exit(EXIT.USAGE);
|
|
5222
5915
|
}
|
|
5223
|
-
const relFile = `spec-engine/${
|
|
5224
|
-
const specPath =
|
|
5916
|
+
const relFile = `spec-engine/${TERM_KEY2}/SPEC.json`;
|
|
5917
|
+
const specPath = join18(platformDir, "spec-engine", TERM_KEY2, "SPEC.json");
|
|
5225
5918
|
const domain2 = await readTermEnvelope(platformDir, "term revise");
|
|
5226
5919
|
const requirements = Array.isArray(domain2.requirements) ? domain2.requirements : [];
|
|
5227
5920
|
const req = requirements.find((r) => r?.id === id);
|
|
@@ -5279,7 +5972,7 @@ async function resolveConfirmTarget(platformDir, termId) {
|
|
|
5279
5972
|
const termReqs = Array.isArray(termDomain.requirements) ? termDomain.requirements : [];
|
|
5280
5973
|
const term = termReqs.find((r) => r?.id === termId);
|
|
5281
5974
|
if (term === undefined) {
|
|
5282
|
-
console.error(`spec term confirm: no term ${termId} in spec-engine/${
|
|
5975
|
+
console.error(`spec term confirm: no term ${termId} in spec-engine/${TERM_KEY2}/SPEC.json`);
|
|
5283
5976
|
process.exit(EXIT.USAGE);
|
|
5284
5977
|
}
|
|
5285
5978
|
const targetPin = typeof termDomain.specVersion === "number" ? termDomain.specVersion : 1;
|
|
@@ -5297,8 +5990,8 @@ async function resolveConfirmTarget(platformDir, termId) {
|
|
|
5297
5990
|
async function locateCitation(platformDir, reqId, termId) {
|
|
5298
5991
|
const citKey = reqId.slice(0, reqId.indexOf("-"));
|
|
5299
5992
|
const relFile = `spec-engine/${citKey}/SPEC.json`;
|
|
5300
|
-
const specPath =
|
|
5301
|
-
if (!
|
|
5993
|
+
const specPath = join18(platformDir, "spec-engine", citKey, "SPEC.json");
|
|
5994
|
+
if (!existsSync11(specPath)) {
|
|
5302
5995
|
console.error(`spec term confirm: no domain ${citKey} (expected ${relFile} under ${platformDir})`);
|
|
5303
5996
|
process.exit(EXIT.USAGE);
|
|
5304
5997
|
}
|
|
@@ -5309,6 +6002,12 @@ async function locateCitation(platformDir, reqId, termId) {
|
|
|
5309
6002
|
console.error(`spec term confirm: no entry ${reqId} in ${relFile}`);
|
|
5310
6003
|
process.exit(EXIT.USAGE);
|
|
5311
6004
|
}
|
|
6005
|
+
const rawStatus = typeof citReq.status === "string" ? citReq.status : "";
|
|
6006
|
+
const statusLc = rawStatus.toLowerCase();
|
|
6007
|
+
if (statusLc !== "active" && statusLc !== "draft") {
|
|
6008
|
+
console.error(`spec term confirm: ${reqId} is ${rawStatus} \u2014 only Active/Draft entries re-pin a citation (a superseded entry is history)`);
|
|
6009
|
+
process.exit(EXIT.USAGE);
|
|
6010
|
+
}
|
|
5312
6011
|
const cites = Array.isArray(citReq.cites) ? citReq.cites : [];
|
|
5313
6012
|
const cite = cites.find((c) => c?.term === termId);
|
|
5314
6013
|
if (cite === undefined) {
|
|
@@ -5323,11 +6022,11 @@ async function confirmCitation(opts) {
|
|
|
5323
6022
|
console.error(`spec term confirm: req id must be a requirement id (KEY-NNN); got ${reqId}`);
|
|
5324
6023
|
process.exit(EXIT.USAGE);
|
|
5325
6024
|
}
|
|
5326
|
-
if (!ID_RE.test(termId) || termId.slice(0, termId.indexOf("-")) !==
|
|
6025
|
+
if (!ID_RE.test(termId) || termId.slice(0, termId.indexOf("-")) !== TERM_KEY2) {
|
|
5327
6026
|
console.error(`spec term confirm: ${termId} is not a TERM id \u2014 confirm re-pins a TERM citation`);
|
|
5328
6027
|
process.exit(EXIT.USAGE);
|
|
5329
6028
|
}
|
|
5330
|
-
const platformDir =
|
|
6029
|
+
const platformDir = resolvePlatform2(opts.platformDirRaw);
|
|
5331
6030
|
const { targetId, targetPin } = await resolveConfirmTarget(platformDir, termId);
|
|
5332
6031
|
const { citDomain, specPath, relFile, cite } = await locateCitation(platformDir, reqId, termId);
|
|
5333
6032
|
cite.term = targetId;
|
|
@@ -5347,18 +6046,18 @@ async function confirmCitation(opts) {
|
|
|
5347
6046
|
console.log(`confirmed ${reqId} cites ${targetId} @${targetPin} in ${relFile}`);
|
|
5348
6047
|
}
|
|
5349
6048
|
}
|
|
5350
|
-
var
|
|
6049
|
+
var TERM_KEY2 = "TERM", termListCommand, termReviseCommand, termConfirmCommand, termCommand;
|
|
5351
6050
|
var init_term = __esm(() => {
|
|
5352
6051
|
init_src();
|
|
5353
6052
|
init_domains();
|
|
5354
6053
|
init_constants();
|
|
5355
6054
|
init_discover();
|
|
5356
6055
|
init_pipeline();
|
|
5357
|
-
|
|
6056
|
+
init_grammar3();
|
|
5358
6057
|
init_sqlite();
|
|
5359
6058
|
init__shared();
|
|
5360
6059
|
init_req();
|
|
5361
|
-
termListCommand =
|
|
6060
|
+
termListCommand = defineCommand12({
|
|
5362
6061
|
meta: {
|
|
5363
6062
|
name: "list",
|
|
5364
6063
|
description: "List the glossary TERM entries (id, name, status), sorted by id"
|
|
@@ -5378,7 +6077,7 @@ var init_term = __esm(() => {
|
|
|
5378
6077
|
await listTerms(args.platformDir, Boolean(args.json));
|
|
5379
6078
|
}
|
|
5380
6079
|
});
|
|
5381
|
-
termReviseCommand =
|
|
6080
|
+
termReviseCommand = defineCommand12({
|
|
5382
6081
|
meta: {
|
|
5383
6082
|
name: "revise",
|
|
5384
6083
|
description: "Revise a TERM's definition IN PLACE (same id) and BUMP the envelope specVersion \u2014 the drift signal (A2). Requirements have no such op."
|
|
@@ -5412,7 +6111,7 @@ var init_term = __esm(() => {
|
|
|
5412
6111
|
});
|
|
5413
6112
|
}
|
|
5414
6113
|
});
|
|
5415
|
-
termConfirmCommand =
|
|
6114
|
+
termConfirmCommand = defineCommand12({
|
|
5416
6115
|
meta: {
|
|
5417
6116
|
name: "confirm",
|
|
5418
6117
|
description: "Re-pin a requirement's TERM citation to the term's current version (clears TERM_DRIFT); re-points to the successor when the term is superseded (clears SUPERSEDED_TERM_REFERENCED)."
|
|
@@ -5447,7 +6146,7 @@ var init_term = __esm(() => {
|
|
|
5447
6146
|
});
|
|
5448
6147
|
}
|
|
5449
6148
|
});
|
|
5450
|
-
termCommand =
|
|
6149
|
+
termCommand = defineCommand12({
|
|
5451
6150
|
meta: {
|
|
5452
6151
|
name: "term",
|
|
5453
6152
|
description: "Author a glossary TERM (spec term <name> --def <definition>), list terms (spec term list), or revise a definition in place (spec term revise TERM-NNN)"
|
|
@@ -5466,261 +6165,58 @@ var init_term = __esm(() => {
|
|
|
5466
6165
|
extra: {
|
|
5467
6166
|
type: "positional",
|
|
5468
6167
|
required: false,
|
|
5469
|
-
description: "After `revise <TERM-NNN>`: the platform directory (default: cwd). After `confirm <KEY-NNN>`: the TERM-NNN id."
|
|
5470
|
-
},
|
|
5471
|
-
extra2: {
|
|
5472
|
-
type: "positional",
|
|
5473
|
-
required: false,
|
|
5474
|
-
description: "After `confirm <KEY-NNN> <TERM-NNN>`: the platform directory (default: cwd)"
|
|
5475
|
-
},
|
|
5476
|
-
def: { type: "string", description: "The term's definition (stored in the statement field)" },
|
|
5477
|
-
text: { type: "string", description: "Alias for --def (the definition)" },
|
|
5478
|
-
aliases: { type: "string", description: "Comma-separated synonyms \u2192 aliases[]" },
|
|
5479
|
-
section: { type: "string", description: "GLOSSARY.md layout bucket (Wave F)" },
|
|
5480
|
-
noBump: { type: "boolean", description: "revise: do not bump the envelope specVersion" },
|
|
5481
|
-
json: {
|
|
5482
|
-
type: "boolean",
|
|
5483
|
-
description: "Author: { id, file } (or { domain, next_id } as an id query). list: the entries array. revise: { id, file, spec_version }."
|
|
5484
|
-
}
|
|
5485
|
-
},
|
|
5486
|
-
async run({ args }) {
|
|
5487
|
-
const verb = args.name;
|
|
5488
|
-
const def = args.def ?? args.text;
|
|
5489
|
-
const json = Boolean(args.json);
|
|
5490
|
-
if (verb === "list") {
|
|
5491
|
-
await listTerms(args.platformDir, json);
|
|
5492
|
-
return;
|
|
5493
|
-
}
|
|
5494
|
-
if (verb === "revise") {
|
|
5495
|
-
await reviseTerm({
|
|
5496
|
-
id: args.platformDir ?? "",
|
|
5497
|
-
platformDirRaw: args.extra,
|
|
5498
|
-
def,
|
|
5499
|
-
noBump: Boolean(args.noBump),
|
|
5500
|
-
json
|
|
5501
|
-
});
|
|
5502
|
-
return;
|
|
5503
|
-
}
|
|
5504
|
-
if (verb === "confirm") {
|
|
5505
|
-
await confirmCitation({
|
|
5506
|
-
reqId: args.platformDir ?? "",
|
|
5507
|
-
termId: args.extra ?? "",
|
|
5508
|
-
platformDirRaw: args.extra2,
|
|
5509
|
-
json
|
|
5510
|
-
});
|
|
5511
|
-
return;
|
|
5512
|
-
}
|
|
5513
|
-
await authorOrQuery({
|
|
5514
|
-
name: verb,
|
|
5515
|
-
platformDirRaw: args.platformDir,
|
|
5516
|
-
def,
|
|
5517
|
-
aliasesRaw: args.aliases,
|
|
5518
|
-
section: args.section,
|
|
5519
|
-
json
|
|
5520
|
-
});
|
|
5521
|
-
}
|
|
5522
|
-
});
|
|
5523
|
-
});
|
|
5524
|
-
|
|
5525
|
-
// src/commands/glossary.ts
|
|
5526
|
-
var exports_glossary = {};
|
|
5527
|
-
__export(exports_glossary, {
|
|
5528
|
-
parseGlossary: () => parseGlossary,
|
|
5529
|
-
glossaryCommand: () => glossaryCommand,
|
|
5530
|
-
generateGlossary: () => generateGlossary
|
|
5531
|
-
});
|
|
5532
|
-
import { existsSync as existsSync11, readFileSync as readFileSync2 } from "fs";
|
|
5533
|
-
import { join as join13, resolve as resolve17 } from "path";
|
|
5534
|
-
import { defineCommand as defineCommand12 } from "citty";
|
|
5535
|
-
function parseGlossary(md) {
|
|
5536
|
-
const terms = [];
|
|
5537
|
-
let section = null;
|
|
5538
|
-
let buf = null;
|
|
5539
|
-
const flush = () => {
|
|
5540
|
-
if (buf === null)
|
|
5541
|
-
return;
|
|
5542
|
-
const m = buf.match(/^\*\*(.+?)\*\*/);
|
|
5543
|
-
if (m) {
|
|
5544
|
-
const remainder = buf.slice(m[0].length);
|
|
5545
|
-
const statement = remainder.replace(/^ \u2014 /, "").trim();
|
|
5546
|
-
terms.push({ term: m[1], statement, section });
|
|
5547
|
-
}
|
|
5548
|
-
buf = null;
|
|
5549
|
-
};
|
|
5550
|
-
for (const line of md.split(`
|
|
5551
|
-
`)) {
|
|
5552
|
-
if (line.startsWith("## ")) {
|
|
5553
|
-
flush();
|
|
5554
|
-
section = line.slice(3).trim();
|
|
5555
|
-
} else if (line.startsWith("- ")) {
|
|
5556
|
-
flush();
|
|
5557
|
-
buf = line.slice(2);
|
|
5558
|
-
} else if (buf !== null) {
|
|
5559
|
-
if (line.trim() === "")
|
|
5560
|
-
flush();
|
|
5561
|
-
else
|
|
5562
|
-
buf += ` ${line.trim()}`;
|
|
5563
|
-
}
|
|
5564
|
-
}
|
|
5565
|
-
flush();
|
|
5566
|
-
return terms;
|
|
5567
|
-
}
|
|
5568
|
-
function generateGlossary(terms) {
|
|
5569
|
-
const parts = ["# Glossary", "", GLOSSARY_INTRO];
|
|
5570
|
-
let section;
|
|
5571
|
-
for (const t of terms) {
|
|
5572
|
-
if (t.section !== section) {
|
|
5573
|
-
section = t.section;
|
|
5574
|
-
if (section !== null && section !== undefined) {
|
|
5575
|
-
parts.push("", `## ${section}`, "");
|
|
5576
|
-
}
|
|
5577
|
-
}
|
|
5578
|
-
parts.push(`- **${t.term}** \u2014 ${t.statement}`);
|
|
5579
|
-
}
|
|
5580
|
-
return `${parts.join(`
|
|
5581
|
-
`)}
|
|
5582
|
-
`;
|
|
5583
|
-
}
|
|
5584
|
-
function resolvePlatform2(platformDirRaw) {
|
|
5585
|
-
const platformDir = resolve17(platformDirRaw ?? process.cwd());
|
|
5586
|
-
try {
|
|
5587
|
-
assertSpecPlatform(platformDir);
|
|
5588
|
-
} catch (e) {
|
|
5589
|
-
handleNotAPlatform(e);
|
|
5590
|
-
}
|
|
5591
|
-
return platformDir;
|
|
5592
|
-
}
|
|
5593
|
-
function readStoreTerms(platformDir) {
|
|
5594
|
-
const specPath = join13(platformDir, "spec-engine", TERM_KEY2, "SPEC.json");
|
|
5595
|
-
if (!existsSync11(specPath))
|
|
5596
|
-
return [];
|
|
5597
|
-
const domain2 = JSON.parse(readFileSync2(specPath, "utf8"));
|
|
5598
|
-
const reqs = Array.isArray(domain2.requirements) ? domain2.requirements : [];
|
|
5599
|
-
return reqs.filter((r) => String(r.status).toLowerCase() === "active").sort((a, b) => String(a.id) < String(b.id) ? -1 : String(a.id) > String(b.id) ? 1 : 0).map((r) => ({
|
|
5600
|
-
term: String(r.term ?? ""),
|
|
5601
|
-
statement: String(r.statement ?? ""),
|
|
5602
|
-
section: r.section ?? null
|
|
5603
|
-
}));
|
|
5604
|
-
}
|
|
5605
|
-
function glossaryPath(platformDir) {
|
|
5606
|
-
return join13(platformDir, "GLOSSARY.md");
|
|
5607
|
-
}
|
|
5608
|
-
async function migrateGlossary(platformDir, json) {
|
|
5609
|
-
const relFile = `spec-engine/${TERM_KEY2}/SPEC.json`;
|
|
5610
|
-
const specPath = join13(platformDir, "spec-engine", TERM_KEY2, "SPEC.json");
|
|
5611
|
-
if (!existsSync11(specPath)) {
|
|
5612
|
-
console.error(`spec glossary: no TERM domain (expected ${relFile} under ${platformDir})`);
|
|
5613
|
-
process.exit(EXIT.USAGE);
|
|
5614
|
-
}
|
|
5615
|
-
const domain2 = JSON.parse(readFileSync2(specPath, "utf8"));
|
|
5616
|
-
const existing = Array.isArray(domain2.requirements) ? domain2.requirements : [];
|
|
5617
|
-
if (existing.length > 0) {
|
|
5618
|
-
console.error(`spec glossary: TERM domain already holds ${existing.length} entries \u2014 migration skipped`);
|
|
5619
|
-
if (json)
|
|
5620
|
-
console.log(JSON.stringify({ migrated: 0, skipped: true }));
|
|
5621
|
-
else
|
|
5622
|
-
console.log("migration skipped (TERM domain not empty)");
|
|
5623
|
-
return;
|
|
5624
|
-
}
|
|
5625
|
-
const gPath = glossaryPath(platformDir);
|
|
5626
|
-
if (!existsSync11(gPath)) {
|
|
5627
|
-
console.error(`spec glossary: no GLOSSARY.md to migrate (expected ${gPath})`);
|
|
5628
|
-
process.exit(EXIT.USAGE);
|
|
5629
|
-
}
|
|
5630
|
-
const parsed = parseGlossary(readFileSync2(gPath, "utf8"));
|
|
5631
|
-
domain2.requirements = parsed.map((t, i) => ({
|
|
5632
|
-
id: `${TERM_KEY2}-${String(i + 1).padStart(3, "0")}`,
|
|
5633
|
-
status: "active",
|
|
5634
|
-
statement: t.statement,
|
|
5635
|
-
term: t.term,
|
|
5636
|
-
why: null,
|
|
5637
|
-
supersedes: null,
|
|
5638
|
-
supersededBy: null,
|
|
5639
|
-
relates: [],
|
|
5640
|
-
livesIn: [],
|
|
5641
|
-
issues: [],
|
|
5642
|
-
aliases: [],
|
|
5643
|
-
cites: [],
|
|
5644
|
-
changedAtVersion: 1,
|
|
5645
|
-
section: t.section
|
|
5646
|
-
}));
|
|
5647
|
-
domain2.updated = localToday();
|
|
5648
|
-
const res = await validateAndWrite(specPath, domain2, relFile);
|
|
5649
|
-
if (!res.ok) {
|
|
5650
|
-
for (const diag of res.diagnostics)
|
|
5651
|
-
console.error(`spec glossary: ${diag.detail}`);
|
|
5652
|
-
process.exit(EXIT.USAGE);
|
|
5653
|
-
}
|
|
5654
|
-
const count = parsed.length;
|
|
5655
|
-
if (json)
|
|
5656
|
-
console.log(JSON.stringify({ migrated: count, file: relFile }));
|
|
5657
|
-
else
|
|
5658
|
-
console.log(`migrated ${count} terms into ${relFile}`);
|
|
5659
|
-
}
|
|
5660
|
-
async function writeGlossary(platformDir, json) {
|
|
5661
|
-
const terms = readStoreTerms(platformDir);
|
|
5662
|
-
await Bun.write(glossaryPath(platformDir), generateGlossary(terms));
|
|
5663
|
-
if (json)
|
|
5664
|
-
console.log(JSON.stringify({ generated: terms.length, file: "GLOSSARY.md" }));
|
|
5665
|
-
else
|
|
5666
|
-
console.log(`generated GLOSSARY.md from ${terms.length} terms`);
|
|
5667
|
-
}
|
|
5668
|
-
function checkGlossary(platformDir, json) {
|
|
5669
|
-
const generated = generateGlossary(readStoreTerms(platformDir));
|
|
5670
|
-
const gPath = glossaryPath(platformDir);
|
|
5671
|
-
const committed = existsSync11(gPath) ? readFileSync2(gPath, "utf8") : "";
|
|
5672
|
-
if (generated === committed) {
|
|
5673
|
-
if (json)
|
|
5674
|
-
console.log(JSON.stringify({ ok: true }));
|
|
5675
|
-
else
|
|
5676
|
-
console.error("glossary round-trip: OK (committed GLOSSARY.md == generated)");
|
|
5677
|
-
return;
|
|
5678
|
-
}
|
|
5679
|
-
console.error("glossary drift: committed GLOSSARY.md != generated from the TERM store");
|
|
5680
|
-
console.error("run `spec glossary .` to regenerate the human view from the store");
|
|
5681
|
-
if (json)
|
|
5682
|
-
console.log(JSON.stringify({ ok: false }));
|
|
5683
|
-
process.exit(EXIT.FAILURE);
|
|
5684
|
-
}
|
|
5685
|
-
var TERM_KEY2 = "TERM", GLOSSARY_INTRO = `Canonical names for Spec Engine concepts. When prose, code comments, diagnostics, or
|
|
5686
|
-
docs need one of these ideas, use the term below \u2014 not a synonym. Terms are ordered
|
|
5687
|
-
by the data model, outside-in.`, glossaryCommand;
|
|
5688
|
-
var init_glossary = __esm(() => {
|
|
5689
|
-
init_src();
|
|
5690
|
-
init_constants();
|
|
5691
|
-
init_discover();
|
|
5692
|
-
init__shared();
|
|
5693
|
-
glossaryCommand = defineCommand12({
|
|
5694
|
-
meta: {
|
|
5695
|
-
name: "glossary",
|
|
5696
|
-
description: "Generate GLOSSARY.md from the TERM store (byte-stable); --migrate parses GLOSSARY.md into TERM-001..N; --check fails on drift (committed != generated)"
|
|
5697
|
-
},
|
|
5698
|
-
args: {
|
|
5699
|
-
platformDir: {
|
|
5700
|
-
type: "positional",
|
|
5701
|
-
required: false,
|
|
5702
|
-
description: "Platform directory containing spec-engine/ (default: cwd)"
|
|
6168
|
+
description: "After `revise <TERM-NNN>`: the platform directory (default: cwd). After `confirm <KEY-NNN>`: the TERM-NNN id."
|
|
5703
6169
|
},
|
|
5704
|
-
|
|
5705
|
-
type: "
|
|
5706
|
-
|
|
6170
|
+
extra2: {
|
|
6171
|
+
type: "positional",
|
|
6172
|
+
required: false,
|
|
6173
|
+
description: "After `confirm <KEY-NNN> <TERM-NNN>`: the platform directory (default: cwd)"
|
|
5707
6174
|
},
|
|
5708
|
-
|
|
6175
|
+
def: { type: "string", description: "The term's definition (stored in the statement field)" },
|
|
6176
|
+
text: { type: "string", description: "Alias for --def (the definition)" },
|
|
6177
|
+
aliases: { type: "string", description: "Comma-separated synonyms \u2192 aliases[]" },
|
|
6178
|
+
section: { type: "string", description: "GLOSSARY.md layout bucket (Wave F)" },
|
|
6179
|
+
noBump: { type: "boolean", description: "revise: do not bump the envelope specVersion" },
|
|
6180
|
+
json: {
|
|
5709
6181
|
type: "boolean",
|
|
5710
|
-
description: "
|
|
5711
|
-
}
|
|
5712
|
-
json: { type: "boolean", description: "Emit a JSON result instead of the text summary" }
|
|
6182
|
+
description: "Author: { id, file } (or { domain, next_id } as an id query). list: the entries array. revise: { id, file, spec_version }."
|
|
6183
|
+
}
|
|
5713
6184
|
},
|
|
5714
6185
|
async run({ args }) {
|
|
5715
|
-
const
|
|
6186
|
+
const verb = args.name;
|
|
6187
|
+
const def = args.def ?? args.text;
|
|
5716
6188
|
const json = Boolean(args.json);
|
|
5717
|
-
if (
|
|
5718
|
-
await
|
|
5719
|
-
|
|
5720
|
-
checkGlossary(platformDir, json);
|
|
5721
|
-
} else {
|
|
5722
|
-
await writeGlossary(platformDir, json);
|
|
6189
|
+
if (verb === "list") {
|
|
6190
|
+
await listTerms(args.platformDir, json);
|
|
6191
|
+
return;
|
|
5723
6192
|
}
|
|
6193
|
+
if (verb === "revise") {
|
|
6194
|
+
await reviseTerm({
|
|
6195
|
+
id: args.platformDir ?? "",
|
|
6196
|
+
platformDirRaw: args.extra,
|
|
6197
|
+
def,
|
|
6198
|
+
noBump: Boolean(args.noBump),
|
|
6199
|
+
json
|
|
6200
|
+
});
|
|
6201
|
+
return;
|
|
6202
|
+
}
|
|
6203
|
+
if (verb === "confirm") {
|
|
6204
|
+
await confirmCitation({
|
|
6205
|
+
reqId: args.platformDir ?? "",
|
|
6206
|
+
termId: args.extra ?? "",
|
|
6207
|
+
platformDirRaw: args.extra2,
|
|
6208
|
+
json
|
|
6209
|
+
});
|
|
6210
|
+
return;
|
|
6211
|
+
}
|
|
6212
|
+
await authorOrQuery({
|
|
6213
|
+
name: verb,
|
|
6214
|
+
platformDirRaw: args.platformDir,
|
|
6215
|
+
def,
|
|
6216
|
+
aliasesRaw: args.aliases,
|
|
6217
|
+
section: args.section,
|
|
6218
|
+
json
|
|
6219
|
+
});
|
|
5724
6220
|
}
|
|
5725
6221
|
});
|
|
5726
6222
|
});
|
|
@@ -5775,7 +6271,7 @@ __export(exports_supersede, {
|
|
|
5775
6271
|
supersedeCommand: () => supersedeCommand
|
|
5776
6272
|
});
|
|
5777
6273
|
import { existsSync as existsSync12 } from "fs";
|
|
5778
|
-
import { join as
|
|
6274
|
+
import { join as join19, resolve as resolve18 } from "path";
|
|
5779
6275
|
import { defineCommand as defineCommand13 } from "citty";
|
|
5780
6276
|
async function resolveSupersedeTarget(id, platformDir) {
|
|
5781
6277
|
if (!ID_RE.test(id)) {
|
|
@@ -5789,7 +6285,7 @@ async function resolveSupersedeTarget(id, platformDir) {
|
|
|
5789
6285
|
}
|
|
5790
6286
|
const key = id.slice(0, id.indexOf("-"));
|
|
5791
6287
|
const relFile = `spec-engine/${key}/SPEC.json`;
|
|
5792
|
-
const specPath =
|
|
6288
|
+
const specPath = join19(platformDir, "spec-engine", key, "SPEC.json");
|
|
5793
6289
|
if (!existsSync12(specPath)) {
|
|
5794
6290
|
console.error(`spec supersede: no domain ${key} (expected ${relFile} under ${platformDir})`);
|
|
5795
6291
|
process.exit(EXIT.USAGE);
|
|
@@ -5838,8 +6334,7 @@ function resolveSuccessorFields(args, req) {
|
|
|
5838
6334
|
const predLives = Array.isArray(req.livesIn) && req.livesIn.length > 0 ? String(req.livesIn[0]) : "";
|
|
5839
6335
|
const why = (args.why ?? predWhy).trim();
|
|
5840
6336
|
const lives = (args.lives ?? predLives).trim();
|
|
5841
|
-
|
|
5842
|
-
return { why, lives, binds };
|
|
6337
|
+
return { why, lives };
|
|
5843
6338
|
}
|
|
5844
6339
|
function resolveSuccessorTermFields(args, req) {
|
|
5845
6340
|
const predTerm = typeof req.term === "string" ? req.term : undefined;
|
|
@@ -5848,10 +6343,18 @@ function resolveSuccessorTermFields(args, req) {
|
|
|
5848
6343
|
const aliases = typeof rawAliases === "string" ? rawAliases.split(",").map((s) => s.trim()).filter((s) => s !== "") : Array.isArray(req.aliases) ? req.aliases.map((a) => String(a)) : [];
|
|
5849
6344
|
return { term, aliases };
|
|
5850
6345
|
}
|
|
5851
|
-
function
|
|
5852
|
-
const { domain: domain2, requirements, req } = target;
|
|
6346
|
+
function flipPredecessor(req, newId, issue) {
|
|
5853
6347
|
req.status = "superseded";
|
|
5854
6348
|
req.supersededBy = newId;
|
|
6349
|
+
if (issue) {
|
|
6350
|
+
const issues = Array.isArray(req.issues) ? req.issues : [];
|
|
6351
|
+
issues.push({ role: "supersedes-via", id: issue });
|
|
6352
|
+
req.issues = issues;
|
|
6353
|
+
}
|
|
6354
|
+
}
|
|
6355
|
+
function applySupersedeEdit(target, newId, requirement, why, lives, noBump, termFields, issue) {
|
|
6356
|
+
const { domain: domain2, requirements, req } = target;
|
|
6357
|
+
flipPredecessor(req, newId, issue);
|
|
5855
6358
|
const successor = {
|
|
5856
6359
|
id: newId,
|
|
5857
6360
|
status: "active",
|
|
@@ -5861,8 +6364,7 @@ function applySupersedeEdit(target, newId, requirement, why, lives, noBump, term
|
|
|
5861
6364
|
supersededBy: null,
|
|
5862
6365
|
relates: [],
|
|
5863
6366
|
livesIn: lives === "" ? [] : [lives],
|
|
5864
|
-
issues: []
|
|
5865
|
-
changedAtVersion: 1
|
|
6367
|
+
issues: issue ? [{ role: "created", id: issue }] : []
|
|
5866
6368
|
};
|
|
5867
6369
|
if (termFields !== null) {
|
|
5868
6370
|
if (termFields.term !== undefined)
|
|
@@ -5871,20 +6373,25 @@ function applySupersedeEdit(target, newId, requirement, why, lives, noBump, term
|
|
|
5871
6373
|
}
|
|
5872
6374
|
requirements.push(successor);
|
|
5873
6375
|
domain2.requirements = requirements;
|
|
5874
|
-
|
|
5875
|
-
if (domain2.key === "TERM") {
|
|
5876
|
-
const currentVersion = typeof domain2.specVersion === "number" ? domain2.specVersion : 1;
|
|
5877
|
-
reportedVersion = noBump ? null : currentVersion + 1;
|
|
5878
|
-
if (reportedVersion !== null)
|
|
5879
|
-
domain2.specVersion = reportedVersion;
|
|
5880
|
-
req.supersededAtVersion = reportedVersion ?? currentVersion;
|
|
5881
|
-
} else {
|
|
5882
|
-
reportedVersion = deriveDomainVersion(requirements);
|
|
5883
|
-
req.supersededAtVersion = reportedVersion;
|
|
5884
|
-
}
|
|
6376
|
+
const reportedVersion = applyVersionStage(target, successor, noBump);
|
|
5885
6377
|
domain2.updated = localToday();
|
|
5886
6378
|
return reportedVersion;
|
|
5887
6379
|
}
|
|
6380
|
+
function applyVersionStage(target, successor, noBump) {
|
|
6381
|
+
const { domain: domain2, requirements, req } = target;
|
|
6382
|
+
if (domain2.key !== "TERM") {
|
|
6383
|
+
const derived = deriveDomainVersion(requirements);
|
|
6384
|
+
req.supersededAtVersion = derived;
|
|
6385
|
+
return derived;
|
|
6386
|
+
}
|
|
6387
|
+
const currentVersion = typeof domain2.specVersion === "number" ? domain2.specVersion : 1;
|
|
6388
|
+
const reported = noBump ? null : currentVersion + 1;
|
|
6389
|
+
if (reported !== null)
|
|
6390
|
+
domain2.specVersion = reported;
|
|
6391
|
+
req.supersededAtVersion = reported ?? currentVersion;
|
|
6392
|
+
successor.changedAtVersion = reported ?? currentVersion;
|
|
6393
|
+
return reported;
|
|
6394
|
+
}
|
|
5888
6395
|
async function reindexAndCollectRetag(platformDir, id) {
|
|
5889
6396
|
const tags = await reindexAndListTags(platformDir, id);
|
|
5890
6397
|
return tags.map(({ req_id, repo, file, line, kind, level }) => ({
|
|
@@ -5900,10 +6407,11 @@ var supersedeCommand;
|
|
|
5900
6407
|
var init_supersede = __esm(() => {
|
|
5901
6408
|
init_src();
|
|
5902
6409
|
init_domains();
|
|
6410
|
+
init_grammar2();
|
|
5903
6411
|
init_constants();
|
|
5904
6412
|
init_discover();
|
|
5905
6413
|
init_domainJson();
|
|
5906
|
-
|
|
6414
|
+
init_grammar3();
|
|
5907
6415
|
init__shared();
|
|
5908
6416
|
init_req();
|
|
5909
6417
|
supersedeCommand = defineCommand13({
|
|
@@ -5930,10 +6438,6 @@ var init_supersede = __esm(() => {
|
|
|
5930
6438
|
type: "string",
|
|
5931
6439
|
description: "Successor's Why it matters (default: copied from the old entry)"
|
|
5932
6440
|
},
|
|
5933
|
-
binds: {
|
|
5934
|
-
type: "string",
|
|
5935
|
-
description: "Binds value (validated for @-refs; not persisted in JSON \u2014 STOR-01)"
|
|
5936
|
-
},
|
|
5937
6441
|
lives: {
|
|
5938
6442
|
type: "string",
|
|
5939
6443
|
description: "Successor's Lives in (default: copied from the old entry)"
|
|
@@ -5950,6 +6454,10 @@ var init_supersede = __esm(() => {
|
|
|
5950
6454
|
type: "boolean",
|
|
5951
6455
|
description: "Do not bump the envelope specVersion"
|
|
5952
6456
|
},
|
|
6457
|
+
issue: {
|
|
6458
|
+
type: "string",
|
|
6459
|
+
description: "Ticket that caused this supersession \u2014 recorded as supersedes-via on the predecessor and created on the successor (opaque provenance)"
|
|
6460
|
+
},
|
|
5953
6461
|
json: {
|
|
5954
6462
|
type: "boolean",
|
|
5955
6463
|
description: "Emit { old_id, new_id, file, spec_version, retag } as JSON instead of the text summary"
|
|
@@ -5960,11 +6468,12 @@ var init_supersede = __esm(() => {
|
|
|
5960
6468
|
const platformDir = resolve18(args.platformDir ?? process.cwd());
|
|
5961
6469
|
const target = await resolveSupersedeTarget(id, platformDir);
|
|
5962
6470
|
const requirement = await resolveSuccessorText(args, id);
|
|
5963
|
-
|
|
5964
|
-
|
|
6471
|
+
await enforceStatementGrammar(platformDir, target.key, requirement, "spec supersede");
|
|
6472
|
+
const { why, lives } = resolveSuccessorFields(args, target.req);
|
|
6473
|
+
warnUnresolvableRefs(platformDir, [requirement, why, lives]);
|
|
5965
6474
|
const newId = await nextRequirementId(platformDir, target.key);
|
|
5966
6475
|
const termFields = target.key === "TERM" ? resolveSuccessorTermFields(args, target.req) : null;
|
|
5967
|
-
const newVersion = applySupersedeEdit(target, newId, requirement, why, lives, Boolean(args.noBump), termFields);
|
|
6476
|
+
const newVersion = applySupersedeEdit(target, newId, requirement, why, lives, Boolean(args.noBump), termFields, (args.issue ?? "").trim() || undefined);
|
|
5968
6477
|
const res = await validateAndWrite(target.specPath, target.domain, target.relFile);
|
|
5969
6478
|
if (!res.ok) {
|
|
5970
6479
|
for (const diag of res.diagnostics) {
|
|
@@ -5998,18 +6507,133 @@ var init_supersede = __esm(() => {
|
|
|
5998
6507
|
});
|
|
5999
6508
|
});
|
|
6000
6509
|
|
|
6510
|
+
// src/commands/deprecate.ts
|
|
6511
|
+
var exports_deprecate = {};
|
|
6512
|
+
__export(exports_deprecate, {
|
|
6513
|
+
deprecateCommand: () => deprecateCommand
|
|
6514
|
+
});
|
|
6515
|
+
import { existsSync as existsSync13 } from "fs";
|
|
6516
|
+
import { join as join20, resolve as resolve19 } from "path";
|
|
6517
|
+
import { defineCommand as defineCommand14 } from "citty";
|
|
6518
|
+
async function resolveDeprecateTarget(platformDir, id) {
|
|
6519
|
+
const key = id.slice(0, id.indexOf("-"));
|
|
6520
|
+
const relFile = `spec-engine/${key}/SPEC.json`;
|
|
6521
|
+
const specPath = join20(platformDir, "spec-engine", key, "SPEC.json");
|
|
6522
|
+
if (!existsSync13(specPath)) {
|
|
6523
|
+
console.error(`spec deprecate: no domain ${key} (expected ${relFile} under ${platformDir})`);
|
|
6524
|
+
process.exit(EXIT.USAGE);
|
|
6525
|
+
}
|
|
6526
|
+
const domain2 = JSON.parse(await Bun.file(specPath).text());
|
|
6527
|
+
const requirements = Array.isArray(domain2.requirements) ? domain2.requirements : [];
|
|
6528
|
+
const req = requirements.find((r) => r?.id === id);
|
|
6529
|
+
if (req === undefined) {
|
|
6530
|
+
console.error(`spec deprecate: no entry ${id} in ${relFile}`);
|
|
6531
|
+
process.exit(EXIT.USAGE);
|
|
6532
|
+
}
|
|
6533
|
+
const statusLc = (typeof req.status === "string" ? req.status : "").toLowerCase();
|
|
6534
|
+
if (statusLc === "deprecated" || statusLc === "retired") {
|
|
6535
|
+
console.error(`spec deprecate: ${id} is already deprecated`);
|
|
6536
|
+
process.exit(EXIT.USAGE);
|
|
6537
|
+
}
|
|
6538
|
+
if (statusLc !== "active" && statusLc !== "draft") {
|
|
6539
|
+
console.error(`spec deprecate: ${id} is ${req.status} \u2014 only Active/Draft entries deprecate (a superseded entry is already history)`);
|
|
6540
|
+
process.exit(EXIT.USAGE);
|
|
6541
|
+
}
|
|
6542
|
+
return { domain: domain2, req, specPath, relFile };
|
|
6543
|
+
}
|
|
6544
|
+
var deprecateCommand;
|
|
6545
|
+
var init_deprecate = __esm(() => {
|
|
6546
|
+
init_src();
|
|
6547
|
+
init_constants();
|
|
6548
|
+
init_discover();
|
|
6549
|
+
init_grammar3();
|
|
6550
|
+
init__shared();
|
|
6551
|
+
deprecateCommand = defineCommand14({
|
|
6552
|
+
meta: {
|
|
6553
|
+
name: "deprecate",
|
|
6554
|
+
description: "Mark a requirement end-of-life with a recorded reason (a requirement is never deleted)"
|
|
6555
|
+
},
|
|
6556
|
+
args: {
|
|
6557
|
+
id: {
|
|
6558
|
+
type: "positional",
|
|
6559
|
+
required: true,
|
|
6560
|
+
description: "Requirement id to deprecate (KEY-NNN)"
|
|
6561
|
+
},
|
|
6562
|
+
platformDir: {
|
|
6563
|
+
type: "positional",
|
|
6564
|
+
required: false,
|
|
6565
|
+
description: "Platform directory (default: cwd)"
|
|
6566
|
+
},
|
|
6567
|
+
reason: {
|
|
6568
|
+
type: "string",
|
|
6569
|
+
description: "Why this requirement is end-of-life (required \u2014 the durable record)"
|
|
6570
|
+
},
|
|
6571
|
+
json: {
|
|
6572
|
+
type: "boolean",
|
|
6573
|
+
description: "Print { id, file, reason, sites } instead of text"
|
|
6574
|
+
}
|
|
6575
|
+
},
|
|
6576
|
+
async run({ args }) {
|
|
6577
|
+
const id = args.id;
|
|
6578
|
+
const platformDir = resolve19(args.platformDir ?? process.cwd());
|
|
6579
|
+
const reason = (args.reason ?? "").trim();
|
|
6580
|
+
if (!ID_RE.test(id)) {
|
|
6581
|
+
console.error(`spec deprecate: id must be a requirement id (KEY-NNN); got ${id}`);
|
|
6582
|
+
process.exit(EXIT.USAGE);
|
|
6583
|
+
}
|
|
6584
|
+
if (reason === "") {
|
|
6585
|
+
console.error("spec deprecate: --reason is required \u2014 the reason is the durable record of why this requirement ended");
|
|
6586
|
+
process.exit(EXIT.USAGE);
|
|
6587
|
+
}
|
|
6588
|
+
try {
|
|
6589
|
+
assertSpecPlatform(platformDir);
|
|
6590
|
+
} catch (e) {
|
|
6591
|
+
handleNotAPlatform(e);
|
|
6592
|
+
}
|
|
6593
|
+
const { domain: domain2, req, specPath, relFile } = await resolveDeprecateTarget(platformDir, id);
|
|
6594
|
+
req.status = "deprecated";
|
|
6595
|
+
req.deprecatedReason = reason;
|
|
6596
|
+
domain2.updated = localToday();
|
|
6597
|
+
const res = await validateAndWrite(specPath, domain2, relFile);
|
|
6598
|
+
if (!res.ok) {
|
|
6599
|
+
for (const diag of res.diagnostics)
|
|
6600
|
+
console.error(`spec deprecate: ${diag.detail}`);
|
|
6601
|
+
process.exit(EXIT.USAGE);
|
|
6602
|
+
}
|
|
6603
|
+
const tags = await reindexAndListTags(platformDir, id);
|
|
6604
|
+
const sites = tags.map(({ req_id, repo, file, line, kind, level }) => ({
|
|
6605
|
+
req_id,
|
|
6606
|
+
repo,
|
|
6607
|
+
file,
|
|
6608
|
+
line,
|
|
6609
|
+
kind,
|
|
6610
|
+
level: level ?? null
|
|
6611
|
+
}));
|
|
6612
|
+
if (args.json) {
|
|
6613
|
+
console.log(JSON.stringify({ id, file: relFile, reason, sites }));
|
|
6614
|
+
return;
|
|
6615
|
+
}
|
|
6616
|
+
console.log(`deprecated ${id} in ${relFile} \u2014 reason recorded`);
|
|
6617
|
+
if (sites.length > 0) {
|
|
6618
|
+
console.error(`${sites.length} code tag(s) still bind ${id} \u2014 remove or retag them (spec check reports each as DEPRECATED_REFERENCED):`);
|
|
6619
|
+
console.error(renderReqTags(sites, "text"));
|
|
6620
|
+
}
|
|
6621
|
+
}
|
|
6622
|
+
});
|
|
6623
|
+
});
|
|
6624
|
+
|
|
6001
6625
|
// src/commands/move.ts
|
|
6002
6626
|
var exports_move = {};
|
|
6003
6627
|
__export(exports_move, {
|
|
6004
6628
|
moveCommand: () => moveCommand
|
|
6005
6629
|
});
|
|
6006
|
-
import { existsSync as
|
|
6007
|
-
import { join as
|
|
6008
|
-
import { defineCommand as
|
|
6630
|
+
import { existsSync as existsSync14 } from "fs";
|
|
6631
|
+
import { join as join21, resolve as resolve20 } from "path";
|
|
6632
|
+
import { defineCommand as defineCommand15 } from "citty";
|
|
6009
6633
|
async function loadDomainSide(platformDir, key) {
|
|
6010
6634
|
const relFile = `spec-engine/${key}/SPEC.json`;
|
|
6011
|
-
const specPath =
|
|
6012
|
-
if (!
|
|
6635
|
+
const specPath = join21(platformDir, "spec-engine", key, "SPEC.json");
|
|
6636
|
+
if (!existsSync14(specPath))
|
|
6013
6637
|
return null;
|
|
6014
6638
|
const domain2 = JSON.parse(await Bun.file(specPath).text());
|
|
6015
6639
|
const requirements = Array.isArray(domain2.requirements) ? domain2.requirements : [];
|
|
@@ -6068,6 +6692,15 @@ function resolveSuccessorFields2(args, req) {
|
|
|
6068
6692
|
lives: (args.lives ?? srcLives).trim()
|
|
6069
6693
|
};
|
|
6070
6694
|
}
|
|
6695
|
+
async function assertMovableStatement(platformDir, id, targetKey, statement, args) {
|
|
6696
|
+
if (statement === "") {
|
|
6697
|
+
console.error(`spec move: ${id} has an empty Requirement \u2014 cannot move a blank statement`);
|
|
6698
|
+
process.exit(EXIT.USAGE);
|
|
6699
|
+
}
|
|
6700
|
+
if (typeof args.text === "string") {
|
|
6701
|
+
await enforceStatementGrammar(platformDir, targetKey, statement, "spec move");
|
|
6702
|
+
}
|
|
6703
|
+
}
|
|
6071
6704
|
function resolveMoveVersion(domain2, key, noBump) {
|
|
6072
6705
|
domain2.updated = localToday();
|
|
6073
6706
|
if (key !== "TERM") {
|
|
@@ -6123,15 +6756,16 @@ var moveCommand;
|
|
|
6123
6756
|
var init_move = __esm(() => {
|
|
6124
6757
|
init_src();
|
|
6125
6758
|
init_domains();
|
|
6759
|
+
init_grammar2();
|
|
6126
6760
|
init_constants();
|
|
6127
6761
|
init_discover();
|
|
6128
6762
|
init_pipeline();
|
|
6129
6763
|
init_domainJson();
|
|
6130
|
-
|
|
6764
|
+
init_grammar3();
|
|
6131
6765
|
init_sqlite();
|
|
6132
6766
|
init__shared();
|
|
6133
6767
|
init_req();
|
|
6134
|
-
moveCommand =
|
|
6768
|
+
moveCommand = defineCommand15({
|
|
6135
6769
|
meta: {
|
|
6136
6770
|
name: "move",
|
|
6137
6771
|
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 emit the retag worklist."
|
|
@@ -6176,13 +6810,10 @@ var init_move = __esm(() => {
|
|
|
6176
6810
|
async run({ args }) {
|
|
6177
6811
|
const id = args.id;
|
|
6178
6812
|
const rawTargetKey = args.newDomain;
|
|
6179
|
-
const platformDir =
|
|
6813
|
+
const platformDir = resolve20(args.platformDir ?? process.cwd());
|
|
6180
6814
|
const target = await resolveMoveTarget(id, rawTargetKey, platformDir);
|
|
6181
6815
|
const fields = resolveSuccessorFields2(args, target.req);
|
|
6182
|
-
|
|
6183
|
-
console.error(`spec move: ${id} has an empty Requirement \u2014 cannot move a blank statement`);
|
|
6184
|
-
process.exit(EXIT.USAGE);
|
|
6185
|
-
}
|
|
6816
|
+
await assertMovableStatement(platformDir, id, target.target.key, fields.statement, args);
|
|
6186
6817
|
warnUnresolvableRefs(platformDir, [fields.statement, fields.why, fields.lives]);
|
|
6187
6818
|
const newId = await nextRequirementId(platformDir, target.target.key);
|
|
6188
6819
|
const { sourceVersion, targetVersion } = applyMoveEdit(target, newId, fields, Boolean(args.noBump));
|
|
@@ -6234,17 +6865,17 @@ var exports_amend = {};
|
|
|
6234
6865
|
__export(exports_amend, {
|
|
6235
6866
|
amendCommand: () => amendCommand
|
|
6236
6867
|
});
|
|
6237
|
-
import { existsSync as
|
|
6238
|
-
import { join as
|
|
6239
|
-
import { defineCommand as
|
|
6868
|
+
import { existsSync as existsSync15 } from "fs";
|
|
6869
|
+
import { join as join22, resolve as resolve21 } from "path";
|
|
6870
|
+
import { defineCommand as defineCommand16 } from "citty";
|
|
6240
6871
|
function validateAmendFields(args) {
|
|
6241
6872
|
const hasText = typeof args.text === "string";
|
|
6873
|
+
const hasIssue = typeof args.issue === "string" && args.issue.trim() !== "";
|
|
6242
6874
|
const hasWhy = typeof args.why === "string";
|
|
6243
6875
|
const hasLives = typeof args.lives === "string";
|
|
6244
|
-
const hasBinds = typeof args.binds === "string";
|
|
6245
6876
|
const hasTerm = typeof args.term === "string";
|
|
6246
6877
|
const hasAliases = typeof args.aliases === "string";
|
|
6247
|
-
if (!hasText && !hasWhy && !hasLives && !hasTerm && !hasAliases) {
|
|
6878
|
+
if (!hasText && !hasWhy && !hasLives && !hasTerm && !hasAliases && !hasIssue) {
|
|
6248
6879
|
console.error("spec amend: nothing to amend \u2014 pass at least one of --text / --why / --lives / --term / --aliases");
|
|
6249
6880
|
process.exit(EXIT.USAGE);
|
|
6250
6881
|
}
|
|
@@ -6256,13 +6887,13 @@ function validateAmendFields(args) {
|
|
|
6256
6887
|
console.error("spec amend: --term must be a non-empty headword");
|
|
6257
6888
|
process.exit(EXIT.USAGE);
|
|
6258
6889
|
}
|
|
6259
|
-
return { hasText, hasWhy, hasLives,
|
|
6890
|
+
return { hasText, hasWhy, hasLives, hasTerm, hasAliases, hasIssue };
|
|
6260
6891
|
}
|
|
6261
6892
|
async function locateAmendEntry(platformDir, id) {
|
|
6262
6893
|
const key = id.slice(0, id.indexOf("-"));
|
|
6263
6894
|
const relFile = `spec-engine/${key}/SPEC.json`;
|
|
6264
|
-
const specPath =
|
|
6265
|
-
if (!
|
|
6895
|
+
const specPath = join22(platformDir, "spec-engine", key, "SPEC.json");
|
|
6896
|
+
if (!existsSync15(specPath)) {
|
|
6266
6897
|
console.error(`spec amend: no domain ${key} (expected ${relFile} under ${platformDir})`);
|
|
6267
6898
|
process.exit(EXIT.USAGE);
|
|
6268
6899
|
}
|
|
@@ -6315,15 +6946,18 @@ function applyAmendMutations(req, args, fields) {
|
|
|
6315
6946
|
fieldsChanged.push("lives");
|
|
6316
6947
|
refValues.push(v);
|
|
6317
6948
|
}
|
|
6318
|
-
if (fields.hasBinds) {
|
|
6319
|
-
refValues.push(args.binds.trim());
|
|
6320
|
-
}
|
|
6321
6949
|
if (fields.hasTerm) {
|
|
6322
6950
|
const v = args.term.trim();
|
|
6323
6951
|
req.term = v;
|
|
6324
6952
|
fieldsChanged.push("term");
|
|
6325
6953
|
refValues.push(v);
|
|
6326
6954
|
}
|
|
6955
|
+
if (fields.hasIssue) {
|
|
6956
|
+
const issues = Array.isArray(req.issues) ? req.issues : [];
|
|
6957
|
+
issues.push({ role: "amends-via", id: args.issue.trim() });
|
|
6958
|
+
req.issues = issues;
|
|
6959
|
+
fieldsChanged.push("issue");
|
|
6960
|
+
}
|
|
6327
6961
|
if (fields.hasAliases) {
|
|
6328
6962
|
const raw = args.aliases.trim();
|
|
6329
6963
|
req.aliases = raw === "" ? [] : raw.split(",").map((s) => s.trim()).filter((s) => s !== "");
|
|
@@ -6334,12 +6968,13 @@ function applyAmendMutations(req, args, fields) {
|
|
|
6334
6968
|
var amendCommand;
|
|
6335
6969
|
var init_amend = __esm(() => {
|
|
6336
6970
|
init_src();
|
|
6971
|
+
init_grammar2();
|
|
6337
6972
|
init_constants();
|
|
6338
6973
|
init_discover();
|
|
6339
|
-
|
|
6974
|
+
init_grammar3();
|
|
6340
6975
|
init__shared();
|
|
6341
6976
|
init_req();
|
|
6342
|
-
amendCommand =
|
|
6977
|
+
amendCommand = defineCommand16({
|
|
6343
6978
|
meta: {
|
|
6344
6979
|
name: "amend",
|
|
6345
6980
|
description: "Revise an unshipped requirement's fields in place (same id, no specVersion bump). Supersede shipped truth instead."
|
|
@@ -6357,11 +6992,11 @@ var init_amend = __esm(() => {
|
|
|
6357
6992
|
},
|
|
6358
6993
|
text: { type: "string", description: "New Requirement (statement) field value" },
|
|
6359
6994
|
why: { type: "string", description: "New Why it matters field value" },
|
|
6360
|
-
|
|
6995
|
+
lives: { type: "string", description: "New Lives in (livesIn) field value" },
|
|
6996
|
+
issue: {
|
|
6361
6997
|
type: "string",
|
|
6362
|
-
description: "
|
|
6998
|
+
description: "Ticket that motivated this amendment \u2014 recorded as amends-via provenance (opaque, never a requirement id)"
|
|
6363
6999
|
},
|
|
6364
|
-
lives: { type: "string", description: "New Lives in (livesIn) field value" },
|
|
6365
7000
|
term: { type: "string", description: "New TERM headword (term field; TERM ids)" },
|
|
6366
7001
|
aliases: {
|
|
6367
7002
|
type: "string",
|
|
@@ -6379,7 +7014,7 @@ var init_amend = __esm(() => {
|
|
|
6379
7014
|
process.exit(EXIT.USAGE);
|
|
6380
7015
|
return;
|
|
6381
7016
|
}
|
|
6382
|
-
const platformDir =
|
|
7017
|
+
const platformDir = resolve21(args.platformDir ?? process.cwd());
|
|
6383
7018
|
try {
|
|
6384
7019
|
assertSpecPlatform(platformDir);
|
|
6385
7020
|
} catch (e) {
|
|
@@ -6392,6 +7027,10 @@ var init_amend = __esm(() => {
|
|
|
6392
7027
|
if (statusLc === "active") {
|
|
6393
7028
|
await assertUnshipped(platformDir, id);
|
|
6394
7029
|
}
|
|
7030
|
+
if (fields.hasText) {
|
|
7031
|
+
const key = id.slice(0, id.indexOf("-"));
|
|
7032
|
+
await enforceStatementGrammar(platformDir, key, args.text.trim(), "spec amend");
|
|
7033
|
+
}
|
|
6395
7034
|
const { fieldsChanged, refValues } = applyAmendMutations(req, args, fields);
|
|
6396
7035
|
warnUnresolvableRefs(platformDir, refValues);
|
|
6397
7036
|
domain2.updated = localToday();
|
|
@@ -6428,13 +7067,14 @@ Target domain: ${opts.domain} (no charter/scope recorded \u2014 check placement
|
|
|
6428
7067
|
opts.brief,
|
|
6429
7068
|
"",
|
|
6430
7069
|
"Steps:",
|
|
6431
|
-
"1. One requirement per
|
|
6432
|
-
"2. Placement:
|
|
6433
|
-
|
|
6434
|
-
"4.
|
|
6435
|
-
"5.
|
|
6436
|
-
|
|
6437
|
-
|
|
7070
|
+
"1. One requirement per behavior \u2014 never one per ticket; a brief fans out into several.",
|
|
7071
|
+
"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.",
|
|
7072
|
+
'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."',
|
|
7073
|
+
"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.",
|
|
7074
|
+
"5. Tie each draft to its source: quote the exact phrase of the brief it derives from; label anything you inferred as inferred.",
|
|
7075
|
+
"6. Present the full batch (statement, why, placement, source quote) for approval BEFORE writing.",
|
|
7076
|
+
'7. Mint each approved requirement: `spec req <domain> --text "\u2026" --why "\u2026" --lives "\u2026"`.',
|
|
7077
|
+
"8. The originating ticket is provenance only \u2014 never a requirement id, never a code tag."
|
|
6438
7078
|
].join(`
|
|
6439
7079
|
`);
|
|
6440
7080
|
}
|
|
@@ -6571,7 +7211,7 @@ var init_mcp = __esm(() => {
|
|
|
6571
7211
|
init_domains();
|
|
6572
7212
|
init_constants();
|
|
6573
7213
|
init_pipeline();
|
|
6574
|
-
|
|
7214
|
+
init_grammar3();
|
|
6575
7215
|
init_sqlite();
|
|
6576
7216
|
});
|
|
6577
7217
|
|
|
@@ -6580,15 +7220,15 @@ var exports_mcp = {};
|
|
|
6580
7220
|
__export(exports_mcp, {
|
|
6581
7221
|
mcpCommand: () => mcpCommand
|
|
6582
7222
|
});
|
|
6583
|
-
import { resolve as
|
|
7223
|
+
import { resolve as resolve22 } from "path";
|
|
6584
7224
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6585
|
-
import { defineCommand as
|
|
7225
|
+
import { defineCommand as defineCommand17 } from "citty";
|
|
6586
7226
|
var mcpCommand;
|
|
6587
7227
|
var init_mcp2 = __esm(() => {
|
|
6588
7228
|
init_discover();
|
|
6589
7229
|
init_mcp();
|
|
6590
7230
|
init__shared();
|
|
6591
|
-
mcpCommand =
|
|
7231
|
+
mcpCommand = defineCommand17({
|
|
6592
7232
|
meta: {
|
|
6593
7233
|
name: "mcp",
|
|
6594
7234
|
description: "Serve Spec Engine as an MCP (Model Context Protocol) server over stdio \u2014 agent-native query/resolve/check/report tools."
|
|
@@ -6601,7 +7241,7 @@ var init_mcp2 = __esm(() => {
|
|
|
6601
7241
|
}
|
|
6602
7242
|
},
|
|
6603
7243
|
async run({ args }) {
|
|
6604
|
-
const platformDir =
|
|
7244
|
+
const platformDir = resolve22(args.platformDir ?? process.cwd());
|
|
6605
7245
|
try {
|
|
6606
7246
|
assertSpecPlatform(platformDir);
|
|
6607
7247
|
} catch (e) {
|
|
@@ -6619,9 +7259,9 @@ var exports_resolve = {};
|
|
|
6619
7259
|
__export(exports_resolve, {
|
|
6620
7260
|
resolveCommand: () => resolveCommand
|
|
6621
7261
|
});
|
|
6622
|
-
import { statSync as
|
|
6623
|
-
import { join as
|
|
6624
|
-
import { defineCommand as
|
|
7262
|
+
import { statSync as statSync6 } from "fs";
|
|
7263
|
+
import { join as join23, relative, resolve as resolve23, sep as sep3 } from "path";
|
|
7264
|
+
import { defineCommand as defineCommand18 } from "citty";
|
|
6625
7265
|
function classifyArg(a) {
|
|
6626
7266
|
if (a.startsWith("--")) {
|
|
6627
7267
|
const eq = a.indexOf("=");
|
|
@@ -6656,10 +7296,10 @@ function splitFilesAndPlatformDir(positionals, fallbackPlatformDir) {
|
|
|
6656
7296
|
let lastIsPlatformDir = false;
|
|
6657
7297
|
let lastIsDirWithoutSpecEngine = false;
|
|
6658
7298
|
try {
|
|
6659
|
-
const resolved =
|
|
6660
|
-
if (
|
|
7299
|
+
const resolved = resolve23(last);
|
|
7300
|
+
if (statSync6(resolved).isDirectory()) {
|
|
6661
7301
|
try {
|
|
6662
|
-
if (
|
|
7302
|
+
if (statSync6(join23(resolved, "spec-engine")).isDirectory()) {
|
|
6663
7303
|
lastIsPlatformDir = true;
|
|
6664
7304
|
} else {
|
|
6665
7305
|
lastIsDirWithoutSpecEngine = true;
|
|
@@ -6753,7 +7393,7 @@ async function dispatchReverseQuery(reqArg, resolved, args) {
|
|
|
6753
7393
|
}
|
|
6754
7394
|
await runReverseQuery({
|
|
6755
7395
|
reqId: reqArg,
|
|
6756
|
-
platformDir:
|
|
7396
|
+
platformDir: resolve23(lone ?? process.cwd()),
|
|
6757
7397
|
outArg: args.out,
|
|
6758
7398
|
json: Boolean(args.json),
|
|
6759
7399
|
fresh: Boolean(args.fresh),
|
|
@@ -6763,7 +7403,7 @@ async function dispatchReverseQuery(reqArg, resolved, args) {
|
|
|
6763
7403
|
function normalizeFileInputs(files, platformDir) {
|
|
6764
7404
|
const normalized = [];
|
|
6765
7405
|
for (const input of files) {
|
|
6766
|
-
const rel = relative(platformDir,
|
|
7406
|
+
const rel = relative(platformDir, resolve23(platformDir, input));
|
|
6767
7407
|
const segments = rel.split(sep3);
|
|
6768
7408
|
if (rel === "" || rel === "." || segments[0] === "..") {
|
|
6769
7409
|
console.error(`spec resolve: file path must be inside platformDir: ${input}`);
|
|
@@ -6781,7 +7421,7 @@ var init_resolve2 = __esm(() => {
|
|
|
6781
7421
|
init_prompt();
|
|
6782
7422
|
init__shared();
|
|
6783
7423
|
VALUE_FLAGS = new Set(["--out", "-o", "--platformDir", "--req"]);
|
|
6784
|
-
resolveCommand =
|
|
7424
|
+
resolveCommand = defineCommand18({
|
|
6785
7425
|
meta: {
|
|
6786
7426
|
name: "resolve",
|
|
6787
7427
|
description: "Return the requirements tagged in the given files (RSLV-01). 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."
|
|
@@ -6836,7 +7476,7 @@ var init_resolve2 = __esm(() => {
|
|
|
6836
7476
|
process.exit(EXIT.USAGE);
|
|
6837
7477
|
return;
|
|
6838
7478
|
}
|
|
6839
|
-
const platformDir =
|
|
7479
|
+
const platformDir = resolve23(resolved.platformDir ?? process.cwd());
|
|
6840
7480
|
const outArg = args.out;
|
|
6841
7481
|
const dbPath = resolveDbPath(platformDir, outArg);
|
|
6842
7482
|
if (outArg)
|
|
@@ -6905,6 +7545,18 @@ function classifyGate(input) {
|
|
|
6905
7545
|
pinned_spec_version: pin
|
|
6906
7546
|
};
|
|
6907
7547
|
}
|
|
7548
|
+
if (req.status === "Deprecated") {
|
|
7549
|
+
return {
|
|
7550
|
+
pass: false,
|
|
7551
|
+
reason: "DEPRECATED",
|
|
7552
|
+
repo: requestedRepoName,
|
|
7553
|
+
req_id: requestedReqId,
|
|
7554
|
+
detail: `Requirement ${requestedReqId} is Deprecated \u2014 it is end-of-life with no successor; do not build against it`,
|
|
7555
|
+
status: req.status,
|
|
7556
|
+
changed_at_version: req.changed_at_version,
|
|
7557
|
+
pinned_spec_version: pin
|
|
7558
|
+
};
|
|
7559
|
+
}
|
|
6908
7560
|
if (req.changed_at_version > pin) {
|
|
6909
7561
|
return {
|
|
6910
7562
|
pass: false,
|
|
@@ -6942,9 +7594,9 @@ var exports_gate = {};
|
|
|
6942
7594
|
__export(exports_gate, {
|
|
6943
7595
|
gateCommand: () => gateCommand
|
|
6944
7596
|
});
|
|
6945
|
-
import { existsSync as
|
|
6946
|
-
import { dirname as dirname10, join as
|
|
6947
|
-
import { defineCommand as
|
|
7597
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync7 } from "fs";
|
|
7598
|
+
import { dirname as dirname10, join as join24, resolve as resolve24 } from "path";
|
|
7599
|
+
import { defineCommand as defineCommand19 } from "citty";
|
|
6948
7600
|
function isEpipe(e) {
|
|
6949
7601
|
if (e === null || typeof e !== "object")
|
|
6950
7602
|
return false;
|
|
@@ -6958,8 +7610,8 @@ function resolveGateArgs(args) {
|
|
|
6958
7610
|
console.error("spec gate: <repo> and <reqId> are required (e.g., spec gate api BILLING-009)");
|
|
6959
7611
|
process.exit(EXIT.USAGE);
|
|
6960
7612
|
}
|
|
6961
|
-
const platformDir =
|
|
6962
|
-
const hasSpecEngineDir =
|
|
7613
|
+
const platformDir = resolve24(args.platformDir ?? process.cwd());
|
|
7614
|
+
const hasSpecEngineDir = existsSync16(join24(platformDir, "spec-engine"));
|
|
6963
7615
|
if (!hasSpecEngineDir) {
|
|
6964
7616
|
console.error(formatNotASpecPlatform(platformDir));
|
|
6965
7617
|
process.exit(EXIT.USAGE);
|
|
@@ -6998,7 +7650,7 @@ var init_gate = __esm(() => {
|
|
|
6998
7650
|
init_prompt();
|
|
6999
7651
|
init_sqlite();
|
|
7000
7652
|
init__shared();
|
|
7001
|
-
gateCommand =
|
|
7653
|
+
gateCommand = defineCommand19({
|
|
7002
7654
|
meta: {
|
|
7003
7655
|
name: "gate",
|
|
7004
7656
|
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."
|
|
@@ -7084,26 +7736,8 @@ var init_gate = __esm(() => {
|
|
|
7084
7736
|
});
|
|
7085
7737
|
});
|
|
7086
7738
|
|
|
7087
|
-
// src/guard/directives.ts
|
|
7088
|
-
function scanApprovals(text) {
|
|
7089
|
-
const out = [];
|
|
7090
|
-
for (const line of text.split(`
|
|
7091
|
-
`)) {
|
|
7092
|
-
const m = APPROVE_RE.exec(line);
|
|
7093
|
-
if (m !== null) {
|
|
7094
|
-
out.push({ req_id: m[1], reason: m[2].trim() });
|
|
7095
|
-
}
|
|
7096
|
-
}
|
|
7097
|
-
return out;
|
|
7098
|
-
}
|
|
7099
|
-
var APPROVE_RE;
|
|
7100
|
-
var init_directives = __esm(() => {
|
|
7101
|
-
APPROVE_RE = /@spec\s+approve\s+([A-Z][A-Z0-9]*-\d+)\s+(\S.*)$/;
|
|
7102
|
-
});
|
|
7103
|
-
|
|
7104
7739
|
// src/guard/collect.ts
|
|
7105
|
-
import {
|
|
7106
|
-
import { join as join19 } from "path";
|
|
7740
|
+
import { join as join25 } from "path";
|
|
7107
7741
|
function isIgnored(path) {
|
|
7108
7742
|
return isPathIgnored(path);
|
|
7109
7743
|
}
|
|
@@ -7160,7 +7794,7 @@ async function collectWorktreeReqs(canonicalDir) {
|
|
|
7160
7794
|
const activeIds = new Set;
|
|
7161
7795
|
const supersedesTargets = new Set;
|
|
7162
7796
|
for (const rel of await findDomainJsonFiles(canonicalDir)) {
|
|
7163
|
-
const text = await Bun.file(
|
|
7797
|
+
const text = await Bun.file(join25(canonicalDir, rel)).text();
|
|
7164
7798
|
for (const r of parseReqs(text)) {
|
|
7165
7799
|
ids.add(r.id);
|
|
7166
7800
|
if (r.status.toLowerCase() === "active")
|
|
@@ -7181,20 +7815,6 @@ function tallyWorktreeTags(storage2) {
|
|
|
7181
7815
|
}
|
|
7182
7816
|
return { implCount, verifyCount };
|
|
7183
7817
|
}
|
|
7184
|
-
async function collectApprovals(platformDir, changed) {
|
|
7185
|
-
const approved = new Set;
|
|
7186
|
-
for (const entry of changed) {
|
|
7187
|
-
if (entry.status === "D")
|
|
7188
|
-
continue;
|
|
7189
|
-
const abs = join19(platformDir, entry.path);
|
|
7190
|
-
if (!existsSync16(abs))
|
|
7191
|
-
continue;
|
|
7192
|
-
const text = await Bun.file(abs).text();
|
|
7193
|
-
for (const a of scanApprovals(text))
|
|
7194
|
-
approved.add(a.req_id);
|
|
7195
|
-
}
|
|
7196
|
-
return approved;
|
|
7197
|
-
}
|
|
7198
7818
|
async function collectFacts(platformDir, ref, storage2) {
|
|
7199
7819
|
const changed = gitDiffNameStatus(platformDir, ref);
|
|
7200
7820
|
const specPaths = changed.filter((e) => isSpecFile(e.path)).map((e) => e.path);
|
|
@@ -7202,9 +7822,8 @@ async function collectFacts(platformDir, ref, storage2) {
|
|
|
7202
7822
|
const deletedSpecFiles = changed.filter((e) => e.status === "D" && isSpecFile(e.path)).map((e) => e.path);
|
|
7203
7823
|
const base = collectBaseReqs2(platformDir, ref, specPaths);
|
|
7204
7824
|
const baseTags = collectBaseTags(platformDir, ref, codePaths);
|
|
7205
|
-
const worktree = await collectWorktreeReqs(
|
|
7825
|
+
const worktree = await collectWorktreeReqs(join25(platformDir, CANONICAL_SPECS_DIR));
|
|
7206
7826
|
const counts = tallyWorktreeTags(storage2);
|
|
7207
|
-
const approved = await collectApprovals(platformDir, changed);
|
|
7208
7827
|
return {
|
|
7209
7828
|
baseReqs: base.reqs,
|
|
7210
7829
|
baseReqPath: base.path,
|
|
@@ -7215,7 +7834,6 @@ async function collectFacts(platformDir, ref, storage2) {
|
|
|
7215
7834
|
worktreeSupersedesTargets: worktree.supersedesTargets,
|
|
7216
7835
|
worktreeImplCount: counts.implCount,
|
|
7217
7836
|
worktreeVerifyCount: counts.verifyCount,
|
|
7218
|
-
approved,
|
|
7219
7837
|
deletedSpecFiles
|
|
7220
7838
|
};
|
|
7221
7839
|
}
|
|
@@ -7225,7 +7843,6 @@ var init_collect = __esm(() => {
|
|
|
7225
7843
|
init_constants();
|
|
7226
7844
|
init_fs();
|
|
7227
7845
|
init_tags();
|
|
7228
|
-
init_directives();
|
|
7229
7846
|
CODE_EXTS = new Set(DEFAULT_EXTS);
|
|
7230
7847
|
});
|
|
7231
7848
|
|
|
@@ -7263,7 +7880,7 @@ function deletesClause(losses) {
|
|
|
7263
7880
|
return joinClauses(parts);
|
|
7264
7881
|
}
|
|
7265
7882
|
function requirementBlock(reqId, losses) {
|
|
7266
|
-
return `${STOP} spec-guard: ${reqId} is Active and this change deletes ${deletesClause(losses)}. ` + "Requirements are superseded, never deleted. " + `Either run \`spec supersede ${reqId}\` with a successor, ` +
|
|
7883
|
+
return `${STOP} spec-guard: ${reqId} is Active and this change deletes ${deletesClause(losses)}. ` + "Requirements are superseded, never deleted. " + `Either run \`spec supersede ${reqId}\` with a successor, or run ` + `\`spec deprecate ${reqId} --reason "..."\` to end it with a recorded reason.`;
|
|
7267
7884
|
}
|
|
7268
7885
|
function fileBlock(loss) {
|
|
7269
7886
|
return `${STOP} spec-guard: the canonical spec file ${loss.file} was deleted. ` + "Requirements are superseded, never deleted \u2014 restore the file or supersede " + "its requirements into another domain before removing it.";
|
|
@@ -7314,7 +7931,17 @@ function reqDeletedLoss(base, f) {
|
|
|
7314
7931
|
req_id: base.id,
|
|
7315
7932
|
file: f.baseReqPath.get(base.id) ?? "",
|
|
7316
7933
|
line: 0,
|
|
7317
|
-
detail: `${base.id} was Active at the ref but is absent from the working-tree spec
|
|
7934
|
+
detail: `${base.id} was Active at the ref but is absent from the working-tree spec and was not superseded \u2014 a requirement is never deleted`
|
|
7935
|
+
};
|
|
7936
|
+
}
|
|
7937
|
+
function historyRemovedLoss(base, f) {
|
|
7938
|
+
const display = base.status.charAt(0).toUpperCase() + base.status.slice(1).toLowerCase();
|
|
7939
|
+
return {
|
|
7940
|
+
kind: "REQUIREMENT_REMOVED",
|
|
7941
|
+
req_id: base.id,
|
|
7942
|
+
file: f.baseReqPath.get(base.id) ?? "",
|
|
7943
|
+
line: 0,
|
|
7944
|
+
detail: `${base.id} was ${display} at the ref but is absent from the working-tree spec \u2014 a requirement is never deleted, and its history is the record`
|
|
7318
7945
|
};
|
|
7319
7946
|
}
|
|
7320
7947
|
function implLostLoss(id, site) {
|
|
@@ -7345,18 +7972,16 @@ function specFileDeletedLoss(path) {
|
|
|
7345
7972
|
};
|
|
7346
7973
|
}
|
|
7347
7974
|
function reqDeletedFor(base, f) {
|
|
7348
|
-
if (!isActive(base.status))
|
|
7349
|
-
return null;
|
|
7350
|
-
if (f.approved.has(base.id))
|
|
7351
|
-
return null;
|
|
7352
7975
|
if (f.worktreeReqIds.has(base.id))
|
|
7353
7976
|
return null;
|
|
7977
|
+
if (!isActive(base.status))
|
|
7978
|
+
return historyRemovedLoss(base, f);
|
|
7354
7979
|
if (isSuperseded(base, f))
|
|
7355
7980
|
return null;
|
|
7356
7981
|
return reqDeletedLoss(base, f);
|
|
7357
7982
|
}
|
|
7358
7983
|
function lastTagGone(id, count, f) {
|
|
7359
|
-
return
|
|
7984
|
+
return f.worktreeActiveIds.has(id) && count === 0;
|
|
7360
7985
|
}
|
|
7361
7986
|
function classifyLosses(f) {
|
|
7362
7987
|
const out = [];
|
|
@@ -7383,14 +8008,14 @@ var exports_guard = {};
|
|
|
7383
8008
|
__export(exports_guard, {
|
|
7384
8009
|
guardCommand: () => guardCommand
|
|
7385
8010
|
});
|
|
7386
|
-
import { resolve as
|
|
7387
|
-
import { defineCommand as
|
|
8011
|
+
import { resolve as resolve25 } from "path";
|
|
8012
|
+
import { defineCommand as defineCommand20 } from "citty";
|
|
7388
8013
|
var guardCommand;
|
|
7389
8014
|
var init_guard = __esm(() => {
|
|
7390
8015
|
init_constants();
|
|
7391
8016
|
init_collect();
|
|
7392
8017
|
init__shared();
|
|
7393
|
-
guardCommand =
|
|
8018
|
+
guardCommand = defineCommand20({
|
|
7394
8019
|
meta: {
|
|
7395
8020
|
name: "guard",
|
|
7396
8021
|
description: "Loss detection: diff the requirement derivation at a git ref (default HEAD) against the working tree and block requirements about to be steamrolled."
|
|
@@ -7411,7 +8036,7 @@ var init_guard = __esm(() => {
|
|
|
7411
8036
|
}
|
|
7412
8037
|
},
|
|
7413
8038
|
async run({ args }) {
|
|
7414
|
-
const platformDir =
|
|
8039
|
+
const platformDir = resolve25(args.platformDir ?? process.cwd());
|
|
7415
8040
|
const ref = args.against ?? "HEAD";
|
|
7416
8041
|
const jsonMode = Boolean(args.json);
|
|
7417
8042
|
if (!gitRefResolves(platformDir, ref)) {
|
|
@@ -7482,6 +8107,9 @@ function link(href, key, label, active) {
|
|
|
7482
8107
|
function comingSoon(label) {
|
|
7483
8108
|
return html`<span class="sidebar-link disabled" aria-disabled="true"><span class="sidebar-dot"></span><span class="sidebar-label">${label}<span class="soon">coming soon</span></span></span>`;
|
|
7484
8109
|
}
|
|
8110
|
+
function flagged(feature, href, key, label, active) {
|
|
8111
|
+
return featureEnabled(feature) ? link(href, key, label, active) : comingSoon(label);
|
|
8112
|
+
}
|
|
7485
8113
|
function navBar(active = "") {
|
|
7486
8114
|
return html`<nav class="sidebar">
|
|
7487
8115
|
<a href="/" class="sidebar-brand">${clampMark}<span>Spec Engine</span></a>
|
|
@@ -7490,22 +8118,22 @@ function navBar(active = "") {
|
|
|
7490
8118
|
<div class="sidebar-group-label">Registry</div>
|
|
7491
8119
|
${link("/", "coverage", "Coverage", active)}
|
|
7492
8120
|
${link("/requirements", "requirements", "Requirements", active)}
|
|
7493
|
-
${
|
|
8121
|
+
${flagged("glossary", "/glossary", "glossary", "Glossary", active)}
|
|
7494
8122
|
</div>
|
|
7495
8123
|
<div class="sidebar-group">
|
|
7496
8124
|
<div class="sidebar-group-label">Trace</div>
|
|
7497
|
-
${
|
|
7498
|
-
${
|
|
8125
|
+
${flagged("relations", "/relations", "relations", "Relations", active)}
|
|
8126
|
+
${flagged("provenance", "/provenance", "provenance", "Provenance", active)}
|
|
7499
8127
|
</div>
|
|
7500
8128
|
<div class="sidebar-group">
|
|
7501
8129
|
<div class="sidebar-group-label">Workspace</div>
|
|
7502
|
-
${
|
|
7503
|
-
${
|
|
8130
|
+
${flagged("query", "/query", "query", "Query", active)}
|
|
8131
|
+
${flagged("editor", "/editor", "editor", "Editor", active)}
|
|
7504
8132
|
</div>
|
|
7505
8133
|
<div class="sidebar-group">
|
|
7506
8134
|
<div class="sidebar-group-label">System</div>
|
|
7507
8135
|
${link("/setup", "setup", "Setup", active)}
|
|
7508
|
-
${
|
|
8136
|
+
${flagged("logs", "/logs", "logs", "Logs", active)}
|
|
7509
8137
|
</div>
|
|
7510
8138
|
</div>
|
|
7511
8139
|
<div class="sidebar-footer">
|
|
@@ -7516,6 +8144,7 @@ function navBar(active = "") {
|
|
|
7516
8144
|
}
|
|
7517
8145
|
var clampMark;
|
|
7518
8146
|
var init_nav = __esm(() => {
|
|
8147
|
+
init_src();
|
|
7519
8148
|
clampMark = html`<svg width="22" height="22" viewBox="0 0 64 64" fill="none" aria-hidden="true">
|
|
7520
8149
|
<path d="M25 13 H13 V51 H25" stroke="var(--fg)" stroke-width="6" />
|
|
7521
8150
|
<path d="M39 13 H51 V51 H39" stroke="var(--fg)" stroke-width="6" />
|
|
@@ -9053,7 +9682,7 @@ var init_components = __esm(() => {
|
|
|
9053
9682
|
Active: "ACTIVE",
|
|
9054
9683
|
Superseded: "SUPERSEDED",
|
|
9055
9684
|
Draft: "DRAFT",
|
|
9056
|
-
|
|
9685
|
+
Deprecated: "DEPRECATED"
|
|
9057
9686
|
};
|
|
9058
9687
|
});
|
|
9059
9688
|
|
|
@@ -9184,8 +9813,8 @@ function mountCoverage(app) {
|
|
|
9184
9813
|
const reqs = await apiJson(app, "/api/requirements");
|
|
9185
9814
|
const titleOf = new Map(reqs.map((r) => [r.id, r.text]));
|
|
9186
9815
|
const showAll = c.req.query("all") === "1";
|
|
9187
|
-
const visible = showAll ? rows : rows.filter((r) => r.req_status !== "Superseded" && r.req_status !== "
|
|
9188
|
-
const hiddenCount = new Set(rows.filter((r) => r.req_status === "Superseded" || r.req_status === "
|
|
9816
|
+
const visible = showAll ? rows : rows.filter((r) => r.req_status !== "Superseded" && r.req_status !== "Deprecated");
|
|
9817
|
+
const hiddenCount = new Set(rows.filter((r) => r.req_status === "Superseded" || r.req_status === "Deprecated").map((r) => r.req_id)).size;
|
|
9189
9818
|
const { repos, domains } = pivot(visible);
|
|
9190
9819
|
const reqCount = domains.reduce((n, d) => n + d.reqs.length, 0);
|
|
9191
9820
|
const reportRows = await apiJson(app, "/api/report");
|
|
@@ -9216,7 +9845,7 @@ function mountCoverage(app) {
|
|
|
9216
9845
|
<span class="legend-sep"></span>
|
|
9217
9846
|
<span class="legend-item">${requirementStatusBadge("Active")}</span>
|
|
9218
9847
|
${showAll ? html4`<span class="legend-item">${requirementStatusBadge("Superseded")}</span>
|
|
9219
|
-
<span class="legend-item">${requirementStatusBadge("
|
|
9848
|
+
<span class="legend-item">${requirementStatusBadge("Deprecated")}</span>` : ""}
|
|
9220
9849
|
<span class="panel-spacer"></span>
|
|
9221
9850
|
<span class="req-controls">
|
|
9222
9851
|
${showAll ? html4`<a class="req-btn status-toggle" href="/">Hide superseded & retired</a>` : html4`<a class="req-btn status-toggle" href="/?all=1">Show superseded & retired${hiddenCount > 0 ? html4` (${hiddenCount})` : ""}</a>`}
|
|
@@ -9391,6 +10020,8 @@ async function renderResult(res) {
|
|
|
9391
10020
|
}
|
|
9392
10021
|
function mountEditor(app) {
|
|
9393
10022
|
app.get("/editor", (c) => {
|
|
10023
|
+
if (!featureEnabled("editor"))
|
|
10024
|
+
return c.html(comingSoonDoc("editor", "Editor"));
|
|
9394
10025
|
const body = html5`
|
|
9395
10026
|
${navBar("editor")}
|
|
9396
10027
|
<div class="eyebrow">/ Editor</div>
|
|
@@ -9445,6 +10076,8 @@ function mountEditor(app) {
|
|
|
9445
10076
|
return c.html(page("Editor", body));
|
|
9446
10077
|
});
|
|
9447
10078
|
app.post("/editor/create", editorBodyLimit, async (c) => {
|
|
10079
|
+
if (!featureEnabled("editor"))
|
|
10080
|
+
return c.html(rejectPage(featureDisabledMessage("editor")), 404);
|
|
9448
10081
|
if (crossOriginRejected(c))
|
|
9449
10082
|
return c.html(rejectPage("cross-origin request rejected"), 403);
|
|
9450
10083
|
const form = await c.req.parseBody();
|
|
@@ -9467,6 +10100,8 @@ function mountEditor(app) {
|
|
|
9467
10100
|
return c.html(await renderResult(res));
|
|
9468
10101
|
});
|
|
9469
10102
|
app.post("/editor/amend", editorBodyLimit, async (c) => {
|
|
10103
|
+
if (!featureEnabled("editor"))
|
|
10104
|
+
return c.html(rejectPage(featureDisabledMessage("editor")), 404);
|
|
9470
10105
|
if (crossOriginRejected(c))
|
|
9471
10106
|
return c.html(rejectPage("cross-origin request rejected"), 403);
|
|
9472
10107
|
const form = await c.req.parseBody();
|
|
@@ -9489,6 +10124,7 @@ function mountEditor(app) {
|
|
|
9489
10124
|
var styleTag4, MAX_EDITOR_BODY_BYTES, editorBodyLimit;
|
|
9490
10125
|
var init_editor = __esm(() => {
|
|
9491
10126
|
init_src();
|
|
10127
|
+
init_components();
|
|
9492
10128
|
init_nav();
|
|
9493
10129
|
init_styles();
|
|
9494
10130
|
styleTag4 = raw4(`<style>${styles_default}</style>`);
|
|
@@ -9571,7 +10207,7 @@ var init_propagation3 = __esm(() => {
|
|
|
9571
10207
|
import { html as html7, raw as raw6 } from "hono/html";
|
|
9572
10208
|
function mountProvenance(app) {
|
|
9573
10209
|
app.get("/provenance", async (c) => {
|
|
9574
|
-
if (!
|
|
10210
|
+
if (!featureEnabled("provenance"))
|
|
9575
10211
|
return c.html(comingSoonDoc("provenance", "Provenance matrix"));
|
|
9576
10212
|
const text = await apiText(app, "/api/provenance?resolve=1");
|
|
9577
10213
|
let body;
|
|
@@ -9604,8 +10240,9 @@ function mountProvenance(app) {
|
|
|
9604
10240
|
</html>`);
|
|
9605
10241
|
});
|
|
9606
10242
|
}
|
|
9607
|
-
var styleTag6
|
|
10243
|
+
var styleTag6;
|
|
9608
10244
|
var init_provenance2 = __esm(() => {
|
|
10245
|
+
init_src();
|
|
9609
10246
|
init_components();
|
|
9610
10247
|
init_data();
|
|
9611
10248
|
init_nav();
|
|
@@ -9617,7 +10254,7 @@ var init_provenance2 = __esm(() => {
|
|
|
9617
10254
|
import { html as html8, raw as raw7 } from "hono/html";
|
|
9618
10255
|
function mountQuery(app) {
|
|
9619
10256
|
app.get("/query", async (c) => {
|
|
9620
|
-
if (!
|
|
10257
|
+
if (!featureEnabled("query"))
|
|
9621
10258
|
return c.html(comingSoonDoc("query", "Query"));
|
|
9622
10259
|
const q = (c.req.query("q") ?? "").trim();
|
|
9623
10260
|
const form = html8`
|
|
@@ -9703,8 +10340,9 @@ function mountQuery(app) {
|
|
|
9703
10340
|
</html>`);
|
|
9704
10341
|
});
|
|
9705
10342
|
}
|
|
9706
|
-
var styleTag7
|
|
10343
|
+
var styleTag7;
|
|
9707
10344
|
var init_query2 = __esm(() => {
|
|
10345
|
+
init_src();
|
|
9708
10346
|
init_components();
|
|
9709
10347
|
init_data();
|
|
9710
10348
|
init_nav();
|
|
@@ -9716,7 +10354,7 @@ var init_query2 = __esm(() => {
|
|
|
9716
10354
|
import { html as html9, raw as raw8 } from "hono/html";
|
|
9717
10355
|
function mountRelations(app) {
|
|
9718
10356
|
app.get("/relations", async (c) => {
|
|
9719
|
-
if (!
|
|
10357
|
+
if (!featureEnabled("relations"))
|
|
9720
10358
|
return c.html(comingSoonDoc("relations", "Relates graph"));
|
|
9721
10359
|
const rows = await apiJson(app, "/api/relations");
|
|
9722
10360
|
let body;
|
|
@@ -9754,8 +10392,9 @@ function mountRelations(app) {
|
|
|
9754
10392
|
</html>`);
|
|
9755
10393
|
});
|
|
9756
10394
|
}
|
|
9757
|
-
var styleTag8
|
|
10395
|
+
var styleTag8;
|
|
9758
10396
|
var init_relations2 = __esm(() => {
|
|
10397
|
+
init_src();
|
|
9759
10398
|
init_components();
|
|
9760
10399
|
init_data();
|
|
9761
10400
|
init_nav();
|
|
@@ -9829,7 +10468,7 @@ function verPill(r, versioned) {
|
|
|
9829
10468
|
return html10`<span class="ver-pill ${versioned ? "versioned" : ""}" title="domain spec version (envelope)">v${r.spec_version}</span>`;
|
|
9830
10469
|
}
|
|
9831
10470
|
function chainVerLabel(v) {
|
|
9832
|
-
const dead = v.status === "Superseded" || v.status === "
|
|
10471
|
+
const dead = v.status === "Superseded" || v.status === "Deprecated";
|
|
9833
10472
|
if (dead && v.superseded_at_version != null) {
|
|
9834
10473
|
return html10`<span class="chain-ver">superseded at v${v.superseded_at_version}</span>`;
|
|
9835
10474
|
}
|
|
@@ -9972,8 +10611,8 @@ function mountRequirements(app) {
|
|
|
9972
10611
|
const reqs = await apiJson(app, "/api/requirements");
|
|
9973
10612
|
const coverage = await apiJson(app, "/api/coverage");
|
|
9974
10613
|
const showAll = c.req.query("all") === "1";
|
|
9975
|
-
const listed = showAll ? reqs : reqs.filter((r) => r.status !== "Superseded" && r.status !== "
|
|
9976
|
-
const hiddenCount = reqs.filter((r) => r.status === "Superseded" || r.status === "
|
|
10614
|
+
const listed = showAll ? reqs : reqs.filter((r) => r.status !== "Superseded" && r.status !== "Deprecated");
|
|
10615
|
+
const hiddenCount = reqs.filter((r) => r.status === "Superseded" || r.status === "Deprecated").length;
|
|
9977
10616
|
const tagLists = await Promise.all(listed.map((r) => apiJson(app, `/api/resolve?req=${encodeURIComponent(r.id)}`)));
|
|
9978
10617
|
const tagsById = new Map(listed.map((r, i) => [r.id, tagLists[i] ?? []]));
|
|
9979
10618
|
const { domains, defaultId } = buildDomains(reqs, listed, coverage, tagsById);
|
|
@@ -10249,7 +10888,7 @@ var init_server = __esm(() => {
|
|
|
10249
10888
|
|
|
10250
10889
|
// src/server/api.ts
|
|
10251
10890
|
import { existsSync as existsSync17 } from "fs";
|
|
10252
|
-
import { join as
|
|
10891
|
+
import { join as join26 } from "path";
|
|
10253
10892
|
function hasTraversalSegment(p) {
|
|
10254
10893
|
return p.split(/[/\\]/).some((seg) => seg === "..");
|
|
10255
10894
|
}
|
|
@@ -10356,7 +10995,7 @@ function resolveCreateTarget(c, platformDir, body) {
|
|
|
10356
10995
|
return { ok: false, res: c.json({ error: "unknown domain key" }, 400) };
|
|
10357
10996
|
}
|
|
10358
10997
|
const relFile = `spec-engine/${key}/SPEC.json`;
|
|
10359
|
-
const specPath =
|
|
10998
|
+
const specPath = join26(platformDir, "spec-engine", key, "SPEC.json");
|
|
10360
10999
|
if (!existsSync17(specPath))
|
|
10361
11000
|
return { ok: false, res: c.json({ error: "not found" }, 404) };
|
|
10362
11001
|
return { ok: true, key, relFile, specPath };
|
|
@@ -10376,7 +11015,7 @@ function buildRequirement(body, id) {
|
|
|
10376
11015
|
}
|
|
10377
11016
|
async function locateRequirement(platformDir, id) {
|
|
10378
11017
|
for (const key of listDomainKeys(platformDir)) {
|
|
10379
|
-
const specPath =
|
|
11018
|
+
const specPath = join26(platformDir, "spec-engine", key, "SPEC.json");
|
|
10380
11019
|
const relFile = `spec-engine/${key}/SPEC.json`;
|
|
10381
11020
|
let domain2;
|
|
10382
11021
|
try {
|
|
@@ -10406,6 +11045,40 @@ function applyAmendFields(req, body) {
|
|
|
10406
11045
|
req.livesIn = toLivesIn(body.livesIn);
|
|
10407
11046
|
return true;
|
|
10408
11047
|
}
|
|
11048
|
+
function hasAmendFields(body) {
|
|
11049
|
+
return typeof body.statement === "string" || "why" in body || "livesIn" in body;
|
|
11050
|
+
}
|
|
11051
|
+
async function amendGateRejection(c, platformDir, storage2, id, req) {
|
|
11052
|
+
const rawStatus = typeof req.status === "string" ? req.status : "";
|
|
11053
|
+
const statusLc = rawStatus.toLowerCase();
|
|
11054
|
+
if (statusLc !== "active" && statusLc !== "draft") {
|
|
11055
|
+
return c.json({
|
|
11056
|
+
error: `${id} is ${rawStatus} \u2014 only Active/Draft entries amend (a superseded entry is history; supersede its successor instead)`
|
|
11057
|
+
}, 409);
|
|
11058
|
+
}
|
|
11059
|
+
if (statusLc === "active") {
|
|
11060
|
+
await runIndex({ platformDir, storage: storage2 });
|
|
11061
|
+
const bound = storage2.listTags({ req_id: id }).filter((t) => t.kind === "implements" || t.kind === "verifies");
|
|
11062
|
+
if (bound.length > 0) {
|
|
11063
|
+
const site = bound[0];
|
|
11064
|
+
return c.json({
|
|
11065
|
+
error: `${id} is shipped \u2014 ${bound.length} code tag(s) bind it (e.g. ${site.file}:${site.line}). A bound requirement is immutable; supersede it with a successor instead of amending in place.`
|
|
11066
|
+
}, 409);
|
|
11067
|
+
}
|
|
11068
|
+
}
|
|
11069
|
+
return null;
|
|
11070
|
+
}
|
|
11071
|
+
function searchFtsResponse(c, storage2, q, limit) {
|
|
11072
|
+
try {
|
|
11073
|
+
return c.json(storage2.searchFts(q, limit));
|
|
11074
|
+
} catch (e) {
|
|
11075
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
11076
|
+
if (msg.startsWith(FTS_SYNTAX_ERROR_PREFIX)) {
|
|
11077
|
+
return c.json({ error: "FTS5 grammar error; wrap phrases in double quotes" }, 400);
|
|
11078
|
+
}
|
|
11079
|
+
throw e;
|
|
11080
|
+
}
|
|
11081
|
+
}
|
|
10409
11082
|
function parseQueryLimit(c, rawLimit) {
|
|
10410
11083
|
if (!POSITIVE_INT_RE.test(rawLimit)) {
|
|
10411
11084
|
return {
|
|
@@ -10449,6 +11122,9 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
|
|
|
10449
11122
|
return row ? c.json(row) : c.json({ error: "not found" }, 404);
|
|
10450
11123
|
}));
|
|
10451
11124
|
app.post("/api/requirements", guarded(async function createRequirement(c) {
|
|
11125
|
+
if (!featureEnabled("editor")) {
|
|
11126
|
+
return c.json({ error: featureDisabledMessage("editor") }, 404);
|
|
11127
|
+
}
|
|
10452
11128
|
const originReject = rejectCrossOrigin(c);
|
|
10453
11129
|
if (originReject)
|
|
10454
11130
|
return originReject;
|
|
@@ -10483,6 +11159,9 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
|
|
|
10483
11159
|
});
|
|
10484
11160
|
}));
|
|
10485
11161
|
app.put("/api/requirements/:id", guarded(async function amendRequirement(c) {
|
|
11162
|
+
if (!featureEnabled("editor")) {
|
|
11163
|
+
return c.json({ error: featureDisabledMessage("editor") }, 404);
|
|
11164
|
+
}
|
|
10486
11165
|
const originReject = rejectCrossOrigin(c);
|
|
10487
11166
|
if (originReject)
|
|
10488
11167
|
return originReject;
|
|
@@ -10501,9 +11180,13 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
|
|
|
10501
11180
|
if (found.kind === "invalid") {
|
|
10502
11181
|
return c.json({ error: "INVALID_DOMAIN_FILE", detail: `${found.relFile} is not valid JSON` }, 400);
|
|
10503
11182
|
}
|
|
10504
|
-
if (!
|
|
11183
|
+
if (!hasAmendFields(body)) {
|
|
10505
11184
|
return c.json({ error: "nothing to amend \u2014 provide statement, why, or livesIn" }, 400);
|
|
10506
11185
|
}
|
|
11186
|
+
const gateReject = await amendGateRejection(c, platformDir, storage2, id, found.req);
|
|
11187
|
+
if (gateReject)
|
|
11188
|
+
return gateReject;
|
|
11189
|
+
applyAmendFields(found.req, body);
|
|
10507
11190
|
found.domain.updated = localToday();
|
|
10508
11191
|
const res = await validateAndWrite(found.specPath, found.domain, found.relFile);
|
|
10509
11192
|
if (!res.ok) {
|
|
@@ -10515,6 +11198,9 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
|
|
|
10515
11198
|
}));
|
|
10516
11199
|
app.get("/api/propagation/:id", guarded((c) => c.json(storage2.propagationFor(c.req.param("id") ?? ""))));
|
|
10517
11200
|
app.get("/api/query", guarded((c) => {
|
|
11201
|
+
if (!featureEnabled("query")) {
|
|
11202
|
+
return c.json({ error: featureDisabledMessage("query") }, 404);
|
|
11203
|
+
}
|
|
10518
11204
|
const q = (c.req.query("q") ?? "").trim();
|
|
10519
11205
|
if (!q) {
|
|
10520
11206
|
return c.json({ error: "q is required (non-empty FTS5 MATCH query)" }, 400);
|
|
@@ -10523,17 +11209,12 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
|
|
|
10523
11209
|
const parsedLimit = parseQueryLimit(c, rawLimit);
|
|
10524
11210
|
if (!parsedLimit.ok)
|
|
10525
11211
|
return parsedLimit.res;
|
|
10526
|
-
|
|
10527
|
-
return c.json(storage2.searchFts(q, parsedLimit.limit));
|
|
10528
|
-
} catch (e) {
|
|
10529
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
10530
|
-
if (msg.startsWith(FTS_SYNTAX_ERROR_PREFIX)) {
|
|
10531
|
-
return c.json({ error: "FTS5 grammar error; wrap phrases in double quotes" }, 400);
|
|
10532
|
-
}
|
|
10533
|
-
throw e;
|
|
10534
|
-
}
|
|
11212
|
+
return searchFtsResponse(c, storage2, q, parsedLimit.limit);
|
|
10535
11213
|
}));
|
|
10536
11214
|
app.get("/api/relations", guarded((c) => {
|
|
11215
|
+
if (!featureEnabled("relations")) {
|
|
11216
|
+
return c.json({ error: featureDisabledMessage("relations") }, 404);
|
|
11217
|
+
}
|
|
10537
11218
|
const format = c.req.query("format");
|
|
10538
11219
|
if (format !== undefined && format !== "mermaid") {
|
|
10539
11220
|
return c.json({ error: 'format must be "mermaid" when provided' }, 400);
|
|
@@ -10545,6 +11226,9 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
|
|
|
10545
11226
|
return c.json(sortRelations(rows));
|
|
10546
11227
|
}));
|
|
10547
11228
|
app.get("/api/provenance", guarded(async (c) => {
|
|
11229
|
+
if (!featureEnabled("provenance")) {
|
|
11230
|
+
return c.json({ error: featureDisabledMessage("provenance") }, 404);
|
|
11231
|
+
}
|
|
10548
11232
|
const resolveParam = c.req.query("resolve");
|
|
10549
11233
|
if (resolveParam !== undefined && resolveParam !== "1") {
|
|
10550
11234
|
return c.json({ error: 'resolve must be "1" when provided' }, 400);
|
|
@@ -10556,7 +11240,15 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
|
|
|
10556
11240
|
}
|
|
10557
11241
|
return c.json(rows);
|
|
10558
11242
|
}));
|
|
10559
|
-
app.get("/api/provenance
|
|
11243
|
+
app.get("/api/provenance/by-issue", guarded((c) => {
|
|
11244
|
+
if (!featureEnabled("provenance")) {
|
|
11245
|
+
return c.json({ error: featureDisabledMessage("provenance") }, 404);
|
|
11246
|
+
}
|
|
11247
|
+
const issue = (c.req.query("issue") ?? "").trim();
|
|
11248
|
+
if (issue === "")
|
|
11249
|
+
return c.json({ error: "issue is required (non-empty)" }, 400);
|
|
11250
|
+
return c.json(storage2.provenanceByIssue(issue));
|
|
11251
|
+
}));
|
|
10560
11252
|
app.get("/api/resolve", guarded((c) => {
|
|
10561
11253
|
const reqParam = c.req.query("req");
|
|
10562
11254
|
if (reqParam !== undefined)
|
|
@@ -10597,8 +11289,8 @@ __export(exports_serve, {
|
|
|
10597
11289
|
composeServeApp: () => composeServeApp
|
|
10598
11290
|
});
|
|
10599
11291
|
import { existsSync as existsSync18, mkdirSync as mkdirSync8 } from "fs";
|
|
10600
|
-
import { dirname as dirname11, resolve as
|
|
10601
|
-
import { defineCommand as
|
|
11292
|
+
import { dirname as dirname11, resolve as resolve26 } from "path";
|
|
11293
|
+
import { defineCommand as defineCommand21 } from "citty";
|
|
10602
11294
|
import { Hono as Hono2 } from "hono";
|
|
10603
11295
|
function composeServeApp(storage2, platformDir = process.cwd()) {
|
|
10604
11296
|
const app = new Hono2;
|
|
@@ -10646,7 +11338,7 @@ var init_serve = __esm(() => {
|
|
|
10646
11338
|
init_api();
|
|
10647
11339
|
init_sqlite();
|
|
10648
11340
|
init__shared();
|
|
10649
|
-
serveCommand =
|
|
11341
|
+
serveCommand = defineCommand21({
|
|
10650
11342
|
meta: {
|
|
10651
11343
|
name: "serve",
|
|
10652
11344
|
description: "Run the local webapp. Binds 127.0.0.1 on --port (loopback only \u2014 no --host). --probe is the SERV-04 asset-embedding smoke."
|
|
@@ -10692,7 +11384,7 @@ var init_serve = __esm(() => {
|
|
|
10692
11384
|
process.exit(EXIT.USAGE);
|
|
10693
11385
|
return;
|
|
10694
11386
|
}
|
|
10695
|
-
const platformDir =
|
|
11387
|
+
const platformDir = resolve26(args.platformDir ?? process.cwd());
|
|
10696
11388
|
const outArg = args.out;
|
|
10697
11389
|
const dbPath = resolveDbPath(platformDir, outArg);
|
|
10698
11390
|
if (outArg)
|
|
@@ -10743,18 +11435,20 @@ var init_serve = __esm(() => {
|
|
|
10743
11435
|
// src/commands/docs.ts
|
|
10744
11436
|
var exports_docs = {};
|
|
10745
11437
|
__export(exports_docs, {
|
|
11438
|
+
runProbe: () => runProbe2,
|
|
10746
11439
|
resolveDocsRootFrom: () => resolveDocsRootFrom,
|
|
10747
11440
|
resolveDocsRoot: () => resolveDocsRoot,
|
|
11441
|
+
exitNoDocsRoot: () => exitNoDocsRoot,
|
|
10748
11442
|
docsCommand: () => docsCommand,
|
|
10749
11443
|
createDocsFetchHandler: () => createDocsFetchHandler
|
|
10750
11444
|
});
|
|
10751
|
-
import { existsSync as existsSync19, statSync as
|
|
10752
|
-
import { join as
|
|
11445
|
+
import { existsSync as existsSync19, statSync as statSync7 } from "fs";
|
|
11446
|
+
import { join as join27, normalize, sep as sep4 } from "path";
|
|
10753
11447
|
import { fileURLToPath } from "url";
|
|
10754
|
-
import { defineCommand as
|
|
11448
|
+
import { defineCommand as defineCommand22 } from "citty";
|
|
10755
11449
|
function resolveDocsRootFrom(candidates) {
|
|
10756
11450
|
for (const candidate of candidates) {
|
|
10757
|
-
if (existsSync19(
|
|
11451
|
+
if (existsSync19(join27(candidate, "index.html")))
|
|
10758
11452
|
return candidate;
|
|
10759
11453
|
}
|
|
10760
11454
|
return null;
|
|
@@ -10778,16 +11472,16 @@ function createDocsFetchHandler(root) {
|
|
|
10778
11472
|
if (pathname.includes("\x00")) {
|
|
10779
11473
|
return new Response("Bad request", { status: 400 });
|
|
10780
11474
|
}
|
|
10781
|
-
const target = normalize(
|
|
11475
|
+
const target = normalize(join27(normalRoot, pathname));
|
|
10782
11476
|
if (target !== normalRoot && !target.startsWith(rootPrefix)) {
|
|
10783
11477
|
return new Response("Not found", { status: 404 });
|
|
10784
11478
|
}
|
|
10785
|
-
for (const candidate of [target,
|
|
10786
|
-
if (existsSync19(candidate) &&
|
|
11479
|
+
for (const candidate of [target, join27(target, "index.html"), `${target}.html`]) {
|
|
11480
|
+
if (existsSync19(candidate) && statSync7(candidate).isFile()) {
|
|
10787
11481
|
return new Response(Bun.file(candidate));
|
|
10788
11482
|
}
|
|
10789
11483
|
}
|
|
10790
|
-
const notFoundPage =
|
|
11484
|
+
const notFoundPage = join27(normalRoot, "404.html");
|
|
10791
11485
|
if (existsSync19(notFoundPage)) {
|
|
10792
11486
|
return new Response(Bun.file(notFoundPage), { status: 404 });
|
|
10793
11487
|
}
|
|
@@ -10822,10 +11516,14 @@ async function runProbe2(root) {
|
|
|
10822
11516
|
server.stop();
|
|
10823
11517
|
process.exit(EXIT.OK);
|
|
10824
11518
|
}
|
|
11519
|
+
function exitNoDocsRoot() {
|
|
11520
|
+
console.error("spec docs: no built documentation found. From a checkout run `bun run build:site` " + "(writes packages/site/dist); the published npm package ships it at dist/docs.");
|
|
11521
|
+
process.exit(EXIT.USAGE);
|
|
11522
|
+
}
|
|
10825
11523
|
var docsCommand;
|
|
10826
11524
|
var init_docs2 = __esm(() => {
|
|
10827
11525
|
init_constants();
|
|
10828
|
-
docsCommand =
|
|
11526
|
+
docsCommand = defineCommand22({
|
|
10829
11527
|
meta: {
|
|
10830
11528
|
name: "docs",
|
|
10831
11529
|
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."
|
|
@@ -10844,8 +11542,7 @@ var init_docs2 = __esm(() => {
|
|
|
10844
11542
|
async run({ args }) {
|
|
10845
11543
|
const root = resolveDocsRoot();
|
|
10846
11544
|
if (!root) {
|
|
10847
|
-
|
|
10848
|
-
process.exit(EXIT.USAGE);
|
|
11545
|
+
exitNoDocsRoot();
|
|
10849
11546
|
return;
|
|
10850
11547
|
}
|
|
10851
11548
|
if (args.probe) {
|
|
@@ -10887,10 +11584,10 @@ __export(exports___smoke, {
|
|
|
10887
11584
|
schemaSmokeCommand: () => schemaSmokeCommand,
|
|
10888
11585
|
schemaMismatchSmokeCommand: () => schemaMismatchSmokeCommand
|
|
10889
11586
|
});
|
|
10890
|
-
import { defineCommand as
|
|
11587
|
+
import { defineCommand as defineCommand23 } from "citty";
|
|
10891
11588
|
var schemaSmokeCommand, schemaMismatchSmokeCommand;
|
|
10892
11589
|
var init___smoke = __esm(() => {
|
|
10893
|
-
schemaSmokeCommand =
|
|
11590
|
+
schemaSmokeCommand = defineCommand23({
|
|
10894
11591
|
meta: {
|
|
10895
11592
|
name: "__schema-smoke",
|
|
10896
11593
|
description: "(hidden CI) open a fresh DB, exec DDL, assert the schema shape",
|
|
@@ -10933,7 +11630,7 @@ var init___smoke = __esm(() => {
|
|
|
10933
11630
|
console.log("__schema-smoke OK");
|
|
10934
11631
|
}
|
|
10935
11632
|
});
|
|
10936
|
-
schemaMismatchSmokeCommand =
|
|
11633
|
+
schemaMismatchSmokeCommand = defineCommand23({
|
|
10937
11634
|
meta: {
|
|
10938
11635
|
name: "__schema-mismatch-smoke",
|
|
10939
11636
|
description: "(hidden CI) corrupt _schema_version, re-open, assert silent rebuild",
|
|
@@ -10958,11 +11655,11 @@ var init___smoke = __esm(() => {
|
|
|
10958
11655
|
});
|
|
10959
11656
|
|
|
10960
11657
|
// src/cli.ts
|
|
10961
|
-
import { defineCommand as
|
|
11658
|
+
import { defineCommand as defineCommand24, runMain, showUsage } from "citty";
|
|
10962
11659
|
// package.json
|
|
10963
11660
|
var package_default = {
|
|
10964
11661
|
name: "@spec-engine/spec-engine",
|
|
10965
|
-
version: "0.
|
|
11662
|
+
version: "0.3.0",
|
|
10966
11663
|
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).",
|
|
10967
11664
|
license: "MIT",
|
|
10968
11665
|
type: "module",
|
|
@@ -10972,6 +11669,10 @@ var package_default = {
|
|
|
10972
11669
|
files: [
|
|
10973
11670
|
"dist"
|
|
10974
11671
|
],
|
|
11672
|
+
publishConfig: {
|
|
11673
|
+
access: "public",
|
|
11674
|
+
provenance: true
|
|
11675
|
+
},
|
|
10975
11676
|
engines: {
|
|
10976
11677
|
bun: ">=1.3.0"
|
|
10977
11678
|
},
|
|
@@ -11010,7 +11711,7 @@ var package_default = {
|
|
|
11010
11711
|
};
|
|
11011
11712
|
|
|
11012
11713
|
// src/cli.ts
|
|
11013
|
-
var main =
|
|
11714
|
+
var main = defineCommand24({
|
|
11014
11715
|
meta: {
|
|
11015
11716
|
name: "spec",
|
|
11016
11717
|
version: package_default.version,
|
|
@@ -11030,6 +11731,7 @@ var main = defineCommand23({
|
|
|
11030
11731
|
term: () => Promise.resolve().then(() => (init_term(), exports_term)).then((m) => m.termCommand),
|
|
11031
11732
|
glossary: () => Promise.resolve().then(() => (init_glossary(), exports_glossary)).then((m) => m.glossaryCommand),
|
|
11032
11733
|
supersede: () => Promise.resolve().then(() => (init_supersede(), exports_supersede)).then((m) => m.supersedeCommand),
|
|
11734
|
+
deprecate: () => Promise.resolve().then(() => (init_deprecate(), exports_deprecate)).then((m) => m.deprecateCommand),
|
|
11033
11735
|
move: () => Promise.resolve().then(() => (init_move(), exports_move)).then((m) => m.moveCommand),
|
|
11034
11736
|
amend: () => Promise.resolve().then(() => (init_amend(), exports_amend)).then((m) => m.amendCommand),
|
|
11035
11737
|
mcp: () => Promise.resolve().then(() => (init_mcp2(), exports_mcp)).then((m) => m.mcpCommand),
|