@spec-engine/spec-engine 0.8.0 → 0.8.1

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.
@@ -97,7 +97,7 @@
97
97
  <div class="sl-heading-wrapper level-h2"><h2 id="path-rules-for-spec-resolve">Path rules for <code dir="auto">spec resolve</code></h2><a class="sl-anchor-link" href="#path-rules-for-spec-resolve"><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 “Path rules for spec resolve”</span></a></div>
98
98
  <ul>
99
99
  <li><strong>Multi-repo platform:</strong> pass <code dir="auto">&lt;repo&gt;/&lt;path&gt;</code> — e.g., <code dir="auto">spec resolve api/src/renew.ts .</code></li>
100
- <li><strong>Single-repo (rung-1):</strong> both repo-relative (<code dir="auto">src/orders.ts</code>) and basename-prefixed forms accepted</li>
100
+ <li><strong>Single-repo (rung-1):</strong> pass the path from the platform root (<code dir="auto">src/orders.ts</code>, <code dir="auto">.github/workflows/ci.yml</code>); a lone repository’s files carry no directory-name prefix</li>
101
101
  <li>Absolute paths normalized against <code dir="auto">platformDir</code>; paths outside it exit 2</li>
102
102
  </ul>
103
103
  <div class="sl-heading-wrapper level-h2"><h2 id="gotchas">Gotchas</h2><a class="sl-anchor-link" href="#gotchas"><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 “Gotchas”</span></a></div>
