@spec-engine/spec-engine 0.1.1 → 0.2.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 +11 -10
- 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_435dca1.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_4bdd17f.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_523ebff.pf_fragment +0 -0
- package/dist/docs/pagefind/fragment/en_649983a.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/index/en_d1196c8.pf_index +0 -0
- package/dist/docs/pagefind/pagefind-entry.json +1 -1
- package/dist/docs/pagefind/pagefind.en_f3d65f37ce.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 +1201 -638
- 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();
|
|
@@ -772,7 +883,8 @@ var init_domainJson = __esm(() => {
|
|
|
772
883
|
active: "Active",
|
|
773
884
|
draft: "Draft",
|
|
774
885
|
superseded: "Superseded",
|
|
775
|
-
|
|
886
|
+
deprecated: "Deprecated",
|
|
887
|
+
retired: "Deprecated"
|
|
776
888
|
});
|
|
777
889
|
VALID_ROLES = new Set(["created", "supersedes-via", "amends-via"]);
|
|
778
890
|
});
|
|
@@ -1292,6 +1404,8 @@ class SqliteStorage {
|
|
|
1292
1404
|
for (const sql of [
|
|
1293
1405
|
Q1_DANGLING_TAG_SQL,
|
|
1294
1406
|
Q2_SUPERSEDED_REFERENCED_SQL,
|
|
1407
|
+
Q2B_DEPRECATED_REFERENCED_SQL,
|
|
1408
|
+
Q2D_DRAFT_REFERENCED_SQL,
|
|
1295
1409
|
Q3_DRIFT_SQL,
|
|
1296
1410
|
Q4_ORPHAN_REQ_SQL,
|
|
1297
1411
|
Q5_UNVERIFIED_REQ_SQL,
|
|
@@ -1651,6 +1765,34 @@ FROM tags t
|
|
|
1651
1765
|
JOIN requirements r ON r.id = t.req_id
|
|
1652
1766
|
WHERE r.status = 'Superseded'
|
|
1653
1767
|
ORDER BY t.repo, t.file, t.line
|
|
1768
|
+
`, Q2B_DEPRECATED_REFERENCED_SQL = `
|
|
1769
|
+
SELECT
|
|
1770
|
+
'DEPRECATED_REFERENCED' AS code,
|
|
1771
|
+
t.repo AS repo,
|
|
1772
|
+
t.file AS source_file,
|
|
1773
|
+
t.line AS line,
|
|
1774
|
+
t.req_id AS req_id,
|
|
1775
|
+
('Tag references deprecated requirement ' || t.req_id ||
|
|
1776
|
+
' \u2014 it is end-of-life with no successor') AS detail,
|
|
1777
|
+
'error' AS severity
|
|
1778
|
+
FROM tags t
|
|
1779
|
+
JOIN requirements r ON r.id = t.req_id
|
|
1780
|
+
WHERE r.status = 'Deprecated'
|
|
1781
|
+
ORDER BY t.repo, t.file, t.line
|
|
1782
|
+
`, Q2D_DRAFT_REFERENCED_SQL = `
|
|
1783
|
+
SELECT
|
|
1784
|
+
'DRAFT_REFERENCED' AS code,
|
|
1785
|
+
t.repo AS repo,
|
|
1786
|
+
t.file AS source_file,
|
|
1787
|
+
t.line AS line,
|
|
1788
|
+
t.req_id AS req_id,
|
|
1789
|
+
('Tag references Draft requirement ' || t.req_id ||
|
|
1790
|
+
' \u2014 code is bound to a promise nobody approved yet') AS detail,
|
|
1791
|
+
'warning' AS severity
|
|
1792
|
+
FROM tags t
|
|
1793
|
+
JOIN requirements r ON r.id = t.req_id
|
|
1794
|
+
WHERE r.status = 'Draft'
|
|
1795
|
+
ORDER BY t.repo, t.file, t.line
|
|
1654
1796
|
`, Q3_DRIFT_SQL = `
|
|
1655
1797
|
SELECT
|
|
1656
1798
|
'DRIFT' AS code,
|
|
@@ -1680,6 +1822,7 @@ WHERE r.status = 'Active'
|
|
|
1680
1822
|
-- code-coverage obligation \u2014 exclude the reserved TERM domain so a migrated
|
|
1681
1823
|
-- term never fires ORPHAN_REQ. Literal 'TERM' only, never a real domain key;
|
|
1682
1824
|
-- mirrors the coverage VIEW exclusion (WHERE r.key != 'TERM') in schema.ts.
|
|
1825
|
+
-- @spec SCHM-017
|
|
1683
1826
|
AND r.key != 'TERM'
|
|
1684
1827
|
AND NOT EXISTS (
|
|
1685
1828
|
SELECT 1 FROM tags t WHERE t.req_id = r.id AND t.kind != 'documents'
|
|
@@ -1853,7 +1996,7 @@ SELECT
|
|
|
1853
1996
|
FROM repos
|
|
1854
1997
|
WHERE repos.name != 'spec-engine'
|
|
1855
1998
|
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
|
|
1999
|
+
`, 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_TAGS_SQL = `SELECT id, req_id, repo, file, line, kind, level FROM tags
|
|
1857
2000
|
WHERE ($repo IS NULL OR repo = $repo)
|
|
1858
2001
|
AND ($req_id IS NULL OR req_id = $req_id)
|
|
1859
2002
|
AND ($file IS NULL OR file = $file)
|
|
@@ -2659,8 +2802,8 @@ var init_errors = __esm(() => {
|
|
|
2659
2802
|
});
|
|
2660
2803
|
|
|
2661
2804
|
// src/commands/_shared.ts
|
|
2662
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
|
|
2663
|
-
import { dirname as dirname5 } from "path";
|
|
2805
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync, statSync as statSync4 } from "fs";
|
|
2806
|
+
import { dirname as dirname5, join as join7 } from "path";
|
|
2664
2807
|
function assertContainedPath(resolved, platformDir, subject) {
|
|
2665
2808
|
if (!isContainedPath(resolved, platformDir)) {
|
|
2666
2809
|
console.error(`${subject} path must be inside platformDir (resolved to ${resolved})`);
|
|
@@ -2692,6 +2835,23 @@ async function reindexAndListTags(platformDir, reqId) {
|
|
|
2692
2835
|
storage2.close();
|
|
2693
2836
|
}
|
|
2694
2837
|
}
|
|
2838
|
+
function warnIfIndexStale(platformDir, dbPath) {
|
|
2839
|
+
try {
|
|
2840
|
+
const dbMtime = statSync4(dbPath).mtimeMs;
|
|
2841
|
+
const specsDir = join7(platformDir, "spec-engine");
|
|
2842
|
+
for (const entry of readdirSync(specsDir, { withFileTypes: true })) {
|
|
2843
|
+
if (!entry.isDirectory())
|
|
2844
|
+
continue;
|
|
2845
|
+
const specPath = join7(specsDir, entry.name, "SPEC.json");
|
|
2846
|
+
if (!existsSync4(specPath))
|
|
2847
|
+
continue;
|
|
2848
|
+
if (statSync4(specPath).mtimeMs > dbMtime) {
|
|
2849
|
+
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.`);
|
|
2850
|
+
return;
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
} catch {}
|
|
2854
|
+
}
|
|
2695
2855
|
async function withReadStorage(opts, fn) {
|
|
2696
2856
|
const { platformDir, dbPath, fresh } = opts;
|
|
2697
2857
|
try {
|
|
@@ -2704,6 +2864,8 @@ async function withReadStorage(opts, fn) {
|
|
|
2704
2864
|
try {
|
|
2705
2865
|
if (needsIndex || storage2.listRepos().length === 0) {
|
|
2706
2866
|
await runIndex({ platformDir, storage: storage2 });
|
|
2867
|
+
} else {
|
|
2868
|
+
warnIfIndexStale(platformDir, dbPath);
|
|
2707
2869
|
}
|
|
2708
2870
|
await fn(storage2);
|
|
2709
2871
|
} finally {
|
|
@@ -2987,6 +3149,147 @@ function renderDiagnostics(rows, mode) {
|
|
|
2987
3149
|
`);
|
|
2988
3150
|
}
|
|
2989
3151
|
|
|
3152
|
+
// src/authoring/domains.ts
|
|
3153
|
+
import { existsSync as existsSync5, readdirSync as readdirSync2 } from "fs";
|
|
3154
|
+
import { join as join8 } from "path";
|
|
3155
|
+
function normalizeDomainKey(raw) {
|
|
3156
|
+
return raw.toUpperCase().replace(/\s+/g, "");
|
|
3157
|
+
}
|
|
3158
|
+
function listDomainKeys(platformDir) {
|
|
3159
|
+
const specsDir = join8(platformDir, "spec-engine");
|
|
3160
|
+
return readdirSync2(specsDir, { withFileTypes: true }).filter((e) => e.isDirectory() && existsSync5(join8(specsDir, e.name, "SPEC.json"))).map((e) => e.name).sort();
|
|
3161
|
+
}
|
|
3162
|
+
async function domainScope(platformDir, key) {
|
|
3163
|
+
const jsonPath = join8(platformDir, "spec-engine", key, "SPEC.json");
|
|
3164
|
+
if (!existsSync5(jsonPath))
|
|
3165
|
+
return null;
|
|
3166
|
+
try {
|
|
3167
|
+
const env = JSON.parse(await Bun.file(jsonPath).text());
|
|
3168
|
+
if (typeof env.scope !== "string" || env.scope.trim() === "")
|
|
3169
|
+
return null;
|
|
3170
|
+
return env.scope;
|
|
3171
|
+
} catch (e) {
|
|
3172
|
+
if (e instanceof SyntaxError)
|
|
3173
|
+
return null;
|
|
3174
|
+
console.error(`spec: could not read scope for ${key}: ${e.message}`);
|
|
3175
|
+
return null;
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
async function domainsWithScope(platformDir) {
|
|
3179
|
+
const keys = listDomainKeys(platformDir);
|
|
3180
|
+
return Promise.all(keys.map(async (key) => ({ key, scope: await domainScope(platformDir, key) })));
|
|
3181
|
+
}
|
|
3182
|
+
function scaffoldDomainObject(key, today) {
|
|
3183
|
+
return {
|
|
3184
|
+
key,
|
|
3185
|
+
owner: null,
|
|
3186
|
+
...key === "TERM" ? { specVersion: 1 } : {},
|
|
3187
|
+
updated: today,
|
|
3188
|
+
requirements: []
|
|
3189
|
+
};
|
|
3190
|
+
}
|
|
3191
|
+
function maxSeqOf(domain2) {
|
|
3192
|
+
const reqs = Array.isArray(domain2.requirements) ? domain2.requirements : [];
|
|
3193
|
+
return reqs.reduce((m, r) => {
|
|
3194
|
+
const id = typeof r?.id === "string" ? r.id : "";
|
|
3195
|
+
const seq = Number(id.split("-")[1]);
|
|
3196
|
+
return Number.isFinite(seq) ? Math.max(m, seq) : m;
|
|
3197
|
+
}, 0);
|
|
3198
|
+
}
|
|
3199
|
+
async function headMaxSeq(platformDir, key) {
|
|
3200
|
+
try {
|
|
3201
|
+
const proc = Bun.spawn(["git", "-C", join8(platformDir, "spec-engine", key), "show", "HEAD:./SPEC.json"], { stdout: "pipe", stderr: "ignore" });
|
|
3202
|
+
const text = await new Response(proc.stdout).text();
|
|
3203
|
+
if (await proc.exited !== 0)
|
|
3204
|
+
return 0;
|
|
3205
|
+
return maxSeqOf(JSON.parse(text));
|
|
3206
|
+
} catch {
|
|
3207
|
+
return 0;
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
async function nextRequirementId(platformDir, key) {
|
|
3211
|
+
const jsonPath = join8(platformDir, "spec-engine", key, "SPEC.json");
|
|
3212
|
+
if (!existsSync5(jsonPath))
|
|
3213
|
+
return `${key}-001`;
|
|
3214
|
+
const domain2 = JSON.parse(await Bun.file(jsonPath).text());
|
|
3215
|
+
const maxSeq = Math.max(maxSeqOf(domain2), await headMaxSeq(platformDir, key));
|
|
3216
|
+
const next = String(maxSeq + 1).padStart(3, "0");
|
|
3217
|
+
return `${key}-${next}`;
|
|
3218
|
+
}
|
|
3219
|
+
var init_domains = __esm(() => {
|
|
3220
|
+
init_src();
|
|
3221
|
+
});
|
|
3222
|
+
|
|
3223
|
+
// src/check/grammar.ts
|
|
3224
|
+
import { join as join9 } from "path";
|
|
3225
|
+
function lineOf(rawText, id) {
|
|
3226
|
+
const lines = rawText.split(`
|
|
3227
|
+
`);
|
|
3228
|
+
const needle = `"id": "${id}"`;
|
|
3229
|
+
for (let i = 0;i < lines.length; i++) {
|
|
3230
|
+
if (lines[i].includes(needle))
|
|
3231
|
+
return i + 1;
|
|
3232
|
+
}
|
|
3233
|
+
return 0;
|
|
3234
|
+
}
|
|
3235
|
+
async function readDeclaredDomain(platformDir, key) {
|
|
3236
|
+
try {
|
|
3237
|
+
const raw = await Bun.file(join9(platformDir, "spec-engine", key, "SPEC.json")).text();
|
|
3238
|
+
const doc = JSON.parse(raw);
|
|
3239
|
+
if (doc.grammar !== "ears")
|
|
3240
|
+
return null;
|
|
3241
|
+
return {
|
|
3242
|
+
raw,
|
|
3243
|
+
severity: doc.grammarSeverity === "error" ? "error" : "warning",
|
|
3244
|
+
requirements: Array.isArray(doc.requirements) ? doc.requirements : []
|
|
3245
|
+
};
|
|
3246
|
+
} catch {
|
|
3247
|
+
return null;
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
function rowsForDomain(key, domain2) {
|
|
3251
|
+
const rows = [];
|
|
3252
|
+
for (const req of domain2.requirements) {
|
|
3253
|
+
if (typeof req?.id !== "string" || typeof req.statement !== "string")
|
|
3254
|
+
continue;
|
|
3255
|
+
const status = typeof req.status === "string" ? req.status.toLowerCase() : "";
|
|
3256
|
+
if (!CHECKED_STATUSES.has(status))
|
|
3257
|
+
continue;
|
|
3258
|
+
const result = parseEarsStatement(req.statement);
|
|
3259
|
+
if (result.ok)
|
|
3260
|
+
continue;
|
|
3261
|
+
rows.push({
|
|
3262
|
+
code: DiagnosticCode.STATEMENT_GRAMMAR,
|
|
3263
|
+
severity: domain2.severity,
|
|
3264
|
+
repo: null,
|
|
3265
|
+
source_file: `spec-engine/${key}/SPEC.json`,
|
|
3266
|
+
line: lineOf(domain2.raw, req.id),
|
|
3267
|
+
req_id: req.id,
|
|
3268
|
+
detail: `${result.problem}. Expected one of:
|
|
3269
|
+
${result.expected}`
|
|
3270
|
+
});
|
|
3271
|
+
}
|
|
3272
|
+
return rows;
|
|
3273
|
+
}
|
|
3274
|
+
async function statementGrammarDiagnostics(platformDir) {
|
|
3275
|
+
const rows = [];
|
|
3276
|
+
for (const key of listDomainKeys(platformDir)) {
|
|
3277
|
+
if (key === "TERM")
|
|
3278
|
+
continue;
|
|
3279
|
+
const domain2 = await readDeclaredDomain(platformDir, key);
|
|
3280
|
+
if (domain2 !== null)
|
|
3281
|
+
rows.push(...rowsForDomain(key, domain2));
|
|
3282
|
+
}
|
|
3283
|
+
rows.sort((a, b) => (a.source_file ?? "").localeCompare(b.source_file ?? "") || (a.line ?? 0) - (b.line ?? 0) || (a.req_id ?? "").localeCompare(b.req_id ?? ""));
|
|
3284
|
+
return rows;
|
|
3285
|
+
}
|
|
3286
|
+
var CHECKED_STATUSES;
|
|
3287
|
+
var init_grammar = __esm(() => {
|
|
3288
|
+
init_src();
|
|
3289
|
+
init_domains();
|
|
3290
|
+
CHECKED_STATUSES = new Set(["active", "draft"]);
|
|
3291
|
+
});
|
|
3292
|
+
|
|
2990
3293
|
// src/check/proven.ts
|
|
2991
3294
|
function normalizeToSegments(path) {
|
|
2992
3295
|
const out = [];
|
|
@@ -3154,7 +3457,8 @@ function requirementRemoved(baseReqs, changeReqs, relPathById) {
|
|
|
3154
3457
|
for (const b of baseReqs) {
|
|
3155
3458
|
if (changeIds.has(b.id))
|
|
3156
3459
|
continue;
|
|
3157
|
-
const
|
|
3460
|
+
const activeAtBase = (b.status ?? "").toLowerCase() === "active";
|
|
3461
|
+
const exempt = activeAtBase && (b.supersededBy != null && changeIds.has(b.supersededBy) || changeReqs.some((y) => y.supersedes === b.id));
|
|
3158
3462
|
if (exempt)
|
|
3159
3463
|
continue;
|
|
3160
3464
|
out.push({
|
|
@@ -3163,7 +3467,7 @@ function requirementRemoved(baseReqs, changeReqs, relPathById) {
|
|
|
3163
3467
|
line: 0,
|
|
3164
3468
|
repo: null,
|
|
3165
3469
|
req_id: b.id,
|
|
3166
|
-
detail: `${b.id} was present in the base ref but is absent from the change with no approved supersession`,
|
|
3470
|
+
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
3471
|
severity: "error"
|
|
3168
3472
|
});
|
|
3169
3473
|
}
|
|
@@ -3193,11 +3497,11 @@ function normalizeHandle(h) {
|
|
|
3193
3497
|
return (h.startsWith("@") ? h.slice(1) : h).toLowerCase();
|
|
3194
3498
|
}
|
|
3195
3499
|
function isNewTerminalFlip(base, change) {
|
|
3196
|
-
if (change
|
|
3500
|
+
if (!TERMINAL_STATUSES.has(change))
|
|
3197
3501
|
return false;
|
|
3198
3502
|
if (base === undefined)
|
|
3199
3503
|
return false;
|
|
3200
|
-
if (base
|
|
3504
|
+
if (TERMINAL_STATUSES.has(base))
|
|
3201
3505
|
return false;
|
|
3202
3506
|
return base !== change;
|
|
3203
3507
|
}
|
|
@@ -3226,8 +3530,10 @@ function unapprovedStatusFlip(baseReqs, changeReqs, codeowners, approvedBy, relP
|
|
|
3226
3530
|
}
|
|
3227
3531
|
return out;
|
|
3228
3532
|
}
|
|
3533
|
+
var TERMINAL_STATUSES;
|
|
3229
3534
|
var init_statusflip = __esm(() => {
|
|
3230
3535
|
init_src();
|
|
3536
|
+
TERMINAL_STATUSES = new Set(["superseded", "deprecated", "retired"]);
|
|
3231
3537
|
});
|
|
3232
3538
|
|
|
3233
3539
|
// src/check/unsourced.ts
|
|
@@ -3521,76 +3827,307 @@ var init_junit = __esm(() => {
|
|
|
3521
3827
|
]);
|
|
3522
3828
|
});
|
|
3523
3829
|
|
|
3524
|
-
// src/
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3830
|
+
// src/authoring/edit.ts
|
|
3831
|
+
function localToday() {
|
|
3832
|
+
const d = new Date;
|
|
3833
|
+
return `${d.getFullYear()}-` + `${String(d.getMonth() + 1).padStart(2, "0")}-` + `${String(d.getDate()).padStart(2, "0")}`;
|
|
3834
|
+
}
|
|
3835
|
+
|
|
3836
|
+
// src/commands/glossary.ts
|
|
3837
|
+
var exports_glossary = {};
|
|
3838
|
+
__export(exports_glossary, {
|
|
3839
|
+
parseGlossary: () => parseGlossary,
|
|
3840
|
+
glossaryDriftDiagnostic: () => glossaryDriftDiagnostic,
|
|
3841
|
+
glossaryCommand: () => glossaryCommand,
|
|
3842
|
+
generateGlossary: () => generateGlossary
|
|
3528
3843
|
});
|
|
3529
|
-
import { existsSync as
|
|
3530
|
-
import {
|
|
3844
|
+
import { existsSync as existsSync6, readFileSync } from "fs";
|
|
3845
|
+
import { join as join10, resolve as resolve7 } from "path";
|
|
3531
3846
|
import { defineCommand as defineCommand3 } from "citty";
|
|
3532
|
-
function
|
|
3533
|
-
const
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3847
|
+
function parseGlossary(md) {
|
|
3848
|
+
const terms = [];
|
|
3849
|
+
let section = null;
|
|
3850
|
+
let buf = null;
|
|
3851
|
+
const flush = () => {
|
|
3852
|
+
if (buf === null)
|
|
3853
|
+
return;
|
|
3854
|
+
const m = buf.match(/^\*\*(.+?)\*\*/);
|
|
3855
|
+
if (m) {
|
|
3856
|
+
const remainder = buf.slice(m[0].length);
|
|
3857
|
+
const statement = remainder.replace(/^ \u2014 /, "").trim();
|
|
3858
|
+
terms.push({ term: m[1], statement, section });
|
|
3859
|
+
}
|
|
3860
|
+
buf = null;
|
|
3861
|
+
};
|
|
3862
|
+
for (const line of md.split(`
|
|
3863
|
+
`)) {
|
|
3864
|
+
if (line.startsWith("## ")) {
|
|
3865
|
+
flush();
|
|
3866
|
+
section = line.slice(3).trim();
|
|
3867
|
+
} else if (line.startsWith("- ")) {
|
|
3868
|
+
flush();
|
|
3869
|
+
buf = line.slice(2);
|
|
3870
|
+
} else if (buf !== null) {
|
|
3871
|
+
if (line.trim() === "")
|
|
3872
|
+
flush();
|
|
3873
|
+
else
|
|
3874
|
+
buf += ` ${line.trim()}`;
|
|
3875
|
+
}
|
|
3558
3876
|
}
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
return { reqs: [], diagnostics: validated.diagnostics };
|
|
3562
|
-
return { reqs: validated.data.requirements, diagnostics: [] };
|
|
3877
|
+
flush();
|
|
3878
|
+
return terms;
|
|
3563
3879
|
}
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3880
|
+
function generateGlossary(terms) {
|
|
3881
|
+
const parts = ["# Glossary", "", GLOSSARY_INTRO];
|
|
3882
|
+
let section;
|
|
3883
|
+
for (const t of terms) {
|
|
3884
|
+
if (t.section !== section) {
|
|
3885
|
+
section = t.section;
|
|
3886
|
+
if (section !== null && section !== undefined) {
|
|
3887
|
+
parts.push("", `## ${section}`, "");
|
|
3888
|
+
}
|
|
3570
3889
|
}
|
|
3571
|
-
|
|
3890
|
+
parts.push(`- **${t.term}** \u2014 ${t.statement}`);
|
|
3572
3891
|
}
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3892
|
+
return `${parts.join(`
|
|
3893
|
+
`)}
|
|
3894
|
+
`;
|
|
3895
|
+
}
|
|
3896
|
+
function resolvePlatform(platformDirRaw) {
|
|
3897
|
+
const platformDir = resolve7(platformDirRaw ?? process.cwd());
|
|
3898
|
+
try {
|
|
3899
|
+
assertSpecPlatform(platformDir);
|
|
3900
|
+
} catch (e) {
|
|
3901
|
+
handleNotAPlatform(e);
|
|
3580
3902
|
}
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
const
|
|
3585
|
-
|
|
3586
|
-
|
|
3903
|
+
return platformDir;
|
|
3904
|
+
}
|
|
3905
|
+
function readStoreTerms(platformDir) {
|
|
3906
|
+
const specPath = join10(platformDir, "spec-engine", TERM_KEY, "SPEC.json");
|
|
3907
|
+
if (!existsSync6(specPath))
|
|
3908
|
+
return [];
|
|
3909
|
+
const domain2 = JSON.parse(readFileSync(specPath, "utf8"));
|
|
3910
|
+
const reqs = Array.isArray(domain2.requirements) ? domain2.requirements : [];
|
|
3911
|
+
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) => ({
|
|
3912
|
+
term: String(r.term ?? ""),
|
|
3913
|
+
statement: String(r.statement ?? ""),
|
|
3914
|
+
section: r.section ?? null
|
|
3915
|
+
}));
|
|
3916
|
+
}
|
|
3917
|
+
function glossaryPath(platformDir) {
|
|
3918
|
+
return join10(platformDir, "GLOSSARY.md");
|
|
3919
|
+
}
|
|
3920
|
+
async function migrateGlossary(platformDir, json) {
|
|
3921
|
+
const relFile = `spec-engine/${TERM_KEY}/SPEC.json`;
|
|
3922
|
+
const specPath = join10(platformDir, "spec-engine", TERM_KEY, "SPEC.json");
|
|
3923
|
+
if (!existsSync6(specPath)) {
|
|
3924
|
+
console.error(`spec glossary: no TERM domain (expected ${relFile} under ${platformDir})`);
|
|
3925
|
+
process.exit(EXIT.USAGE);
|
|
3926
|
+
}
|
|
3927
|
+
const domain2 = JSON.parse(readFileSync(specPath, "utf8"));
|
|
3928
|
+
const existing = Array.isArray(domain2.requirements) ? domain2.requirements : [];
|
|
3929
|
+
if (existing.length > 0) {
|
|
3930
|
+
console.error(`spec glossary: TERM domain already holds ${existing.length} entries \u2014 migration skipped`);
|
|
3931
|
+
if (json)
|
|
3932
|
+
console.log(JSON.stringify({ migrated: 0, skipped: true }));
|
|
3933
|
+
else
|
|
3934
|
+
console.log("migration skipped (TERM domain not empty)");
|
|
3935
|
+
return;
|
|
3936
|
+
}
|
|
3937
|
+
const gPath = glossaryPath(platformDir);
|
|
3938
|
+
if (!existsSync6(gPath)) {
|
|
3939
|
+
console.error(`spec glossary: no GLOSSARY.md to migrate (expected ${gPath})`);
|
|
3940
|
+
process.exit(EXIT.USAGE);
|
|
3941
|
+
}
|
|
3942
|
+
const parsed = parseGlossary(readFileSync(gPath, "utf8"));
|
|
3943
|
+
domain2.requirements = parsed.map((t, i) => ({
|
|
3944
|
+
id: `${TERM_KEY}-${String(i + 1).padStart(3, "0")}`,
|
|
3945
|
+
status: "active",
|
|
3946
|
+
statement: t.statement,
|
|
3947
|
+
term: t.term,
|
|
3948
|
+
why: null,
|
|
3949
|
+
supersedes: null,
|
|
3950
|
+
supersededBy: null,
|
|
3951
|
+
relates: [],
|
|
3952
|
+
livesIn: [],
|
|
3953
|
+
issues: [],
|
|
3954
|
+
aliases: [],
|
|
3955
|
+
cites: [],
|
|
3956
|
+
changedAtVersion: 1,
|
|
3957
|
+
section: t.section
|
|
3958
|
+
}));
|
|
3959
|
+
domain2.updated = localToday();
|
|
3960
|
+
const res = await validateAndWrite(specPath, domain2, relFile);
|
|
3961
|
+
if (!res.ok) {
|
|
3962
|
+
for (const diag of res.diagnostics)
|
|
3963
|
+
console.error(`spec glossary: ${diag.detail}`);
|
|
3964
|
+
process.exit(EXIT.USAGE);
|
|
3965
|
+
}
|
|
3966
|
+
const count = parsed.length;
|
|
3967
|
+
if (json)
|
|
3968
|
+
console.log(JSON.stringify({ migrated: count, file: relFile }));
|
|
3969
|
+
else
|
|
3970
|
+
console.log(`migrated ${count} terms into ${relFile}`);
|
|
3971
|
+
}
|
|
3972
|
+
async function writeGlossary(platformDir, json) {
|
|
3973
|
+
const terms = readStoreTerms(platformDir);
|
|
3974
|
+
await Bun.write(glossaryPath(platformDir), generateGlossary(terms));
|
|
3975
|
+
if (json)
|
|
3976
|
+
console.log(JSON.stringify({ generated: terms.length, file: "GLOSSARY.md" }));
|
|
3977
|
+
else
|
|
3978
|
+
console.log(`generated GLOSSARY.md from ${terms.length} terms`);
|
|
3979
|
+
}
|
|
3980
|
+
function glossaryDriftDiagnostic(platformDir) {
|
|
3981
|
+
const terms = readStoreTerms(platformDir);
|
|
3982
|
+
if (terms.length === 0)
|
|
3983
|
+
return null;
|
|
3984
|
+
const gPath = glossaryPath(platformDir);
|
|
3985
|
+
if (!existsSync6(gPath))
|
|
3986
|
+
return null;
|
|
3987
|
+
const generated = generateGlossary(terms);
|
|
3988
|
+
const committed = readFileSync(gPath, "utf8");
|
|
3989
|
+
if (generated === committed)
|
|
3990
|
+
return null;
|
|
3991
|
+
return {
|
|
3992
|
+
code: DiagnosticCode.GLOSSARY_DRIFT,
|
|
3993
|
+
severity: "warning",
|
|
3994
|
+
repo: null,
|
|
3995
|
+
source_file: "GLOSSARY.md",
|
|
3996
|
+
line: 0,
|
|
3997
|
+
req_id: null,
|
|
3998
|
+
detail: "GLOSSARY.md does not match the TERM store \u2014 run `spec glossary .` to regenerate the human view"
|
|
3999
|
+
};
|
|
4000
|
+
}
|
|
4001
|
+
function checkGlossary(platformDir, json) {
|
|
4002
|
+
const generated = generateGlossary(readStoreTerms(platformDir));
|
|
4003
|
+
const gPath = glossaryPath(platformDir);
|
|
4004
|
+
const committed = existsSync6(gPath) ? readFileSync(gPath, "utf8") : "";
|
|
4005
|
+
if (generated === committed) {
|
|
4006
|
+
if (json)
|
|
4007
|
+
console.log(JSON.stringify({ ok: true }));
|
|
4008
|
+
else
|
|
4009
|
+
console.error("glossary round-trip: OK (committed GLOSSARY.md == generated)");
|
|
4010
|
+
return;
|
|
4011
|
+
}
|
|
4012
|
+
console.error("glossary drift: committed GLOSSARY.md != generated from the TERM store");
|
|
4013
|
+
console.error("run `spec glossary .` to regenerate the human view from the store");
|
|
4014
|
+
if (json)
|
|
4015
|
+
console.log(JSON.stringify({ ok: false }));
|
|
4016
|
+
process.exit(EXIT.FAILURE);
|
|
4017
|
+
}
|
|
4018
|
+
var TERM_KEY = "TERM", GLOSSARY_INTRO = `Canonical names for Spec Engine concepts. When prose, code comments, diagnostics, or
|
|
4019
|
+
docs need one of these ideas, use the term below \u2014 not a synonym. Terms are ordered
|
|
4020
|
+
by the data model, outside-in.`, glossaryCommand;
|
|
4021
|
+
var init_glossary = __esm(() => {
|
|
4022
|
+
init_src();
|
|
4023
|
+
init_constants();
|
|
4024
|
+
init_discover();
|
|
4025
|
+
init__shared();
|
|
4026
|
+
glossaryCommand = defineCommand3({
|
|
4027
|
+
meta: {
|
|
4028
|
+
name: "glossary",
|
|
4029
|
+
description: "Generate GLOSSARY.md from the TERM store (byte-stable); --migrate parses GLOSSARY.md into TERM-001..N; --check fails on drift (committed != generated)"
|
|
4030
|
+
},
|
|
4031
|
+
args: {
|
|
4032
|
+
platformDir: {
|
|
4033
|
+
type: "positional",
|
|
4034
|
+
required: false,
|
|
4035
|
+
description: "Platform directory containing spec-engine/ (default: cwd)"
|
|
4036
|
+
},
|
|
4037
|
+
migrate: {
|
|
4038
|
+
type: "boolean",
|
|
4039
|
+
description: "One-time: parse the committed GLOSSARY.md into TERM-001..N (idempotent-skip)"
|
|
4040
|
+
},
|
|
4041
|
+
check: {
|
|
4042
|
+
type: "boolean",
|
|
4043
|
+
description: "Fail (exit 1) if the committed GLOSSARY.md differs from the generated output"
|
|
4044
|
+
},
|
|
4045
|
+
json: { type: "boolean", description: "Emit a JSON result instead of the text summary" }
|
|
4046
|
+
},
|
|
4047
|
+
async run({ args }) {
|
|
4048
|
+
const platformDir = resolvePlatform(args.platformDir);
|
|
4049
|
+
const json = Boolean(args.json);
|
|
4050
|
+
if (args.migrate) {
|
|
4051
|
+
await migrateGlossary(platformDir, json);
|
|
4052
|
+
} else if (args.check) {
|
|
4053
|
+
checkGlossary(platformDir, json);
|
|
4054
|
+
} else {
|
|
4055
|
+
await writeGlossary(platformDir, json);
|
|
4056
|
+
}
|
|
4057
|
+
}
|
|
4058
|
+
});
|
|
4059
|
+
});
|
|
4060
|
+
|
|
4061
|
+
// src/commands/check.ts
|
|
4062
|
+
var exports_check = {};
|
|
4063
|
+
__export(exports_check, {
|
|
4064
|
+
checkCommand: () => checkCommand
|
|
4065
|
+
});
|
|
4066
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync2 } from "fs";
|
|
4067
|
+
import { dirname as dirname7, join as join11, resolve as resolve8 } from "path";
|
|
4068
|
+
import { defineCommand as defineCommand4 } from "citty";
|
|
4069
|
+
function readCodeownersText(platformDir) {
|
|
4070
|
+
const path = join11(platformDir, ".github", "CODEOWNERS");
|
|
4071
|
+
if (!existsSync7(path))
|
|
4072
|
+
return null;
|
|
4073
|
+
return readFileSync2(path, "utf-8");
|
|
4074
|
+
}
|
|
4075
|
+
function domainReqsFromText(text, platformRel) {
|
|
4076
|
+
let parsed;
|
|
4077
|
+
try {
|
|
4078
|
+
parsed = JSON.parse(text);
|
|
4079
|
+
} catch (err) {
|
|
4080
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4081
|
+
return {
|
|
4082
|
+
reqs: [],
|
|
4083
|
+
diagnostics: [
|
|
4084
|
+
{
|
|
4085
|
+
code: DiagnosticCode.INVALID_DOMAIN_FILE,
|
|
4086
|
+
source_file: platformRel,
|
|
4087
|
+
line: 0,
|
|
4088
|
+
repo: null,
|
|
4089
|
+
req_id: null,
|
|
4090
|
+
detail: `not valid JSON: ${msg}`,
|
|
4091
|
+
severity: "error"
|
|
4092
|
+
}
|
|
4093
|
+
]
|
|
4094
|
+
};
|
|
4095
|
+
}
|
|
4096
|
+
const validated = validateDomainFile(parsed, platformRel);
|
|
4097
|
+
if (!validated.ok)
|
|
4098
|
+
return { reqs: [], diagnostics: validated.diagnostics };
|
|
4099
|
+
return { reqs: validated.data.requirements, diagnostics: [] };
|
|
4100
|
+
}
|
|
4101
|
+
async function ingestResults(storage2, platformDir, resultsArg, jsonMode, diagnostics2) {
|
|
4102
|
+
if (!resultsArg) {
|
|
4103
|
+
if (jsonMode) {
|
|
4104
|
+
console.error("spec check: no --results supplied; proofs unconfirmed (PROOFS_UNCONFIRMED) \u2014 run with --results <junit.xml> to enforce trusted-red");
|
|
4105
|
+
} else {
|
|
4106
|
+
diagnostics2.push(proofsUnconfirmedWarning());
|
|
4107
|
+
}
|
|
4108
|
+
return { resultsParsed: undefined, verifyingTags: undefined };
|
|
4109
|
+
}
|
|
4110
|
+
const resultsPath = resolve8(platformDir, resultsArg);
|
|
4111
|
+
if (!isContainedPath(resultsPath, platformDir)) {
|
|
4112
|
+
console.error(`spec check: --results path must be inside platformDir (resolved to ${resultsPath})`);
|
|
4113
|
+
try {
|
|
4114
|
+
storage2.close();
|
|
4115
|
+
} catch {}
|
|
4116
|
+
process.exit(EXIT.USAGE);
|
|
4117
|
+
}
|
|
4118
|
+
const xml = await Bun.file(resultsPath).text();
|
|
4119
|
+
const resultsParsed = parseJUnit(xml);
|
|
4120
|
+
const verifyingTags = storage2.listTags().filter((t) => t.kind === "verifies");
|
|
4121
|
+
const active = storage2.listRequirements({ status: "Active" });
|
|
4122
|
+
diagnostics2.push(...provenDetermination(active, verifyingTags, resultsParsed));
|
|
4123
|
+
return { resultsParsed, verifyingTags };
|
|
3587
4124
|
}
|
|
3588
4125
|
async function collectChangeReqs(canonicalDir) {
|
|
3589
4126
|
const reqs = [];
|
|
3590
4127
|
const relPathById = new Map;
|
|
3591
4128
|
for (const rel of await findDomainJsonFiles(canonicalDir)) {
|
|
3592
4129
|
const platformRel = `spec-engine/${rel}`;
|
|
3593
|
-
const text = await Bun.file(
|
|
4130
|
+
const text = await Bun.file(join11(canonicalDir, rel)).text();
|
|
3594
4131
|
const parsed = domainReqsFromText(text, platformRel);
|
|
3595
4132
|
for (const r of parsed.reqs) {
|
|
3596
4133
|
reqs.push(r);
|
|
@@ -3630,7 +4167,7 @@ async function runGovernanceGate(storage2, platformDir, args, diagnostics2, resu
|
|
|
3630
4167
|
} catch {}
|
|
3631
4168
|
process.exit(EXIT.USAGE);
|
|
3632
4169
|
}
|
|
3633
|
-
const canonicalDir =
|
|
4170
|
+
const canonicalDir = join11(platformDir, "spec-engine");
|
|
3634
4171
|
const change = await collectChangeReqs(canonicalDir);
|
|
3635
4172
|
const base = collectBaseReqs(platformDir, ref, diagnostics2);
|
|
3636
4173
|
diagnostics2.push(...requirementRemoved(base.reqs, change.reqs, (id) => base.relPathById.get(id) ?? null));
|
|
@@ -3656,6 +4193,7 @@ function exitAfterCrash(e, storage2) {
|
|
|
3656
4193
|
var checkCommand;
|
|
3657
4194
|
var init_check = __esm(() => {
|
|
3658
4195
|
init_src();
|
|
4196
|
+
init_grammar();
|
|
3659
4197
|
init_propagation_teeth();
|
|
3660
4198
|
init_proven();
|
|
3661
4199
|
init_removed();
|
|
@@ -3669,7 +4207,8 @@ var init_check = __esm(() => {
|
|
|
3669
4207
|
init_fs();
|
|
3670
4208
|
init_sqlite();
|
|
3671
4209
|
init__shared();
|
|
3672
|
-
|
|
4210
|
+
init_glossary();
|
|
4211
|
+
checkCommand = defineCommand4({
|
|
3673
4212
|
meta: {
|
|
3674
4213
|
name: "check",
|
|
3675
4214
|
description: "Cross-repo integrity check. --ci rebuilds the index cold (rm db+wal+shm). Exits 1 on any error-severity diagnostic."
|
|
@@ -3718,7 +4257,7 @@ var init_check = __esm(() => {
|
|
|
3718
4257
|
}
|
|
3719
4258
|
},
|
|
3720
4259
|
async run({ args }) {
|
|
3721
|
-
const platformDir =
|
|
4260
|
+
const platformDir = resolve8(args.platformDir ?? process.cwd());
|
|
3722
4261
|
const outArg = args.out;
|
|
3723
4262
|
const dbPath = resolveDbPath(platformDir, outArg);
|
|
3724
4263
|
if (outArg)
|
|
@@ -3747,12 +4286,22 @@ var init_check = __esm(() => {
|
|
|
3747
4286
|
const provenance = storage2.listProvenance();
|
|
3748
4287
|
diagnostics2.push(...unsourcedChanges(superseded, provenance));
|
|
3749
4288
|
}
|
|
4289
|
+
diagnostics2.push(...await statementGrammarDiagnostics(platformDir));
|
|
4290
|
+
const glossaryDrift = glossaryDriftDiagnostic(platformDir);
|
|
4291
|
+
if (glossaryDrift !== null)
|
|
4292
|
+
diagnostics2.push(glossaryDrift);
|
|
3750
4293
|
const results = await ingestResults(storage2, platformDir, args.results, !!args.json, diagnostics2);
|
|
3751
4294
|
await runGovernanceGate(storage2, platformDir, {
|
|
3752
4295
|
base: args.base,
|
|
3753
4296
|
approvedBy: args.approvedBy,
|
|
3754
4297
|
requireOwnerApproval: args.requireOwnerApproval
|
|
3755
4298
|
}, diagnostics2, results);
|
|
4299
|
+
if (!args.base && gitRefResolves(platformDir, "HEAD")) {
|
|
4300
|
+
const change = await collectChangeReqs(join11(platformDir, "spec-engine"));
|
|
4301
|
+
const baseParseSink = [];
|
|
4302
|
+
const base = collectBaseReqs(platformDir, "HEAD", baseParseSink);
|
|
4303
|
+
diagnostics2.push(...requirementRemoved(base.reqs, change.reqs, (id) => base.relPathById.get(id) ?? null));
|
|
4304
|
+
}
|
|
3756
4305
|
const output = renderDiagnostics(diagnostics2, args.json ? "json" : "text");
|
|
3757
4306
|
console.log(output);
|
|
3758
4307
|
if (!args.json) {
|
|
@@ -3768,63 +4317,6 @@ var init_check = __esm(() => {
|
|
|
3768
4317
|
});
|
|
3769
4318
|
});
|
|
3770
4319
|
|
|
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
4320
|
// src/commands/domain.ts
|
|
3829
4321
|
var exports_domain = {};
|
|
3830
4322
|
__export(exports_domain, {
|
|
@@ -3832,9 +4324,9 @@ __export(exports_domain, {
|
|
|
3832
4324
|
domainListCommand: () => domainListCommand,
|
|
3833
4325
|
domainCommand: () => domainCommand
|
|
3834
4326
|
});
|
|
3835
|
-
import { existsSync as
|
|
3836
|
-
import { dirname as dirname8, join as
|
|
3837
|
-
import { defineCommand as
|
|
4327
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5 } from "fs";
|
|
4328
|
+
import { dirname as dirname8, join as join12, resolve as resolve9 } from "path";
|
|
4329
|
+
import { defineCommand as defineCommand5 } from "citty";
|
|
3838
4330
|
var domainNewCommand, domainListCommand, domainCommand;
|
|
3839
4331
|
var init_domain2 = __esm(() => {
|
|
3840
4332
|
init_src();
|
|
@@ -3842,7 +4334,7 @@ var init_domain2 = __esm(() => {
|
|
|
3842
4334
|
init_constants();
|
|
3843
4335
|
init_discover();
|
|
3844
4336
|
init__shared();
|
|
3845
|
-
domainNewCommand =
|
|
4337
|
+
domainNewCommand = defineCommand5({
|
|
3846
4338
|
meta: {
|
|
3847
4339
|
name: "new",
|
|
3848
4340
|
description: "Scaffold a fresh spec-engine/<KEY>/SPEC.json (input normalized)"
|
|
@@ -3861,7 +4353,7 @@ var init_domain2 = __esm(() => {
|
|
|
3861
4353
|
},
|
|
3862
4354
|
async run({ args }) {
|
|
3863
4355
|
const raw = args.name;
|
|
3864
|
-
const platformDir =
|
|
4356
|
+
const platformDir = resolve9(args.platformDir ?? process.cwd());
|
|
3865
4357
|
const key = normalizeDomainKey(raw);
|
|
3866
4358
|
if (key !== raw) {
|
|
3867
4359
|
console.log(`spec domain: normalized "${raw}" \u2192 ${key}`);
|
|
@@ -3871,21 +4363,21 @@ var init_domain2 = __esm(() => {
|
|
|
3871
4363
|
process.exit(EXIT.USAGE);
|
|
3872
4364
|
return;
|
|
3873
4365
|
}
|
|
3874
|
-
const dest =
|
|
3875
|
-
const resolvedDest =
|
|
3876
|
-
const resolvedRoot =
|
|
4366
|
+
const dest = join12(platformDir, "spec-engine", key, "SPEC.json");
|
|
4367
|
+
const resolvedDest = resolve9(dest);
|
|
4368
|
+
const resolvedRoot = resolve9(platformDir);
|
|
3877
4369
|
if (!(resolvedDest === resolvedRoot || resolvedDest.startsWith(`${resolvedRoot}/`))) {
|
|
3878
4370
|
console.error(`spec domain new: refusing to write outside platformDir (${resolvedDest})`);
|
|
3879
4371
|
process.exit(EXIT.USAGE);
|
|
3880
4372
|
return;
|
|
3881
4373
|
}
|
|
3882
|
-
if (
|
|
4374
|
+
if (existsSync8(dest)) {
|
|
3883
4375
|
console.error(`refusing to overwrite ${dest}`);
|
|
3884
4376
|
process.exit(EXIT.USAGE);
|
|
3885
4377
|
return;
|
|
3886
4378
|
}
|
|
3887
|
-
const mdSibling =
|
|
3888
|
-
if (
|
|
4379
|
+
const mdSibling = join12(dirname8(dest), "SPEC.md");
|
|
4380
|
+
if (existsSync8(mdSibling)) {
|
|
3889
4381
|
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
4382
|
process.exit(EXIT.USAGE);
|
|
3891
4383
|
return;
|
|
@@ -3905,7 +4397,7 @@ var init_domain2 = __esm(() => {
|
|
|
3905
4397
|
console.log(`created ${relFile}`);
|
|
3906
4398
|
}
|
|
3907
4399
|
});
|
|
3908
|
-
domainListCommand =
|
|
4400
|
+
domainListCommand = defineCommand5({
|
|
3909
4401
|
meta: {
|
|
3910
4402
|
name: "list",
|
|
3911
4403
|
description: "List domain keys (sorted, read from the filesystem)"
|
|
@@ -3922,7 +4414,7 @@ var init_domain2 = __esm(() => {
|
|
|
3922
4414
|
}
|
|
3923
4415
|
},
|
|
3924
4416
|
async run({ args }) {
|
|
3925
|
-
const platformDir =
|
|
4417
|
+
const platformDir = resolve9(args.platformDir ?? process.cwd());
|
|
3926
4418
|
try {
|
|
3927
4419
|
assertSpecPlatform(platformDir);
|
|
3928
4420
|
} catch (e) {
|
|
@@ -3937,7 +4429,7 @@ var init_domain2 = __esm(() => {
|
|
|
3937
4429
|
}
|
|
3938
4430
|
}
|
|
3939
4431
|
});
|
|
3940
|
-
domainCommand =
|
|
4432
|
+
domainCommand = defineCommand5({
|
|
3941
4433
|
meta: {
|
|
3942
4434
|
name: "domain",
|
|
3943
4435
|
description: "Manage spec domains (spec-engine/<KEY>/)"
|
|
@@ -4016,15 +4508,15 @@ var exports_map = {};
|
|
|
4016
4508
|
__export(exports_map, {
|
|
4017
4509
|
mapCommand: () => mapCommand
|
|
4018
4510
|
});
|
|
4019
|
-
import { resolve as
|
|
4020
|
-
import { defineCommand as
|
|
4511
|
+
import { resolve as resolve10 } from "path";
|
|
4512
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
4021
4513
|
var mapCommand;
|
|
4022
4514
|
var init_map = __esm(() => {
|
|
4023
4515
|
init_constants();
|
|
4024
4516
|
init_discover();
|
|
4025
4517
|
init_prompt();
|
|
4026
4518
|
init__shared();
|
|
4027
|
-
mapCommand =
|
|
4519
|
+
mapCommand = defineCommand6({
|
|
4028
4520
|
meta: {
|
|
4029
4521
|
name: "map",
|
|
4030
4522
|
description: "Render the cross-repo coverage matrix from the coverage VIEW. --json emits a deterministically sorted JSON array."
|
|
@@ -4053,7 +4545,7 @@ var init_map = __esm(() => {
|
|
|
4053
4545
|
}
|
|
4054
4546
|
},
|
|
4055
4547
|
async run({ args }) {
|
|
4056
|
-
const platformDir =
|
|
4548
|
+
const platformDir = resolve10(args.platformDir ?? process.cwd());
|
|
4057
4549
|
const outArg = args.out;
|
|
4058
4550
|
const dbPath = resolveDbPath(platformDir, outArg);
|
|
4059
4551
|
if (outArg)
|
|
@@ -4103,15 +4595,15 @@ var exports_propagation = {};
|
|
|
4103
4595
|
__export(exports_propagation, {
|
|
4104
4596
|
propagationCommand: () => propagationCommand
|
|
4105
4597
|
});
|
|
4106
|
-
import { resolve as
|
|
4107
|
-
import { defineCommand as
|
|
4598
|
+
import { resolve as resolve11 } from "path";
|
|
4599
|
+
import { defineCommand as defineCommand7 } from "citty";
|
|
4108
4600
|
var propagationCommand;
|
|
4109
4601
|
var init_propagation2 = __esm(() => {
|
|
4110
4602
|
init_constants();
|
|
4111
4603
|
init_discover();
|
|
4112
4604
|
init_prompt();
|
|
4113
4605
|
init__shared();
|
|
4114
|
-
propagationCommand =
|
|
4606
|
+
propagationCommand = defineCommand7({
|
|
4115
4607
|
meta: {
|
|
4116
4608
|
name: "propagation",
|
|
4117
4609
|
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 +4643,7 @@ var init_propagation2 = __esm(() => {
|
|
|
4151
4643
|
process.exit(EXIT.USAGE);
|
|
4152
4644
|
return;
|
|
4153
4645
|
}
|
|
4154
|
-
const platformDir =
|
|
4646
|
+
const platformDir = resolve11(args.platformDir ?? process.cwd());
|
|
4155
4647
|
const outArg = args.out;
|
|
4156
4648
|
const dbPath = resolveDbPath(platformDir, outArg);
|
|
4157
4649
|
if (outArg)
|
|
@@ -4225,8 +4717,8 @@ var exports_query = {};
|
|
|
4225
4717
|
__export(exports_query, {
|
|
4226
4718
|
queryCommand: () => queryCommand
|
|
4227
4719
|
});
|
|
4228
|
-
import { resolve as
|
|
4229
|
-
import { defineCommand as
|
|
4720
|
+
import { resolve as resolve12 } from "path";
|
|
4721
|
+
import { defineCommand as defineCommand8 } from "citty";
|
|
4230
4722
|
var queryCommand;
|
|
4231
4723
|
var init_query = __esm(() => {
|
|
4232
4724
|
init_src();
|
|
@@ -4234,7 +4726,7 @@ var init_query = __esm(() => {
|
|
|
4234
4726
|
init_discover();
|
|
4235
4727
|
init_prompt();
|
|
4236
4728
|
init__shared();
|
|
4237
|
-
queryCommand =
|
|
4729
|
+
queryCommand = defineCommand8({
|
|
4238
4730
|
meta: {
|
|
4239
4731
|
name: "query",
|
|
4240
4732
|
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 +4783,7 @@ var init_query = __esm(() => {
|
|
|
4291
4783
|
process.exit(EXIT.USAGE);
|
|
4292
4784
|
return;
|
|
4293
4785
|
}
|
|
4294
|
-
const platformDir =
|
|
4786
|
+
const platformDir = resolve12(args.platformDir ?? process.cwd());
|
|
4295
4787
|
const outArg = args.out;
|
|
4296
4788
|
const dbPath = resolveDbPath(platformDir, outArg);
|
|
4297
4789
|
if (outArg)
|
|
@@ -4380,8 +4872,8 @@ var exports_relations = {};
|
|
|
4380
4872
|
__export(exports_relations, {
|
|
4381
4873
|
relationsCommand: () => relationsCommand
|
|
4382
4874
|
});
|
|
4383
|
-
import { resolve as
|
|
4384
|
-
import { defineCommand as
|
|
4875
|
+
import { resolve as resolve13 } from "path";
|
|
4876
|
+
import { defineCommand as defineCommand9 } from "citty";
|
|
4385
4877
|
function formatNoRelations(platformDir) {
|
|
4386
4878
|
return [
|
|
4387
4879
|
`No Relates links indexed under ${platformDir}.`,
|
|
@@ -4394,7 +4886,7 @@ var init_relations = __esm(() => {
|
|
|
4394
4886
|
init_constants();
|
|
4395
4887
|
init_prompt();
|
|
4396
4888
|
init__shared();
|
|
4397
|
-
relationsCommand =
|
|
4889
|
+
relationsCommand = defineCommand9({
|
|
4398
4890
|
meta: {
|
|
4399
4891
|
name: "relations",
|
|
4400
4892
|
description: "Render the Relates links between requirements as a mermaid entity diagram. --json emits the sorted relation rows."
|
|
@@ -4423,7 +4915,7 @@ var init_relations = __esm(() => {
|
|
|
4423
4915
|
}
|
|
4424
4916
|
},
|
|
4425
4917
|
async run({ args }) {
|
|
4426
|
-
const platformDir =
|
|
4918
|
+
const platformDir = resolve13(args.platformDir ?? process.cwd());
|
|
4427
4919
|
const outArg = args.out;
|
|
4428
4920
|
const dbPath = resolveDbPath(platformDir, outArg);
|
|
4429
4921
|
if (outArg)
|
|
@@ -4510,7 +5002,8 @@ function renderProvenance(rows, mode) {
|
|
|
4510
5002
|
function decorateRow(row, resolved) {
|
|
4511
5003
|
const prefix = ` ${row.role} ${row.issue_id} ${row.source_file}:${row.line}`;
|
|
4512
5004
|
if (resolved?.ok && resolved.title && resolved.url) {
|
|
4513
|
-
|
|
5005
|
+
const staleMark = resolved.stale ? " (cached \u2014 refresh failed)" : "";
|
|
5006
|
+
return `${prefix} ${resolved.title} [${resolved.status ?? ""}] ${resolved.url}${staleMark}`;
|
|
4514
5007
|
}
|
|
4515
5008
|
return `${prefix} (${TOKEN_HINT})`;
|
|
4516
5009
|
}
|
|
@@ -4519,7 +5012,12 @@ function renderProvenanceDecorated(rows, resolved, mode) {
|
|
|
4519
5012
|
if (mode === "json") {
|
|
4520
5013
|
return JSON.stringify(sorted.map((r) => {
|
|
4521
5014
|
const hit = resolved.get(r.issue_id);
|
|
4522
|
-
const resolvedField = hit?.ok === true ? {
|
|
5015
|
+
const resolvedField = hit?.ok === true ? {
|
|
5016
|
+
title: hit.title ?? "",
|
|
5017
|
+
status: hit.status ?? "",
|
|
5018
|
+
url: hit.url ?? "",
|
|
5019
|
+
stale: hit.stale === true
|
|
5020
|
+
} : null;
|
|
4523
5021
|
return { ...r, resolved: resolvedField };
|
|
4524
5022
|
}));
|
|
4525
5023
|
}
|
|
@@ -4553,7 +5051,7 @@ var init_adapter = () => {};
|
|
|
4553
5051
|
|
|
4554
5052
|
// ../tracker/src/cache.ts
|
|
4555
5053
|
import { mkdirSync as mkdirSync6 } from "fs";
|
|
4556
|
-
import { dirname as dirname9, join as
|
|
5054
|
+
import { dirname as dirname9, join as join13 } from "path";
|
|
4557
5055
|
function isFresh(entry, now, ttlMs = CACHE_TTL_MS) {
|
|
4558
5056
|
const t = Date.parse(entry.fetched_at);
|
|
4559
5057
|
if (Number.isNaN(t))
|
|
@@ -4567,7 +5065,7 @@ function isValidEntry(e) {
|
|
|
4567
5065
|
return typeof r.title === "string" && typeof r.status === "string" && typeof r.url === "string" && typeof r.fetched_at === "string";
|
|
4568
5066
|
}
|
|
4569
5067
|
function sidecarPath(platformDir) {
|
|
4570
|
-
return
|
|
5068
|
+
return join13(platformDir, ".spec-engine", SIDECAR_FILE);
|
|
4571
5069
|
}
|
|
4572
5070
|
async function readCache(platformDir) {
|
|
4573
5071
|
try {
|
|
@@ -4735,7 +5233,13 @@ async function resolveAndCache(rows, platformDir, adapter = makeLinearAdapter(),
|
|
|
4735
5233
|
const out = new Map;
|
|
4736
5234
|
for (const id of uniqueIds) {
|
|
4737
5235
|
const entry = next[id];
|
|
4738
|
-
out.set(id, entry ? {
|
|
5236
|
+
out.set(id, entry ? {
|
|
5237
|
+
ok: true,
|
|
5238
|
+
title: entry.title,
|
|
5239
|
+
status: entry.status,
|
|
5240
|
+
url: entry.url,
|
|
5241
|
+
stale: !isFresh(entry, now)
|
|
5242
|
+
} : { ok: false });
|
|
4739
5243
|
}
|
|
4740
5244
|
return out;
|
|
4741
5245
|
}
|
|
@@ -4748,9 +5252,9 @@ var exports_provenance = {};
|
|
|
4748
5252
|
__export(exports_provenance, {
|
|
4749
5253
|
provenanceCommand: () => provenanceCommand
|
|
4750
5254
|
});
|
|
4751
|
-
import { statSync as
|
|
4752
|
-
import { resolve as
|
|
4753
|
-
import { defineCommand as
|
|
5255
|
+
import { statSync as statSync5 } from "fs";
|
|
5256
|
+
import { resolve as resolve14 } from "path";
|
|
5257
|
+
import { defineCommand as defineCommand10 } from "citty";
|
|
4754
5258
|
function formatNoProvenance(platformDir) {
|
|
4755
5259
|
return [
|
|
4756
5260
|
`No provenance links indexed under ${platformDir}.`,
|
|
@@ -4767,7 +5271,7 @@ var init_provenance = __esm(() => {
|
|
|
4767
5271
|
init_prompt();
|
|
4768
5272
|
init_resolve();
|
|
4769
5273
|
init__shared();
|
|
4770
|
-
provenanceCommand =
|
|
5274
|
+
provenanceCommand = defineCommand10({
|
|
4771
5275
|
meta: {
|
|
4772
5276
|
name: "provenance",
|
|
4773
5277
|
description: "Render the per-requirement provenance matrix (creating/revising issues + backing tests + git pointer). --json emits the deterministically-sorted matrix rows."
|
|
@@ -4810,7 +5314,7 @@ var init_provenance = __esm(() => {
|
|
|
4810
5314
|
if (issueId && platformArg === undefined) {
|
|
4811
5315
|
let isDir = false;
|
|
4812
5316
|
try {
|
|
4813
|
-
isDir =
|
|
5317
|
+
isDir = statSync5(resolve14(issueId)).isDirectory();
|
|
4814
5318
|
} catch {
|
|
4815
5319
|
isDir = false;
|
|
4816
5320
|
}
|
|
@@ -4819,7 +5323,7 @@ var init_provenance = __esm(() => {
|
|
|
4819
5323
|
issueId = "";
|
|
4820
5324
|
}
|
|
4821
5325
|
}
|
|
4822
|
-
const platformDir =
|
|
5326
|
+
const platformDir = resolve14(platformArg ?? process.cwd());
|
|
4823
5327
|
const outArg = args.out;
|
|
4824
5328
|
const dbPath = resolveDbPath(platformDir, outArg);
|
|
4825
5329
|
if (outArg)
|
|
@@ -4844,15 +5348,9 @@ var init_provenance = __esm(() => {
|
|
|
4844
5348
|
});
|
|
4845
5349
|
});
|
|
4846
5350
|
|
|
4847
|
-
// src/authoring/edit.ts
|
|
4848
|
-
function localToday() {
|
|
4849
|
-
const d = new Date;
|
|
4850
|
-
return `${d.getFullYear()}-` + `${String(d.getMonth() + 1).padStart(2, "0")}-` + `${String(d.getDate()).padStart(2, "0")}`;
|
|
4851
|
-
}
|
|
4852
|
-
|
|
4853
5351
|
// src/authoring/filerefs.ts
|
|
4854
|
-
import { existsSync as
|
|
4855
|
-
import { resolve as
|
|
5352
|
+
import { existsSync as existsSync9 } from "fs";
|
|
5353
|
+
import { resolve as resolve15 } from "path";
|
|
4856
5354
|
function extractRefsFromText(s) {
|
|
4857
5355
|
const out = [];
|
|
4858
5356
|
for (const m of s.matchAll(FILE_REF_RE)) {
|
|
@@ -4863,17 +5361,55 @@ function extractRefsFromText(s) {
|
|
|
4863
5361
|
return out;
|
|
4864
5362
|
}
|
|
4865
5363
|
function resolveFileRef(platformDir, ref) {
|
|
4866
|
-
const root =
|
|
4867
|
-
const abs =
|
|
5364
|
+
const root = resolve15(platformDir);
|
|
5365
|
+
const abs = resolve15(root, ref);
|
|
4868
5366
|
if (!(abs === root || abs.startsWith(`${root}/`)))
|
|
4869
5367
|
return false;
|
|
4870
|
-
return
|
|
5368
|
+
return existsSync9(abs);
|
|
4871
5369
|
}
|
|
4872
5370
|
var FILE_REF_RE;
|
|
4873
5371
|
var init_filerefs = __esm(() => {
|
|
4874
5372
|
FILE_REF_RE = /(?<![^\s])@([A-Za-z0-9_][A-Za-z0-9_\-./]*)/g;
|
|
4875
5373
|
});
|
|
4876
5374
|
|
|
5375
|
+
// src/authoring/grammar.ts
|
|
5376
|
+
import { join as join14 } from "path";
|
|
5377
|
+
async function grammarConfig(platformDir, key) {
|
|
5378
|
+
try {
|
|
5379
|
+
const doc = JSON.parse(await Bun.file(join14(platformDir, "spec-engine", key, "SPEC.json")).text());
|
|
5380
|
+
return {
|
|
5381
|
+
grammar: doc.grammar === "ears" ? "ears" : "freeform",
|
|
5382
|
+
severity: doc.grammarSeverity === "error" ? "error" : "warning"
|
|
5383
|
+
};
|
|
5384
|
+
} catch {
|
|
5385
|
+
return { grammar: "freeform", severity: "warning" };
|
|
5386
|
+
}
|
|
5387
|
+
}
|
|
5388
|
+
async function enforceStatementGrammar(platformDir, key, statement, cmdName) {
|
|
5389
|
+
if (key === "TERM")
|
|
5390
|
+
return;
|
|
5391
|
+
const cfg = await grammarConfig(platformDir, key);
|
|
5392
|
+
if (cfg.grammar !== "ears")
|
|
5393
|
+
return;
|
|
5394
|
+
const result = parseEarsStatement(statement);
|
|
5395
|
+
if (result.ok)
|
|
5396
|
+
return;
|
|
5397
|
+
const message = `${cmdName}: the statement does not match the domain's EARS shape \u2014 ${result.problem}.
|
|
5398
|
+
` + `Expected one of:
|
|
5399
|
+
${result.expected}`;
|
|
5400
|
+
if (cfg.severity === "error") {
|
|
5401
|
+
console.error(`${message}
|
|
5402
|
+
(${key} sets grammarSeverity: "error", so the write is refused)`);
|
|
5403
|
+
process.exit(EXIT.USAGE);
|
|
5404
|
+
}
|
|
5405
|
+
console.error(`${message}
|
|
5406
|
+
(warning \u2014 written anyway; ${key} sets grammar: "ears")`);
|
|
5407
|
+
}
|
|
5408
|
+
var init_grammar2 = __esm(() => {
|
|
5409
|
+
init_src();
|
|
5410
|
+
init_constants();
|
|
5411
|
+
});
|
|
5412
|
+
|
|
4877
5413
|
// src/commands/req.ts
|
|
4878
5414
|
var exports_req = {};
|
|
4879
5415
|
__export(exports_req, {
|
|
@@ -4882,10 +5418,10 @@ __export(exports_req, {
|
|
|
4882
5418
|
askLine: () => askLine,
|
|
4883
5419
|
appendEntry: () => appendEntry
|
|
4884
5420
|
});
|
|
4885
|
-
import { existsSync as
|
|
4886
|
-
import { join as
|
|
5421
|
+
import { existsSync as existsSync10 } from "fs";
|
|
5422
|
+
import { join as join15, resolve as resolve16 } from "path";
|
|
4887
5423
|
import { createInterface as createInterface2 } from "readline";
|
|
4888
|
-
import { defineCommand as
|
|
5424
|
+
import { defineCommand as defineCommand11 } from "citty";
|
|
4889
5425
|
function resolveDomainKey(input, platformDir) {
|
|
4890
5426
|
const prefix = normalizeDomainKey(input);
|
|
4891
5427
|
const keys = listDomainKeys(platformDir);
|
|
@@ -4919,10 +5455,12 @@ async function authorFromFieldFlags(platformDir, key, nextId, textFlag, opts) {
|
|
|
4919
5455
|
requirement,
|
|
4920
5456
|
why,
|
|
4921
5457
|
binds,
|
|
4922
|
-
lives
|
|
5458
|
+
lives,
|
|
5459
|
+
issue: (opts.issue ?? "").trim() || undefined
|
|
4923
5460
|
});
|
|
4924
5461
|
if (opts.json) {
|
|
4925
|
-
|
|
5462
|
+
const parsed = parseEarsStatement(requirement);
|
|
5463
|
+
console.log(JSON.stringify(parsed.ok ? { id: nextId, file: relFile, clauses: parsed.clauses } : { id: nextId, file: relFile }));
|
|
4926
5464
|
} else {
|
|
4927
5465
|
console.log(`appended ${nextId} to ${relFile}`);
|
|
4928
5466
|
}
|
|
@@ -4946,8 +5484,9 @@ function warnUnresolvableRefs(platformDir, fieldValues) {
|
|
|
4946
5484
|
}
|
|
4947
5485
|
async function appendEntry(platformDir, key, id, fields) {
|
|
4948
5486
|
const relFile = `spec-engine/${key}/SPEC.json`;
|
|
4949
|
-
const specPath =
|
|
4950
|
-
|
|
5487
|
+
const specPath = join15(platformDir, "spec-engine", key, "SPEC.json");
|
|
5488
|
+
await enforceStatementGrammar(platformDir, key, fields.requirement, "spec req");
|
|
5489
|
+
const domain2 = existsSync10(specPath) ? JSON.parse(await Bun.file(specPath).text()) : null;
|
|
4951
5490
|
if (domain2 === null) {
|
|
4952
5491
|
console.error(`spec req: no domain ${key} (expected ${relFile} under ${platformDir})`);
|
|
4953
5492
|
process.exit(EXIT.USAGE);
|
|
@@ -4962,7 +5501,7 @@ async function appendEntry(platformDir, key, id, fields) {
|
|
|
4962
5501
|
supersededBy: null,
|
|
4963
5502
|
relates: [],
|
|
4964
5503
|
livesIn: fields.lives ? [fields.lives] : [],
|
|
4965
|
-
issues: []
|
|
5504
|
+
issues: fields.issue ? [{ role: "created", id: fields.issue }] : []
|
|
4966
5505
|
});
|
|
4967
5506
|
domain2.requirements = requirements;
|
|
4968
5507
|
domain2.updated = localToday();
|
|
@@ -4988,10 +5527,11 @@ var init_req = __esm(() => {
|
|
|
4988
5527
|
init_src();
|
|
4989
5528
|
init_domains();
|
|
4990
5529
|
init_filerefs();
|
|
5530
|
+
init_grammar2();
|
|
4991
5531
|
init_constants();
|
|
4992
5532
|
init_discover();
|
|
4993
5533
|
init__shared();
|
|
4994
|
-
reqCommand =
|
|
5534
|
+
reqCommand = defineCommand11({
|
|
4995
5535
|
meta: {
|
|
4996
5536
|
name: "req",
|
|
4997
5537
|
description: "Author a new requirement interactively (TTY) or print the next unused id (non-TTY)"
|
|
@@ -5026,11 +5566,15 @@ var init_req = __esm(() => {
|
|
|
5026
5566
|
lives: {
|
|
5027
5567
|
type: "string",
|
|
5028
5568
|
description: "Lives in field (only with --text)"
|
|
5569
|
+
},
|
|
5570
|
+
issue: {
|
|
5571
|
+
type: "string",
|
|
5572
|
+
description: "Originating ticket id, recorded as created-provenance on the entry (only with --text; opaque \u2014 never a requirement id)"
|
|
5029
5573
|
}
|
|
5030
5574
|
},
|
|
5031
5575
|
async run({ args }) {
|
|
5032
5576
|
const input = args.domainPrefix;
|
|
5033
|
-
const platformDir =
|
|
5577
|
+
const platformDir = resolve16(args.platformDir ?? process.cwd());
|
|
5034
5578
|
try {
|
|
5035
5579
|
assertSpecPlatform(platformDir);
|
|
5036
5580
|
} catch (e) {
|
|
@@ -5039,9 +5583,9 @@ var init_req = __esm(() => {
|
|
|
5039
5583
|
const key = resolveDomainKey(input, platformDir);
|
|
5040
5584
|
const nextId = await nextRequirementId(platformDir, key);
|
|
5041
5585
|
const textFlag = args.text;
|
|
5042
|
-
const hasFieldFlags = typeof args.why === "string" || typeof args.binds === "string" || typeof args.lives === "string";
|
|
5586
|
+
const hasFieldFlags = typeof args.why === "string" || typeof args.binds === "string" || typeof args.lives === "string" || typeof args.issue === "string";
|
|
5043
5587
|
if (textFlag === undefined && hasFieldFlags) {
|
|
5044
|
-
console.error("spec req: --why/--binds/--lives require --text (the Requirement field is mandatory)");
|
|
5588
|
+
console.error("spec req: --why/--binds/--lives/--issue require --text (the Requirement field is mandatory)");
|
|
5045
5589
|
process.exit(EXIT.USAGE);
|
|
5046
5590
|
return;
|
|
5047
5591
|
}
|
|
@@ -5050,6 +5594,7 @@ var init_req = __esm(() => {
|
|
|
5050
5594
|
why: args.why,
|
|
5051
5595
|
binds: args.binds,
|
|
5052
5596
|
lives: args.lives,
|
|
5597
|
+
issue: args.issue,
|
|
5053
5598
|
json: Boolean(args.json)
|
|
5054
5599
|
});
|
|
5055
5600
|
return;
|
|
@@ -5086,7 +5631,7 @@ var init_req = __esm(() => {
|
|
|
5086
5631
|
});
|
|
5087
5632
|
|
|
5088
5633
|
// src/parser/grammar.ts
|
|
5089
|
-
var
|
|
5634
|
+
var init_grammar3 = __esm(() => {
|
|
5090
5635
|
init_src();
|
|
5091
5636
|
});
|
|
5092
5637
|
|
|
@@ -5098,16 +5643,16 @@ __export(exports_term, {
|
|
|
5098
5643
|
termConfirmCommand: () => termConfirmCommand,
|
|
5099
5644
|
termCommand: () => termCommand
|
|
5100
5645
|
});
|
|
5101
|
-
import { existsSync as
|
|
5102
|
-
import { join as
|
|
5103
|
-
import { defineCommand as
|
|
5646
|
+
import { existsSync as existsSync11 } from "fs";
|
|
5647
|
+
import { join as join16, resolve as resolve17 } from "path";
|
|
5648
|
+
import { defineCommand as defineCommand12 } from "citty";
|
|
5104
5649
|
function parseAliases(raw) {
|
|
5105
5650
|
if (typeof raw !== "string" || raw.trim() === "")
|
|
5106
5651
|
return [];
|
|
5107
5652
|
return raw.split(",").map((s) => s.trim()).filter((s) => s !== "");
|
|
5108
5653
|
}
|
|
5109
|
-
function
|
|
5110
|
-
const platformDir =
|
|
5654
|
+
function resolvePlatform2(platformDirRaw) {
|
|
5655
|
+
const platformDir = resolve17(platformDirRaw ?? process.cwd());
|
|
5111
5656
|
try {
|
|
5112
5657
|
assertSpecPlatform(platformDir);
|
|
5113
5658
|
} catch (e) {
|
|
@@ -5116,16 +5661,16 @@ function resolvePlatform(platformDirRaw) {
|
|
|
5116
5661
|
return platformDir;
|
|
5117
5662
|
}
|
|
5118
5663
|
async function readTermEnvelope(platformDir, cmd) {
|
|
5119
|
-
const specPath =
|
|
5120
|
-
if (!
|
|
5664
|
+
const specPath = join16(platformDir, "spec-engine", TERM_KEY2, "SPEC.json");
|
|
5665
|
+
if (!existsSync11(specPath)) {
|
|
5121
5666
|
console.error(`spec ${cmd}: no TERM domain (expected spec-engine/TERM/SPEC.json under ${platformDir})`);
|
|
5122
5667
|
process.exit(EXIT.USAGE);
|
|
5123
5668
|
}
|
|
5124
5669
|
return JSON.parse(await Bun.file(specPath).text());
|
|
5125
5670
|
}
|
|
5126
5671
|
async function appendTermEntry(platformDir, id, fields) {
|
|
5127
|
-
const relFile = `spec-engine/${
|
|
5128
|
-
const specPath =
|
|
5672
|
+
const relFile = `spec-engine/${TERM_KEY2}/SPEC.json`;
|
|
5673
|
+
const specPath = join16(platformDir, "spec-engine", TERM_KEY2, "SPEC.json");
|
|
5129
5674
|
const domain2 = await readTermEnvelope(platformDir, "term");
|
|
5130
5675
|
const requirements = Array.isArray(domain2.requirements) ? domain2.requirements : [];
|
|
5131
5676
|
const entry = {
|
|
@@ -5158,11 +5703,11 @@ async function appendTermEntry(platformDir, id, fields) {
|
|
|
5158
5703
|
return relFile;
|
|
5159
5704
|
}
|
|
5160
5705
|
async function authorOrQuery(opts) {
|
|
5161
|
-
const platformDir =
|
|
5162
|
-
const nextId = await nextRequirementId(platformDir,
|
|
5706
|
+
const platformDir = resolvePlatform2(opts.platformDirRaw);
|
|
5707
|
+
const nextId = await nextRequirementId(platformDir, TERM_KEY2);
|
|
5163
5708
|
if (opts.def === undefined) {
|
|
5164
5709
|
if (opts.json) {
|
|
5165
|
-
console.log(JSON.stringify({ domain:
|
|
5710
|
+
console.log(JSON.stringify({ domain: TERM_KEY2, next_id: nextId }));
|
|
5166
5711
|
} else {
|
|
5167
5712
|
console.log(nextId);
|
|
5168
5713
|
}
|
|
@@ -5188,9 +5733,9 @@ async function authorOrQuery(opts) {
|
|
|
5188
5733
|
}
|
|
5189
5734
|
}
|
|
5190
5735
|
async function listTerms(platformDirRaw, json) {
|
|
5191
|
-
const platformDir =
|
|
5192
|
-
const specPath =
|
|
5193
|
-
const domain2 =
|
|
5736
|
+
const platformDir = resolvePlatform2(platformDirRaw);
|
|
5737
|
+
const specPath = join16(platformDir, "spec-engine", TERM_KEY2, "SPEC.json");
|
|
5738
|
+
const domain2 = existsSync11(specPath) ? JSON.parse(await Bun.file(specPath).text()) : { requirements: [] };
|
|
5194
5739
|
const requirements = Array.isArray(domain2.requirements) ? domain2.requirements : [];
|
|
5195
5740
|
const rows = requirements.map((r) => ({
|
|
5196
5741
|
id: typeof r.id === "string" ? r.id : "",
|
|
@@ -5210,18 +5755,18 @@ async function resolveReviseTarget(id, platformDirRaw, defRaw) {
|
|
|
5210
5755
|
console.error(`spec term revise: id must be a requirement id (TERM-NNN); got ${id}`);
|
|
5211
5756
|
process.exit(EXIT.USAGE);
|
|
5212
5757
|
}
|
|
5213
|
-
if (id.slice(0, id.indexOf("-")) !==
|
|
5758
|
+
if (id.slice(0, id.indexOf("-")) !== TERM_KEY2) {
|
|
5214
5759
|
console.error(`spec term revise: ${id} is not a TERM id \u2014 revise operates on the TERM store`);
|
|
5215
5760
|
process.exit(EXIT.USAGE);
|
|
5216
5761
|
}
|
|
5217
|
-
const platformDir =
|
|
5762
|
+
const platformDir = resolvePlatform2(platformDirRaw);
|
|
5218
5763
|
const def = (defRaw ?? "").trim();
|
|
5219
5764
|
if (def === "") {
|
|
5220
5765
|
console.error("spec term revise: --def <definition> is required (a non-empty revision)");
|
|
5221
5766
|
process.exit(EXIT.USAGE);
|
|
5222
5767
|
}
|
|
5223
|
-
const relFile = `spec-engine/${
|
|
5224
|
-
const specPath =
|
|
5768
|
+
const relFile = `spec-engine/${TERM_KEY2}/SPEC.json`;
|
|
5769
|
+
const specPath = join16(platformDir, "spec-engine", TERM_KEY2, "SPEC.json");
|
|
5225
5770
|
const domain2 = await readTermEnvelope(platformDir, "term revise");
|
|
5226
5771
|
const requirements = Array.isArray(domain2.requirements) ? domain2.requirements : [];
|
|
5227
5772
|
const req = requirements.find((r) => r?.id === id);
|
|
@@ -5279,7 +5824,7 @@ async function resolveConfirmTarget(platformDir, termId) {
|
|
|
5279
5824
|
const termReqs = Array.isArray(termDomain.requirements) ? termDomain.requirements : [];
|
|
5280
5825
|
const term = termReqs.find((r) => r?.id === termId);
|
|
5281
5826
|
if (term === undefined) {
|
|
5282
|
-
console.error(`spec term confirm: no term ${termId} in spec-engine/${
|
|
5827
|
+
console.error(`spec term confirm: no term ${termId} in spec-engine/${TERM_KEY2}/SPEC.json`);
|
|
5283
5828
|
process.exit(EXIT.USAGE);
|
|
5284
5829
|
}
|
|
5285
5830
|
const targetPin = typeof termDomain.specVersion === "number" ? termDomain.specVersion : 1;
|
|
@@ -5297,8 +5842,8 @@ async function resolveConfirmTarget(platformDir, termId) {
|
|
|
5297
5842
|
async function locateCitation(platformDir, reqId, termId) {
|
|
5298
5843
|
const citKey = reqId.slice(0, reqId.indexOf("-"));
|
|
5299
5844
|
const relFile = `spec-engine/${citKey}/SPEC.json`;
|
|
5300
|
-
const specPath =
|
|
5301
|
-
if (!
|
|
5845
|
+
const specPath = join16(platformDir, "spec-engine", citKey, "SPEC.json");
|
|
5846
|
+
if (!existsSync11(specPath)) {
|
|
5302
5847
|
console.error(`spec term confirm: no domain ${citKey} (expected ${relFile} under ${platformDir})`);
|
|
5303
5848
|
process.exit(EXIT.USAGE);
|
|
5304
5849
|
}
|
|
@@ -5309,6 +5854,12 @@ async function locateCitation(platformDir, reqId, termId) {
|
|
|
5309
5854
|
console.error(`spec term confirm: no entry ${reqId} in ${relFile}`);
|
|
5310
5855
|
process.exit(EXIT.USAGE);
|
|
5311
5856
|
}
|
|
5857
|
+
const rawStatus = typeof citReq.status === "string" ? citReq.status : "";
|
|
5858
|
+
const statusLc = rawStatus.toLowerCase();
|
|
5859
|
+
if (statusLc !== "active" && statusLc !== "draft") {
|
|
5860
|
+
console.error(`spec term confirm: ${reqId} is ${rawStatus} \u2014 only Active/Draft entries re-pin a citation (a superseded entry is history)`);
|
|
5861
|
+
process.exit(EXIT.USAGE);
|
|
5862
|
+
}
|
|
5312
5863
|
const cites = Array.isArray(citReq.cites) ? citReq.cites : [];
|
|
5313
5864
|
const cite = cites.find((c) => c?.term === termId);
|
|
5314
5865
|
if (cite === undefined) {
|
|
@@ -5323,11 +5874,11 @@ async function confirmCitation(opts) {
|
|
|
5323
5874
|
console.error(`spec term confirm: req id must be a requirement id (KEY-NNN); got ${reqId}`);
|
|
5324
5875
|
process.exit(EXIT.USAGE);
|
|
5325
5876
|
}
|
|
5326
|
-
if (!ID_RE.test(termId) || termId.slice(0, termId.indexOf("-")) !==
|
|
5877
|
+
if (!ID_RE.test(termId) || termId.slice(0, termId.indexOf("-")) !== TERM_KEY2) {
|
|
5327
5878
|
console.error(`spec term confirm: ${termId} is not a TERM id \u2014 confirm re-pins a TERM citation`);
|
|
5328
5879
|
process.exit(EXIT.USAGE);
|
|
5329
5880
|
}
|
|
5330
|
-
const platformDir =
|
|
5881
|
+
const platformDir = resolvePlatform2(opts.platformDirRaw);
|
|
5331
5882
|
const { targetId, targetPin } = await resolveConfirmTarget(platformDir, termId);
|
|
5332
5883
|
const { citDomain, specPath, relFile, cite } = await locateCitation(platformDir, reqId, termId);
|
|
5333
5884
|
cite.term = targetId;
|
|
@@ -5347,18 +5898,18 @@ async function confirmCitation(opts) {
|
|
|
5347
5898
|
console.log(`confirmed ${reqId} cites ${targetId} @${targetPin} in ${relFile}`);
|
|
5348
5899
|
}
|
|
5349
5900
|
}
|
|
5350
|
-
var
|
|
5901
|
+
var TERM_KEY2 = "TERM", termListCommand, termReviseCommand, termConfirmCommand, termCommand;
|
|
5351
5902
|
var init_term = __esm(() => {
|
|
5352
5903
|
init_src();
|
|
5353
5904
|
init_domains();
|
|
5354
5905
|
init_constants();
|
|
5355
5906
|
init_discover();
|
|
5356
5907
|
init_pipeline();
|
|
5357
|
-
|
|
5908
|
+
init_grammar3();
|
|
5358
5909
|
init_sqlite();
|
|
5359
5910
|
init__shared();
|
|
5360
5911
|
init_req();
|
|
5361
|
-
termListCommand =
|
|
5912
|
+
termListCommand = defineCommand12({
|
|
5362
5913
|
meta: {
|
|
5363
5914
|
name: "list",
|
|
5364
5915
|
description: "List the glossary TERM entries (id, name, status), sorted by id"
|
|
@@ -5378,7 +5929,7 @@ var init_term = __esm(() => {
|
|
|
5378
5929
|
await listTerms(args.platformDir, Boolean(args.json));
|
|
5379
5930
|
}
|
|
5380
5931
|
});
|
|
5381
|
-
termReviseCommand =
|
|
5932
|
+
termReviseCommand = defineCommand12({
|
|
5382
5933
|
meta: {
|
|
5383
5934
|
name: "revise",
|
|
5384
5935
|
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 +5963,7 @@ var init_term = __esm(() => {
|
|
|
5412
5963
|
});
|
|
5413
5964
|
}
|
|
5414
5965
|
});
|
|
5415
|
-
termConfirmCommand =
|
|
5966
|
+
termConfirmCommand = defineCommand12({
|
|
5416
5967
|
meta: {
|
|
5417
5968
|
name: "confirm",
|
|
5418
5969
|
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 +5998,7 @@ var init_term = __esm(() => {
|
|
|
5447
5998
|
});
|
|
5448
5999
|
}
|
|
5449
6000
|
});
|
|
5450
|
-
termCommand =
|
|
6001
|
+
termCommand = defineCommand12({
|
|
5451
6002
|
meta: {
|
|
5452
6003
|
name: "term",
|
|
5453
6004
|
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)"
|
|
@@ -5456,271 +6007,68 @@ var init_term = __esm(() => {
|
|
|
5456
6007
|
name: {
|
|
5457
6008
|
type: "positional",
|
|
5458
6009
|
required: true,
|
|
5459
|
-
description: "The term's headword, or the literal `list` / `revise` verb. Without --def (author form), prints the next unused TERM id."
|
|
5460
|
-
},
|
|
5461
|
-
platformDir: {
|
|
5462
|
-
type: "positional",
|
|
5463
|
-
required: false,
|
|
5464
|
-
description: "Platform directory (author/list), or \u2014 after `revise` \u2014 the TERM-NNN id to revise, or \u2014 after `confirm` \u2014 the citing KEY-NNN id"
|
|
5465
|
-
},
|
|
5466
|
-
extra: {
|
|
5467
|
-
type: "positional",
|
|
5468
|
-
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: {
|
|
6010
|
+
description: "The term's headword, or the literal `list` / `revise` verb. Without --def (author form), prints the next unused TERM id."
|
|
6011
|
+
},
|
|
5699
6012
|
platformDir: {
|
|
5700
6013
|
type: "positional",
|
|
5701
6014
|
required: false,
|
|
5702
|
-
description: "Platform directory
|
|
6015
|
+
description: "Platform directory (author/list), or \u2014 after `revise` \u2014 the TERM-NNN id to revise, or \u2014 after `confirm` \u2014 the citing KEY-NNN id"
|
|
5703
6016
|
},
|
|
5704
|
-
|
|
5705
|
-
type: "
|
|
5706
|
-
|
|
6017
|
+
extra: {
|
|
6018
|
+
type: "positional",
|
|
6019
|
+
required: false,
|
|
6020
|
+
description: "After `revise <TERM-NNN>`: the platform directory (default: cwd). After `confirm <KEY-NNN>`: the TERM-NNN id."
|
|
5707
6021
|
},
|
|
5708
|
-
|
|
5709
|
-
type: "
|
|
5710
|
-
|
|
6022
|
+
extra2: {
|
|
6023
|
+
type: "positional",
|
|
6024
|
+
required: false,
|
|
6025
|
+
description: "After `confirm <KEY-NNN> <TERM-NNN>`: the platform directory (default: cwd)"
|
|
5711
6026
|
},
|
|
5712
|
-
|
|
6027
|
+
def: { type: "string", description: "The term's definition (stored in the statement field)" },
|
|
6028
|
+
text: { type: "string", description: "Alias for --def (the definition)" },
|
|
6029
|
+
aliases: { type: "string", description: "Comma-separated synonyms \u2192 aliases[]" },
|
|
6030
|
+
section: { type: "string", description: "GLOSSARY.md layout bucket (Wave F)" },
|
|
6031
|
+
noBump: { type: "boolean", description: "revise: do not bump the envelope specVersion" },
|
|
6032
|
+
json: {
|
|
6033
|
+
type: "boolean",
|
|
6034
|
+
description: "Author: { id, file } (or { domain, next_id } as an id query). list: the entries array. revise: { id, file, spec_version }."
|
|
6035
|
+
}
|
|
5713
6036
|
},
|
|
5714
6037
|
async run({ args }) {
|
|
5715
|
-
const
|
|
6038
|
+
const verb = args.name;
|
|
6039
|
+
const def = args.def ?? args.text;
|
|
5716
6040
|
const json = Boolean(args.json);
|
|
5717
|
-
if (
|
|
5718
|
-
await
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
await
|
|
6041
|
+
if (verb === "list") {
|
|
6042
|
+
await listTerms(args.platformDir, json);
|
|
6043
|
+
return;
|
|
6044
|
+
}
|
|
6045
|
+
if (verb === "revise") {
|
|
6046
|
+
await reviseTerm({
|
|
6047
|
+
id: args.platformDir ?? "",
|
|
6048
|
+
platformDirRaw: args.extra,
|
|
6049
|
+
def,
|
|
6050
|
+
noBump: Boolean(args.noBump),
|
|
6051
|
+
json
|
|
6052
|
+
});
|
|
6053
|
+
return;
|
|
6054
|
+
}
|
|
6055
|
+
if (verb === "confirm") {
|
|
6056
|
+
await confirmCitation({
|
|
6057
|
+
reqId: args.platformDir ?? "",
|
|
6058
|
+
termId: args.extra ?? "",
|
|
6059
|
+
platformDirRaw: args.extra2,
|
|
6060
|
+
json
|
|
6061
|
+
});
|
|
6062
|
+
return;
|
|
5723
6063
|
}
|
|
6064
|
+
await authorOrQuery({
|
|
6065
|
+
name: verb,
|
|
6066
|
+
platformDirRaw: args.platformDir,
|
|
6067
|
+
def,
|
|
6068
|
+
aliasesRaw: args.aliases,
|
|
6069
|
+
section: args.section,
|
|
6070
|
+
json
|
|
6071
|
+
});
|
|
5724
6072
|
}
|
|
5725
6073
|
});
|
|
5726
6074
|
});
|
|
@@ -5775,7 +6123,7 @@ __export(exports_supersede, {
|
|
|
5775
6123
|
supersedeCommand: () => supersedeCommand
|
|
5776
6124
|
});
|
|
5777
6125
|
import { existsSync as existsSync12 } from "fs";
|
|
5778
|
-
import { join as
|
|
6126
|
+
import { join as join17, resolve as resolve18 } from "path";
|
|
5779
6127
|
import { defineCommand as defineCommand13 } from "citty";
|
|
5780
6128
|
async function resolveSupersedeTarget(id, platformDir) {
|
|
5781
6129
|
if (!ID_RE.test(id)) {
|
|
@@ -5789,7 +6137,7 @@ async function resolveSupersedeTarget(id, platformDir) {
|
|
|
5789
6137
|
}
|
|
5790
6138
|
const key = id.slice(0, id.indexOf("-"));
|
|
5791
6139
|
const relFile = `spec-engine/${key}/SPEC.json`;
|
|
5792
|
-
const specPath =
|
|
6140
|
+
const specPath = join17(platformDir, "spec-engine", key, "SPEC.json");
|
|
5793
6141
|
if (!existsSync12(specPath)) {
|
|
5794
6142
|
console.error(`spec supersede: no domain ${key} (expected ${relFile} under ${platformDir})`);
|
|
5795
6143
|
process.exit(EXIT.USAGE);
|
|
@@ -5848,10 +6196,18 @@ function resolveSuccessorTermFields(args, req) {
|
|
|
5848
6196
|
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
6197
|
return { term, aliases };
|
|
5850
6198
|
}
|
|
5851
|
-
function
|
|
5852
|
-
const { domain: domain2, requirements, req } = target;
|
|
6199
|
+
function flipPredecessor(req, newId, issue) {
|
|
5853
6200
|
req.status = "superseded";
|
|
5854
6201
|
req.supersededBy = newId;
|
|
6202
|
+
if (issue) {
|
|
6203
|
+
const issues = Array.isArray(req.issues) ? req.issues : [];
|
|
6204
|
+
issues.push({ role: "supersedes-via", id: issue });
|
|
6205
|
+
req.issues = issues;
|
|
6206
|
+
}
|
|
6207
|
+
}
|
|
6208
|
+
function applySupersedeEdit(target, newId, requirement, why, lives, noBump, termFields, issue) {
|
|
6209
|
+
const { domain: domain2, requirements, req } = target;
|
|
6210
|
+
flipPredecessor(req, newId, issue);
|
|
5855
6211
|
const successor = {
|
|
5856
6212
|
id: newId,
|
|
5857
6213
|
status: "active",
|
|
@@ -5861,7 +6217,7 @@ function applySupersedeEdit(target, newId, requirement, why, lives, noBump, term
|
|
|
5861
6217
|
supersededBy: null,
|
|
5862
6218
|
relates: [],
|
|
5863
6219
|
livesIn: lives === "" ? [] : [lives],
|
|
5864
|
-
issues: [],
|
|
6220
|
+
issues: issue ? [{ role: "created", id: issue }] : [],
|
|
5865
6221
|
changedAtVersion: 1
|
|
5866
6222
|
};
|
|
5867
6223
|
if (termFields !== null) {
|
|
@@ -5900,10 +6256,11 @@ var supersedeCommand;
|
|
|
5900
6256
|
var init_supersede = __esm(() => {
|
|
5901
6257
|
init_src();
|
|
5902
6258
|
init_domains();
|
|
6259
|
+
init_grammar2();
|
|
5903
6260
|
init_constants();
|
|
5904
6261
|
init_discover();
|
|
5905
6262
|
init_domainJson();
|
|
5906
|
-
|
|
6263
|
+
init_grammar3();
|
|
5907
6264
|
init__shared();
|
|
5908
6265
|
init_req();
|
|
5909
6266
|
supersedeCommand = defineCommand13({
|
|
@@ -5950,6 +6307,10 @@ var init_supersede = __esm(() => {
|
|
|
5950
6307
|
type: "boolean",
|
|
5951
6308
|
description: "Do not bump the envelope specVersion"
|
|
5952
6309
|
},
|
|
6310
|
+
issue: {
|
|
6311
|
+
type: "string",
|
|
6312
|
+
description: "Ticket that caused this supersession \u2014 recorded as supersedes-via on the predecessor and created on the successor (opaque provenance)"
|
|
6313
|
+
},
|
|
5953
6314
|
json: {
|
|
5954
6315
|
type: "boolean",
|
|
5955
6316
|
description: "Emit { old_id, new_id, file, spec_version, retag } as JSON instead of the text summary"
|
|
@@ -5960,11 +6321,12 @@ var init_supersede = __esm(() => {
|
|
|
5960
6321
|
const platformDir = resolve18(args.platformDir ?? process.cwd());
|
|
5961
6322
|
const target = await resolveSupersedeTarget(id, platformDir);
|
|
5962
6323
|
const requirement = await resolveSuccessorText(args, id);
|
|
6324
|
+
await enforceStatementGrammar(platformDir, target.key, requirement, "spec supersede");
|
|
5963
6325
|
const { why, lives, binds } = resolveSuccessorFields(args, target.req);
|
|
5964
6326
|
warnUnresolvableRefs(platformDir, [requirement, why, binds, lives]);
|
|
5965
6327
|
const newId = await nextRequirementId(platformDir, target.key);
|
|
5966
6328
|
const termFields = target.key === "TERM" ? resolveSuccessorTermFields(args, target.req) : null;
|
|
5967
|
-
const newVersion = applySupersedeEdit(target, newId, requirement, why, lives, Boolean(args.noBump), termFields);
|
|
6329
|
+
const newVersion = applySupersedeEdit(target, newId, requirement, why, lives, Boolean(args.noBump), termFields, (args.issue ?? "").trim() || undefined);
|
|
5968
6330
|
const res = await validateAndWrite(target.specPath, target.domain, target.relFile);
|
|
5969
6331
|
if (!res.ok) {
|
|
5970
6332
|
for (const diag of res.diagnostics) {
|
|
@@ -5998,18 +6360,133 @@ var init_supersede = __esm(() => {
|
|
|
5998
6360
|
});
|
|
5999
6361
|
});
|
|
6000
6362
|
|
|
6363
|
+
// src/commands/deprecate.ts
|
|
6364
|
+
var exports_deprecate = {};
|
|
6365
|
+
__export(exports_deprecate, {
|
|
6366
|
+
deprecateCommand: () => deprecateCommand
|
|
6367
|
+
});
|
|
6368
|
+
import { existsSync as existsSync13 } from "fs";
|
|
6369
|
+
import { join as join18, resolve as resolve19 } from "path";
|
|
6370
|
+
import { defineCommand as defineCommand14 } from "citty";
|
|
6371
|
+
async function resolveDeprecateTarget(platformDir, id) {
|
|
6372
|
+
const key = id.slice(0, id.indexOf("-"));
|
|
6373
|
+
const relFile = `spec-engine/${key}/SPEC.json`;
|
|
6374
|
+
const specPath = join18(platformDir, "spec-engine", key, "SPEC.json");
|
|
6375
|
+
if (!existsSync13(specPath)) {
|
|
6376
|
+
console.error(`spec deprecate: no domain ${key} (expected ${relFile} under ${platformDir})`);
|
|
6377
|
+
process.exit(EXIT.USAGE);
|
|
6378
|
+
}
|
|
6379
|
+
const domain2 = JSON.parse(await Bun.file(specPath).text());
|
|
6380
|
+
const requirements = Array.isArray(domain2.requirements) ? domain2.requirements : [];
|
|
6381
|
+
const req = requirements.find((r) => r?.id === id);
|
|
6382
|
+
if (req === undefined) {
|
|
6383
|
+
console.error(`spec deprecate: no entry ${id} in ${relFile}`);
|
|
6384
|
+
process.exit(EXIT.USAGE);
|
|
6385
|
+
}
|
|
6386
|
+
const statusLc = (typeof req.status === "string" ? req.status : "").toLowerCase();
|
|
6387
|
+
if (statusLc === "deprecated" || statusLc === "retired") {
|
|
6388
|
+
console.error(`spec deprecate: ${id} is already deprecated`);
|
|
6389
|
+
process.exit(EXIT.USAGE);
|
|
6390
|
+
}
|
|
6391
|
+
if (statusLc !== "active" && statusLc !== "draft") {
|
|
6392
|
+
console.error(`spec deprecate: ${id} is ${req.status} \u2014 only Active/Draft entries deprecate (a superseded entry is already history)`);
|
|
6393
|
+
process.exit(EXIT.USAGE);
|
|
6394
|
+
}
|
|
6395
|
+
return { domain: domain2, req, specPath, relFile };
|
|
6396
|
+
}
|
|
6397
|
+
var deprecateCommand;
|
|
6398
|
+
var init_deprecate = __esm(() => {
|
|
6399
|
+
init_src();
|
|
6400
|
+
init_constants();
|
|
6401
|
+
init_discover();
|
|
6402
|
+
init_grammar3();
|
|
6403
|
+
init__shared();
|
|
6404
|
+
deprecateCommand = defineCommand14({
|
|
6405
|
+
meta: {
|
|
6406
|
+
name: "deprecate",
|
|
6407
|
+
description: "Mark a requirement end-of-life with a recorded reason (a requirement is never deleted)"
|
|
6408
|
+
},
|
|
6409
|
+
args: {
|
|
6410
|
+
id: {
|
|
6411
|
+
type: "positional",
|
|
6412
|
+
required: true,
|
|
6413
|
+
description: "Requirement id to deprecate (KEY-NNN)"
|
|
6414
|
+
},
|
|
6415
|
+
platformDir: {
|
|
6416
|
+
type: "positional",
|
|
6417
|
+
required: false,
|
|
6418
|
+
description: "Platform directory (default: cwd)"
|
|
6419
|
+
},
|
|
6420
|
+
reason: {
|
|
6421
|
+
type: "string",
|
|
6422
|
+
description: "Why this requirement is end-of-life (required \u2014 the durable record)"
|
|
6423
|
+
},
|
|
6424
|
+
json: {
|
|
6425
|
+
type: "boolean",
|
|
6426
|
+
description: "Print { id, file, reason, sites } instead of text"
|
|
6427
|
+
}
|
|
6428
|
+
},
|
|
6429
|
+
async run({ args }) {
|
|
6430
|
+
const id = args.id;
|
|
6431
|
+
const platformDir = resolve19(args.platformDir ?? process.cwd());
|
|
6432
|
+
const reason = (args.reason ?? "").trim();
|
|
6433
|
+
if (!ID_RE.test(id)) {
|
|
6434
|
+
console.error(`spec deprecate: id must be a requirement id (KEY-NNN); got ${id}`);
|
|
6435
|
+
process.exit(EXIT.USAGE);
|
|
6436
|
+
}
|
|
6437
|
+
if (reason === "") {
|
|
6438
|
+
console.error("spec deprecate: --reason is required \u2014 the reason is the durable record of why this requirement ended");
|
|
6439
|
+
process.exit(EXIT.USAGE);
|
|
6440
|
+
}
|
|
6441
|
+
try {
|
|
6442
|
+
assertSpecPlatform(platformDir);
|
|
6443
|
+
} catch (e) {
|
|
6444
|
+
handleNotAPlatform(e);
|
|
6445
|
+
}
|
|
6446
|
+
const { domain: domain2, req, specPath, relFile } = await resolveDeprecateTarget(platformDir, id);
|
|
6447
|
+
req.status = "deprecated";
|
|
6448
|
+
req.deprecatedReason = reason;
|
|
6449
|
+
domain2.updated = localToday();
|
|
6450
|
+
const res = await validateAndWrite(specPath, domain2, relFile);
|
|
6451
|
+
if (!res.ok) {
|
|
6452
|
+
for (const diag of res.diagnostics)
|
|
6453
|
+
console.error(`spec deprecate: ${diag.detail}`);
|
|
6454
|
+
process.exit(EXIT.USAGE);
|
|
6455
|
+
}
|
|
6456
|
+
const tags = await reindexAndListTags(platformDir, id);
|
|
6457
|
+
const sites = tags.map(({ req_id, repo, file, line, kind, level }) => ({
|
|
6458
|
+
req_id,
|
|
6459
|
+
repo,
|
|
6460
|
+
file,
|
|
6461
|
+
line,
|
|
6462
|
+
kind,
|
|
6463
|
+
level: level ?? null
|
|
6464
|
+
}));
|
|
6465
|
+
if (args.json) {
|
|
6466
|
+
console.log(JSON.stringify({ id, file: relFile, reason, sites }));
|
|
6467
|
+
return;
|
|
6468
|
+
}
|
|
6469
|
+
console.log(`deprecated ${id} in ${relFile} \u2014 reason recorded`);
|
|
6470
|
+
if (sites.length > 0) {
|
|
6471
|
+
console.error(`${sites.length} code tag(s) still bind ${id} \u2014 remove or retag them (spec check reports each as DEPRECATED_REFERENCED):`);
|
|
6472
|
+
console.error(renderReqTags(sites, "text"));
|
|
6473
|
+
}
|
|
6474
|
+
}
|
|
6475
|
+
});
|
|
6476
|
+
});
|
|
6477
|
+
|
|
6001
6478
|
// src/commands/move.ts
|
|
6002
6479
|
var exports_move = {};
|
|
6003
6480
|
__export(exports_move, {
|
|
6004
6481
|
moveCommand: () => moveCommand
|
|
6005
6482
|
});
|
|
6006
|
-
import { existsSync as
|
|
6007
|
-
import { join as
|
|
6008
|
-
import { defineCommand as
|
|
6483
|
+
import { existsSync as existsSync14 } from "fs";
|
|
6484
|
+
import { join as join19, resolve as resolve20 } from "path";
|
|
6485
|
+
import { defineCommand as defineCommand15 } from "citty";
|
|
6009
6486
|
async function loadDomainSide(platformDir, key) {
|
|
6010
6487
|
const relFile = `spec-engine/${key}/SPEC.json`;
|
|
6011
|
-
const specPath =
|
|
6012
|
-
if (!
|
|
6488
|
+
const specPath = join19(platformDir, "spec-engine", key, "SPEC.json");
|
|
6489
|
+
if (!existsSync14(specPath))
|
|
6013
6490
|
return null;
|
|
6014
6491
|
const domain2 = JSON.parse(await Bun.file(specPath).text());
|
|
6015
6492
|
const requirements = Array.isArray(domain2.requirements) ? domain2.requirements : [];
|
|
@@ -6068,6 +6545,15 @@ function resolveSuccessorFields2(args, req) {
|
|
|
6068
6545
|
lives: (args.lives ?? srcLives).trim()
|
|
6069
6546
|
};
|
|
6070
6547
|
}
|
|
6548
|
+
async function assertMovableStatement(platformDir, id, targetKey, statement, args) {
|
|
6549
|
+
if (statement === "") {
|
|
6550
|
+
console.error(`spec move: ${id} has an empty Requirement \u2014 cannot move a blank statement`);
|
|
6551
|
+
process.exit(EXIT.USAGE);
|
|
6552
|
+
}
|
|
6553
|
+
if (typeof args.text === "string") {
|
|
6554
|
+
await enforceStatementGrammar(platformDir, targetKey, statement, "spec move");
|
|
6555
|
+
}
|
|
6556
|
+
}
|
|
6071
6557
|
function resolveMoveVersion(domain2, key, noBump) {
|
|
6072
6558
|
domain2.updated = localToday();
|
|
6073
6559
|
if (key !== "TERM") {
|
|
@@ -6123,15 +6609,16 @@ var moveCommand;
|
|
|
6123
6609
|
var init_move = __esm(() => {
|
|
6124
6610
|
init_src();
|
|
6125
6611
|
init_domains();
|
|
6612
|
+
init_grammar2();
|
|
6126
6613
|
init_constants();
|
|
6127
6614
|
init_discover();
|
|
6128
6615
|
init_pipeline();
|
|
6129
6616
|
init_domainJson();
|
|
6130
|
-
|
|
6617
|
+
init_grammar3();
|
|
6131
6618
|
init_sqlite();
|
|
6132
6619
|
init__shared();
|
|
6133
6620
|
init_req();
|
|
6134
|
-
moveCommand =
|
|
6621
|
+
moveCommand = defineCommand15({
|
|
6135
6622
|
meta: {
|
|
6136
6623
|
name: "move",
|
|
6137
6624
|
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 +6663,10 @@ var init_move = __esm(() => {
|
|
|
6176
6663
|
async run({ args }) {
|
|
6177
6664
|
const id = args.id;
|
|
6178
6665
|
const rawTargetKey = args.newDomain;
|
|
6179
|
-
const platformDir =
|
|
6666
|
+
const platformDir = resolve20(args.platformDir ?? process.cwd());
|
|
6180
6667
|
const target = await resolveMoveTarget(id, rawTargetKey, platformDir);
|
|
6181
6668
|
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
|
-
}
|
|
6669
|
+
await assertMovableStatement(platformDir, id, target.target.key, fields.statement, args);
|
|
6186
6670
|
warnUnresolvableRefs(platformDir, [fields.statement, fields.why, fields.lives]);
|
|
6187
6671
|
const newId = await nextRequirementId(platformDir, target.target.key);
|
|
6188
6672
|
const { sourceVersion, targetVersion } = applyMoveEdit(target, newId, fields, Boolean(args.noBump));
|
|
@@ -6234,17 +6718,18 @@ var exports_amend = {};
|
|
|
6234
6718
|
__export(exports_amend, {
|
|
6235
6719
|
amendCommand: () => amendCommand
|
|
6236
6720
|
});
|
|
6237
|
-
import { existsSync as
|
|
6238
|
-
import { join as
|
|
6239
|
-
import { defineCommand as
|
|
6721
|
+
import { existsSync as existsSync15 } from "fs";
|
|
6722
|
+
import { join as join20, resolve as resolve21 } from "path";
|
|
6723
|
+
import { defineCommand as defineCommand16 } from "citty";
|
|
6240
6724
|
function validateAmendFields(args) {
|
|
6241
6725
|
const hasText = typeof args.text === "string";
|
|
6726
|
+
const hasIssue = typeof args.issue === "string" && args.issue.trim() !== "";
|
|
6242
6727
|
const hasWhy = typeof args.why === "string";
|
|
6243
6728
|
const hasLives = typeof args.lives === "string";
|
|
6244
6729
|
const hasBinds = typeof args.binds === "string";
|
|
6245
6730
|
const hasTerm = typeof args.term === "string";
|
|
6246
6731
|
const hasAliases = typeof args.aliases === "string";
|
|
6247
|
-
if (!hasText && !hasWhy && !hasLives && !hasTerm && !hasAliases) {
|
|
6732
|
+
if (!hasText && !hasWhy && !hasLives && !hasTerm && !hasAliases && !hasIssue) {
|
|
6248
6733
|
console.error("spec amend: nothing to amend \u2014 pass at least one of --text / --why / --lives / --term / --aliases");
|
|
6249
6734
|
process.exit(EXIT.USAGE);
|
|
6250
6735
|
}
|
|
@@ -6256,13 +6741,13 @@ function validateAmendFields(args) {
|
|
|
6256
6741
|
console.error("spec amend: --term must be a non-empty headword");
|
|
6257
6742
|
process.exit(EXIT.USAGE);
|
|
6258
6743
|
}
|
|
6259
|
-
return { hasText, hasWhy, hasLives, hasBinds, hasTerm, hasAliases };
|
|
6744
|
+
return { hasText, hasWhy, hasLives, hasBinds, hasTerm, hasAliases, hasIssue };
|
|
6260
6745
|
}
|
|
6261
6746
|
async function locateAmendEntry(platformDir, id) {
|
|
6262
6747
|
const key = id.slice(0, id.indexOf("-"));
|
|
6263
6748
|
const relFile = `spec-engine/${key}/SPEC.json`;
|
|
6264
|
-
const specPath =
|
|
6265
|
-
if (!
|
|
6749
|
+
const specPath = join20(platformDir, "spec-engine", key, "SPEC.json");
|
|
6750
|
+
if (!existsSync15(specPath)) {
|
|
6266
6751
|
console.error(`spec amend: no domain ${key} (expected ${relFile} under ${platformDir})`);
|
|
6267
6752
|
process.exit(EXIT.USAGE);
|
|
6268
6753
|
}
|
|
@@ -6324,6 +6809,12 @@ function applyAmendMutations(req, args, fields) {
|
|
|
6324
6809
|
fieldsChanged.push("term");
|
|
6325
6810
|
refValues.push(v);
|
|
6326
6811
|
}
|
|
6812
|
+
if (fields.hasIssue) {
|
|
6813
|
+
const issues = Array.isArray(req.issues) ? req.issues : [];
|
|
6814
|
+
issues.push({ role: "amends-via", id: args.issue.trim() });
|
|
6815
|
+
req.issues = issues;
|
|
6816
|
+
fieldsChanged.push("issue");
|
|
6817
|
+
}
|
|
6327
6818
|
if (fields.hasAliases) {
|
|
6328
6819
|
const raw = args.aliases.trim();
|
|
6329
6820
|
req.aliases = raw === "" ? [] : raw.split(",").map((s) => s.trim()).filter((s) => s !== "");
|
|
@@ -6334,12 +6825,13 @@ function applyAmendMutations(req, args, fields) {
|
|
|
6334
6825
|
var amendCommand;
|
|
6335
6826
|
var init_amend = __esm(() => {
|
|
6336
6827
|
init_src();
|
|
6828
|
+
init_grammar2();
|
|
6337
6829
|
init_constants();
|
|
6338
6830
|
init_discover();
|
|
6339
|
-
|
|
6831
|
+
init_grammar3();
|
|
6340
6832
|
init__shared();
|
|
6341
6833
|
init_req();
|
|
6342
|
-
amendCommand =
|
|
6834
|
+
amendCommand = defineCommand16({
|
|
6343
6835
|
meta: {
|
|
6344
6836
|
name: "amend",
|
|
6345
6837
|
description: "Revise an unshipped requirement's fields in place (same id, no specVersion bump). Supersede shipped truth instead."
|
|
@@ -6362,6 +6854,10 @@ var init_amend = __esm(() => {
|
|
|
6362
6854
|
description: "Binds value (validated for @-refs; not persisted in JSON \u2014 STOR-01)"
|
|
6363
6855
|
},
|
|
6364
6856
|
lives: { type: "string", description: "New Lives in (livesIn) field value" },
|
|
6857
|
+
issue: {
|
|
6858
|
+
type: "string",
|
|
6859
|
+
description: "Ticket that motivated this amendment \u2014 recorded as amends-via provenance (opaque, never a requirement id)"
|
|
6860
|
+
},
|
|
6365
6861
|
term: { type: "string", description: "New TERM headword (term field; TERM ids)" },
|
|
6366
6862
|
aliases: {
|
|
6367
6863
|
type: "string",
|
|
@@ -6379,7 +6875,7 @@ var init_amend = __esm(() => {
|
|
|
6379
6875
|
process.exit(EXIT.USAGE);
|
|
6380
6876
|
return;
|
|
6381
6877
|
}
|
|
6382
|
-
const platformDir =
|
|
6878
|
+
const platformDir = resolve21(args.platformDir ?? process.cwd());
|
|
6383
6879
|
try {
|
|
6384
6880
|
assertSpecPlatform(platformDir);
|
|
6385
6881
|
} catch (e) {
|
|
@@ -6392,6 +6888,10 @@ var init_amend = __esm(() => {
|
|
|
6392
6888
|
if (statusLc === "active") {
|
|
6393
6889
|
await assertUnshipped(platformDir, id);
|
|
6394
6890
|
}
|
|
6891
|
+
if (fields.hasText) {
|
|
6892
|
+
const key = id.slice(0, id.indexOf("-"));
|
|
6893
|
+
await enforceStatementGrammar(platformDir, key, args.text.trim(), "spec amend");
|
|
6894
|
+
}
|
|
6395
6895
|
const { fieldsChanged, refValues } = applyAmendMutations(req, args, fields);
|
|
6396
6896
|
warnUnresolvableRefs(platformDir, refValues);
|
|
6397
6897
|
domain2.updated = localToday();
|
|
@@ -6428,13 +6928,14 @@ Target domain: ${opts.domain} (no charter/scope recorded \u2014 check placement
|
|
|
6428
6928
|
opts.brief,
|
|
6429
6929
|
"",
|
|
6430
6930
|
"Steps:",
|
|
6431
|
-
"1. One requirement per
|
|
6432
|
-
"2. Placement:
|
|
6433
|
-
|
|
6434
|
-
"4.
|
|
6435
|
-
"5.
|
|
6436
|
-
|
|
6437
|
-
|
|
6931
|
+
"1. One requirement per behavior \u2014 never one per ticket; a brief fans out into several.",
|
|
6932
|
+
"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.",
|
|
6933
|
+
'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."',
|
|
6934
|
+
"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.",
|
|
6935
|
+
"5. Tie each draft to its source: quote the exact phrase of the brief it derives from; label anything you inferred as inferred.",
|
|
6936
|
+
"6. Present the full batch (statement, why, placement, source quote) for approval BEFORE writing.",
|
|
6937
|
+
'7. Mint each approved requirement: `spec req <domain> --text "\u2026" --why "\u2026" --lives "\u2026"`.',
|
|
6938
|
+
"8. The originating ticket is provenance only \u2014 never a requirement id, never a code tag."
|
|
6438
6939
|
].join(`
|
|
6439
6940
|
`);
|
|
6440
6941
|
}
|
|
@@ -6571,7 +7072,7 @@ var init_mcp = __esm(() => {
|
|
|
6571
7072
|
init_domains();
|
|
6572
7073
|
init_constants();
|
|
6573
7074
|
init_pipeline();
|
|
6574
|
-
|
|
7075
|
+
init_grammar3();
|
|
6575
7076
|
init_sqlite();
|
|
6576
7077
|
});
|
|
6577
7078
|
|
|
@@ -6580,15 +7081,15 @@ var exports_mcp = {};
|
|
|
6580
7081
|
__export(exports_mcp, {
|
|
6581
7082
|
mcpCommand: () => mcpCommand
|
|
6582
7083
|
});
|
|
6583
|
-
import { resolve as
|
|
7084
|
+
import { resolve as resolve22 } from "path";
|
|
6584
7085
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6585
|
-
import { defineCommand as
|
|
7086
|
+
import { defineCommand as defineCommand17 } from "citty";
|
|
6586
7087
|
var mcpCommand;
|
|
6587
7088
|
var init_mcp2 = __esm(() => {
|
|
6588
7089
|
init_discover();
|
|
6589
7090
|
init_mcp();
|
|
6590
7091
|
init__shared();
|
|
6591
|
-
mcpCommand =
|
|
7092
|
+
mcpCommand = defineCommand17({
|
|
6592
7093
|
meta: {
|
|
6593
7094
|
name: "mcp",
|
|
6594
7095
|
description: "Serve Spec Engine as an MCP (Model Context Protocol) server over stdio \u2014 agent-native query/resolve/check/report tools."
|
|
@@ -6601,7 +7102,7 @@ var init_mcp2 = __esm(() => {
|
|
|
6601
7102
|
}
|
|
6602
7103
|
},
|
|
6603
7104
|
async run({ args }) {
|
|
6604
|
-
const platformDir =
|
|
7105
|
+
const platformDir = resolve22(args.platformDir ?? process.cwd());
|
|
6605
7106
|
try {
|
|
6606
7107
|
assertSpecPlatform(platformDir);
|
|
6607
7108
|
} catch (e) {
|
|
@@ -6619,9 +7120,9 @@ var exports_resolve = {};
|
|
|
6619
7120
|
__export(exports_resolve, {
|
|
6620
7121
|
resolveCommand: () => resolveCommand
|
|
6621
7122
|
});
|
|
6622
|
-
import { statSync as
|
|
6623
|
-
import { join as
|
|
6624
|
-
import { defineCommand as
|
|
7123
|
+
import { statSync as statSync6 } from "fs";
|
|
7124
|
+
import { join as join21, relative, resolve as resolve23, sep as sep3 } from "path";
|
|
7125
|
+
import { defineCommand as defineCommand18 } from "citty";
|
|
6625
7126
|
function classifyArg(a) {
|
|
6626
7127
|
if (a.startsWith("--")) {
|
|
6627
7128
|
const eq = a.indexOf("=");
|
|
@@ -6656,10 +7157,10 @@ function splitFilesAndPlatformDir(positionals, fallbackPlatformDir) {
|
|
|
6656
7157
|
let lastIsPlatformDir = false;
|
|
6657
7158
|
let lastIsDirWithoutSpecEngine = false;
|
|
6658
7159
|
try {
|
|
6659
|
-
const resolved =
|
|
6660
|
-
if (
|
|
7160
|
+
const resolved = resolve23(last);
|
|
7161
|
+
if (statSync6(resolved).isDirectory()) {
|
|
6661
7162
|
try {
|
|
6662
|
-
if (
|
|
7163
|
+
if (statSync6(join21(resolved, "spec-engine")).isDirectory()) {
|
|
6663
7164
|
lastIsPlatformDir = true;
|
|
6664
7165
|
} else {
|
|
6665
7166
|
lastIsDirWithoutSpecEngine = true;
|
|
@@ -6753,7 +7254,7 @@ async function dispatchReverseQuery(reqArg, resolved, args) {
|
|
|
6753
7254
|
}
|
|
6754
7255
|
await runReverseQuery({
|
|
6755
7256
|
reqId: reqArg,
|
|
6756
|
-
platformDir:
|
|
7257
|
+
platformDir: resolve23(lone ?? process.cwd()),
|
|
6757
7258
|
outArg: args.out,
|
|
6758
7259
|
json: Boolean(args.json),
|
|
6759
7260
|
fresh: Boolean(args.fresh),
|
|
@@ -6763,7 +7264,7 @@ async function dispatchReverseQuery(reqArg, resolved, args) {
|
|
|
6763
7264
|
function normalizeFileInputs(files, platformDir) {
|
|
6764
7265
|
const normalized = [];
|
|
6765
7266
|
for (const input of files) {
|
|
6766
|
-
const rel = relative(platformDir,
|
|
7267
|
+
const rel = relative(platformDir, resolve23(platformDir, input));
|
|
6767
7268
|
const segments = rel.split(sep3);
|
|
6768
7269
|
if (rel === "" || rel === "." || segments[0] === "..") {
|
|
6769
7270
|
console.error(`spec resolve: file path must be inside platformDir: ${input}`);
|
|
@@ -6781,7 +7282,7 @@ var init_resolve2 = __esm(() => {
|
|
|
6781
7282
|
init_prompt();
|
|
6782
7283
|
init__shared();
|
|
6783
7284
|
VALUE_FLAGS = new Set(["--out", "-o", "--platformDir", "--req"]);
|
|
6784
|
-
resolveCommand =
|
|
7285
|
+
resolveCommand = defineCommand18({
|
|
6785
7286
|
meta: {
|
|
6786
7287
|
name: "resolve",
|
|
6787
7288
|
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 +7337,7 @@ var init_resolve2 = __esm(() => {
|
|
|
6836
7337
|
process.exit(EXIT.USAGE);
|
|
6837
7338
|
return;
|
|
6838
7339
|
}
|
|
6839
|
-
const platformDir =
|
|
7340
|
+
const platformDir = resolve23(resolved.platformDir ?? process.cwd());
|
|
6840
7341
|
const outArg = args.out;
|
|
6841
7342
|
const dbPath = resolveDbPath(platformDir, outArg);
|
|
6842
7343
|
if (outArg)
|
|
@@ -6905,6 +7406,18 @@ function classifyGate(input) {
|
|
|
6905
7406
|
pinned_spec_version: pin
|
|
6906
7407
|
};
|
|
6907
7408
|
}
|
|
7409
|
+
if (req.status === "Deprecated") {
|
|
7410
|
+
return {
|
|
7411
|
+
pass: false,
|
|
7412
|
+
reason: "DEPRECATED",
|
|
7413
|
+
repo: requestedRepoName,
|
|
7414
|
+
req_id: requestedReqId,
|
|
7415
|
+
detail: `Requirement ${requestedReqId} is Deprecated \u2014 it is end-of-life with no successor; do not build against it`,
|
|
7416
|
+
status: req.status,
|
|
7417
|
+
changed_at_version: req.changed_at_version,
|
|
7418
|
+
pinned_spec_version: pin
|
|
7419
|
+
};
|
|
7420
|
+
}
|
|
6908
7421
|
if (req.changed_at_version > pin) {
|
|
6909
7422
|
return {
|
|
6910
7423
|
pass: false,
|
|
@@ -6942,9 +7455,9 @@ var exports_gate = {};
|
|
|
6942
7455
|
__export(exports_gate, {
|
|
6943
7456
|
gateCommand: () => gateCommand
|
|
6944
7457
|
});
|
|
6945
|
-
import { existsSync as
|
|
6946
|
-
import { dirname as dirname10, join as
|
|
6947
|
-
import { defineCommand as
|
|
7458
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync7 } from "fs";
|
|
7459
|
+
import { dirname as dirname10, join as join22, resolve as resolve24 } from "path";
|
|
7460
|
+
import { defineCommand as defineCommand19 } from "citty";
|
|
6948
7461
|
function isEpipe(e) {
|
|
6949
7462
|
if (e === null || typeof e !== "object")
|
|
6950
7463
|
return false;
|
|
@@ -6958,8 +7471,8 @@ function resolveGateArgs(args) {
|
|
|
6958
7471
|
console.error("spec gate: <repo> and <reqId> are required (e.g., spec gate api BILLING-009)");
|
|
6959
7472
|
process.exit(EXIT.USAGE);
|
|
6960
7473
|
}
|
|
6961
|
-
const platformDir =
|
|
6962
|
-
const hasSpecEngineDir =
|
|
7474
|
+
const platformDir = resolve24(args.platformDir ?? process.cwd());
|
|
7475
|
+
const hasSpecEngineDir = existsSync16(join22(platformDir, "spec-engine"));
|
|
6963
7476
|
if (!hasSpecEngineDir) {
|
|
6964
7477
|
console.error(formatNotASpecPlatform(platformDir));
|
|
6965
7478
|
process.exit(EXIT.USAGE);
|
|
@@ -6998,7 +7511,7 @@ var init_gate = __esm(() => {
|
|
|
6998
7511
|
init_prompt();
|
|
6999
7512
|
init_sqlite();
|
|
7000
7513
|
init__shared();
|
|
7001
|
-
gateCommand =
|
|
7514
|
+
gateCommand = defineCommand19({
|
|
7002
7515
|
meta: {
|
|
7003
7516
|
name: "gate",
|
|
7004
7517
|
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 +7597,8 @@ var init_gate = __esm(() => {
|
|
|
7084
7597
|
});
|
|
7085
7598
|
});
|
|
7086
7599
|
|
|
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
7600
|
// src/guard/collect.ts
|
|
7105
|
-
import {
|
|
7106
|
-
import { join as join19 } from "path";
|
|
7601
|
+
import { join as join23 } from "path";
|
|
7107
7602
|
function isIgnored(path) {
|
|
7108
7603
|
return isPathIgnored(path);
|
|
7109
7604
|
}
|
|
@@ -7160,7 +7655,7 @@ async function collectWorktreeReqs(canonicalDir) {
|
|
|
7160
7655
|
const activeIds = new Set;
|
|
7161
7656
|
const supersedesTargets = new Set;
|
|
7162
7657
|
for (const rel of await findDomainJsonFiles(canonicalDir)) {
|
|
7163
|
-
const text = await Bun.file(
|
|
7658
|
+
const text = await Bun.file(join23(canonicalDir, rel)).text();
|
|
7164
7659
|
for (const r of parseReqs(text)) {
|
|
7165
7660
|
ids.add(r.id);
|
|
7166
7661
|
if (r.status.toLowerCase() === "active")
|
|
@@ -7181,20 +7676,6 @@ function tallyWorktreeTags(storage2) {
|
|
|
7181
7676
|
}
|
|
7182
7677
|
return { implCount, verifyCount };
|
|
7183
7678
|
}
|
|
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
7679
|
async function collectFacts(platformDir, ref, storage2) {
|
|
7199
7680
|
const changed = gitDiffNameStatus(platformDir, ref);
|
|
7200
7681
|
const specPaths = changed.filter((e) => isSpecFile(e.path)).map((e) => e.path);
|
|
@@ -7202,9 +7683,8 @@ async function collectFacts(platformDir, ref, storage2) {
|
|
|
7202
7683
|
const deletedSpecFiles = changed.filter((e) => e.status === "D" && isSpecFile(e.path)).map((e) => e.path);
|
|
7203
7684
|
const base = collectBaseReqs2(platformDir, ref, specPaths);
|
|
7204
7685
|
const baseTags = collectBaseTags(platformDir, ref, codePaths);
|
|
7205
|
-
const worktree = await collectWorktreeReqs(
|
|
7686
|
+
const worktree = await collectWorktreeReqs(join23(platformDir, CANONICAL_SPECS_DIR));
|
|
7206
7687
|
const counts = tallyWorktreeTags(storage2);
|
|
7207
|
-
const approved = await collectApprovals(platformDir, changed);
|
|
7208
7688
|
return {
|
|
7209
7689
|
baseReqs: base.reqs,
|
|
7210
7690
|
baseReqPath: base.path,
|
|
@@ -7215,7 +7695,6 @@ async function collectFacts(platformDir, ref, storage2) {
|
|
|
7215
7695
|
worktreeSupersedesTargets: worktree.supersedesTargets,
|
|
7216
7696
|
worktreeImplCount: counts.implCount,
|
|
7217
7697
|
worktreeVerifyCount: counts.verifyCount,
|
|
7218
|
-
approved,
|
|
7219
7698
|
deletedSpecFiles
|
|
7220
7699
|
};
|
|
7221
7700
|
}
|
|
@@ -7225,7 +7704,6 @@ var init_collect = __esm(() => {
|
|
|
7225
7704
|
init_constants();
|
|
7226
7705
|
init_fs();
|
|
7227
7706
|
init_tags();
|
|
7228
|
-
init_directives();
|
|
7229
7707
|
CODE_EXTS = new Set(DEFAULT_EXTS);
|
|
7230
7708
|
});
|
|
7231
7709
|
|
|
@@ -7263,7 +7741,7 @@ function deletesClause(losses) {
|
|
|
7263
7741
|
return joinClauses(parts);
|
|
7264
7742
|
}
|
|
7265
7743
|
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, ` +
|
|
7744
|
+
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
7745
|
}
|
|
7268
7746
|
function fileBlock(loss) {
|
|
7269
7747
|
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 +7792,17 @@ function reqDeletedLoss(base, f) {
|
|
|
7314
7792
|
req_id: base.id,
|
|
7315
7793
|
file: f.baseReqPath.get(base.id) ?? "",
|
|
7316
7794
|
line: 0,
|
|
7317
|
-
detail: `${base.id} was Active at the ref but is absent from the working-tree spec
|
|
7795
|
+
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`
|
|
7796
|
+
};
|
|
7797
|
+
}
|
|
7798
|
+
function historyRemovedLoss(base, f) {
|
|
7799
|
+
const display = base.status.charAt(0).toUpperCase() + base.status.slice(1).toLowerCase();
|
|
7800
|
+
return {
|
|
7801
|
+
kind: "REQUIREMENT_REMOVED",
|
|
7802
|
+
req_id: base.id,
|
|
7803
|
+
file: f.baseReqPath.get(base.id) ?? "",
|
|
7804
|
+
line: 0,
|
|
7805
|
+
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
7806
|
};
|
|
7319
7807
|
}
|
|
7320
7808
|
function implLostLoss(id, site) {
|
|
@@ -7345,18 +7833,16 @@ function specFileDeletedLoss(path) {
|
|
|
7345
7833
|
};
|
|
7346
7834
|
}
|
|
7347
7835
|
function reqDeletedFor(base, f) {
|
|
7348
|
-
if (!isActive(base.status))
|
|
7349
|
-
return null;
|
|
7350
|
-
if (f.approved.has(base.id))
|
|
7351
|
-
return null;
|
|
7352
7836
|
if (f.worktreeReqIds.has(base.id))
|
|
7353
7837
|
return null;
|
|
7838
|
+
if (!isActive(base.status))
|
|
7839
|
+
return historyRemovedLoss(base, f);
|
|
7354
7840
|
if (isSuperseded(base, f))
|
|
7355
7841
|
return null;
|
|
7356
7842
|
return reqDeletedLoss(base, f);
|
|
7357
7843
|
}
|
|
7358
7844
|
function lastTagGone(id, count, f) {
|
|
7359
|
-
return
|
|
7845
|
+
return f.worktreeActiveIds.has(id) && count === 0;
|
|
7360
7846
|
}
|
|
7361
7847
|
function classifyLosses(f) {
|
|
7362
7848
|
const out = [];
|
|
@@ -7383,14 +7869,14 @@ var exports_guard = {};
|
|
|
7383
7869
|
__export(exports_guard, {
|
|
7384
7870
|
guardCommand: () => guardCommand
|
|
7385
7871
|
});
|
|
7386
|
-
import { resolve as
|
|
7387
|
-
import { defineCommand as
|
|
7872
|
+
import { resolve as resolve25 } from "path";
|
|
7873
|
+
import { defineCommand as defineCommand20 } from "citty";
|
|
7388
7874
|
var guardCommand;
|
|
7389
7875
|
var init_guard = __esm(() => {
|
|
7390
7876
|
init_constants();
|
|
7391
7877
|
init_collect();
|
|
7392
7878
|
init__shared();
|
|
7393
|
-
guardCommand =
|
|
7879
|
+
guardCommand = defineCommand20({
|
|
7394
7880
|
meta: {
|
|
7395
7881
|
name: "guard",
|
|
7396
7882
|
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 +7897,7 @@ var init_guard = __esm(() => {
|
|
|
7411
7897
|
}
|
|
7412
7898
|
},
|
|
7413
7899
|
async run({ args }) {
|
|
7414
|
-
const platformDir =
|
|
7900
|
+
const platformDir = resolve25(args.platformDir ?? process.cwd());
|
|
7415
7901
|
const ref = args.against ?? "HEAD";
|
|
7416
7902
|
const jsonMode = Boolean(args.json);
|
|
7417
7903
|
if (!gitRefResolves(platformDir, ref)) {
|
|
@@ -7482,6 +7968,9 @@ function link(href, key, label, active) {
|
|
|
7482
7968
|
function comingSoon(label) {
|
|
7483
7969
|
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
7970
|
}
|
|
7971
|
+
function flagged(feature, href, key, label, active) {
|
|
7972
|
+
return featureEnabled(feature) ? link(href, key, label, active) : comingSoon(label);
|
|
7973
|
+
}
|
|
7485
7974
|
function navBar(active = "") {
|
|
7486
7975
|
return html`<nav class="sidebar">
|
|
7487
7976
|
<a href="/" class="sidebar-brand">${clampMark}<span>Spec Engine</span></a>
|
|
@@ -7490,22 +7979,22 @@ function navBar(active = "") {
|
|
|
7490
7979
|
<div class="sidebar-group-label">Registry</div>
|
|
7491
7980
|
${link("/", "coverage", "Coverage", active)}
|
|
7492
7981
|
${link("/requirements", "requirements", "Requirements", active)}
|
|
7493
|
-
${
|
|
7982
|
+
${flagged("glossary", "/glossary", "glossary", "Glossary", active)}
|
|
7494
7983
|
</div>
|
|
7495
7984
|
<div class="sidebar-group">
|
|
7496
7985
|
<div class="sidebar-group-label">Trace</div>
|
|
7497
|
-
${
|
|
7498
|
-
${
|
|
7986
|
+
${flagged("relations", "/relations", "relations", "Relations", active)}
|
|
7987
|
+
${flagged("provenance", "/provenance", "provenance", "Provenance", active)}
|
|
7499
7988
|
</div>
|
|
7500
7989
|
<div class="sidebar-group">
|
|
7501
7990
|
<div class="sidebar-group-label">Workspace</div>
|
|
7502
|
-
${
|
|
7503
|
-
${
|
|
7991
|
+
${flagged("query", "/query", "query", "Query", active)}
|
|
7992
|
+
${flagged("editor", "/editor", "editor", "Editor", active)}
|
|
7504
7993
|
</div>
|
|
7505
7994
|
<div class="sidebar-group">
|
|
7506
7995
|
<div class="sidebar-group-label">System</div>
|
|
7507
7996
|
${link("/setup", "setup", "Setup", active)}
|
|
7508
|
-
${
|
|
7997
|
+
${flagged("logs", "/logs", "logs", "Logs", active)}
|
|
7509
7998
|
</div>
|
|
7510
7999
|
</div>
|
|
7511
8000
|
<div class="sidebar-footer">
|
|
@@ -7516,6 +8005,7 @@ function navBar(active = "") {
|
|
|
7516
8005
|
}
|
|
7517
8006
|
var clampMark;
|
|
7518
8007
|
var init_nav = __esm(() => {
|
|
8008
|
+
init_src();
|
|
7519
8009
|
clampMark = html`<svg width="22" height="22" viewBox="0 0 64 64" fill="none" aria-hidden="true">
|
|
7520
8010
|
<path d="M25 13 H13 V51 H25" stroke="var(--fg)" stroke-width="6" />
|
|
7521
8011
|
<path d="M39 13 H51 V51 H39" stroke="var(--fg)" stroke-width="6" />
|
|
@@ -9053,7 +9543,7 @@ var init_components = __esm(() => {
|
|
|
9053
9543
|
Active: "ACTIVE",
|
|
9054
9544
|
Superseded: "SUPERSEDED",
|
|
9055
9545
|
Draft: "DRAFT",
|
|
9056
|
-
|
|
9546
|
+
Deprecated: "DEPRECATED"
|
|
9057
9547
|
};
|
|
9058
9548
|
});
|
|
9059
9549
|
|
|
@@ -9184,8 +9674,8 @@ function mountCoverage(app) {
|
|
|
9184
9674
|
const reqs = await apiJson(app, "/api/requirements");
|
|
9185
9675
|
const titleOf = new Map(reqs.map((r) => [r.id, r.text]));
|
|
9186
9676
|
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 === "
|
|
9677
|
+
const visible = showAll ? rows : rows.filter((r) => r.req_status !== "Superseded" && r.req_status !== "Deprecated");
|
|
9678
|
+
const hiddenCount = new Set(rows.filter((r) => r.req_status === "Superseded" || r.req_status === "Deprecated").map((r) => r.req_id)).size;
|
|
9189
9679
|
const { repos, domains } = pivot(visible);
|
|
9190
9680
|
const reqCount = domains.reduce((n, d) => n + d.reqs.length, 0);
|
|
9191
9681
|
const reportRows = await apiJson(app, "/api/report");
|
|
@@ -9216,7 +9706,7 @@ function mountCoverage(app) {
|
|
|
9216
9706
|
<span class="legend-sep"></span>
|
|
9217
9707
|
<span class="legend-item">${requirementStatusBadge("Active")}</span>
|
|
9218
9708
|
${showAll ? html4`<span class="legend-item">${requirementStatusBadge("Superseded")}</span>
|
|
9219
|
-
<span class="legend-item">${requirementStatusBadge("
|
|
9709
|
+
<span class="legend-item">${requirementStatusBadge("Deprecated")}</span>` : ""}
|
|
9220
9710
|
<span class="panel-spacer"></span>
|
|
9221
9711
|
<span class="req-controls">
|
|
9222
9712
|
${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 +9881,8 @@ async function renderResult(res) {
|
|
|
9391
9881
|
}
|
|
9392
9882
|
function mountEditor(app) {
|
|
9393
9883
|
app.get("/editor", (c) => {
|
|
9884
|
+
if (!featureEnabled("editor"))
|
|
9885
|
+
return c.html(comingSoonDoc("editor", "Editor"));
|
|
9394
9886
|
const body = html5`
|
|
9395
9887
|
${navBar("editor")}
|
|
9396
9888
|
<div class="eyebrow">/ Editor</div>
|
|
@@ -9445,6 +9937,8 @@ function mountEditor(app) {
|
|
|
9445
9937
|
return c.html(page("Editor", body));
|
|
9446
9938
|
});
|
|
9447
9939
|
app.post("/editor/create", editorBodyLimit, async (c) => {
|
|
9940
|
+
if (!featureEnabled("editor"))
|
|
9941
|
+
return c.html(rejectPage(featureDisabledMessage("editor")), 404);
|
|
9448
9942
|
if (crossOriginRejected(c))
|
|
9449
9943
|
return c.html(rejectPage("cross-origin request rejected"), 403);
|
|
9450
9944
|
const form = await c.req.parseBody();
|
|
@@ -9467,6 +9961,8 @@ function mountEditor(app) {
|
|
|
9467
9961
|
return c.html(await renderResult(res));
|
|
9468
9962
|
});
|
|
9469
9963
|
app.post("/editor/amend", editorBodyLimit, async (c) => {
|
|
9964
|
+
if (!featureEnabled("editor"))
|
|
9965
|
+
return c.html(rejectPage(featureDisabledMessage("editor")), 404);
|
|
9470
9966
|
if (crossOriginRejected(c))
|
|
9471
9967
|
return c.html(rejectPage("cross-origin request rejected"), 403);
|
|
9472
9968
|
const form = await c.req.parseBody();
|
|
@@ -9489,6 +9985,7 @@ function mountEditor(app) {
|
|
|
9489
9985
|
var styleTag4, MAX_EDITOR_BODY_BYTES, editorBodyLimit;
|
|
9490
9986
|
var init_editor = __esm(() => {
|
|
9491
9987
|
init_src();
|
|
9988
|
+
init_components();
|
|
9492
9989
|
init_nav();
|
|
9493
9990
|
init_styles();
|
|
9494
9991
|
styleTag4 = raw4(`<style>${styles_default}</style>`);
|
|
@@ -9571,7 +10068,7 @@ var init_propagation3 = __esm(() => {
|
|
|
9571
10068
|
import { html as html7, raw as raw6 } from "hono/html";
|
|
9572
10069
|
function mountProvenance(app) {
|
|
9573
10070
|
app.get("/provenance", async (c) => {
|
|
9574
|
-
if (!
|
|
10071
|
+
if (!featureEnabled("provenance"))
|
|
9575
10072
|
return c.html(comingSoonDoc("provenance", "Provenance matrix"));
|
|
9576
10073
|
const text = await apiText(app, "/api/provenance?resolve=1");
|
|
9577
10074
|
let body;
|
|
@@ -9604,8 +10101,9 @@ function mountProvenance(app) {
|
|
|
9604
10101
|
</html>`);
|
|
9605
10102
|
});
|
|
9606
10103
|
}
|
|
9607
|
-
var styleTag6
|
|
10104
|
+
var styleTag6;
|
|
9608
10105
|
var init_provenance2 = __esm(() => {
|
|
10106
|
+
init_src();
|
|
9609
10107
|
init_components();
|
|
9610
10108
|
init_data();
|
|
9611
10109
|
init_nav();
|
|
@@ -9617,7 +10115,7 @@ var init_provenance2 = __esm(() => {
|
|
|
9617
10115
|
import { html as html8, raw as raw7 } from "hono/html";
|
|
9618
10116
|
function mountQuery(app) {
|
|
9619
10117
|
app.get("/query", async (c) => {
|
|
9620
|
-
if (!
|
|
10118
|
+
if (!featureEnabled("query"))
|
|
9621
10119
|
return c.html(comingSoonDoc("query", "Query"));
|
|
9622
10120
|
const q = (c.req.query("q") ?? "").trim();
|
|
9623
10121
|
const form = html8`
|
|
@@ -9703,8 +10201,9 @@ function mountQuery(app) {
|
|
|
9703
10201
|
</html>`);
|
|
9704
10202
|
});
|
|
9705
10203
|
}
|
|
9706
|
-
var styleTag7
|
|
10204
|
+
var styleTag7;
|
|
9707
10205
|
var init_query2 = __esm(() => {
|
|
10206
|
+
init_src();
|
|
9708
10207
|
init_components();
|
|
9709
10208
|
init_data();
|
|
9710
10209
|
init_nav();
|
|
@@ -9716,7 +10215,7 @@ var init_query2 = __esm(() => {
|
|
|
9716
10215
|
import { html as html9, raw as raw8 } from "hono/html";
|
|
9717
10216
|
function mountRelations(app) {
|
|
9718
10217
|
app.get("/relations", async (c) => {
|
|
9719
|
-
if (!
|
|
10218
|
+
if (!featureEnabled("relations"))
|
|
9720
10219
|
return c.html(comingSoonDoc("relations", "Relates graph"));
|
|
9721
10220
|
const rows = await apiJson(app, "/api/relations");
|
|
9722
10221
|
let body;
|
|
@@ -9754,8 +10253,9 @@ function mountRelations(app) {
|
|
|
9754
10253
|
</html>`);
|
|
9755
10254
|
});
|
|
9756
10255
|
}
|
|
9757
|
-
var styleTag8
|
|
10256
|
+
var styleTag8;
|
|
9758
10257
|
var init_relations2 = __esm(() => {
|
|
10258
|
+
init_src();
|
|
9759
10259
|
init_components();
|
|
9760
10260
|
init_data();
|
|
9761
10261
|
init_nav();
|
|
@@ -9829,7 +10329,7 @@ function verPill(r, versioned) {
|
|
|
9829
10329
|
return html10`<span class="ver-pill ${versioned ? "versioned" : ""}" title="domain spec version (envelope)">v${r.spec_version}</span>`;
|
|
9830
10330
|
}
|
|
9831
10331
|
function chainVerLabel(v) {
|
|
9832
|
-
const dead = v.status === "Superseded" || v.status === "
|
|
10332
|
+
const dead = v.status === "Superseded" || v.status === "Deprecated";
|
|
9833
10333
|
if (dead && v.superseded_at_version != null) {
|
|
9834
10334
|
return html10`<span class="chain-ver">superseded at v${v.superseded_at_version}</span>`;
|
|
9835
10335
|
}
|
|
@@ -9972,8 +10472,8 @@ function mountRequirements(app) {
|
|
|
9972
10472
|
const reqs = await apiJson(app, "/api/requirements");
|
|
9973
10473
|
const coverage = await apiJson(app, "/api/coverage");
|
|
9974
10474
|
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 === "
|
|
10475
|
+
const listed = showAll ? reqs : reqs.filter((r) => r.status !== "Superseded" && r.status !== "Deprecated");
|
|
10476
|
+
const hiddenCount = reqs.filter((r) => r.status === "Superseded" || r.status === "Deprecated").length;
|
|
9977
10477
|
const tagLists = await Promise.all(listed.map((r) => apiJson(app, `/api/resolve?req=${encodeURIComponent(r.id)}`)));
|
|
9978
10478
|
const tagsById = new Map(listed.map((r, i) => [r.id, tagLists[i] ?? []]));
|
|
9979
10479
|
const { domains, defaultId } = buildDomains(reqs, listed, coverage, tagsById);
|
|
@@ -10249,7 +10749,7 @@ var init_server = __esm(() => {
|
|
|
10249
10749
|
|
|
10250
10750
|
// src/server/api.ts
|
|
10251
10751
|
import { existsSync as existsSync17 } from "fs";
|
|
10252
|
-
import { join as
|
|
10752
|
+
import { join as join24 } from "path";
|
|
10253
10753
|
function hasTraversalSegment(p) {
|
|
10254
10754
|
return p.split(/[/\\]/).some((seg) => seg === "..");
|
|
10255
10755
|
}
|
|
@@ -10356,7 +10856,7 @@ function resolveCreateTarget(c, platformDir, body) {
|
|
|
10356
10856
|
return { ok: false, res: c.json({ error: "unknown domain key" }, 400) };
|
|
10357
10857
|
}
|
|
10358
10858
|
const relFile = `spec-engine/${key}/SPEC.json`;
|
|
10359
|
-
const specPath =
|
|
10859
|
+
const specPath = join24(platformDir, "spec-engine", key, "SPEC.json");
|
|
10360
10860
|
if (!existsSync17(specPath))
|
|
10361
10861
|
return { ok: false, res: c.json({ error: "not found" }, 404) };
|
|
10362
10862
|
return { ok: true, key, relFile, specPath };
|
|
@@ -10376,7 +10876,7 @@ function buildRequirement(body, id) {
|
|
|
10376
10876
|
}
|
|
10377
10877
|
async function locateRequirement(platformDir, id) {
|
|
10378
10878
|
for (const key of listDomainKeys(platformDir)) {
|
|
10379
|
-
const specPath =
|
|
10879
|
+
const specPath = join24(platformDir, "spec-engine", key, "SPEC.json");
|
|
10380
10880
|
const relFile = `spec-engine/${key}/SPEC.json`;
|
|
10381
10881
|
let domain2;
|
|
10382
10882
|
try {
|
|
@@ -10406,6 +10906,40 @@ function applyAmendFields(req, body) {
|
|
|
10406
10906
|
req.livesIn = toLivesIn(body.livesIn);
|
|
10407
10907
|
return true;
|
|
10408
10908
|
}
|
|
10909
|
+
function hasAmendFields(body) {
|
|
10910
|
+
return typeof body.statement === "string" || "why" in body || "livesIn" in body;
|
|
10911
|
+
}
|
|
10912
|
+
async function amendGateRejection(c, platformDir, storage2, id, req) {
|
|
10913
|
+
const rawStatus = typeof req.status === "string" ? req.status : "";
|
|
10914
|
+
const statusLc = rawStatus.toLowerCase();
|
|
10915
|
+
if (statusLc !== "active" && statusLc !== "draft") {
|
|
10916
|
+
return c.json({
|
|
10917
|
+
error: `${id} is ${rawStatus} \u2014 only Active/Draft entries amend (a superseded entry is history; supersede its successor instead)`
|
|
10918
|
+
}, 409);
|
|
10919
|
+
}
|
|
10920
|
+
if (statusLc === "active") {
|
|
10921
|
+
await runIndex({ platformDir, storage: storage2 });
|
|
10922
|
+
const bound = storage2.listTags({ req_id: id }).filter((t) => t.kind === "implements" || t.kind === "verifies");
|
|
10923
|
+
if (bound.length > 0) {
|
|
10924
|
+
const site = bound[0];
|
|
10925
|
+
return c.json({
|
|
10926
|
+
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.`
|
|
10927
|
+
}, 409);
|
|
10928
|
+
}
|
|
10929
|
+
}
|
|
10930
|
+
return null;
|
|
10931
|
+
}
|
|
10932
|
+
function searchFtsResponse(c, storage2, q, limit) {
|
|
10933
|
+
try {
|
|
10934
|
+
return c.json(storage2.searchFts(q, limit));
|
|
10935
|
+
} catch (e) {
|
|
10936
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
10937
|
+
if (msg.startsWith(FTS_SYNTAX_ERROR_PREFIX)) {
|
|
10938
|
+
return c.json({ error: "FTS5 grammar error; wrap phrases in double quotes" }, 400);
|
|
10939
|
+
}
|
|
10940
|
+
throw e;
|
|
10941
|
+
}
|
|
10942
|
+
}
|
|
10409
10943
|
function parseQueryLimit(c, rawLimit) {
|
|
10410
10944
|
if (!POSITIVE_INT_RE.test(rawLimit)) {
|
|
10411
10945
|
return {
|
|
@@ -10449,6 +10983,9 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
|
|
|
10449
10983
|
return row ? c.json(row) : c.json({ error: "not found" }, 404);
|
|
10450
10984
|
}));
|
|
10451
10985
|
app.post("/api/requirements", guarded(async function createRequirement(c) {
|
|
10986
|
+
if (!featureEnabled("editor")) {
|
|
10987
|
+
return c.json({ error: featureDisabledMessage("editor") }, 404);
|
|
10988
|
+
}
|
|
10452
10989
|
const originReject = rejectCrossOrigin(c);
|
|
10453
10990
|
if (originReject)
|
|
10454
10991
|
return originReject;
|
|
@@ -10483,6 +11020,9 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
|
|
|
10483
11020
|
});
|
|
10484
11021
|
}));
|
|
10485
11022
|
app.put("/api/requirements/:id", guarded(async function amendRequirement(c) {
|
|
11023
|
+
if (!featureEnabled("editor")) {
|
|
11024
|
+
return c.json({ error: featureDisabledMessage("editor") }, 404);
|
|
11025
|
+
}
|
|
10486
11026
|
const originReject = rejectCrossOrigin(c);
|
|
10487
11027
|
if (originReject)
|
|
10488
11028
|
return originReject;
|
|
@@ -10501,9 +11041,13 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
|
|
|
10501
11041
|
if (found.kind === "invalid") {
|
|
10502
11042
|
return c.json({ error: "INVALID_DOMAIN_FILE", detail: `${found.relFile} is not valid JSON` }, 400);
|
|
10503
11043
|
}
|
|
10504
|
-
if (!
|
|
11044
|
+
if (!hasAmendFields(body)) {
|
|
10505
11045
|
return c.json({ error: "nothing to amend \u2014 provide statement, why, or livesIn" }, 400);
|
|
10506
11046
|
}
|
|
11047
|
+
const gateReject = await amendGateRejection(c, platformDir, storage2, id, found.req);
|
|
11048
|
+
if (gateReject)
|
|
11049
|
+
return gateReject;
|
|
11050
|
+
applyAmendFields(found.req, body);
|
|
10507
11051
|
found.domain.updated = localToday();
|
|
10508
11052
|
const res = await validateAndWrite(found.specPath, found.domain, found.relFile);
|
|
10509
11053
|
if (!res.ok) {
|
|
@@ -10515,6 +11059,9 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
|
|
|
10515
11059
|
}));
|
|
10516
11060
|
app.get("/api/propagation/:id", guarded((c) => c.json(storage2.propagationFor(c.req.param("id") ?? ""))));
|
|
10517
11061
|
app.get("/api/query", guarded((c) => {
|
|
11062
|
+
if (!featureEnabled("query")) {
|
|
11063
|
+
return c.json({ error: featureDisabledMessage("query") }, 404);
|
|
11064
|
+
}
|
|
10518
11065
|
const q = (c.req.query("q") ?? "").trim();
|
|
10519
11066
|
if (!q) {
|
|
10520
11067
|
return c.json({ error: "q is required (non-empty FTS5 MATCH query)" }, 400);
|
|
@@ -10523,17 +11070,12 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
|
|
|
10523
11070
|
const parsedLimit = parseQueryLimit(c, rawLimit);
|
|
10524
11071
|
if (!parsedLimit.ok)
|
|
10525
11072
|
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
|
-
}
|
|
11073
|
+
return searchFtsResponse(c, storage2, q, parsedLimit.limit);
|
|
10535
11074
|
}));
|
|
10536
11075
|
app.get("/api/relations", guarded((c) => {
|
|
11076
|
+
if (!featureEnabled("relations")) {
|
|
11077
|
+
return c.json({ error: featureDisabledMessage("relations") }, 404);
|
|
11078
|
+
}
|
|
10537
11079
|
const format = c.req.query("format");
|
|
10538
11080
|
if (format !== undefined && format !== "mermaid") {
|
|
10539
11081
|
return c.json({ error: 'format must be "mermaid" when provided' }, 400);
|
|
@@ -10545,6 +11087,9 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
|
|
|
10545
11087
|
return c.json(sortRelations(rows));
|
|
10546
11088
|
}));
|
|
10547
11089
|
app.get("/api/provenance", guarded(async (c) => {
|
|
11090
|
+
if (!featureEnabled("provenance")) {
|
|
11091
|
+
return c.json({ error: featureDisabledMessage("provenance") }, 404);
|
|
11092
|
+
}
|
|
10548
11093
|
const resolveParam = c.req.query("resolve");
|
|
10549
11094
|
if (resolveParam !== undefined && resolveParam !== "1") {
|
|
10550
11095
|
return c.json({ error: 'resolve must be "1" when provided' }, 400);
|
|
@@ -10556,7 +11101,15 @@ function mountApi(app, storage2, platformDir = process.cwd()) {
|
|
|
10556
11101
|
}
|
|
10557
11102
|
return c.json(rows);
|
|
10558
11103
|
}));
|
|
10559
|
-
app.get("/api/provenance
|
|
11104
|
+
app.get("/api/provenance/by-issue", guarded((c) => {
|
|
11105
|
+
if (!featureEnabled("provenance")) {
|
|
11106
|
+
return c.json({ error: featureDisabledMessage("provenance") }, 404);
|
|
11107
|
+
}
|
|
11108
|
+
const issue = (c.req.query("issue") ?? "").trim();
|
|
11109
|
+
if (issue === "")
|
|
11110
|
+
return c.json({ error: "issue is required (non-empty)" }, 400);
|
|
11111
|
+
return c.json(storage2.provenanceByIssue(issue));
|
|
11112
|
+
}));
|
|
10560
11113
|
app.get("/api/resolve", guarded((c) => {
|
|
10561
11114
|
const reqParam = c.req.query("req");
|
|
10562
11115
|
if (reqParam !== undefined)
|
|
@@ -10597,8 +11150,8 @@ __export(exports_serve, {
|
|
|
10597
11150
|
composeServeApp: () => composeServeApp
|
|
10598
11151
|
});
|
|
10599
11152
|
import { existsSync as existsSync18, mkdirSync as mkdirSync8 } from "fs";
|
|
10600
|
-
import { dirname as dirname11, resolve as
|
|
10601
|
-
import { defineCommand as
|
|
11153
|
+
import { dirname as dirname11, resolve as resolve26 } from "path";
|
|
11154
|
+
import { defineCommand as defineCommand21 } from "citty";
|
|
10602
11155
|
import { Hono as Hono2 } from "hono";
|
|
10603
11156
|
function composeServeApp(storage2, platformDir = process.cwd()) {
|
|
10604
11157
|
const app = new Hono2;
|
|
@@ -10646,7 +11199,7 @@ var init_serve = __esm(() => {
|
|
|
10646
11199
|
init_api();
|
|
10647
11200
|
init_sqlite();
|
|
10648
11201
|
init__shared();
|
|
10649
|
-
serveCommand =
|
|
11202
|
+
serveCommand = defineCommand21({
|
|
10650
11203
|
meta: {
|
|
10651
11204
|
name: "serve",
|
|
10652
11205
|
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 +11245,7 @@ var init_serve = __esm(() => {
|
|
|
10692
11245
|
process.exit(EXIT.USAGE);
|
|
10693
11246
|
return;
|
|
10694
11247
|
}
|
|
10695
|
-
const platformDir =
|
|
11248
|
+
const platformDir = resolve26(args.platformDir ?? process.cwd());
|
|
10696
11249
|
const outArg = args.out;
|
|
10697
11250
|
const dbPath = resolveDbPath(platformDir, outArg);
|
|
10698
11251
|
if (outArg)
|
|
@@ -10743,18 +11296,20 @@ var init_serve = __esm(() => {
|
|
|
10743
11296
|
// src/commands/docs.ts
|
|
10744
11297
|
var exports_docs = {};
|
|
10745
11298
|
__export(exports_docs, {
|
|
11299
|
+
runProbe: () => runProbe2,
|
|
10746
11300
|
resolveDocsRootFrom: () => resolveDocsRootFrom,
|
|
10747
11301
|
resolveDocsRoot: () => resolveDocsRoot,
|
|
11302
|
+
exitNoDocsRoot: () => exitNoDocsRoot,
|
|
10748
11303
|
docsCommand: () => docsCommand,
|
|
10749
11304
|
createDocsFetchHandler: () => createDocsFetchHandler
|
|
10750
11305
|
});
|
|
10751
|
-
import { existsSync as existsSync19, statSync as
|
|
10752
|
-
import { join as
|
|
11306
|
+
import { existsSync as existsSync19, statSync as statSync7 } from "fs";
|
|
11307
|
+
import { join as join25, normalize, sep as sep4 } from "path";
|
|
10753
11308
|
import { fileURLToPath } from "url";
|
|
10754
|
-
import { defineCommand as
|
|
11309
|
+
import { defineCommand as defineCommand22 } from "citty";
|
|
10755
11310
|
function resolveDocsRootFrom(candidates) {
|
|
10756
11311
|
for (const candidate of candidates) {
|
|
10757
|
-
if (existsSync19(
|
|
11312
|
+
if (existsSync19(join25(candidate, "index.html")))
|
|
10758
11313
|
return candidate;
|
|
10759
11314
|
}
|
|
10760
11315
|
return null;
|
|
@@ -10778,16 +11333,16 @@ function createDocsFetchHandler(root) {
|
|
|
10778
11333
|
if (pathname.includes("\x00")) {
|
|
10779
11334
|
return new Response("Bad request", { status: 400 });
|
|
10780
11335
|
}
|
|
10781
|
-
const target = normalize(
|
|
11336
|
+
const target = normalize(join25(normalRoot, pathname));
|
|
10782
11337
|
if (target !== normalRoot && !target.startsWith(rootPrefix)) {
|
|
10783
11338
|
return new Response("Not found", { status: 404 });
|
|
10784
11339
|
}
|
|
10785
|
-
for (const candidate of [target,
|
|
10786
|
-
if (existsSync19(candidate) &&
|
|
11340
|
+
for (const candidate of [target, join25(target, "index.html"), `${target}.html`]) {
|
|
11341
|
+
if (existsSync19(candidate) && statSync7(candidate).isFile()) {
|
|
10787
11342
|
return new Response(Bun.file(candidate));
|
|
10788
11343
|
}
|
|
10789
11344
|
}
|
|
10790
|
-
const notFoundPage =
|
|
11345
|
+
const notFoundPage = join25(normalRoot, "404.html");
|
|
10791
11346
|
if (existsSync19(notFoundPage)) {
|
|
10792
11347
|
return new Response(Bun.file(notFoundPage), { status: 404 });
|
|
10793
11348
|
}
|
|
@@ -10822,10 +11377,14 @@ async function runProbe2(root) {
|
|
|
10822
11377
|
server.stop();
|
|
10823
11378
|
process.exit(EXIT.OK);
|
|
10824
11379
|
}
|
|
11380
|
+
function exitNoDocsRoot() {
|
|
11381
|
+
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.");
|
|
11382
|
+
process.exit(EXIT.USAGE);
|
|
11383
|
+
}
|
|
10825
11384
|
var docsCommand;
|
|
10826
11385
|
var init_docs2 = __esm(() => {
|
|
10827
11386
|
init_constants();
|
|
10828
|
-
docsCommand =
|
|
11387
|
+
docsCommand = defineCommand22({
|
|
10829
11388
|
meta: {
|
|
10830
11389
|
name: "docs",
|
|
10831
11390
|
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 +11403,7 @@ var init_docs2 = __esm(() => {
|
|
|
10844
11403
|
async run({ args }) {
|
|
10845
11404
|
const root = resolveDocsRoot();
|
|
10846
11405
|
if (!root) {
|
|
10847
|
-
|
|
10848
|
-
process.exit(EXIT.USAGE);
|
|
11406
|
+
exitNoDocsRoot();
|
|
10849
11407
|
return;
|
|
10850
11408
|
}
|
|
10851
11409
|
if (args.probe) {
|
|
@@ -10887,10 +11445,10 @@ __export(exports___smoke, {
|
|
|
10887
11445
|
schemaSmokeCommand: () => schemaSmokeCommand,
|
|
10888
11446
|
schemaMismatchSmokeCommand: () => schemaMismatchSmokeCommand
|
|
10889
11447
|
});
|
|
10890
|
-
import { defineCommand as
|
|
11448
|
+
import { defineCommand as defineCommand23 } from "citty";
|
|
10891
11449
|
var schemaSmokeCommand, schemaMismatchSmokeCommand;
|
|
10892
11450
|
var init___smoke = __esm(() => {
|
|
10893
|
-
schemaSmokeCommand =
|
|
11451
|
+
schemaSmokeCommand = defineCommand23({
|
|
10894
11452
|
meta: {
|
|
10895
11453
|
name: "__schema-smoke",
|
|
10896
11454
|
description: "(hidden CI) open a fresh DB, exec DDL, assert the schema shape",
|
|
@@ -10933,7 +11491,7 @@ var init___smoke = __esm(() => {
|
|
|
10933
11491
|
console.log("__schema-smoke OK");
|
|
10934
11492
|
}
|
|
10935
11493
|
});
|
|
10936
|
-
schemaMismatchSmokeCommand =
|
|
11494
|
+
schemaMismatchSmokeCommand = defineCommand23({
|
|
10937
11495
|
meta: {
|
|
10938
11496
|
name: "__schema-mismatch-smoke",
|
|
10939
11497
|
description: "(hidden CI) corrupt _schema_version, re-open, assert silent rebuild",
|
|
@@ -10958,11 +11516,11 @@ var init___smoke = __esm(() => {
|
|
|
10958
11516
|
});
|
|
10959
11517
|
|
|
10960
11518
|
// src/cli.ts
|
|
10961
|
-
import { defineCommand as
|
|
11519
|
+
import { defineCommand as defineCommand24, runMain, showUsage } from "citty";
|
|
10962
11520
|
// package.json
|
|
10963
11521
|
var package_default = {
|
|
10964
11522
|
name: "@spec-engine/spec-engine",
|
|
10965
|
-
version: "0.
|
|
11523
|
+
version: "0.2.0",
|
|
10966
11524
|
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
11525
|
license: "MIT",
|
|
10968
11526
|
type: "module",
|
|
@@ -10972,6 +11530,10 @@ var package_default = {
|
|
|
10972
11530
|
files: [
|
|
10973
11531
|
"dist"
|
|
10974
11532
|
],
|
|
11533
|
+
publishConfig: {
|
|
11534
|
+
access: "public",
|
|
11535
|
+
provenance: true
|
|
11536
|
+
},
|
|
10975
11537
|
engines: {
|
|
10976
11538
|
bun: ">=1.3.0"
|
|
10977
11539
|
},
|
|
@@ -11010,7 +11572,7 @@ var package_default = {
|
|
|
11010
11572
|
};
|
|
11011
11573
|
|
|
11012
11574
|
// src/cli.ts
|
|
11013
|
-
var main =
|
|
11575
|
+
var main = defineCommand24({
|
|
11014
11576
|
meta: {
|
|
11015
11577
|
name: "spec",
|
|
11016
11578
|
version: package_default.version,
|
|
@@ -11030,6 +11592,7 @@ var main = defineCommand23({
|
|
|
11030
11592
|
term: () => Promise.resolve().then(() => (init_term(), exports_term)).then((m) => m.termCommand),
|
|
11031
11593
|
glossary: () => Promise.resolve().then(() => (init_glossary(), exports_glossary)).then((m) => m.glossaryCommand),
|
|
11032
11594
|
supersede: () => Promise.resolve().then(() => (init_supersede(), exports_supersede)).then((m) => m.supersedeCommand),
|
|
11595
|
+
deprecate: () => Promise.resolve().then(() => (init_deprecate(), exports_deprecate)).then((m) => m.deprecateCommand),
|
|
11033
11596
|
move: () => Promise.resolve().then(() => (init_move(), exports_move)).then((m) => m.moveCommand),
|
|
11034
11597
|
amend: () => Promise.resolve().then(() => (init_amend(), exports_amend)).then((m) => m.amendCommand),
|
|
11035
11598
|
mcp: () => Promise.resolve().then(() => (init_mcp2(), exports_mcp)).then((m) => m.mcpCommand),
|