@spec-engine/spec-engine 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -162,7 +162,7 @@
162
162
 
163
163
 
164
164
 
165
- <table><thead><tr><th>Code</th><th>Severity</th><th>Condition</th></tr></thead><tbody><tr><td><code dir="auto">INVALID_DOMAIN_FILE</code></td><td>error</td><td>A <code dir="auto">SPEC.json</code> fails the structural schema (bad JSON, malformed <code dir="auto">key</code>/<code dir="auto">id</code>, missing <code dir="auto">statement</code>, unknown envelope key) — loud, never a silent zero</td></tr><tr><td><code dir="auto">DUP_ID</code></td><td>error</td><td>The same requirement ID appears twice</td></tr><tr><td><code dir="auto">BROKEN_SUPERSEDE</code></td><td>error</td><td><code dir="auto">supersededBy</code> points at a missing ID</td></tr><tr><td><code dir="auto">CYCLIC_SUPERSEDE</code></td><td>error</td><td>A circular supersession chain (<code dir="auto">A → B → A</code>) — the chain is the change history, so a cycle is corrupt history</td></tr><tr><td><code dir="auto">BAD_STATUS</code></td><td>error</td><td>Entry status outside <code dir="auto">active</code> / <code dir="auto">draft</code> / <code dir="auto">superseded</code> / <code dir="auto">deprecated</code></td></tr><tr><td><code dir="auto">DANGLING_TAG</code></td><td>error</td><td>A <code dir="auto">@spec</code> tag references no requirement</td></tr><tr><td><code dir="auto">SUPERSEDED_REFERENCED</code></td><td>error</td><td>Code still tags a superseded requirement (retag to the successor)</td></tr><tr><td><code dir="auto">ORPHAN_REQ</code></td><td>error</td><td>An active requirement has no implementing <code dir="auto">@spec</code> tag</td></tr><tr><td><code dir="auto">UNVERIFIED_REQ</code></td><td>error</td><td>An active requirement is implemented but has no verifying test tag</td></tr><tr><td><code dir="auto">DRIFT</code></td><td>error</td><td>A member is pinned behind the version at which a requirement it references changed</td></tr><tr><td><code dir="auto">NO_SPEC_CONFIG</code></td><td>warning</td><td>A sibling repo-root under the platform has no <code dir="auto">spec-engine.member.json</code></td></tr><tr><td><code dir="auto">BROKEN_FILE_REF</code></td><td>warning</td><td>An <code dir="auto">@&#x3C;path></code> reference in requirement text does not resolve</td></tr><tr><td><code dir="auto">BROKEN_RELATES</code></td><td>warning</td><td>A <code dir="auto">relates</code> entry points at a nonexistent requirement ID</td></tr><tr><td><code dir="auto">RELATES_SUPERSEDED</code></td><td>warning</td><td>A <code dir="auto">relates</code> entry points at a superseded requirement</td></tr><tr><td><code dir="auto">SELF_RELATES</code></td><td>warning</td><td>A <code dir="auto">relates</code> entry names its own requirement (dropped, but surfaced)</td></tr><tr><td><code dir="auto">UNKNOWN_ROLE</code></td><td>warning</td><td>An <code dir="auto">issues</code> entry uses a role outside <code dir="auto">created</code> / <code dir="auto">supersedes-via</code> / <code dir="auto">amends-via</code> — surfaced and dropped, never stored</td></tr></tbody></table>
165
+ <table><thead><tr><th>Code</th><th>Severity</th><th>Condition</th></tr></thead><tbody><tr><td><code dir="auto">INVALID_DOMAIN_FILE</code></td><td>error</td><td>A <code dir="auto">SPEC.json</code> fails the structural schema (bad JSON, malformed <code dir="auto">key</code>/<code dir="auto">id</code>, missing <code dir="auto">statement</code>, unknown envelope key) — loud, never a silent zero</td></tr><tr><td><code dir="auto">DUP_ID</code></td><td>error</td><td>The same requirement ID appears twice</td></tr><tr><td><code dir="auto">BROKEN_SUPERSEDE</code></td><td>error</td><td><code dir="auto">supersededBy</code> points at a missing ID</td></tr><tr><td><code dir="auto">CYCLIC_SUPERSEDE</code></td><td>error</td><td>A circular supersession chain (<code dir="auto">A → B → A</code>) — the chain is the change history, so a cycle is corrupt history</td></tr><tr><td><code dir="auto">BAD_STATUS</code></td><td>error</td><td>Entry status outside <code dir="auto">active</code> / <code dir="auto">draft</code> / <code dir="auto">superseded</code> / <code dir="auto">deprecated</code></td></tr><tr><td><code dir="auto">DANGLING_TAG</code></td><td>error</td><td>A <code dir="auto">@spec</code> tag references no requirement</td></tr><tr><td><code dir="auto">SUPERSEDED_REFERENCED</code></td><td>error</td><td>Code still tags a superseded requirement (retag to the successor)</td></tr><tr><td><code dir="auto">ORPHAN_REQ</code></td><td>error</td><td>An active requirement has no implementing <code dir="auto">@spec</code> tag</td></tr><tr><td><code dir="auto">UNVERIFIED_REQ</code></td><td>error</td><td>An active requirement is implemented but has no verifying test tag</td></tr><tr><td><code dir="auto">DRIFT</code></td><td>error</td><td>A member is pinned behind the version at which a requirement it references changed</td></tr><tr><td><code dir="auto">BROKEN_FILE_REF</code></td><td>error</td><td>An active or draft requirement’s <code dir="auto">livesIn</code> entry does not resolve to a file under the platform root (a terminal-status entry is exempt)</td></tr><tr><td><code dir="auto">NO_SPEC_CONFIG</code></td><td>warning</td><td>A sibling repo-root under the platform has no <code dir="auto">spec-engine.member.json</code></td></tr><tr><td><code dir="auto">BROKEN_RELATES</code></td><td>warning</td><td>A <code dir="auto">relates</code> entry points at a nonexistent requirement ID</td></tr><tr><td><code dir="auto">RELATES_SUPERSEDED</code></td><td>warning</td><td>A <code dir="auto">relates</code> entry points at a superseded requirement</td></tr><tr><td><code dir="auto">SELF_RELATES</code></td><td>warning</td><td>A <code dir="auto">relates</code> entry names its own requirement (dropped, but surfaced)</td></tr><tr><td><code dir="auto">UNKNOWN_ROLE</code></td><td>warning</td><td>An <code dir="auto">issues</code> entry uses a role outside <code dir="auto">created</code> / <code dir="auto">supersedes-via</code> / <code dir="auto">amends-via</code> — surfaced and dropped, never stored</td></tr></tbody></table>
166
166
  <div class="sl-heading-wrapper level-h2"><h2 id="glossary-term-diagnostics">Glossary (TERM) diagnostics</h2><a class="sl-anchor-link" href="#glossary-term-diagnostics"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentcolor" d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “Glossary (TERM) diagnostics”</span></a></div>
167
167
 
168
168
 
@@ -193,7 +193,7 @@
193
193
  <p>One-time hard cutover: convert every legacy canonical <code dir="auto">SPEC.md</code> to a schema-validated sibling <code dir="auto">SPEC.json</code>, then delete the <code dir="auto">SPEC.md</code>. Idempotent — skips already-migrated dirs.</p>
194
194
  <p><strong>Exit codes:</strong> 0 / 2</p>
195
195
  <div class="sl-heading-wrapper level-h3"><h3 id="spec-req-domain-prefix-platformdir"><code dir="auto">spec req &#x3C;domain-prefix> [platformDir]</code></h3><a class="sl-anchor-link" href="#spec-req-domain-prefix-platformdir"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentcolor" d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “spec req &#x3C;domain-prefix> [platformDir]”</span></a></div>
196
- <p><strong>Piped:</strong> prints the next unused requirement ID (e.g. <code dir="auto">BILLING-010</code>) — zero prompts, zero writes. <strong>TTY:</strong> interactive authoring. <strong><code dir="auto">--text</code> (+ <code dir="auto">--why</code> / <code dir="auto">--binds</code> / <code dir="auto">--lives</code>):</strong> appends a born-active entry non-interactively through the validated write seam. Prefix is case-insensitive (<code dir="auto">bil</code> → <code dir="auto">BILLING</code>; ambiguous → exit 2). <code dir="auto">spec req &#x3C;domain></code> alone prints the domain’s charter before authoring.</p>
196
+ <p><strong>Piped:</strong> prints the next unused requirement ID (e.g. <code dir="auto">BILLING-010</code>) — zero prompts, zero writes. <strong>TTY:</strong> interactive authoring. <strong><code dir="auto">--text</code> (+ <code dir="auto">--why</code> / <code dir="auto">--lives</code>):</strong> appends a born-active entry non-interactively through the validated write seam. Prefix is case-insensitive (<code dir="auto">bil</code> → <code dir="auto">BILLING</code>; ambiguous → exit 2). <code dir="auto">spec req &#x3C;domain></code> alone prints the domain’s charter before authoring.</p>
197
197
  <p><strong>Exit codes:</strong> 0 / 2</p>
198
198
  <div class="sl-heading-wrapper level-h3"><h3 id="spec-term-"><code dir="auto">spec term …</code></h3><a class="sl-anchor-link" href="#spec-term-"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentcolor" d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “spec term …”</span></a></div>
199
199
  <p>The glossary authoring surface — a term is a requirement row in the reserved <code dir="auto">TERM</code> domain (definition in the statement, headword in <code dir="auto">term</code>, synonyms in <code dir="auto">aliases</code>):</p>
@@ -209,7 +209,7 @@
209
209
  <p>The GLOSSARY.md round-trip. Default: <strong>generate</strong> GLOSSARY.md from the TERM store, byte-stable and LLM-free. <code dir="auto">--migrate</code>: one-time parse of an existing GLOSSARY.md into <code dir="auto">TERM-001..N</code> (idempotent-skip). <code dir="auto">--check</code>: regenerate into a buffer and fail (exit 1) if the committed file drifts — a CI fence.</p>
210
210
  <p><strong>Exit codes:</strong> 0 / 1 / 2</p>
211
211
  <div class="sl-heading-wrapper level-h3"><h3 id="spec-supersede-reqid-platformdir"><code dir="auto">spec supersede &#x3C;reqId> [platformDir]</code></h3><a class="sl-anchor-link" href="#spec-supersede-reqid-platformdir"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentcolor" d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “spec supersede &#x3C;reqId> [platformDir]”</span></a></div>