@@ -1 +1 @@
1
- {"version":"1.5.2","languages":{"en":{"hash":"en_d0b5c328bc","wasm":"en","page_count":12}},"include_characters":["_","‿","⁀","⁔","︳","︴","﹍","﹎","﹏","_"]}
1
+ {"version":"1.5.2","languages":{"en":{"hash":"en_ced49c317e","wasm":"en","page_count":12}},"include_characters":["_","‿","⁀","⁔","︳","︴","﹍","﹎","﹏","_"]}
package/dist/impl.js CHANGED
@@ -20,7 +20,7 @@ var package_default;
20
20
  var init_package = __esm(() => {
21
21
  package_default = {
22
22
  name: "@spec-engine/spec-engine",
23
- version: "0.8.0",
23
+ version: "0.8.1",
24
24
  description: "Cross-repo spec engine \u2014 durable requirements with permanent IDs, bound to code via @spec tags, checked in CI. Ships the spec CLI, the local coverage webapp (spec serve), and offline docs (spec docs).",
25
25
  license: "MIT",
26
26
  type: "module",
@@ -73,6 +73,31 @@ var init_package = __esm(() => {
73
73
  };
74
74
  });
75
75
 
76
+ // src/stdout.ts
77
+ import { format } from "util";
78
+ function isEpipe(e) {
79
+ return typeof e === "object" && e !== null && "code" in e && (e.code === "EPIPE" || e.code === "ERR_STREAM_DESTROYED");
80
+ }
81
+ function routeStdout() {
82
+ process.stdout.on("error", (e) => {
83
+ if (!isEpipe(e))
84
+ throw e;
85
+ });
86
+ console.log = (...args) => {
87
+ const text = `${format(...args)}
88
+ `;
89
+ lastWrite = new Promise((written) => process.stdout.write(text, () => written()));
90
+ };
91
+ }
92
+ async function exit(code) {
93
+ await lastWrite;
94
+ process.exit(code);
95
+ }
96
+ var lastWrite;
97
+ var init_stdout = __esm(() => {
98
+ lastWrite = Promise.resolve();
99
+ });
100
+
76
101
  // ../shared/src/diagnostics.ts
77
102
  var DiagnosticCode, NotASpecPlatformError, InvalidDomainFilesError;
78
103
  var init_diagnostics = __esm(() => {
@@ -2193,33 +2218,9 @@ class SqliteStorage {
2193
2218
  if (files.length > FILES_MAX) {
2194
2219
  throw new Error(`resolveByFiles: too many files (max ${FILES_MAX} per call; got ${files.length})`);
2195
2220
  }
2196
- const queryFiles = this.#expandSelfMemberFiles(files);
2197
- if (queryFiles.length > 32000) {
2198
- throw new Error(`resolveByFiles: expanded bind count ${queryFiles.length} would exceed ` + "SQLITE_MAX_VARIABLE_NUMBER (32766); lower FILES_MAX");
2199
- }
2200
- const placeholders = queryFiles.map(() => "?").join(",");
2221
+ const placeholders = files.map(() => "?").join(",");
2201
2222
  const sql = RESOLVE_BY_FILES_SQL_PREFIX + placeholders + RESOLVE_BY_FILES_SQL_SUFFIX;
2202
- return this.#db.query(sql).all(...queryFiles);
2203
- }
2204
- #selfMemberName() {
2205
- const canonical = this.getRepo("spec-engine");
2206
- if (!canonical)
2207
- return null;
2208
- const platformRoot = dirname2(canonical.path);
2209
- const row = this.#db.query("SELECT name FROM repos WHERE name != 'spec-engine' AND path = $path").get({ path: platformRoot });
2210
- return row?.name ?? null;
2211
- }
2212
- #expandSelfMemberFiles(files) {
2213
- const self = this.#selfMemberName();
2214
- if (!self)
2215
- return files;
2216
- const prefix = `${self}/`;
2217
- const expanded = [...files];
2218
- for (const f of files) {
2219
- if (!f.startsWith(prefix))
2220
- expanded.push(prefix + f);
2221
- }
2222
- return expanded;
2223
+ return this.#db.query(sql).all(...files);
2223
2224
  }
2224
2225
  withWriteTx(fn) {
2225
2226
  const wrapped = this.#db.transaction((arg) => fn(arg));
@@ -2702,7 +2703,10 @@ function scannedTagFile(members, platformDir, platformRel) {
2702
2703
  return null;
2703
2704
  if (!isTagSource(rel) && !isDocSource(rel))
2704
2705
  return null;
2705
- return `${member.name}/${rel}`;
2706
+ return tagFile(member, rel);
2707
+ }
2708
+ function tagFile(member, rel) {
2709
+ return member.selfMember ? rel : `${member.name}/${rel}`;
2706
2710
  }
2707
2711
  async function scanMemberCode(member, extraIgnore) {
2708
2712
  const hitsOut = [];
@@ -2710,7 +2714,7 @@ async function scanMemberCode(member, extraIgnore) {
2710
2714
  for (const rel of codePaths) {
2711
2715
  const absPath = join4(member.path, rel);
2712
2716
  const text = await Bun.file(absPath).text();
2713
- const platformRelative = `${member.name}/${rel}`;
2717
+ const platformRelative = tagFile(member, rel);
2714
2718
  const hits = scanTagsInFile(member.name, platformRelative, text);
2715
2719
  for (const h of hits)
2716
2720
  hitsOut.push(h);
@@ -2724,7 +2728,7 @@ async function scanMemberDocs(member, extraIgnore) {
2724
2728
  for (const rel of docPaths) {
2725
2729
  const absPath = join4(member.path, rel);
2726
2730
  const text = await Bun.file(absPath).text();
2727
- const platformRelative = `${member.name}/${rel}`;
2731
+ const platformRelative = tagFile(member, rel);
2728
2732
  const { tags, mentions } = scanDocFile(member.name, platformRelative, text);
2729
2733
  for (const t of tags)
2730
2734
  tagsOut.push(t);
@@ -3348,14 +3352,14 @@ function handleNotAPlatform(e) {
3348
3352
  }
3349
3353
  throw e;
3350
3354
  }
3351
- function handleInvalidDomainFiles(e, json, code = EXIT.FAILURE) {
3355
+ async function handleInvalidDomainFiles(e, json, code = EXIT.FAILURE) {
3352
3356
  if (!(e instanceof InvalidDomainFilesError))
3353
3357
  return;
3354
3358
  if (json)
3355
3359
  console.log(renderDiagnostics(e.diagnostics, "json"));
3356
3360
  else
3357
3361
  console.error(renderDiagnostics(e.diagnostics, "text"));
3358
- process.exit(code);
3362
+ await exit(code);
3359
3363
  }
3360
3364
  function cliFreshTags(platformDir) {
3361
3365
  const fresh = coldFreshTags(platformDir);
@@ -3363,7 +3367,7 @@ function cliFreshTags(platformDir) {
3363
3367
  try {
3364
3368
  return await fresh(reqId);
3365
3369
  } catch (e) {
3366
- handleInvalidDomainFiles(e, false, EXIT.USAGE);
3370
+ await handleInvalidDomainFiles(e, false, EXIT.USAGE);
3367
3371
  throw e;
3368
3372
  }
3369
3373
  };
@@ -3405,7 +3409,7 @@ async function withReadStorage(opts, fn) {
3405
3409
  });
3406
3410
  } catch (e) {
3407
3411
  handleStorageUnavailable(e, dbPath);
3408
- handleInvalidDomainFiles(e, json);
3412
+ await handleInvalidDomainFiles(e, json);
3409
3413
  handleNotAPlatform(e);
3410
3414
  }
3411
3415
  }
@@ -3414,6 +3418,7 @@ var init__shared = __esm(() => {
3414
3418
  init_constants();
3415
3419
  init_discover();
3416
3420
  init__index();
3421
+ init_stdout();
3417
3422
  init_errors();
3418
3423
  init_sqlite();
3419
3424
  });
@@ -3719,7 +3724,7 @@ var init_commands = __esm(() => {
3719
3724
  console.error(formatNotASpecPlatform(err.platformDir));
3720
3725
  process.exit(EXIT.USAGE);
3721
3726
  }
3722
- handleInvalidDomainFiles(err, !!args.json);
3727
+ await handleInvalidDomainFiles(err, !!args.json);
3723
3728
  console.error("spec index FAILED:", err);
3724
3729
  process.exit(EXIT.FAILURE);
3725
3730
  } finally {
@@ -5699,6 +5704,7 @@ var init_check2 = __esm(() => {
5699
5704
  init_prompt();
5700
5705
  init__index();
5701
5706
  init_check();
5707
+ init_stdout();
5702
5708
  init__args();
5703
5709
  init__shared();
5704
5710
  checkCommand = defineCommand3({
@@ -5772,7 +5778,7 @@ var init_check2 = __esm(() => {
5772
5778
  return result;
5773
5779
  });
5774
5780
  } catch (e) {
5775
- handleInvalidDomainFiles(e, jsonMode);
5781
+ await handleInvalidDomainFiles(e, jsonMode);
5776
5782
  exitAfterCrash(e);
5777
5783
  }
5778
5784
  if (outcome === undefined) {
@@ -5790,7 +5796,7 @@ var init_check2 = __esm(() => {
5790
5796
  console.log(renderDiagnostics(diagnostics2, jsonMode ? "json" : "text"));
5791
5797
  if (!jsonMode)
5792
5798
  console.log(`build_id: ${outcome.buildId}`);
5793
- process.exit(outcome.failing ? EXIT.FAILURE : EXIT.OK);
5799
+ await exit(outcome.failing ? EXIT.FAILURE : EXIT.OK);
5794
5800
  }
5795
5801
  });
5796
5802
  });
@@ -7431,8 +7437,8 @@ async function confirmTermCommand(opts) {
7431
7437
  exitOnFailure("spec term confirm", result);
7432
7438
  await withIndex({ platformDir, build: "fresh" }, () => {
7433
7439
  return;
7434
- }).catch((e) => {
7435
- handleInvalidDomainFiles(e, false, EXIT.USAGE);
7440
+ }).catch(async (e) => {
7441
+ await handleInvalidDomainFiles(e, false, EXIT.USAGE);
7436
7442
  throw e;
7437
7443
  });
7438
7444
  const { pinned, file } = result;
@@ -7639,7 +7645,7 @@ async function generate(platformDir, json) {
7639
7645
  else
7640
7646
  console.log(`generated GLOSSARY.md from ${result.generated} terms`);
7641
7647
  }
7642
- function check2(platformDir, json) {
7648
+ async function check2(platformDir, json) {
7643
7649
  if (checkGlossary(platformDir).clean) {
7644
7650
  if (json)
7645
7651
  console.log(JSON.stringify({ ok: true }));
@@ -7651,13 +7657,14 @@ function check2(platformDir, json) {
7651
7657
  console.error("run `spec glossary .` to regenerate the human view from the store");
7652
7658
  if (json)
7653
7659
  console.log(JSON.stringify({ ok: false }));
7654
- process.exit(EXIT.FAILURE);
7660
+ await exit(EXIT.FAILURE);
7655
7661
  }
7656
7662
  var glossaryCommand;
7657
7663
  var init_glossary2 = __esm(() => {
7658
7664
  init_constants();
7659
7665
  init_discover();
7660
7666
  init_glossary();
7667
+ init_stdout();
7661
7668
  init__args();
7662
7669
  init__shared();
7663
7670
  glossaryCommand = defineCommand14({
@@ -7683,7 +7690,7 @@ var init_glossary2 = __esm(() => {
7683
7690
  if (args.migrate)
7684
7691
  await migrate(platformDir, json);
7685
7692
  else if (args.check)
7686
- check2(platformDir, json);
7693
+ await check2(platformDir, json);
7687
7694
  else
7688
7695
  await generate(platformDir, json);
7689
7696
  }
@@ -9395,7 +9402,7 @@ __export(exports_gate, {
9395
9402
  import { existsSync as existsSync13 } from "fs";
9396
9403
  import { join as join15 } from "path";
9397
9404
  import { defineCommand as defineCommand22 } from "citty";
9398
- function isEpipe(e) {
9405
+ function isEpipe2(e) {
9399
9406
  if (e === null || typeof e !== "object" || !("code" in e))
9400
9407
  return false;
9401
9408
  const code = e.code;
@@ -9405,7 +9412,7 @@ function writeStdoutTolerant(line) {
9405
9412
  try {
9406
9413
  console.log(line);
9407
9414
  } catch (writeErr) {
9408
- if (!isEpipe(writeErr))
9415
+ if (!isEpipe2(writeErr))
9409
9416
  throw writeErr;
9410
9417
  }
9411
9418
  }
@@ -9430,6 +9437,7 @@ var init_gate2 = __esm(() => {
9430
9437
  init_prompt();
9431
9438
  init__index();
9432
9439
  init_gate();
9440
+ init_stdout();
9433
9441
  init__args();
9434
9442
  init__shared();
9435
9443
  gateArgs = {
@@ -9490,7 +9498,7 @@ var init_gate2 = __esm(() => {
9490
9498
  writeStdoutTolerant(`build_id: ${gated.buildId}`);
9491
9499
  }
9492
9500
  } catch (e) {
9493
- handleInvalidDomainFiles(e, Boolean(args.json));
9501
+ await handleInvalidDomainFiles(e, Boolean(args.json));
9494
9502
  const msg = e instanceof Error ? e.message : String(e);
9495
9503
  console.error(`spec gate: crashed: ${msg}`);
9496
9504
  process.exit(EXIT.USAGE);
@@ -9500,7 +9508,7 @@ var init_gate2 = __esm(() => {
9500
9508
  process.exit(EXIT.USAGE);
9501
9509
  return;
9502
9510
  }
9503
- process.exit(gated.outcome.pass ? EXIT.OK : EXIT.FAILURE);
9511
+ await exit(gated.outcome.pass ? EXIT.OK : EXIT.FAILURE);
9504
9512
  }
9505
9513
  });
9506
9514
  });
@@ -9586,6 +9594,7 @@ var guardCommand;
9586
9594
  var init_guard2 = __esm(() => {
9587
9595
  init_constants();
9588
9596
  init_guard();
9597
+ init_stdout();
9589
9598
  init__args();
9590
9599
  init__shared();
9591
9600
  guardCommand = defineCommand23({
@@ -9611,14 +9620,14 @@ var init_guard2 = __esm(() => {
9611
9620
  if (!gitRefResolves(platformDir, ref)) {
9612
9621
  console.error(`spec guard: NOT_A_GIT_REPO \u2014 ref '${ref}' does not resolve in ${platformDir} ` + "(not a git repo, a fresh repo with no commit, or an unfetched/misspelled ref); " + "skipping loss detection");
9613
9622
  console.log(jsonMode ? "[]" : `\u2713 spec guard: no requirements about to be lost (non-git context)`);
9614
- process.exit(EXIT.OK);
9623
+ await exit(EXIT.OK);
9615
9624
  }
9616
9625
  let losses = [];
9617
9626
  await withReadStorage({ platformDir, dbPath: defaultIndexPath(platformDir), fresh: true, json: jsonMode }, async (storage2) => {
9618
9627
  losses = (await guard(storage2, platformDir, ref)).losses;
9619
9628
  });
9620
9629
  console.log(renderGuard(losses, jsonMode ? "json" : "text", ref));
9621
- process.exit(losses.length > 0 ? EXIT.FAILURE : EXIT.OK);
9630
+ await exit(losses.length > 0 ? EXIT.FAILURE : EXIT.OK);
9622
9631
  }
9623
9632
  });
9624
9633
  });
@@ -12901,12 +12910,12 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
12901
12910
  if (!featureEnabled("relations")) {
12902
12911
  return c.json({ error: featureDisabledMessage("relations") }, 404);
12903
12912
  }
12904
- const format = c.req.query("format");
12905
- if (format !== undefined && format !== "mermaid") {
12913
+ const format2 = c.req.query("format");
12914
+ if (format2 !== undefined && format2 !== "mermaid") {
12906
12915
  return c.json({ error: 'format must be "mermaid" when provided' }, 400);
12907
12916
  }
12908
12917
  const rows = relations(storage2).rows;
12909
- if (format === "mermaid") {
12918
+ if (format2 === "mermaid") {
12910
12919
  return c.text(renderRelations(rows, "mermaid"));
12911
12920
  }
12912
12921
  return c.json(sortRelations(rows));
@@ -13161,7 +13170,7 @@ async function runProbe() {
13161
13170
  }
13162
13171
  console.log("serve --probe OK");
13163
13172
  server.stop();
13164
- process.exit(EXIT.OK);
13173
+ await exit(EXIT.OK);
13165
13174
  }
13166
13175
  var serveCommand;
13167
13176
  var init_serve = __esm(() => {
@@ -13173,6 +13182,7 @@ var init_serve = __esm(() => {
13173
13182
  init_prompt();
13174
13183
  init_api();
13175
13184
  init_lan();
13185
+ init_stdout();
13176
13186
  init_sqlite();
13177
13187
  init__args();
13178
13188
  init__shared();
@@ -13270,7 +13280,7 @@ var init_serve = __esm(() => {
13270
13280
  });
13271
13281
  } catch (err) {
13272
13282
  storage2.close();
13273
- handleInvalidDomainFiles(err, false);
13283
+ await handleInvalidDomainFiles(err, false);
13274
13284
  handleStorageUnavailable(err, dbPath);
13275
13285
  console.error(`spec serve: failed to start on ${urlHost(host, port)}:`, err);
13276
13286
  process.exit(EXIT.FAILURE);
@@ -13369,7 +13379,7 @@ async function runProbe2(root) {
13369
13379
  }
13370
13380
  console.log("docs --probe OK");
13371
13381
  server.stop();
13372
- process.exit(EXIT.OK);
13382
+ await exit(EXIT.OK);
13373
13383
  }
13374
13384
  function exitNoDocsRoot() {
13375
13385
  console.error("spec docs: no built documentation found. From a checkout run `bun run build:site` " + "(writes packages/site/dist); the published npm package ships it at dist/docs.");
@@ -13378,6 +13388,7 @@ function exitNoDocsRoot() {
13378
13388
  var docsCommand;
13379
13389
  var init_docs2 = __esm(() => {
13380
13390
  init_constants();
13391
+ init_stdout();
13381
13392
  docsCommand = defineCommand25({
13382
13393
  meta: {
13383
13394
  name: "docs",
@@ -13511,7 +13522,9 @@ var init___smoke = __esm(() => {
13511
13522
 
13512
13523
  // src/cli.ts
13513
13524
  init_package();
13525
+ init_stdout();
13514
13526
  import { defineCommand as defineCommand27, runMain, showUsage } from "citty";
13527
+ routeStdout();
13515
13528
  var main = defineCommand27({
13516
13529
  meta: {
13517
13530
  name: "spec",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spec-engine/spec-engine",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
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",