212
- <p>The post-ship lifecycle move, mechanized. Flips the entry to superseded, mints the successor (<code dir="auto">--text</code> required non-TTY; <code dir="auto">--why</code>/<code dir="auto">--binds</code>/<code dir="auto">--lives</code> default to copies), reindexes, and prints the <strong>retag worklist</strong> — every code site still on the old ID. The target must be Active.</p>
212
+ <p>The post-ship lifecycle move, mechanized. Flips the entry to superseded, mints the successor (<code dir="auto">--text</code> required non-TTY; <code dir="auto">--why</code>/<code dir="auto">--lives</code> default to copies), reindexes, and prints the <strong>retag worklist</strong> — every code site still on the old ID. The target must be Active.</p>
213
213
  <p>On a requirement domain the reported <code dir="auto">spec_version</code> is the <strong>derived</strong> domain version after the supersession — no authored counter is written, and <code dir="auto">--no-bump</code> is a no-op (there is no counter to hold back). On the reserved TERM domain the authored <code dir="auto">specVersion</code> still bumps (it is the term-drift pin) and <code dir="auto">--no-bump</code> opts out.</p>
214
214
  <p><code dir="auto">--json</code>: <code dir="auto">{ old_id, new_id, file, spec_version, retag: [...] }</code></p>
215
215
  <p><strong>Exit codes:</strong> 0 / 2</p>
@@ -217,7 +217,7 @@
217
217
  <p>Cross-domain supersede for taxonomy reorganization: mints the successor as the next id in <code dir="auto">&#x3C;NEW-DOMAIN></code> (which must exist), carrying the source’s fields (<code dir="auto">--text</code>/<code dir="auto">--why</code>/<code dir="auto">--lives</code> override), flips the source to superseded with a cross-domain <code dir="auto">supersededBy</code>, and emits the retag worklist. Both envelopes validate before either is written — a reject never half-applies. Only Active requirements move; moving into the source’s own domain is a usage error (use <code dir="auto">supersede</code>).</p>
218
218
  <p><strong>Exit codes:</strong> 0 / 2</p>
219
219
  <div class="sl-heading-wrapper level-h3"><h3 id="spec-amend-reqid-platformdir"><code dir="auto">spec amend &#x3C;reqId> [platformDir]</code></h3><a class="sl-anchor-link" href="#spec-amend-reqid-platformdir"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentcolor" d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “spec amend &#x3C;reqId> [platformDir]”</span></a></div>
220
- <p>Revise an unshipped requirement’s fields in place — same ID, no version change (the domain version only derives from supersessions). Field flags name what changes (<code dir="auto">--text</code>/<code dir="auto">--why</code>/<code dir="auto">--binds</code>/<code dir="auto">--lives</code>, at least one); untouched fields stay byte-identical. Only Active and Draft entries amend.</p>
220
+ <p>Revise an unshipped requirement’s fields in place — same ID, no version change (the domain version only derives from supersessions). Field flags name what changes (<code dir="auto">--text</code>/<code dir="auto">--why</code>/<code dir="auto">--lives</code>, at least one); untouched fields stay byte-identical. Only Active and Draft entries amend.</p>
221
221
  <p><strong>Exit codes:</strong> 0 / 2</p>
222
222
  <div class="sl-heading-wrapper level-h3"><h3 id="spec-serve-platformdir---port-n"><code dir="auto">spec serve [platformDir] [--port N]</code></h3><a class="sl-anchor-link" href="#spec-serve-platformdir---port-n"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentcolor" d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “spec serve [platformDir] [--port N]”</span></a></div>
223
223
  <p>Launch the local webapp over the derived index. Binds <code dir="auto">127.0.0.1</code> only. <code dir="auto">--probe</code> boots on an ephemeral port, smoke-tests, and exits.</p>
@@ -1 +1 @@
1
- {"version":"1.5.2","languages":{"en":{"hash":"en_f3d65f37ce","wasm":"en","page_count":11}},"include_characters":["_","‿","⁀","⁔","︳","︴","﹍","﹎","﹏","_"]}
1
+ {"version":"1.5.2","languages":{"en":{"hash":"en_4cf42761c7","wasm":"en","page_count":11}},"include_characters":["_","‿","⁀","⁔","︳","︴","﹍","﹎","﹏","_"]}
package/dist/impl.js CHANGED
@@ -661,6 +661,23 @@ var init_constants = __esm(() => {
661
661
  OUT_HELP = `DB path override (default: <platformDir>/${INDEX_DIR}/${INDEX_DB_FILENAME})`;
662
662
  });
663
663
 
664
+ // src/parser/requirementLine.ts
665
+ function findRequirementIdLine(rawLines, id, from = 0) {
666
+ const needle = `"id": "${id}"`;
667
+ const exact = `${SEAM_INDENT}${needle}`;
668
+ const start = from > 0 ? from : 0;
669
+ let loose = -1;
670
+ for (let i = start;i < rawLines.length; i++) {
671
+ const line = rawLines[i];
672
+ if (line === exact || line === `${exact},`)
673
+ return i;
674
+ if (loose < 0 && line.includes(needle))
675
+ loose = i;
676
+ }
677
+ return loose;
678
+ }
679
+ var SEAM_INDENT = " ";
680
+
664
681
  // src/parser/domainJson.ts
665
682
  function parseDomainJsonFile(opts) {
666
683
  let parsed;
@@ -698,6 +715,7 @@ function parseDomainJson(opts) {
698
715
  const updated = data.updated ?? null;
699
716
  const rawLines = text.split(`
700
717
  `);
718
+ let lineCursor = 0;
701
719
  const working = [];
702
720
  const relatesAcc = {
703
721
  relations: [],
@@ -718,7 +736,10 @@ function parseDomainJson(opts) {
718
736
  };
719
737
  for (let idx = 0;idx < data.requirements.length; idx++) {
720
738
  const r = data.requirements[idx];
721
- const line = deriveRequirementLine(rawLines, r.id, idx);
739
+ const found = findRequirementIdLine(rawLines, r.id, lineCursor);
740
+ const line = found >= 0 ? found + 1 : idx + 1;
741
+ if (found >= 0)
742
+ lineCursor = found + 1;
722
743
  working.push(mapRequirementRow(r, key, sourceFile, spec_version, line));
723
744
  flattenRelates(r, sourceFile, line, relatesAcc);
724
745
  flattenIssues(r, sourceFile, line, issuesAcc);
@@ -742,11 +763,6 @@ function parseDomainJson(opts) {
742
763
  term_citations: termAcc.citations
743
764
  };
744
765
  }
745
- function deriveRequirementLine(rawLines, id, idx) {
746
- const needle = `"id": "${id}"`;
747
- const found = rawLines.findIndex((l) => l.includes(needle));
748
- return found >= 0 ? found + 1 : idx + 1;
749
- }
750
766
  function mapRequirementRow(r, key, sourceFile, spec_version, line) {
751
767
  const id = r.id;
752
768
  const seq = Number(id.split("-")[1]);
@@ -1363,10 +1379,11 @@ class SqliteStorage {
1363
1379
  return row ?? null;
1364
1380
  }
1365
1381
  listDomains() {
1366
- return [];
1382
+ return this.#db.query(LIST_DOMAINS_SQL).all();
1367
1383
  }
1368
- getDomain(_key) {
1369
- return null;
1384
+ getDomain(key) {
1385
+ const row = this.#db.query(GET_DOMAIN_SQL).get({ key });
1386
+ return row ?? null;
1370
1387
  }
1371
1388
  listRequirements(opts) {
1372
1389
  const key = opts?.key;
@@ -1996,7 +2013,7 @@ SELECT
1996
2013
  FROM repos
1997
2014
  WHERE repos.name != 'spec-engine'
1998
2015
  ORDER BY repos.name
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
2016
+ `, FTS_SEARCH_SQL = `SELECT r.id AS req_id, r.key AS key, r.text AS text, r.why AS why, r.source_file AS source_file, r.line AS line, bm25(requirements_fts, 1.0, 0.5) AS rank FROM requirements_fts JOIN requirements r ON r.rowid = requirements_fts.rowid WHERE requirements_fts MATCH $query AND r.status NOT IN ('Superseded', 'Deprecated') ORDER BY rank ASC LIMIT $limit`, RESOLVE_BY_FILES_SQL_PREFIX = "SELECT DISTINCT r.id, r.key, r.seq, r.status, r.superseded_by, r.text, r.why, r.source_file, r.line, r.spec_version, r.changed_at_version, r.superseded_at_version FROM tags t JOIN requirements r ON r.id = t.req_id WHERE t.file IN (", RESOLVE_BY_FILES_SQL_SUFFIX = ") ORDER BY r.key, r.seq", LIST_REPOS_SQL = "SELECT name, path, pinned_spec_version FROM repos ORDER BY name", GET_REPO_SQL = "SELECT name, path, pinned_spec_version FROM repos WHERE name = $name", LIST_DOMAINS_SQL = "SELECT key, owner, schema, spec_version, source_repo FROM domains ORDER BY key", GET_DOMAIN_SQL = "SELECT key, owner, schema, spec_version, source_repo FROM domains WHERE key = $key", LIST_TAGS_SQL = `SELECT id, req_id, repo, file, line, kind, level FROM tags
2000
2017
  WHERE ($repo IS NULL OR repo = $repo)
2001
2018
  AND ($req_id IS NULL OR req_id = $req_id)
2002
2019
  AND ($file IS NULL OR file = $file)
@@ -3106,49 +3123,6 @@ function matchesSegment(pat, seg) {
3106
3123
  return p === pat.length;
3107
3124
  }
3108
3125
 
3109
- // src/check/format.ts
3110
- function compareRepoNullsLast(a, b) {
3111
- if (a === b)
3112
- return 0;
3113
- if (a === null)
3114
- return 1;
3115
- if (b === null)
3116
- return -1;
3117
- return a.localeCompare(b);
3118
- }
3119
- function compareDiagnostics(a, b) {
3120
- const codeCmp = a.code.localeCompare(b.code);
3121
- if (codeCmp !== 0)
3122
- return codeCmp;
3123
- const repoCmp = compareRepoNullsLast(a.repo, b.repo);
3124
- if (repoCmp !== 0)
3125
- return repoCmp;
3126
- const fileCmp = (a.source_file ?? "").localeCompare(b.source_file ?? "");
3127
- if (fileCmp !== 0)
3128
- return fileCmp;
3129
- const lineCmp = (a.line ?? 0) - (b.line ?? 0);
3130
- if (lineCmp !== 0)
3131
- return lineCmp;
3132
- return (a.req_id ?? "").localeCompare(b.req_id ?? "");
3133
- }
3134
- function sortDiagnostics(rows) {
3135
- return [...rows].sort(compareDiagnostics);
3136
- }
3137
- function renderDiagnostics(rows, mode) {
3138
- const sorted = sortDiagnostics(rows);
3139
- if (mode === "json") {
3140
- return JSON.stringify(sorted);
3141
- }
3142
- return sorted.map((d) => {
3143
- const repo = d.repo ?? "";
3144
- const file = d.source_file ?? "";
3145
- const line = d.line ?? "";
3146
- const reqId = d.req_id ?? "";
3147
- return `${d.code} ${repo} ${file}:${line} ${reqId} ${d.detail}`;
3148
- }).join(`
3149
- `);
3150
- }
3151
-
3152
3126
  // src/authoring/domains.ts
3153
3127
  import { existsSync as existsSync5, readdirSync as readdirSync2 } from "fs";
3154
3128
  import { join as join8 } from "path";
@@ -3220,21 +3194,150 @@ var init_domains = __esm(() => {
3220
3194
  init_src();
3221
3195
  });
3222
3196
 
3223
- // src/check/grammar.ts
3197
+ // src/authoring/filerefs.ts
3198
+ import { existsSync as existsSync6 } from "fs";
3199
+ import { resolve as resolve7 } from "path";
3200
+ function extractRefsFromText(s) {
3201
+ const out = [];
3202
+ for (const m of s.matchAll(FILE_REF_RE)) {
3203
+ const candidate = m[1].replace(/\.+$/, "");
3204
+ if (candidate.includes("/"))
3205
+ out.push(candidate);
3206
+ }
3207
+ return out;
3208
+ }
3209
+ function resolveFileRef(platformDir, ref) {
3210
+ const root = resolve7(platformDir);
3211
+ const abs = resolve7(root, ref);
3212
+ if (!(abs === root || abs.startsWith(`${root}/`)))
3213
+ return false;
3214
+ return existsSync6(abs);
3215
+ }
3216
+ var FILE_REF_RE;
3217
+ var init_filerefs = __esm(() => {
3218
+ FILE_REF_RE = /(?<![^\s])@([A-Za-z0-9_][A-Za-z0-9_\-./]*)/g;
3219
+ });
3220
+
3221
+ // src/check/filerefs.ts
3224
3222
  import { join as join9 } from "path";
3225
- function lineOf(rawText, id) {
3226
- const lines = rawText.split(`
3223
+ async function readDomain(platformDir, key) {
3224
+ try {
3225
+ const raw = await Bun.file(join9(platformDir, "spec-engine", key, "SPEC.json")).text();
3226
+ const doc = JSON.parse(raw);
3227
+ return {
3228
+ raw,
3229
+ requirements: Array.isArray(doc.requirements) ? doc.requirements : []
3230
+ };
3231
+ } catch {
3232
+ return null;
3233
+ }
3234
+ }
3235
+ function rowsForRequirement(platformDir, key, reqId, livesIn, line) {
3236
+ if (!Array.isArray(livesIn))
3237
+ return [];
3238
+ const rows = [];
3239
+ for (const entry of livesIn) {
3240
+ if (typeof entry !== "string")
3241
+ continue;
3242
+ const ref = entry.startsWith("@") ? entry.slice(1) : entry;
3243
+ if (ref.length === 0 || resolveFileRef(platformDir, ref))
3244
+ continue;
3245
+ rows.push({
3246
+ code: DiagnosticCode.BROKEN_FILE_REF,
3247
+ severity: "error",
3248
+ repo: null,
3249
+ source_file: `spec-engine/${key}/SPEC.json`,
3250
+ line,
3251
+ req_id: reqId,
3252
+ detail: `livesIn entry ${entry} does not resolve to a file under the platform root`
3253
+ });
3254
+ }
3255
+ return rows;
3256
+ }
3257
+ function rowsForDomain(platformDir, key, domain2) {
3258
+ const rows = [];
3259
+ const rawLines = domain2.raw.split(`
3227
3260
  `);
3228
- const needle = `"id": "${id}"`;
3229
- for (let i = 0;i < lines.length; i++) {
3230
- if (lines[i].includes(needle))
3231
- return i + 1;
3261
+ let lineCursor = 0;
3262
+ for (const req of domain2.requirements) {
3263
+ if (typeof req?.id !== "string")
3264
+ continue;
3265
+ const found = findRequirementIdLine(rawLines, req.id, lineCursor);
3266
+ if (found >= 0)
3267
+ lineCursor = found + 1;
3268
+ const status = typeof req.status === "string" ? req.status.toLowerCase() : "";
3269
+ if (!CHECKED_STATUSES.has(status))
3270
+ continue;
3271
+ rows.push(...rowsForRequirement(platformDir, key, req.id, req.livesIn, found >= 0 ? found + 1 : 0));
3272
+ }
3273
+ return rows;
3274
+ }
3275
+ async function brokenFileRefDiagnostics(platformDir) {
3276
+ const rows = [];
3277
+ for (const key of listDomainKeys(platformDir)) {
3278
+ const domain2 = await readDomain(platformDir, key);
3279
+ if (domain2 !== null)
3280
+ rows.push(...rowsForDomain(platformDir, key, domain2));
3281
+ }
3282
+ rows.sort((a, b) => (a.source_file ?? "").localeCompare(b.source_file ?? "") || (a.line ?? 0) - (b.line ?? 0) || (a.req_id ?? "").localeCompare(b.req_id ?? "") || a.detail.localeCompare(b.detail));
3283
+ return rows;
3284
+ }
3285
+ var CHECKED_STATUSES;
3286
+ var init_filerefs2 = __esm(() => {
3287
+ init_src();
3288
+ init_domains();
3289
+ init_filerefs();
3290
+ CHECKED_STATUSES = new Set(["active", "draft"]);
3291
+ });
3292
+
3293
+ // src/check/format.ts
3294
+ function compareRepoNullsLast(a, b) {
3295
+ if (a === b)
3296
+ return 0;
3297
+ if (a === null)
3298
+ return 1;
3299
+ if (b === null)
3300
+ return -1;
3301
+ return a.localeCompare(b);
3302
+ }
3303
+ function compareDiagnostics(a, b) {
3304
+ const codeCmp = a.code.localeCompare(b.code);
3305
+ if (codeCmp !== 0)
3306
+ return codeCmp;
3307
+ const repoCmp = compareRepoNullsLast(a.repo, b.repo);
3308
+ if (repoCmp !== 0)
3309
+ return repoCmp;
3310
+ const fileCmp = (a.source_file ?? "").localeCompare(b.source_file ?? "");
3311
+ if (fileCmp !== 0)
3312
+ return fileCmp;
3313
+ const lineCmp = (a.line ?? 0) - (b.line ?? 0);
3314
+ if (lineCmp !== 0)
3315
+ return lineCmp;
3316
+ return (a.req_id ?? "").localeCompare(b.req_id ?? "");
3317
+ }
3318
+ function sortDiagnostics(rows) {
3319
+ return [...rows].sort(compareDiagnostics);
3320
+ }
3321
+ function renderDiagnostics(rows, mode) {
3322
+ const sorted = sortDiagnostics(rows);
3323
+ if (mode === "json") {
3324
+ return JSON.stringify(sorted);
3232
3325
  }
3233
- return 0;
3326
+ return sorted.map((d) => {
3327
+ const repo = d.repo ?? "";
3328
+ const file = d.source_file ?? "";
3329
+ const line = d.line ?? "";
3330
+ const reqId = d.req_id ?? "";
3331
+ return `${d.code} ${repo} ${file}:${line} ${reqId} ${d.detail}`;
3332
+ }).join(`
3333
+ `);
3234
3334
  }
3335
+
3336
+ // src/check/grammar.ts
3337
+ import { join as join10 } from "path";
3235
3338
  async function readDeclaredDomain(platformDir, key) {
3236
3339
  try {
3237
- const raw = await Bun.file(join9(platformDir, "spec-engine", key, "SPEC.json")).text();
3340
+ const raw = await Bun.file(join10(platformDir, "spec-engine", key, "SPEC.json")).text();
3238
3341
  const doc = JSON.parse(raw);
3239
3342
  if (doc.grammar !== "ears")
3240
3343
  return null;
@@ -3247,13 +3350,21 @@ async function readDeclaredDomain(platformDir, key) {
3247
3350
  return null;
3248
3351
  }
3249
3352
  }
3250
- function rowsForDomain(key, domain2) {
3353
+ function rowsForDomain2(key, domain2) {
3251
3354
  const rows = [];
3355
+ const rawLines = domain2.raw.split(`
3356
+ `);
3357
+ let lineCursor = 0;
3252
3358
  for (const req of domain2.requirements) {
3253
- if (typeof req?.id !== "string" || typeof req.statement !== "string")
3359
+ if (typeof req?.id !== "string")
3360
+ continue;
3361
+ const found = findRequirementIdLine(rawLines, req.id, lineCursor);
3362
+ if (found >= 0)
3363
+ lineCursor = found + 1;
3364
+ if (typeof req.statement !== "string")
3254
3365
  continue;
3255
3366
  const status = typeof req.status === "string" ? req.status.toLowerCase() : "";
3256
- if (!CHECKED_STATUSES.has(status))
3367
+ if (!CHECKED_STATUSES2.has(status))
3257
3368
  continue;
3258
3369
  const result = parseEarsStatement(req.statement);
3259
3370
  if (result.ok)
@@ -3263,7 +3374,7 @@ function rowsForDomain(key, domain2) {
3263
3374
  severity: domain2.severity,
3264
3375
  repo: null,
3265
3376
  source_file: `spec-engine/${key}/SPEC.json`,
3266
- line: lineOf(domain2.raw, req.id),
3377
+ line: found >= 0 ? found + 1 : 0,
3267
3378
  req_id: req.id,
3268
3379
  detail: `${result.problem}. Expected one of:
3269
3380
  ${result.expected}`
@@ -3278,16 +3389,16 @@ async function statementGrammarDiagnostics(platformDir) {
3278
3389
  continue;
3279
3390
  const domain2 = await readDeclaredDomain(platformDir, key);
3280
3391
  if (domain2 !== null)
3281
- rows.push(...rowsForDomain(key, domain2));
3392
+ rows.push(...rowsForDomain2(key, domain2));
3282
3393
  }
3283
3394
  rows.sort((a, b) => (a.source_file ?? "").localeCompare(b.source_file ?? "") || (a.line ?? 0) - (b.line ?? 0) || (a.req_id ?? "").localeCompare(b.req_id ?? ""));
3284
3395
  return rows;
3285
3396
  }
3286
- var CHECKED_STATUSES;
3397
+ var CHECKED_STATUSES2;
3287
3398
  var init_grammar = __esm(() => {
3288
3399
  init_src();
3289
3400
  init_domains();
3290
- CHECKED_STATUSES = new Set(["active", "draft"]);
3401
+ CHECKED_STATUSES2 = new Set(["active", "draft"]);
3291
3402
  });
3292
3403
 
3293
3404
  // src/check/proven.ts
@@ -3536,6 +3647,72 @@ var init_statusflip = __esm(() => {
3536
3647
  TERMINAL_STATUSES = new Set(["superseded", "deprecated", "retired"]);
3537
3648
  });
3538
3649
 
3650
+ // src/check/supersedes.ts
3651
+ import { join as join11 } from "path";
3652
+ async function entriesForDomain(platformDir, key) {
3653
+ let raw;
3654
+ let reqs;
3655
+ try {
3656
+ raw = await Bun.file(join11(platformDir, "spec-engine", key, "SPEC.json")).text();
3657
+ const doc = JSON.parse(raw);
3658
+ reqs = Array.isArray(doc.requirements) ? doc.requirements : [];
3659
+ } catch {
3660
+ return [];
3661
+ }
3662
+ const out = [];
3663
+ const rawLines = raw.split(`
3664
+ `);
3665
+ let lineCursor = 0;
3666
+ for (const r of reqs) {
3667
+ if (typeof r?.id !== "string")
3668
+ continue;
3669
+ const found = findRequirementIdLine(rawLines, r.id, lineCursor);
3670
+ if (found >= 0)
3671
+ lineCursor = found + 1;
3672
+ out.push({
3673
+ id: r.id,
3674
+ supersedes: typeof r.supersedes === "string" ? r.supersedes : null,
3675
+ line: found >= 0 ? found + 1 : 0,
3676
+ sourceFile: `spec-engine/${key}/SPEC.json`
3677
+ });
3678
+ }
3679
+ return out;
3680
+ }
3681
+ async function collectEntries(platformDir) {
3682
+ const out = [];
3683
+ for (const key of listDomainKeys(platformDir)) {
3684
+ out.push(...await entriesForDomain(platformDir, key));
3685
+ }
3686
+ return out;
3687
+ }
3688
+ async function supersedesPointerDiagnostics(platformDir) {
3689
+ const entries = await collectEntries(platformDir);
3690
+ const ids = new Set(entries.map((e) => e.id));
3691
+ const rows = [];
3692
+ for (const e of entries) {
3693
+ if (e.supersedes === null)
3694
+ continue;
3695
+ const detail = e.supersedes === e.id ? `${e.id} declares supersedes on itself` : ids.has(e.supersedes) ? null : `${e.id} supersedes ${e.supersedes} which does not exist`;
3696
+ if (detail === null)
3697
+ continue;
3698
+ rows.push({
3699
+ code: DiagnosticCode.BROKEN_SUPERSEDE,
3700
+ severity: "error",
3701
+ repo: null,
3702
+ source_file: e.sourceFile,
3703
+ line: e.line,
3704
+ req_id: e.id,
3705
+ detail
3706
+ });
3707
+ }
3708
+ rows.sort((a, b) => (a.source_file ?? "").localeCompare(b.source_file ?? "") || (a.line ?? 0) - (b.line ?? 0) || (a.req_id ?? "").localeCompare(b.req_id ?? ""));
3709
+ return rows;
3710
+ }
3711
+ var init_supersedes = __esm(() => {
3712
+ init_src();
3713
+ init_domains();
3714
+ });
3715
+
3539
3716
  // src/check/unsourced.ts
3540
3717
  function unsourcedChanges(requirements, provenance) {
3541
3718
  const sourced = new Set;
@@ -3841,8 +4018,8 @@ __export(exports_glossary, {
3841
4018
  glossaryCommand: () => glossaryCommand,
3842
4019
  generateGlossary: () => generateGlossary
3843
4020
  });
3844
- import { existsSync as existsSync6, readFileSync } from "fs";
3845
- import { join as join10, resolve as resolve7 } from "path";
4021
+ import { existsSync as existsSync7, readFileSync } from "fs";
4022
+ import { join as join12, resolve as resolve8 } from "path";
3846
4023
  import { defineCommand as defineCommand3 } from "citty";
3847
4024
  function parseGlossary(md) {
3848
4025
  const terms = [];
@@ -3894,7 +4071,7 @@ function generateGlossary(terms) {
3894
4071
  `;
3895
4072
  }
3896
4073
  function resolvePlatform(platformDirRaw) {
3897
- const platformDir = resolve7(platformDirRaw ?? process.cwd());
4074
+ const platformDir = resolve8(platformDirRaw ?? process.cwd());
3898
4075
  try {
3899
4076
  assertSpecPlatform(platformDir);
3900
4077
  } catch (e) {
@@ -3903,8 +4080,8 @@ function resolvePlatform(platformDirRaw) {
3903
4080
  return platformDir;
3904
4081
  }
3905
4082
  function readStoreTerms(platformDir) {
3906
- const specPath = join10(platformDir, "spec-engine", TERM_KEY, "SPEC.json");
3907
- if (!existsSync6(specPath))
4083
+ const specPath = join12(platformDir, "spec-engine", TERM_KEY, "SPEC.json");
4084
+ if (!existsSync7(specPath))
3908
4085
  return [];
3909
4086
  const domain2 = JSON.parse(readFileSync(specPath, "utf8"));
3910
4087
  const reqs = Array.isArray(domain2.requirements) ? domain2.requirements : [];
@@ -3915,12 +4092,12 @@ function readStoreTerms(platformDir) {
3915
4092
  }));
3916
4093
  }
3917
4094
  function glossaryPath(platformDir) {
3918
- return join10(platformDir, "GLOSSARY.md");
4095
+ return join12(platformDir, "GLOSSARY.md");
3919
4096
  }
3920
4097
  async function migrateGlossary(platformDir, json) {
3921
4098
  const relFile = `spec-engine/${TERM_KEY}/SPEC.json`;
3922
- const specPath = join10(platformDir, "spec-engine", TERM_KEY, "SPEC.json");
3923
- if (!existsSync6(specPath)) {
4099
+ const specPath = join12(platformDir, "spec-engine", TERM_KEY, "SPEC.json");
4100
+ if (!existsSync7(specPath)) {
3924
4101
  console.error(`spec glossary: no TERM domain (expected ${relFile} under ${platformDir})`);
3925
4102
  process.exit(EXIT.USAGE);
3926
4103
  }
@@ -3935,7 +4112,7 @@ async function migrateGlossary(platformDir, json) {
3935
4112
  return;
3936
4113
  }
3937
4114
  const gPath = glossaryPath(platformDir);
3938
- if (!existsSync6(gPath)) {
4115
+ if (!existsSync7(gPath)) {
3939
4116
  console.error(`spec glossary: no GLOSSARY.md to migrate (expected ${gPath})`);
3940
4117
  process.exit(EXIT.USAGE);
3941
4118
  }
@@ -3982,7 +4159,7 @@ function glossaryDriftDiagnostic(platformDir) {
3982
4159
  if (terms.length === 0)
3983
4160
  return null;
3984
4161
  const gPath = glossaryPath(platformDir);
3985
- if (!existsSync6(gPath))
4162
+ if (!existsSync7(gPath))
3986
4163
  return null;
3987
4164
  const generated = generateGlossary(terms);
3988
4165
  const committed = readFileSync(gPath, "utf8");
@@ -4001,7 +4178,7 @@ function glossaryDriftDiagnostic(platformDir) {
4001
4178
  function checkGlossary(platformDir, json) {
4002
4179
  const generated = generateGlossary(readStoreTerms(platformDir));
4003
4180
  const gPath = glossaryPath(platformDir);
4004
- const committed = existsSync6(gPath) ? readFileSync(gPath, "utf8") : "";
4181
+ const committed = existsSync7(gPath) ? readFileSync(gPath, "utf8") : "";
4005
4182
  if (generated === committed) {
4006
4183
  if (json)
4007
4184
  console.log(JSON.stringify({ ok: true }));
@@ -4063,12 +4240,12 @@ var exports_check = {};
4063
4240
  __export(exports_check, {
4064
4241
  checkCommand: () => checkCommand
4065
4242
  });
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";
4243
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync2 } from "fs";
4244
+ import { dirname as dirname7, join as join13, resolve as resolve9 } from "path";
4068
4245
  import { defineCommand as defineCommand4 } from "citty";
4069
4246
  function readCodeownersText(platformDir) {
4070
- const path = join11(platformDir, ".github", "CODEOWNERS");
4071
- if (!existsSync7(path))
4247
+ const path = join13(platformDir, ".github", "CODEOWNERS");
4248
+ if (!existsSync8(path))
4072
4249
  return null;
4073
4250
  return readFileSync2(path, "utf-8");
4074
4251
  }
@@ -4107,7 +4284,7 @@ async function ingestResults(storage2, platformDir, resultsArg, jsonMode, diagno
4107
4284
  }
4108
4285
  return { resultsParsed: undefined, verifyingTags: undefined };
4109
4286
  }
4110
- const resultsPath = resolve8(platformDir, resultsArg);
4287
+ const resultsPath = resolve9(platformDir, resultsArg);
4111
4288
  if (!isContainedPath(resultsPath, platformDir)) {
4112
4289
  console.error(`spec check: --results path must be inside platformDir (resolved to ${resultsPath})`);
4113
4290
  try {
@@ -4127,7 +4304,7 @@ async function collectChangeReqs(canonicalDir) {
4127
4304
  const relPathById = new Map;
4128
4305
  for (const rel of await findDomainJsonFiles(canonicalDir)) {
4129
4306
  const platformRel = `spec-engine/${rel}`;
4130
- const text = await Bun.file(join11(canonicalDir, rel)).text();
4307
+ const text = await Bun.file(join13(canonicalDir, rel)).text();
4131
4308
  const parsed = domainReqsFromText(text, platformRel);
4132
4309
  for (const r of parsed.reqs) {
4133
4310
  reqs.push(r);
@@ -4167,7 +4344,7 @@ async function runGovernanceGate(storage2, platformDir, args, diagnostics2, resu
4167
4344
  } catch {}
4168
4345
  process.exit(EXIT.USAGE);
4169
4346
  }
4170
- const canonicalDir = join11(platformDir, "spec-engine");
4347
+ const canonicalDir = join13(platformDir, "spec-engine");
4171
4348
  const change = await collectChangeReqs(canonicalDir);
4172
4349
  const base = collectBaseReqs(platformDir, ref, diagnostics2);
4173
4350
  diagnostics2.push(...requirementRemoved(base.reqs, change.reqs, (id) => base.relPathById.get(id) ?? null));
@@ -4193,11 +4370,13 @@ function exitAfterCrash(e, storage2) {
4193
4370
  var checkCommand;
4194
4371
  var init_check = __esm(() => {
4195
4372
  init_src();
4373
+ init_filerefs2();
4196
4374
  init_grammar();
4197
4375
  init_propagation_teeth();
4198
4376
  init_proven();
4199
4377
  init_removed();
4200
4378
  init_statusflip();
4379
+ init_supersedes();
4201
4380
  init_unsourced();
4202
4381
  init_constants();
4203
4382
  init_discover();
@@ -4257,7 +4436,7 @@ var init_check = __esm(() => {
4257
4436
  }
4258
4437
  },
4259
4438
  async run({ args }) {
4260
- const platformDir = resolve8(args.platformDir ?? process.cwd());
4439
+ const platformDir = resolve9(args.platformDir ?? process.cwd());
4261
4440
  const outArg = args.out;
4262
4441
  const dbPath = resolveDbPath(platformDir, outArg);
4263
4442
  if (outArg)
@@ -4287,6 +4466,8 @@ var init_check = __esm(() => {
4287
4466
  diagnostics2.push(...unsourcedChanges(superseded, provenance));
4288
4467
  }
4289
4468
  diagnostics2.push(...await statementGrammarDiagnostics(platformDir));
4469
+ diagnostics2.push(...await brokenFileRefDiagnostics(platformDir));
4470
+ diagnostics2.push(...await supersedesPointerDiagnostics(platformDir));
4290
4471
  const glossaryDrift = glossaryDriftDiagnostic(platformDir);
4291
4472
  if (glossaryDrift !== null)
4292
4473
  diagnostics2.push(glossaryDrift);
@@ -4297,7 +4478,7 @@ var init_check = __esm(() => {
4297
4478
  requireOwnerApproval: args.requireOwnerApproval
4298
4479
  }, diagnostics2, results);
4299
4480
  if (!args.base && gitRefResolves(platformDir, "HEAD")) {
4300
- const change = await collectChangeReqs(join11(platformDir, "spec-engine"));
4481
+ const change = await collectChangeReqs(join13(platformDir, "spec-engine"));
4301
4482
  const baseParseSink = [];
4302
4483
  const base = collectBaseReqs(platformDir, "HEAD", baseParseSink);
4303
4484
  diagnostics2.push(...requirementRemoved(base.reqs, change.reqs, (id) => base.relPathById.get(id) ?? null));
@@ -4324,8 +4505,8 @@ __export(exports_domain, {
4324
4505
  domainListCommand: () => domainListCommand,
4325
4506
  domainCommand: () => domainCommand
4326
4507
  });
4327
- import { existsSync as existsSync8, mkdirSync as mkdirSync5 } from "fs";
4328
- import { dirname as dirname8, join as join12, resolve as resolve9 } from "path";
4508
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5 } from "fs";
4509
+ import { dirname as dirname8, join as join14, resolve as resolve10 } from "path";
4329
4510
  import { defineCommand as defineCommand5 } from "citty";
4330
4511
  var domainNewCommand, domainListCommand, domainCommand;
4331
4512
  var init_domain2 = __esm(() => {
@@ -4353,7 +4534,7 @@ var init_domain2 = __esm(() => {
4353
4534
  },
4354
4535
  async run({ args }) {
4355
4536
  const raw = args.name;
4356
- const platformDir = resolve9(args.platformDir ?? process.cwd());
4537
+ const platformDir = resolve10(args.platformDir ?? process.cwd());
4357
4538
  const key = normalizeDomainKey(raw);
4358
4539
  if (key !== raw) {
4359
4540
  console.log(`spec domain: normalized "${raw}" \u2192 ${key}`);
@@ -4363,21 +4544,21 @@ var init_domain2 = __esm(() => {
4363
4544
  process.exit(EXIT.USAGE);
4364
4545
  return;
4365
4546
  }
4366
- const dest = join12(platformDir, "spec-engine", key, "SPEC.json");
4367
- const resolvedDest = resolve9(dest);
4368
- const resolvedRoot = resolve9(platformDir);
4547
+ const dest = join14(platformDir, "spec-engine", key, "SPEC.json");
4548
+ const resolvedDest = resolve10(dest);
4549
+ const resolvedRoot = resolve10(platformDir);
4369
4550
  if (!(resolvedDest === resolvedRoot || resolvedDest.startsWith(`${resolvedRoot}/`))) {
4370
4551
  console.error(`spec domain new: refusing to write outside platformDir (${resolvedDest})`);
4371
4552
  process.exit(EXIT.USAGE);
4372
4553
  return;
4373
4554
  }
4374
- if (existsSync8(dest)) {
4555
+ if (existsSync9(dest)) {
4375
4556
  console.error(`refusing to overwrite ${dest}`);
4376
4557
  process.exit(EXIT.USAGE);
4377
4558
  return;
4378
4559
  }
4379
- const mdSibling = join12(dirname8(dest), "SPEC.md");
4380
- if (existsSync8(mdSibling)) {
4560
+ const mdSibling = join14(dirname8(dest), "SPEC.md");
4561
+ if (existsSync9(mdSibling)) {
4381
4562
  console.error(`refusing to create ${dest}: domain ${key} already exists as spec-engine/${key}/SPEC.md ` + "\u2014 migrate the SPEC.md first (a fresh empty SPEC.json would shadow it on the next index)");
4382
4563
  process.exit(EXIT.USAGE);
4383
4564
  return;
@@ -4414,7 +4595,7 @@ var init_domain2 = __esm(() => {
4414
4595
  }
4415
4596
  },
4416
4597
  async run({ args }) {
4417
- const platformDir = resolve9(args.platformDir ?? process.cwd());
4598
+ const platformDir = resolve10(args.platformDir ?? process.cwd());
4418
4599
  try {
4419
4600
  assertSpecPlatform(platformDir);
4420
4601
  } catch (e) {
@@ -4508,7 +4689,7 @@ var exports_map = {};
4508
4689
  __export(exports_map, {
4509
4690
  mapCommand: () => mapCommand
4510
4691
  });
4511
- import { resolve as resolve10 } from "path";
4692
+ import { resolve as resolve11 } from "path";
4512
4693
  import { defineCommand as defineCommand6 } from "citty";
4513
4694
  var mapCommand;
4514
4695
  var init_map = __esm(() => {
@@ -4545,7 +4726,7 @@ var init_map = __esm(() => {
4545
4726
  }
4546
4727
  },
4547
4728
  async run({ args }) {
4548
- const platformDir = resolve10(args.platformDir ?? process.cwd());
4729
+ const platformDir = resolve11(args.platformDir ?? process.cwd());
4549
4730
  const outArg = args.out;
4550
4731
  const dbPath = resolveDbPath(platformDir, outArg);
4551
4732
  if (outArg)
@@ -4595,7 +4776,7 @@ var exports_propagation = {};
4595
4776
  __export(exports_propagation, {
4596
4777
  propagationCommand: () => propagationCommand
4597
4778
  });
4598
- import { resolve as resolve11 } from "path";
4779
+ import { resolve as resolve12 } from "path";
4599
4780
  import { defineCommand as defineCommand7 } from "citty";
4600
4781
  var propagationCommand;
4601
4782
  var init_propagation2 = __esm(() => {
@@ -4643,7 +4824,7 @@ var init_propagation2 = __esm(() => {
4643
4824
  process.exit(EXIT.USAGE);
4644
4825
  return;
4645
4826
  }
4646
- const platformDir = resolve11(args.platformDir ?? process.cwd());
4827
+ const platformDir = resolve12(args.platformDir ?? process.cwd());
4647
4828
  const outArg = args.out;
4648
4829
  const dbPath = resolveDbPath(platformDir, outArg);
4649
4830
  if (outArg)
@@ -4717,7 +4898,7 @@ var exports_query = {};
4717
4898
  __export(exports_query, {
4718
4899
  queryCommand: () => queryCommand
4719
4900
  });
4720
- import { resolve as resolve12 } from "path";
4901
+ import { resolve as resolve13 } from "path";
4721
4902
  import { defineCommand as defineCommand8 } from "citty";
4722
4903
  var queryCommand;
4723
4904
  var init_query = __esm(() => {
@@ -4783,7 +4964,7 @@ var init_query = __esm(() => {
4783
4964
  process.exit(EXIT.USAGE);
4784
4965
  return;
4785
4966
  }
4786
- const platformDir = resolve12(args.platformDir ?? process.cwd());
4967
+ const platformDir = resolve13(args.platformDir ?? process.cwd());
4787
4968
  const outArg = args.out;
4788
4969
  const dbPath = resolveDbPath(platformDir, outArg);
4789
4970
  if (outArg)
@@ -4872,7 +5053,7 @@ var exports_relations = {};
4872
5053
  __export(exports_relations, {
4873
5054
  relationsCommand: () => relationsCommand
4874
5055
  });
4875
- import { resolve as resolve13 } from "path";
5056
+ import { resolve as resolve14 } from "path";
4876
5057
  import { defineCommand as defineCommand9 } from "citty";
4877
5058
  function formatNoRelations(platformDir) {
4878
5059
  return [
@@ -4915,7 +5096,7 @@ var init_relations = __esm(() => {
4915
5096
  }
4916
5097
  },
4917
5098
  async run({ args }) {
4918
- const platformDir = resolve13(args.platformDir ?? process.cwd());
5099
+ const platformDir = resolve14(args.platformDir ?? process.cwd());
4919
5100
  const outArg = args.out;
4920
5101
  const dbPath = resolveDbPath(platformDir, outArg);
4921
5102
  if (outArg)
@@ -5051,7 +5232,7 @@ var init_adapter = () => {};
5051
5232
 
5052
5233
  // ../tracker/src/cache.ts
5053
5234
  import { mkdirSync as mkdirSync6 } from "fs";
5054
- import { dirname as dirname9, join as join13 } from "path";
5235
+ import { dirname as dirname9, join as join15 } from "path";
5055
5236
  function isFresh(entry, now, ttlMs = CACHE_TTL_MS) {
5056
5237
  const t = Date.parse(entry.fetched_at);
5057
5238
  if (Number.isNaN(t))
@@ -5065,7 +5246,7 @@ function isValidEntry(e) {
5065
5246
  return typeof r.title === "string" && typeof r.status === "string" && typeof r.url === "string" && typeof r.fetched_at === "string";
5066
5247
  }
5067
5248
  function sidecarPath(platformDir) {
5068
- return join13(platformDir, ".spec-engine", SIDECAR_FILE);
5249
+ return join15(platformDir, ".spec-engine", SIDECAR_FILE);
5069
5250
  }
5070
5251
  async function readCache(platformDir) {
5071
5252
  try {
@@ -5253,7 +5434,7 @@ __export(exports_provenance, {
5253
5434
  provenanceCommand: () => provenanceCommand
5254
5435
  });
5255
5436
  import { statSync as statSync5 } from "fs";
5256
- import { resolve as resolve14 } from "path";
5437
+ import { resolve as resolve15 } from "path";
5257
5438
  import { defineCommand as defineCommand10 } from "citty";
5258
5439
  function formatNoProvenance(platformDir) {
5259
5440
  return [
@@ -5314,7 +5495,7 @@ var init_provenance = __esm(() => {
5314
5495
  if (issueId && platformArg === undefined) {
5315
5496
  let isDir = false;
5316
5497
  try {
5317
- isDir = statSync5(resolve14(issueId)).isDirectory();
5498
+ isDir = statSync5(resolve15(issueId)).isDirectory();
5318
5499
  } catch {
5319
5500
  isDir = false;
5320
5501
  }
@@ -5323,7 +5504,7 @@ var init_provenance = __esm(() => {
5323
5504
  issueId = "";
5324
5505
  }
5325
5506
  }
5326
- const platformDir = resolve14(platformArg ?? process.cwd());
5507
+ const platformDir = resolve15(platformArg ?? process.cwd());
5327
5508
  const outArg = args.out;
5328
5509
  const dbPath = resolveDbPath(platformDir, outArg);
5329
5510
  if (outArg)
@@ -5348,35 +5529,11 @@ var init_provenance = __esm(() => {
5348
5529
  });
5349
5530
  });
5350
5531
 
5351
- // src/authoring/filerefs.ts
5352
- import { existsSync as existsSync9 } from "fs";
5353
- import { resolve as resolve15 } from "path";
5354
- function extractRefsFromText(s) {
5355
- const out = [];
5356
- for (const m of s.matchAll(FILE_REF_RE)) {
5357
- const candidate = m[1].replace(/\.+$/, "");
5358
- if (candidate.includes("/"))
5359
- out.push(candidate);
5360
- }
5361
- return out;
5362
- }
5363
- function resolveFileRef(platformDir, ref) {
5364
- const root = resolve15(platformDir);
5365
- const abs = resolve15(root, ref);
5366
- if (!(abs === root || abs.startsWith(`${root}/`)))
5367
- return false;
5368
- return existsSync9(abs);
5369
- }
5370
- var FILE_REF_RE;
5371
- var init_filerefs = __esm(() => {
5372
- FILE_REF_RE = /(?<![^\s])@([A-Za-z0-9_][A-Za-z0-9_\-./]*)/g;
5373
- });
5374
-
5375
5532
  // src/authoring/grammar.ts
5376
- import { join as join14 } from "path";
5533
+ import { join as join16 } from "path";
5377
5534
  async function grammarConfig(platformDir, key) {
5378
5535
  try {
5379
- const doc = JSON.parse(await Bun.file(join14(platformDir, "spec-engine", key, "SPEC.json")).text());
5536
+ const doc = JSON.parse(await Bun.file(join16(platformDir, "spec-engine", key, "SPEC.json")).text());
5380
5537
  return {
5381
5538
  grammar: doc.grammar === "ears" ? "ears" : "freeform",
5382
5539
  severity: doc.grammarSeverity === "error" ? "error" : "warning"
@@ -5419,7 +5576,7 @@ __export(exports_req, {
5419
5576
  appendEntry: () => appendEntry
5420
5577
  });
5421
5578
  import { existsSync as existsSync10 } from "fs";
5422
- import { join as join15, resolve as resolve16 } from "path";
5579
+ import { join as join17, resolve as resolve16 } from "path";
5423
5580
  import { createInterface as createInterface2 } from "readline";
5424
5581
  import { defineCommand as defineCommand11 } from "citty";
5425
5582
  function resolveDomainKey(input, platformDir) {
@@ -5448,13 +5605,11 @@ async function authorFromFieldFlags(platformDir, key, nextId, textFlag, opts) {
5448
5605
  }
5449
5606
  await printResolvedCharter(platformDir, key);
5450
5607
  const why = (opts.why ?? "").trim();
5451
- const binds = (opts.binds ?? "").trim();
5452
5608
  const lives = (opts.lives ?? "").trim();
5453
- warnUnresolvableRefs(platformDir, [requirement, why, binds, lives]);
5609
+ warnUnresolvableRefs(platformDir, [requirement, why, lives]);
5454
5610
  const relFile = await appendEntry(platformDir, key, nextId, {
5455
5611
  requirement,
5456
5612
  why,
5457
- binds,
5458
5613
  lives,
5459
5614
  issue: (opts.issue ?? "").trim() || undefined
5460
5615
  });
@@ -5484,7 +5639,7 @@ function warnUnresolvableRefs(platformDir, fieldValues) {
5484
5639
  }
5485
5640
  async function appendEntry(platformDir, key, id, fields) {
5486
5641
  const relFile = `spec-engine/${key}/SPEC.json`;
5487
- const specPath = join15(platformDir, "spec-engine", key, "SPEC.json");
5642
+ const specPath = join17(platformDir, "spec-engine", key, "SPEC.json");
5488
5643
  await enforceStatementGrammar(platformDir, key, fields.requirement, "spec req");
5489
5644
  const domain2 = existsSync10(specPath) ? JSON.parse(await Bun.file(specPath).text()) : null;
5490
5645
  if (domain2 === null) {
@@ -5559,10 +5714,6 @@ var init_req = __esm(() => {
5559
5714
  type: "string",
5560
5715
  description: "Why it matters field (only with --text)"
5561
5716
  },
5562
- binds: {
5563
- type: "string",
5564
- description: "Binds field (only with --text)"
5565
- },
5566
5717
  lives: {
5567
5718
  type: "string",
5568
5719
  description: "Lives in field (only with --text)"
@@ -5583,16 +5734,15 @@ var init_req = __esm(() => {
5583
5734
  const key = resolveDomainKey(input, platformDir);
5584
5735
  const nextId = await nextRequirementId(platformDir, key);
5585
5736
  const textFlag = args.text;
5586
- const hasFieldFlags = typeof args.why === "string" || typeof args.binds === "string" || typeof args.lives === "string" || typeof args.issue === "string";
5737
+ const hasFieldFlags = typeof args.why === "string" || typeof args.lives === "string" || typeof args.issue === "string";
5587
5738
  if (textFlag === undefined && hasFieldFlags) {
5588
- console.error("spec req: --why/--binds/--lives/--issue require --text (the Requirement field is mandatory)");
5739
+ console.error("spec req: --why/--lives/--issue require --text (the Requirement field is mandatory)");
5589
5740
  process.exit(EXIT.USAGE);
5590
5741
  return;
5591
5742
  }
5592
5743
  if (textFlag !== undefined) {
5593
5744
  await authorFromFieldFlags(platformDir, key, nextId, textFlag, {
5594
5745
  why: args.why,
5595
- binds: args.binds,
5596
5746
  lives: args.lives,
5597
5747
  issue: args.issue,
5598
5748
  json: Boolean(args.json)
@@ -5616,13 +5766,11 @@ var init_req = __esm(() => {
5616
5766
  return;
5617
5767
  }
5618
5768
  const why = (await askLine("Why it matters: ")).trim();
5619
- const binds = (await askLine("Binds: ")).trim();
5620
5769
  const lives = (await askLine("Lives in: ")).trim();
5621
- warnUnresolvableRefs(platformDir, [requirement, why, binds, lives]);
5770
+ warnUnresolvableRefs(platformDir, [requirement, why, lives]);
5622
5771
  const relFile = await appendEntry(platformDir, key, nextId, {
5623
5772
  requirement,
5624
5773
  why,
5625
- binds,
5626
5774
  lives
5627
5775
  });
5628
5776
  console.log(`appended ${nextId} to ${relFile}`);
@@ -5644,7 +5792,7 @@ __export(exports_term, {
5644
5792
  termCommand: () => termCommand
5645
5793
  });
5646
5794
  import { existsSync as existsSync11 } from "fs";
5647
- import { join as join16, resolve as resolve17 } from "path";
5795
+ import { join as join18, resolve as resolve17 } from "path";
5648
5796
  import { defineCommand as defineCommand12 } from "citty";
5649
5797
  function parseAliases(raw) {
5650
5798
  if (typeof raw !== "string" || raw.trim() === "")
@@ -5661,7 +5809,7 @@ function resolvePlatform2(platformDirRaw) {
5661
5809
  return platformDir;
5662
5810
  }
5663
5811
  async function readTermEnvelope(platformDir, cmd) {
5664
- const specPath = join16(platformDir, "spec-engine", TERM_KEY2, "SPEC.json");
5812
+ const specPath = join18(platformDir, "spec-engine", TERM_KEY2, "SPEC.json");
5665
5813
  if (!existsSync11(specPath)) {
5666
5814
  console.error(`spec ${cmd}: no TERM domain (expected spec-engine/TERM/SPEC.json under ${platformDir})`);
5667
5815
  process.exit(EXIT.USAGE);
@@ -5670,7 +5818,7 @@ async function readTermEnvelope(platformDir, cmd) {
5670
5818
  }
5671
5819
  async function appendTermEntry(platformDir, id, fields) {
5672
5820
  const relFile = `spec-engine/${TERM_KEY2}/SPEC.json`;
5673
- const specPath = join16(platformDir, "spec-engine", TERM_KEY2, "SPEC.json");
5821
+ const specPath = join18(platformDir, "spec-engine", TERM_KEY2, "SPEC.json");
5674
5822
  const domain2 = await readTermEnvelope(platformDir, "term");
5675
5823
  const requirements = Array.isArray(domain2.requirements) ? domain2.requirements : [];
5676
5824
  const entry = {
@@ -5734,7 +5882,7 @@ async function authorOrQuery(opts) {
5734
5882
  }
5735
5883
  async function listTerms(platformDirRaw, json) {
5736
5884
  const platformDir = resolvePlatform2(platformDirRaw);
5737
- const specPath = join16(platformDir, "spec-engine", TERM_KEY2, "SPEC.json");
5885
+ const specPath = join18(platformDir, "spec-engine", TERM_KEY2, "SPEC.json");
5738
5886
  const domain2 = existsSync11(specPath) ? JSON.parse(await Bun.file(specPath).text()) : { requirements: [] };
5739
5887
  const requirements = Array.isArray(domain2.requirements) ? domain2.requirements : [];
5740
5888
  const rows = requirements.map((r) => ({
@@ -5766,7 +5914,7 @@ async function resolveReviseTarget(id, platformDirRaw, defRaw) {
5766
5914
  process.exit(EXIT.USAGE);
5767
5915
  }
5768
5916
  const relFile = `spec-engine/${TERM_KEY2}/SPEC.json`;
5769
- const specPath = join16(platformDir, "spec-engine", TERM_KEY2, "SPEC.json");
5917
+ const specPath = join18(platformDir, "spec-engine", TERM_KEY2, "SPEC.json");
5770
5918
  const domain2 = await readTermEnvelope(platformDir, "term revise");
5771
5919
  const requirements = Array.isArray(domain2.requirements) ? domain2.requirements : [];
5772
5920
  const req = requirements.find((r) => r?.id === id);
@@ -5842,7 +5990,7 @@ async function resolveConfirmTarget(platformDir, termId) {
5842
5990
  async function locateCitation(platformDir, reqId, termId) {
5843
5991
  const citKey = reqId.slice(0, reqId.indexOf("-"));
5844
5992
  const relFile = `spec-engine/${citKey}/SPEC.json`;
5845
- const specPath = join16(platformDir, "spec-engine", citKey, "SPEC.json");
5993
+ const specPath = join18(platformDir, "spec-engine", citKey, "SPEC.json");
5846
5994
  if (!existsSync11(specPath)) {
5847
5995
  console.error(`spec term confirm: no domain ${citKey} (expected ${relFile} under ${platformDir})`);
5848
5996
  process.exit(EXIT.USAGE);
@@ -6123,7 +6271,7 @@ __export(exports_supersede, {
6123
6271
  supersedeCommand: () => supersedeCommand
6124
6272
  });
6125
6273
  import { existsSync as existsSync12 } from "fs";
6126
- import { join as join17, resolve as resolve18 } from "path";
6274
+ import { join as join19, resolve as resolve18 } from "path";
6127
6275
  import { defineCommand as defineCommand13 } from "citty";
6128
6276
  async function resolveSupersedeTarget(id, platformDir) {
6129
6277
  if (!ID_RE.test(id)) {
@@ -6137,7 +6285,7 @@ async function resolveSupersedeTarget(id, platformDir) {
6137
6285
  }
6138
6286
  const key = id.slice(0, id.indexOf("-"));
6139
6287
  const relFile = `spec-engine/${key}/SPEC.json`;
6140
- const specPath = join17(platformDir, "spec-engine", key, "SPEC.json");
6288
+ const specPath = join19(platformDir, "spec-engine", key, "SPEC.json");
6141
6289
  if (!existsSync12(specPath)) {
6142
6290
  console.error(`spec supersede: no domain ${key} (expected ${relFile} under ${platformDir})`);
6143
6291
  process.exit(EXIT.USAGE);
@@ -6186,8 +6334,7 @@ function resolveSuccessorFields(args, req) {
6186
6334
  const predLives = Array.isArray(req.livesIn) && req.livesIn.length > 0 ? String(req.livesIn[0]) : "";
6187
6335
  const why = (args.why ?? predWhy).trim();
6188
6336
  const lives = (args.lives ?? predLives).trim();
6189
- const binds = (args.binds ?? "").trim();
6190
- return { why, lives, binds };
6337
+ return { why, lives };
6191
6338
  }
6192
6339
  function resolveSuccessorTermFields(args, req) {
6193
6340
  const predTerm = typeof req.term === "string" ? req.term : undefined;
@@ -6217,8 +6364,7 @@ function applySupersedeEdit(target, newId, requirement, why, lives, noBump, term
6217
6364
  supersededBy: null,
6218
6365
  relates: [],
6219
6366
  livesIn: lives === "" ? [] : [lives],
6220
- issues: issue ? [{ role: "created", id: issue }] : [],
6221
- changedAtVersion: 1
6367
+ issues: issue ? [{ role: "created", id: issue }] : []
6222
6368
  };
6223
6369
  if (termFields !== null) {
6224
6370
  if (termFields.term !== undefined)
@@ -6227,20 +6373,25 @@ function applySupersedeEdit(target, newId, requirement, why, lives, noBump, term
6227
6373
  }
6228
6374
  requirements.push(successor);
6229
6375
  domain2.requirements = requirements;
6230
- let reportedVersion;
6231
- if (domain2.key === "TERM") {
6232
- const currentVersion = typeof domain2.specVersion === "number" ? domain2.specVersion : 1;
6233
- reportedVersion = noBump ? null : currentVersion + 1;
6234
- if (reportedVersion !== null)
6235
- domain2.specVersion = reportedVersion;
6236
- req.supersededAtVersion = reportedVersion ?? currentVersion;
6237
- } else {
6238
- reportedVersion = deriveDomainVersion(requirements);
6239
- req.supersededAtVersion = reportedVersion;
6240
- }
6376
+ const reportedVersion = applyVersionStage(target, successor, noBump);
6241
6377
  domain2.updated = localToday();
6242
6378
  return reportedVersion;
6243
6379
  }
6380
+ function applyVersionStage(target, successor, noBump) {
6381
+ const { domain: domain2, requirements, req } = target;
6382
+ if (domain2.key !== "TERM") {
6383
+ const derived = deriveDomainVersion(requirements);
6384
+ req.supersededAtVersion = derived;
6385
+ return derived;
6386
+ }
6387
+ const currentVersion = typeof domain2.specVersion === "number" ? domain2.specVersion : 1;
6388
+ const reported = noBump ? null : currentVersion + 1;
6389
+ if (reported !== null)
6390
+ domain2.specVersion = reported;
6391
+ req.supersededAtVersion = reported ?? currentVersion;
6392
+ successor.changedAtVersion = reported ?? currentVersion;
6393
+ return reported;
6394
+ }
6244
6395
  async function reindexAndCollectRetag(platformDir, id) {
6245
6396
  const tags = await reindexAndListTags(platformDir, id);
6246
6397
  return tags.map(({ req_id, repo, file, line, kind, level }) => ({
@@ -6287,10 +6438,6 @@ var init_supersede = __esm(() => {
6287
6438
  type: "string",
6288
6439
  description: "Successor's Why it matters (default: copied from the old entry)"
6289
6440
  },
6290
- binds: {
6291
- type: "string",
6292
- description: "Binds value (validated for @-refs; not persisted in JSON \u2014 STOR-01)"
6293
- },
6294
6441
  lives: {
6295
6442
  type: "string",
6296
6443
  description: "Successor's Lives in (default: copied from the old entry)"
@@ -6322,8 +6469,8 @@ var init_supersede = __esm(() => {
6322
6469
  const target = await resolveSupersedeTarget(id, platformDir);
6323
6470
  const requirement = await resolveSuccessorText(args, id);
6324
6471
  await enforceStatementGrammar(platformDir, target.key, requirement, "spec supersede");
6325
- const { why, lives, binds } = resolveSuccessorFields(args, target.req);
6326
- warnUnresolvableRefs(platformDir, [requirement, why, binds, lives]);
6472
+ const { why, lives } = resolveSuccessorFields(args, target.req);
6473
+ warnUnresolvableRefs(platformDir, [requirement, why, lives]);
6327
6474
  const newId = await nextRequirementId(platformDir, target.key);
6328
6475
  const termFields = target.key === "TERM" ? resolveSuccessorTermFields(args, target.req) : null;
6329
6476
  const newVersion = applySupersedeEdit(target, newId, requirement, why, lives, Boolean(args.noBump), termFields, (args.issue ?? "").trim() || undefined);
@@ -6366,12 +6513,12 @@ __export(exports_deprecate, {
6366
6513
  deprecateCommand: () => deprecateCommand
6367
6514
  });
6368
6515
  import { existsSync as existsSync13 } from "fs";
6369
- import { join as join18, resolve as resolve19 } from "path";
6516
+ import { join as join20, resolve as resolve19 } from "path";
6370
6517
  import { defineCommand as defineCommand14 } from "citty";
6371
6518
  async function resolveDeprecateTarget(platformDir, id) {
6372
6519
  const key = id.slice(0, id.indexOf("-"));
6373
6520
  const relFile = `spec-engine/${key}/SPEC.json`;
6374
- const specPath = join18(platformDir, "spec-engine", key, "SPEC.json");
6521
+ const specPath = join20(platformDir, "spec-engine", key, "SPEC.json");
6375
6522
  if (!existsSync13(specPath)) {
6376
6523
  console.error(`spec deprecate: no domain ${key} (expected ${relFile} under ${platformDir})`);
6377
6524
  process.exit(EXIT.USAGE);
@@ -6481,11 +6628,11 @@ __export(exports_move, {
6481
6628
  moveCommand: () => moveCommand
6482
6629
  });
6483
6630
  import { existsSync as existsSync14 } from "fs";
6484
- import { join as join19, resolve as resolve20 } from "path";
6631
+ import { join as join21, resolve as resolve20 } from "path";
6485
6632
  import { defineCommand as defineCommand15 } from "citty";
6486
6633
  async function loadDomainSide(platformDir, key) {
6487
6634
  const relFile = `spec-engine/${key}/SPEC.json`;
6488
- const specPath = join19(platformDir, "spec-engine", key, "SPEC.json");
6635
+ const specPath = join21(platformDir, "spec-engine", key, "SPEC.json");
6489
6636
  if (!existsSync14(specPath))
6490
6637
  return null;
6491
6638
  const domain2 = JSON.parse(await Bun.file(specPath).text());
@@ -6719,14 +6866,13 @@ __export(exports_amend, {
6719
6866
  amendCommand: () => amendCommand
6720
6867
  });
6721
6868
  import { existsSync as existsSync15 } from "fs";
6722
- import { join as join20, resolve as resolve21 } from "path";
6869
+ import { join as join22, resolve as resolve21 } from "path";
6723
6870
  import { defineCommand as defineCommand16 } from "citty";
6724
6871
  function validateAmendFields(args) {
6725
6872
  const hasText = typeof args.text === "string";
6726
6873
  const hasIssue = typeof args.issue === "string" && args.issue.trim() !== "";
6727
6874
  const hasWhy = typeof args.why === "string";
6728
6875
  const hasLives = typeof args.lives === "string";
6729
- const hasBinds = typeof args.binds === "string";
6730
6876
  const hasTerm = typeof args.term === "string";
6731
6877
  const hasAliases = typeof args.aliases === "string";
6732
6878
  if (!hasText && !hasWhy && !hasLives && !hasTerm && !hasAliases && !hasIssue) {
@@ -6741,12 +6887,12 @@ function validateAmendFields(args) {
6741
6887
  console.error("spec amend: --term must be a non-empty headword");
6742
6888
  process.exit(EXIT.USAGE);
6743
6889
  }
6744
- return { hasText, hasWhy, hasLives, hasBinds, hasTerm, hasAliases, hasIssue };
6890
+ return { hasText, hasWhy, hasLives, hasTerm, hasAliases, hasIssue };
6745
6891
  }
6746
6892
  async function locateAmendEntry(platformDir, id) {
6747
6893
  const key = id.slice(0, id.indexOf("-"));
6748
6894
  const relFile = `spec-engine/${key}/SPEC.json`;
6749
- const specPath = join20(platformDir, "spec-engine", key, "SPEC.json");
6895
+ const specPath = join22(platformDir, "spec-engine", key, "SPEC.json");
6750
6896
  if (!existsSync15(specPath)) {
6751
6897
  console.error(`spec amend: no domain ${key} (expected ${relFile} under ${platformDir})`);
6752
6898
  process.exit(EXIT.USAGE);
@@ -6800,9 +6946,6 @@ function applyAmendMutations(req, args, fields) {
6800
6946
  fieldsChanged.push("lives");
6801
6947
  refValues.push(v);
6802
6948
  }
6803
- if (fields.hasBinds) {
6804
- refValues.push(args.binds.trim());
6805
- }
6806
6949
  if (fields.hasTerm) {
6807
6950
  const v = args.term.trim();
6808
6951
  req.term = v;
@@ -6849,10 +6992,6 @@ var init_amend = __esm(() => {
6849
6992
  },
6850
6993
  text: { type: "string", description: "New Requirement (statement) field value" },
6851
6994
  why: { type: "string", description: "New Why it matters field value" },
6852
- binds: {
6853
- type: "string",
6854
- description: "Binds value (validated for @-refs; not persisted in JSON \u2014 STOR-01)"
6855
- },
6856
6995
  lives: { type: "string", description: "New Lives in (livesIn) field value" },
6857
6996
  issue: {
6858
6997
  type: "string",
@@ -7121,7 +7260,7 @@ __export(exports_resolve, {
7121
7260
  resolveCommand: () => resolveCommand
7122
7261
  });
7123
7262
  import { statSync as statSync6 } from "fs";
7124
- import { join as join21, relative, resolve as resolve23, sep as sep3 } from "path";
7263
+ import { join as join23, relative, resolve as resolve23, sep as sep3 } from "path";
7125
7264
  import { defineCommand as defineCommand18 } from "citty";
7126
7265
  function classifyArg(a) {
7127
7266
  if (a.startsWith("--")) {
@@ -7160,7 +7299,7 @@ function splitFilesAndPlatformDir(positionals, fallbackPlatformDir) {
7160
7299
  const resolved = resolve23(last);
7161
7300
  if (statSync6(resolved).isDirectory()) {
7162
7301
  try {
7163
- if (statSync6(join21(resolved, "spec-engine")).isDirectory()) {
7302
+ if (statSync6(join23(resolved, "spec-engine")).isDirectory()) {
7164
7303
  lastIsPlatformDir = true;
7165
7304
  } else {
7166
7305
  lastIsDirWithoutSpecEngine = true;
@@ -7456,7 +7595,7 @@ __export(exports_gate, {
7456
7595
  gateCommand: () => gateCommand
7457
7596
  });
7458
7597
  import { existsSync as existsSync16, mkdirSync as mkdirSync7 } from "fs";
7459
- import { dirname as dirname10, join as join22, resolve as resolve24 } from "path";
7598
+ import { dirname as dirname10, join as join24, resolve as resolve24 } from "path";
7460
7599
  import { defineCommand as defineCommand19 } from "citty";
7461
7600
  function isEpipe(e) {
7462
7601
  if (e === null || typeof e !== "object")
@@ -7472,7 +7611,7 @@ function resolveGateArgs(args) {
7472
7611
  process.exit(EXIT.USAGE);
7473
7612
  }
7474
7613
  const platformDir = resolve24(args.platformDir ?? process.cwd());
7475
- const hasSpecEngineDir = existsSync16(join22(platformDir, "spec-engine"));
7614
+ const hasSpecEngineDir = existsSync16(join24(platformDir, "spec-engine"));
7476
7615
  if (!hasSpecEngineDir) {
7477
7616
  console.error(formatNotASpecPlatform(platformDir));
7478
7617
  process.exit(EXIT.USAGE);
@@ -7598,7 +7737,7 @@ var init_gate = __esm(() => {
7598
7737
  });
7599
7738
 
7600
7739
  // src/guard/collect.ts
7601
- import { join as join23 } from "path";
7740
+ import { join as join25 } from "path";
7602
7741
  function isIgnored(path) {
7603
7742
  return isPathIgnored(path);
7604
7743
  }
@@ -7655,7 +7794,7 @@ async function collectWorktreeReqs(canonicalDir) {
7655
7794
  const activeIds = new Set;
7656
7795
  const supersedesTargets = new Set;
7657
7796
  for (const rel of await findDomainJsonFiles(canonicalDir)) {
7658
- const text = await Bun.file(join23(canonicalDir, rel)).text();
7797
+ const text = await Bun.file(join25(canonicalDir, rel)).text();
7659
7798
  for (const r of parseReqs(text)) {
7660
7799
  ids.add(r.id);
7661
7800
  if (r.status.toLowerCase() === "active")
@@ -7683,7 +7822,7 @@ async function collectFacts(platformDir, ref, storage2) {
7683
7822
  const deletedSpecFiles = changed.filter((e) => e.status === "D" && isSpecFile(e.path)).map((e) => e.path);
7684
7823
  const base = collectBaseReqs2(platformDir, ref, specPaths);
7685
7824
  const baseTags = collectBaseTags(platformDir, ref, codePaths);
7686
- const worktree = await collectWorktreeReqs(join23(platformDir, CANONICAL_SPECS_DIR));
7825
+ const worktree = await collectWorktreeReqs(join25(platformDir, CANONICAL_SPECS_DIR));
7687
7826
  const counts = tallyWorktreeTags(storage2);
7688
7827
  return {
7689
7828
  baseReqs: base.reqs,
@@ -10749,7 +10888,7 @@ var init_server = __esm(() => {
10749
10888
 
10750
10889
  // src/server/api.ts
10751
10890
  import { existsSync as existsSync17 } from "fs";
10752
- import { join as join24 } from "path";
10891
+ import { join as join26 } from "path";
10753
10892
  function hasTraversalSegment(p) {
10754
10893
  return p.split(/[/\\]/).some((seg) => seg === "..");
10755
10894
  }
@@ -10856,7 +10995,7 @@ function resolveCreateTarget(c, platformDir, body) {
10856
10995
  return { ok: false, res: c.json({ error: "unknown domain key" }, 400) };
10857
10996
  }
10858
10997
  const relFile = `spec-engine/${key}/SPEC.json`;
10859
- const specPath = join24(platformDir, "spec-engine", key, "SPEC.json");
10998
+ const specPath = join26(platformDir, "spec-engine", key, "SPEC.json");
10860
10999
  if (!existsSync17(specPath))
10861
11000
  return { ok: false, res: c.json({ error: "not found" }, 404) };
10862
11001
  return { ok: true, key, relFile, specPath };
@@ -10876,7 +11015,7 @@ function buildRequirement(body, id) {
10876
11015
  }
10877
11016
  async function locateRequirement(platformDir, id) {
10878
11017
  for (const key of listDomainKeys(platformDir)) {
10879
- const specPath = join24(platformDir, "spec-engine", key, "SPEC.json");
11018
+ const specPath = join26(platformDir, "spec-engine", key, "SPEC.json");
10880
11019
  const relFile = `spec-engine/${key}/SPEC.json`;
10881
11020
  let domain2;
10882
11021
  try {
@@ -11304,12 +11443,12 @@ __export(exports_docs, {
11304
11443
  createDocsFetchHandler: () => createDocsFetchHandler
11305
11444
  });
11306
11445
  import { existsSync as existsSync19, statSync as statSync7 } from "fs";
11307
- import { join as join25, normalize, sep as sep4 } from "path";
11446
+ import { join as join27, normalize, sep as sep4 } from "path";
11308
11447
  import { fileURLToPath } from "url";
11309
11448
  import { defineCommand as defineCommand22 } from "citty";
11310
11449
  function resolveDocsRootFrom(candidates) {
11311
11450
  for (const candidate of candidates) {
11312
- if (existsSync19(join25(candidate, "index.html")))
11451
+ if (existsSync19(join27(candidate, "index.html")))
11313
11452
  return candidate;
11314
11453
  }
11315
11454
  return null;
@@ -11333,16 +11472,16 @@ function createDocsFetchHandler(root) {
11333
11472
  if (pathname.includes("\x00")) {
11334
11473
  return new Response("Bad request", { status: 400 });
11335
11474
  }
11336
- const target = normalize(join25(normalRoot, pathname));
11475
+ const target = normalize(join27(normalRoot, pathname));
11337
11476
  if (target !== normalRoot && !target.startsWith(rootPrefix)) {
11338
11477
  return new Response("Not found", { status: 404 });
11339
11478
  }
11340
- for (const candidate of [target, join25(target, "index.html"), `${target}.html`]) {
11479
+ for (const candidate of [target, join27(target, "index.html"), `${target}.html`]) {
11341
11480
  if (existsSync19(candidate) && statSync7(candidate).isFile()) {
11342
11481
  return new Response(Bun.file(candidate));
11343
11482
  }
11344
11483
  }
11345
- const notFoundPage = join25(normalRoot, "404.html");
11484
+ const notFoundPage = join27(normalRoot, "404.html");
11346
11485
  if (existsSync19(notFoundPage)) {
11347
11486
  return new Response(Bun.file(notFoundPage), { status: 404 });
11348
11487
  }
@@ -11520,7 +11659,7 @@ import { defineCommand as defineCommand24, runMain, showUsage } from "citty";
11520
11659
  // package.json
11521
11660
  var package_default = {
11522
11661
  name: "@spec-engine/spec-engine",
11523
- version: "0.2.0",
11662
+ version: "0.3.0",
11524
11663
  description: "Cross-repo spec engine \u2014 durable requirements with permanent IDs, bound to code via @spec tags, checked in CI. Ships the spec CLI, the local coverage webapp (spec serve), and offline docs (spec docs).",
11525
11664
  license: "MIT",
11526
11665
  type: "module",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spec-engine/spec-engine",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Cross-repo spec engine — 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).",
5
5
  "license": "MIT",
6
6
  "type": "module",