@spec-engine/spec-engine 0.7.0 → 0.7.2

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.
Files changed (54) hide show
  1. package/dist/docs/404.html +4 -4
  2. package/dist/docs/_astro/MobileTableOfContents.astro_astro_type_script_index_0_lang.BcSo_yiZ.js +1 -0
  3. package/dist/docs/_astro/Search.astro_astro_type_script_index_0_lang.Djc9ZKGJ.js +1 -0
  4. package/dist/docs/_astro/TableOfContents.astro_astro_type_script_index_0_lang.Csloo1VZ.js +1 -0
  5. package/dist/docs/_astro/common.BPl3nku0.css +3 -0
  6. package/dist/docs/_astro/components.BeLFs6cp.css +1 -0
  7. package/dist/docs/_astro/page.cNngjUgL.js +1 -0
  8. package/dist/docs/_astro/print.ehPL0gv-.css +1 -0
  9. package/dist/docs/_astro/starlight-toc.CTCQXEge.js +1 -0
  10. package/dist/docs/_astro/ui-core.B94qsovR.js +2 -0
  11. package/dist/docs/agent-reference/index.html +23 -22
  12. package/dist/docs/architecture/index.html +423 -422
  13. package/dist/docs/checks/index.html +247 -246
  14. package/dist/docs/commands/index.html +127 -123
  15. package/dist/docs/format/index.html +92 -91
  16. package/dist/docs/getting-started/index.html +21 -20
  17. package/dist/docs/guides/adoption/index.html +13 -12
  18. package/dist/docs/guides/platform-setup/index.html +15 -14
  19. package/dist/docs/guides/single-repo/index.html +12 -11
  20. package/dist/docs/index.html +2 -152
  21. package/dist/docs/overview/index.html +8 -8
  22. package/dist/docs/pagefind/fragment/en_70e8a6f.pf_fragment +0 -0
  23. package/dist/docs/pagefind/fragment/en_907a67d.pf_fragment +0 -0
  24. package/dist/docs/pagefind/fragment/en_b81dc52.pf_fragment +0 -0
  25. package/dist/docs/pagefind/fragment/en_c8cc99c.pf_fragment +0 -0
  26. package/dist/docs/pagefind/fragment/en_de9d958.pf_fragment +0 -0
  27. package/dist/docs/pagefind/fragment/en_e616a50.pf_fragment +0 -0
  28. package/dist/docs/pagefind/fragment/en_f77a622.pf_fragment +0 -0
  29. package/dist/docs/pagefind/index/en_19cbfc6.pf_index +0 -0
  30. package/dist/docs/pagefind/index/en_4be71c4.pf_index +0 -0
  31. package/dist/docs/pagefind/pagefind-entry.json +1 -1
  32. package/dist/docs/pagefind/pagefind.en_f697eb692e.pf_meta +0 -0
  33. package/dist/docs/platform-mapping/index.html +162 -161
  34. package/dist/docs/tags/index.html +74 -73
  35. package/dist/impl.js +283 -32
  36. package/package.json +2 -2
  37. package/dist/docs/_astro/Code.CAimF0uL.css +0 -1
  38. package/dist/docs/_astro/MobileTableOfContents.astro_astro_type_script_index_0_lang.hwBsy0Mo.js +0 -1
  39. package/dist/docs/_astro/Search.astro_astro_type_script_index_0_lang.lq3t8uE2.js +0 -1
  40. package/dist/docs/_astro/TableOfContents.astro_astro_type_script_index_0_lang.FuRcXuRY.js +0 -1
  41. package/dist/docs/_astro/common.DslAiFsT.css +0 -1
  42. package/dist/docs/_astro/page.B_tncCx8.js +0 -1
  43. package/dist/docs/_astro/print.DNXP8c50.css +0 -1
  44. package/dist/docs/_astro/ui-core.C8327L_n.js +0 -2
  45. package/dist/docs/pagefind/fragment/en_435dca1.pf_fragment +0 -0
  46. package/dist/docs/pagefind/fragment/en_4bdd17f.pf_fragment +0 -0
  47. package/dist/docs/pagefind/fragment/en_6ce6fae.pf_fragment +0 -0
  48. package/dist/docs/pagefind/fragment/en_775cc8c.pf_fragment +0 -0
  49. package/dist/docs/pagefind/fragment/en_a3cd8bf.pf_fragment +0 -0
  50. package/dist/docs/pagefind/fragment/en_b44542a.pf_fragment +0 -0
  51. package/dist/docs/pagefind/fragment/en_d652b3c.pf_fragment +0 -0
  52. package/dist/docs/pagefind/index/en_6b9dc89.pf_index +0 -0
  53. package/dist/docs/pagefind/index/en_c5bf3e1.pf_index +0 -0
  54. package/dist/docs/pagefind/pagefind.en_c6c8e941d5.pf_meta +0 -0
package/dist/impl.js CHANGED
@@ -20,7 +20,7 @@ var package_default;
20
20
  var init_package = __esm(() => {
21
21
  package_default = {
22
22
  name: "@spec-engine/spec-engine",
23
- version: "0.7.0",
23
+ version: "0.7.2",
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",
@@ -62,7 +62,7 @@ var init_package = __esm(() => {
62
62
  "@modelcontextprotocol/sdk": "1.29.0",
63
63
  "@spec-engine/platform-map": "0.5.0",
64
64
  citty: "0.2.2",
65
- hono: "4.12.27",
65
+ hono: "4.13.11",
66
66
  zod: "3.25.76"
67
67
  },
68
68
  devDependencies: {
@@ -414,8 +414,25 @@ var init_config = __esm(() => {
414
414
  });
415
415
 
416
416
  // ../shared/src/ears.ts
417
- function reject(problem) {
418
- return { ok: false, problem, expected: EARS_EXPECTED };
417
+ function reject(problem, expected = SHAPE_EXPECTED) {
418
+ return { ok: false, problem, expected };
419
+ }
420
+ function onePromiseProblem(statement, response) {
421
+ if (/;\s/.test(response)) {
422
+ return 'joins a second clause with "; " after "shall" \u2014 split it into its own requirement';
423
+ }
424
+ if (/,\s+and\s+(?!shall\b)\S[^,;]*?\s+shall\b/i.test(response)) {
425
+ return 'joins a second clause with its own subject (", and <subject> shall") \u2014 split it into its own requirement';
426
+ }
427
+ const rationale = statement.match(RATIONALE_RE);
428
+ if (rationale) {
429
+ return `carries a rationale ("${rationale[1]}") \u2014 the reason belongs in the why field`;
430
+ }
431
+ return null;
432
+ }
433
+ function lint(clauses, statement) {
434
+ const problem = onePromiseProblem(statement, clauses.response);
435
+ return problem === null ? { ok: true, clauses } : reject(problem, ONE_PROMISE_EXPECTED);
419
436
  }
420
437
  function parseEarsStatement(raw) {
421
438
  const statement = raw.trim();
@@ -430,15 +447,12 @@ function parseEarsStatement(raw) {
430
447
  const m2 = statement.match(re);
431
448
  if (m2) {
432
449
  const [, condition = "", system = "", response = ""] = m2;
433
- return {
434
- ok: true,
435
- clauses: {
436
- pattern,
437
- condition: condition.trim(),
438
- system: system.trim().replace(/^the\s+/i, ""),
439
- response: response.trim()
440
- }
441
- };
450
+ return lint({
451
+ pattern,
452
+ condition: condition.trim(),
453
+ system: system.trim().replace(/^the\s+/i, ""),
454
+ response: response.trim()
455
+ }, statement);
442
456
  }
443
457
  if (keyword === "If" && !/,\s*then\s/.test(statement)) {
444
458
  return reject('starts with "If" but has no ", then" \u2014 the failure clause never hands over to the system');
@@ -451,19 +465,16 @@ function parseEarsStatement(raw) {
451
465
  if (/,/.test(system)) {
452
466
  return reject('the clause before "shall" contains a comma but starts with no condition keyword (When/While/If/Where) \u2014 either drop the comma or lead with the condition');
453
467
  }
454
- return {
455
- ok: true,
456
- clauses: {
457
- pattern: "always",
458
- condition: null,
459
- system: system.trim().replace(/^the\s+/i, ""),
460
- response: response.trim()
461
- }
462
- };
468
+ return lint({
469
+ pattern: "always",
470
+ condition: null,
471
+ system: system.trim().replace(/^the\s+/i, ""),
472
+ response: response.trim()
473
+ }, statement);
463
474
  }
464
475
  return reject('nothing follows "shall" \u2014 the observable result is missing');
465
476
  }
466
- var EARS_EXPECTED, CONDITIONAL, ALWAYS_RE;
477
+ var EARS_EXPECTED, CONDITIONAL, ALWAYS_RE, SHAPE_EXPECTED, ONE_PROMISE_EXPECTED = "Expected one promise per statement: a second behavior is its own requirement, and the reason goes in the why.", RATIONALE_RE;
467
478
  var init_ears = __esm(() => {
468
479
  EARS_EXPECTED = [
469
480
  'When <trigger>, the <system> shall <result> \u2014 e.g. "When an unknown id is passed, spec resolve shall print [] and exit 0."',
@@ -480,6 +491,9 @@ var init_ears = __esm(() => {
480
491
  { keyword: "Where", pattern: "where", re: /^Where\s+(.+?),\s*(.+?)\s+shall\s+(.+)$/s }
481
492
  ];
482
493
  ALWAYS_RE = /^(.+?)\s+shall\s+(.+)$/s;
494
+ SHAPE_EXPECTED = `Expected one of:
495
+ ${EARS_EXPECTED}`;
496
+ RATIONALE_RE = /\b(because|so that|in order to)\b/i;
483
497
  });
484
498
 
485
499
  // ../shared/src/flags.ts
@@ -670,7 +684,8 @@ CREATE INDEX IF NOT EXISTS idx_term_citations_term ON term_citations(term_id);
670
684
  -- supersedes-via | amends-via), validated at parse time, never as a DB
671
685
  -- constraint. Exactly one index, on req_id; never on issue_id, which would
672
686
  -- treat the opaque payload as a lookup key.
673
- -- @spec PROV-004
687
+ -- @spec PROV-005
688
+ -- @spec PROV-006
674
689
  CREATE TABLE IF NOT EXISTS provenance (
675
690
  id INTEGER PRIMARY KEY AUTOINCREMENT,
676
691
  req_id TEXT NOT NULL,
@@ -3790,7 +3805,7 @@ var init_proven = __esm(() => {
3790
3805
 
3791
3806
  // src/base/gitBase.ts
3792
3807
  function isSafeRef(ref) {
3793
- return /^[A-Za-z0-9._/-]+$/.test(ref);
3808
+ return /^[A-Za-z0-9._/][A-Za-z0-9._/-]*$/.test(ref);
3794
3809
  }
3795
3810
  function gitRepoPrefix(platformDir) {
3796
3811
  const proc = Bun.spawnSync(["git", "rev-parse", "--show-prefix"], { cwd: platformDir });
@@ -3828,7 +3843,7 @@ function gitLsTree(platformDir, ref, dir) {
3828
3843
  function gitDiffNameStatus(platformDir, ref) {
3829
3844
  if (!isSafeRef(ref))
3830
3845
  return [];
3831
- const proc = Bun.spawnSync(["git", "diff", "--name-status", "--no-renames", "--relative", ref], {
3846
+ const proc = Bun.spawnSync(["git", "diff", "--name-status", "--no-renames", "--relative", ref, "--"], {
3832
3847
  cwd: platformDir
3833
3848
  });
3834
3849
  if (proc.exitCode !== 0)
@@ -4136,8 +4151,7 @@ function rowsForDomain2(key, domain2) {
4136
4151
  source_file: specPaths("", key).rel,
4137
4152
  line: found >= 0 ? found + 1 : 0,
4138
4153
  req_id: req.id,
4139
- detail: `${result.problem}. Expected one of:
4140
- ${result.expected}`
4154
+ detail: `${result.problem}. ${result.expected}`
4141
4155
  });
4142
4156
  }
4143
4157
  return rows;
@@ -4832,8 +4846,7 @@ async function judgeStatementGrammar(platformDir, key, statement) {
4832
4846
  severity: cfg.severity,
4833
4847
  key,
4834
4848
  message: `the statement does not match the domain's EARS shape \u2014 ${result.problem}.
4835
- ` + `Expected one of:
4836
- ${result.expected}`
4849
+ ${result.expected}`
4837
4850
  };
4838
4851
  }
4839
4852
  function grammarWarningText(cmdName, v) {
@@ -5718,10 +5731,34 @@ async function newDomain(platformDir, key, options = {}) {
5718
5731
  async function listDomains(platformDir) {
5719
5732
  return { ok: true, domains: await domainsWithScope(platformDir) };
5720
5733
  }
5734
+ async function setDomainGrammar(platformDir, key, options) {
5735
+ if (options.grammar === undefined && options.grammarSeverity === undefined) {
5736
+ return fail("usage", "pass --grammar, --severity, or both");
5737
+ }
5738
+ const file = await readEnvelope(platformDir, key);
5739
+ if (!file.ok)
5740
+ return file;
5741
+ const domain2 = { ...file.domain, ...definedFields(options) };
5742
+ const res = await validateAndWrite(file.specPath, domain2, file.relFile);
5743
+ if (!res.ok) {
5744
+ return fail("invalid_domain_file", res.diagnostics.map((d) => d.detail).join(`
5745
+ `), {
5746
+ diagnostics: res.diagnostics
5747
+ });
5748
+ }
5749
+ return {
5750
+ ok: true,
5751
+ key,
5752
+ grammar: domain2.grammar,
5753
+ grammarSeverity: domain2.grammarSeverity,
5754
+ file: file.relFile
5755
+ };
5756
+ }
5721
5757
  var init_domain2 = __esm(() => {
5722
5758
  init_src();
5723
5759
  init_domains();
5724
5760
  init_constants();
5761
+ init__envelope();
5725
5762
  init__result();
5726
5763
  });
5727
5764
 
@@ -5730,6 +5767,7 @@ var exports_domain = {};
5730
5767
  __export(exports_domain, {
5731
5768
  domainNewCommand: () => domainNewCommand,
5732
5769
  domainListCommand: () => domainListCommand,
5770
+ domainGrammarCommand: () => domainGrammarCommand,
5733
5771
  domainCommand: () => domainCommand
5734
5772
  });
5735
5773
  import { defineCommand as defineCommand4 } from "citty";
@@ -5740,7 +5778,15 @@ function exitOnNewFailure(failure) {
5740
5778
  }
5741
5779
  exitOnFailure("spec domain new", failure);
5742
5780
  }
5743
- var domainNewCommand, domainListCommand, domainCommand;
5781
+ function enumFlag(flag, value, allowed) {
5782
+ if (value === undefined)
5783
+ return;
5784
+ if (allowed.includes(value))
5785
+ return value;
5786
+ console.error(`spec domain grammar: --${flag} must be one of ${allowed.join(", ")}`);
5787
+ process.exit(EXIT.USAGE);
5788
+ }
5789
+ var domainNewCommand, domainListCommand, GRAMMARS, SEVERITIES, domainGrammarCommand, domainCommand;
5744
5790
  var init_domain3 = __esm(() => {
5745
5791
  init_domains();
5746
5792
  init_constants();
@@ -5808,6 +5854,55 @@ var init_domain3 = __esm(() => {
5808
5854
  }
5809
5855
  }
5810
5856
  });
5857
+ GRAMMARS = ["ears", "freeform"];
5858
+ SEVERITIES = ["warning", "error"];
5859
+ domainGrammarCommand = defineCommand4({
5860
+ meta: {
5861
+ name: "grammar",
5862
+ description: "Set a domain's statement grammar and its severity"
5863
+ },
5864
+ args: {
5865
+ name: {
5866
+ type: "positional",
5867
+ required: true,
5868
+ description: "Domain key (case-insensitive)"
5869
+ },
5870
+ platformDir: platformDirArg,
5871
+ grammar: {
5872
+ type: "string",
5873
+ description: "ears | freeform"
5874
+ },
5875
+ severity: {
5876
+ type: "string",
5877
+ description: "warning | error \u2014 how STATEMENT_GRAMMAR and write-time judging treat a miss"
5878
+ },
5879
+ json: {
5880
+ type: "boolean",
5881
+ description: "Print { key, grammar, grammar_severity, file }"
5882
+ }
5883
+ },
5884
+ async run({ args }) {
5885
+ const platformDir = resolvePlatformDir(args);
5886
+ const grammar = enumFlag("grammar", args.grammar, GRAMMARS);
5887
+ const grammarSeverity = enumFlag("severity", args.severity, SEVERITIES);
5888
+ const result = await setDomainGrammar(platformDir, normalizeDomainKey(args.name), {
5889
+ grammar,
5890
+ grammarSeverity
5891
+ });
5892
+ if (!result.ok)
5893
+ exitOnFailure("spec domain grammar", result);
5894
+ if (args.json) {
5895
+ console.log(JSON.stringify({
5896
+ key: result.key,
5897
+ grammar: result.grammar ?? null,
5898
+ grammar_severity: result.grammarSeverity ?? null,
5899
+ file: result.file
5900
+ }));
5901
+ return;
5902
+ }
5903
+ console.log(`${result.file}: grammar ${result.grammar ?? "freeform"}, severity ${result.grammarSeverity ?? "warning"}`);
5904
+ }
5905
+ });
5811
5906
  domainCommand = defineCommand4({
5812
5907
  meta: {
5813
5908
  name: "domain",
@@ -5815,7 +5910,8 @@ var init_domain3 = __esm(() => {
5815
5910
  },
5816
5911
  subCommands: {
5817
5912
  new: domainNewCommand,
5818
- list: domainListCommand
5913
+ list: domainListCommand,
5914
+ grammar: domainGrammarCommand
5819
5915
  }
5820
5916
  });
5821
5917
  });
@@ -8440,6 +8536,20 @@ function jsonResult(value) {
8440
8536
  function errorResult(message) {
8441
8537
  return { content: [{ type: "text", text: message }], isError: true };
8442
8538
  }
8539
+ function amendFieldsOf(args) {
8540
+ const fields = {};
8541
+ if (args.text !== undefined)
8542
+ fields.statement = args.text;
8543
+ if (args.why !== undefined)
8544
+ fields.why = args.why || null;
8545
+ if (args.lives !== undefined)
8546
+ fields.livesIn = args.lives;
8547
+ if (args.relates !== undefined)
8548
+ fields.relates = args.relates;
8549
+ if (args.issue)
8550
+ fields.issue = args.issue;
8551
+ return fields;
8552
+ }
8443
8553
  function buildMcpServer(platformDir) {
8444
8554
  const server = new McpServer({ name: "spec", version: package_default.version });
8445
8555
  const fresh = (fn) => withIndex({ platformDir, build: "fresh" }, (h) => fn(h.storage));
@@ -8593,6 +8703,94 @@ function buildMcpServer(platformDir) {
8593
8703
  const r = await accept({ platformDir, id: req_id });
8594
8704
  return r.ok ? jsonResult({ id: r.id, file: r.file }) : errorResult(r.detail);
8595
8705
  });
8706
+ server.registerTool("spec_req", {
8707
+ title: "Author a requirement",
8708
+ description: "Append a new requirement to a domain (Active, or Draft with draft: true). The same JSON as `spec req --text \u2026 --json`: { id, file }, plus clauses when the statement parses as EARS.",
8709
+ inputSchema: {
8710
+ domain: z4.string().min(1).describe("Domain key or unique case-insensitive prefix"),
8711
+ text: z4.string().trim().min(1).describe("The requirement statement"),
8712
+ why: z4.string().trim().default("").describe("What breaks without it"),
8713
+ lives: z4.array(z4.string()).default([]).describe("livesIn paths"),
8714
+ relates: z4.array(z4.string()).default([]).describe("Related requirement ids, each comma-split"),
8715
+ issue: z4.string().trim().optional().describe("Originating ticket, recorded as created"),
8716
+ draft: z4.boolean().default(false).describe("Author as Draft instead of Active")
8717
+ }
8718
+ }, async ({ domain: domain2, text, why, lives, relates, issue, draft }) => {
8719
+ const allocated = await nextId(platformDir, domain2);
8720
+ if (!allocated.ok)
8721
+ return errorResult(allocated.detail);
8722
+ const r = await mint({
8723
+ platformDir,
8724
+ key: allocated.key,
8725
+ statement: text,
8726
+ why,
8727
+ livesIn: lives,
8728
+ relates,
8729
+ issue: issue || undefined,
8730
+ status: draft ? "draft" : "active"
8731
+ });
8732
+ if (!r.ok)
8733
+ return errorResult(r.detail);
8734
+ for (const w of r.warnings)
8735
+ console.error(`spec_req: ${w.text}`);
8736
+ return jsonResult(r.clauses ? { id: r.id, file: r.file, clauses: r.clauses } : { id: r.id, file: r.file });
8737
+ });
8738
+ server.registerTool("spec_amend", {
8739
+ title: "Amend a requirement in place",
8740
+ description: "Revise an Active or Draft requirement's fields in place (same id, no version change); a shipped requirement's statement and why are superseded instead. The same JSON as `spec amend --json`.",
8741
+ inputSchema: {
8742
+ id: z4.string().describe("The requirement id to amend (KEY-NNN)"),
8743
+ text: z4.string().trim().min(1).optional().describe("New statement"),
8744
+ why: z4.string().trim().optional().describe("New why; empty clears it"),
8745
+ lives: z4.array(z4.string()).optional().describe("Replacement livesIn list; empty clears"),
8746
+ relates: z4.array(z4.string()).optional().describe("Replacement related ids, each comma-split; empty clears"),
8747
+ issue: z4.string().trim().optional().describe("Ticket recorded as amends-via")
8748
+ }
8749
+ }, async ({ id, text, why, lives, relates, issue }) => {
8750
+ if (!ID_RE.test(id))
8751
+ return errorResult(`id must be a requirement id (KEY-NNN); got ${id}`);
8752
+ const fields = amendFieldsOf({ text, why, lives, relates, issue });
8753
+ if (Object.keys(fields).length === 0) {
8754
+ return errorResult("nothing to amend \u2014 pass at least one of text, why, lives, relates");
8755
+ }
8756
+ const r = await amend({ platformDir, id, fields });
8757
+ if (!r.ok)
8758
+ return errorResult(r.detail);
8759
+ for (const w of r.warnings)
8760
+ console.error(`spec_amend: ${w.text}`);
8761
+ return jsonResult({ id, file: r.file, fields_changed: r.fieldsChanged });
8762
+ });
8763
+ server.registerTool("spec_move", {
8764
+ title: "Move a requirement to another domain",
8765
+ description: "Mint the successor in newDomain carrying the source's fields, mark the source superseded, and return the retag worklist. The same JSON as `spec move --json`.",
8766
+ inputSchema: {
8767
+ id: z4.string().describe("The Active requirement id to move (KEY-NNN)"),
8768
+ newDomain: z4.string().min(1).describe("The existing target domain key"),
8769
+ text: z4.string().optional().describe("Successor statement (default: copied)"),
8770
+ why: z4.string().optional().describe("Successor why (default: copied)"),
8771
+ lives: z4.array(z4.string()).optional().describe("Successor livesIn list; empty clears (default: copied)")
8772
+ }
8773
+ }, async ({ id, newDomain: newDomain2, text, why, lives }) => {
8774
+ if (!ID_RE.test(id))
8775
+ return errorResult(`id must be a requirement id (KEY-NNN); got ${id}`);
8776
+ const targetKey = normalizeDomainKey(newDomain2);
8777
+ if (targetKey === "")
8778
+ return errorResult(`newDomain must be a domain key; got ${newDomain2}`);
8779
+ const r = await move({ platformDir, id, targetKey, statement: text, why, livesIn: lives }, coldFreshTags(platformDir));
8780
+ if (!r.ok)
8781
+ return errorResult(r.detail);
8782
+ for (const w of r.warnings)
8783
+ console.error(`spec_move: ${w.text}`);
8784
+ return jsonResult({
8785
+ old_id: r.oldId,
8786
+ new_id: r.newId,
8787
+ from_file: r.fromFile,
8788
+ to_file: r.toFile,
8789
+ source_spec_version: r.sourceSpecVersion,
8790
+ target_spec_version: r.targetSpecVersion,
8791
+ retag: r.retag
8792
+ });
8793
+ });
8596
8794
  server.registerPrompt("author_requirements", {
8597
8795
  title: "Author requirements from a brief",
8598
8796
  description: "Turn a vague brief/ticket into well-formed Spec Engine requirements: one requirement per testable promise, placed by domain charter, deduped via spec query, drafted to the GUARD template + cold-read rubric, minted with spec req. The engine returns a static playbook template \u2014 your model runs it.",
@@ -8629,8 +8827,11 @@ var init_mcp = __esm(() => {
8629
8827
  init_domains();
8630
8828
  init__index();
8631
8829
  init_accept();
8830
+ init_amend();
8632
8831
  init_check();
8633
8832
  init_deprecate();
8833
+ init_mint();
8834
+ init_move();
8634
8835
  init_nextId();
8635
8836
  init_reads2();
8636
8837
  init_records();
@@ -12153,6 +12354,9 @@ function guarded(handler) {
12153
12354
  try {
12154
12355
  return await handler(c);
12155
12356
  } catch (err) {
12357
+ if (err instanceof InvalidDomainFilesError) {
12358
+ return c.json({ error: "invalid_domain_file", diagnostics: sortDiagnostics(err.diagnostics) }, 422);
12359
+ }
12156
12360
  const info = describeStorageError(err);
12157
12361
  if (info === null)
12158
12362
  throw err;
@@ -12404,6 +12608,52 @@ function mountApi(app, storage2, platformDir = process.cwd(), bindHost = DEFAULT
12404
12608
  }, 201);
12405
12609
  });
12406
12610
  }));
12611
+ app.post("/api/requirements/:id/move", guarded(async function moveRequirement(c) {
12612
+ if (!featureEnabled("editor")) {
12613
+ return c.json({ error: featureDisabledMessage("editor") }, 404);
12614
+ }
12615
+ const originReject = rejectCrossOrigin(c, bindHost);
12616
+ if (originReject)
12617
+ return originReject;
12618
+ const headerReject = rejectBadWriteHeaders(c);
12619
+ if (headerReject)
12620
+ return headerReject;
12621
+ const parsed = await readJsonBody(c);
12622
+ if (!parsed.ok)
12623
+ return parsed.res;
12624
+ const body = parsed.body;
12625
+ const id = c.req.param("id") ?? "";
12626
+ if (!ID_RE.test(id))
12627
+ return c.json({ error: "id must be a requirement id (KEY-NNN)" }, 400);
12628
+ const targetKey = normalizeDomainKey(typeof body.newDomain === "string" ? body.newDomain : "");
12629
+ if (targetKey === "")
12630
+ return c.json({ error: "newDomain is required (a domain key)" }, 400);
12631
+ return withWriteLock(async () => {
12632
+ const r = await move({
12633
+ platformDir,
12634
+ id,
12635
+ targetKey,
12636
+ statement: typeof body.text === "string" ? body.text : undefined,
12637
+ why: typeof body.why === "string" ? body.why : undefined,
12638
+ livesIn: "lives" in body ? toStringList(body.lives) : undefined
12639
+ }, async (reqId) => {
12640
+ await runIndex({ platformDir, storage: storage2 });
12641
+ return storage2.listTags({ req_id: reqId });
12642
+ });
12643
+ if (!r.ok)
12644
+ return failureResponse(c, r);
12645
+ return c.json({
12646
+ ok: true,
12647
+ old_id: r.oldId,
12648
+ new_id: r.newId,
12649
+ from_file: r.fromFile,
12650
+ to_file: r.toFile,
12651
+ source_spec_version: r.sourceSpecVersion,
12652
+ target_spec_version: r.targetSpecVersion,
12653
+ retag: r.retag
12654
+ }, 201);
12655
+ });
12656
+ }));
12407
12657
  app.post("/api/requirements/:id/deprecate", guarded(async function deprecateRequirement(c) {
12408
12658
  if (!featureEnabled("editor")) {
12409
12659
  return c.json({ error: featureDisabledMessage("editor") }, 404);
@@ -12542,6 +12792,7 @@ var init_api = __esm(() => {
12542
12792
  init_amend();
12543
12793
  init_deprecate();
12544
12794
  init_mint();
12795
+ init_move();
12545
12796
  init_reads2();
12546
12797
  init_records();
12547
12798
  init_supersede();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spec-engine/spec-engine",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
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",
@@ -42,7 +42,7 @@
42
42
  "@modelcontextprotocol/sdk": "1.29.0",
43
43
  "@spec-engine/platform-map": "0.5.0",
44
44
  "citty": "0.2.2",
45
- "hono": "4.12.27",
45
+ "hono": "4.13.11",
46
46
  "zod": "3.25.76"
47
47
  },
48
48
  "devDependencies": {
@@ -1 +0,0 @@
1
- @layer starlight.components{svg:where(.astro-lm5mbbcm){color:var(--sl-icon-color);font-size:var(--sl-icon-size, 1em);width:1em;height:1em}}@layer starlight.components{:root{--sl-badge-default-border: var(--sl-color-accent);--sl-badge-default-bg: var(--sl-color-accent-low);--sl-badge-default-text: #fff;--sl-badge-note-border: var(--sl-color-blue);--sl-badge-note-bg: var(--sl-color-blue-low);--sl-badge-note-text: #fff;--sl-badge-danger-border: var(--sl-color-red);--sl-badge-danger-bg: var(--sl-color-red-low);--sl-badge-danger-text: #fff;--sl-badge-success-border: var(--sl-color-green);--sl-badge-success-bg: var(--sl-color-green-low);--sl-badge-success-text: #fff;--sl-badge-caution-border: var(--sl-color-orange);--sl-badge-caution-bg: var(--sl-color-orange-low);--sl-badge-caution-text: #fff;--sl-badge-tip-border: var(--sl-color-purple);--sl-badge-tip-bg: var(--sl-color-purple-low);--sl-badge-tip-text: #fff}[data-theme=light]:root{--sl-badge-default-bg: var(--sl-color-accent-high);--sl-badge-note-bg: var(--sl-color-blue-high);--sl-badge-danger-bg: var(--sl-color-red-high);--sl-badge-success-bg: var(--sl-color-green-high);--sl-badge-caution-bg: var(--sl-color-orange-high);--sl-badge-tip-bg: var(--sl-color-purple-high)}.sl-badge:where(.astro-dr5neazu){display:inline-block;border:1px solid var(--sl-color-border-badge);border-radius:.25rem;font-family:var(--sl-font-system-mono);line-height:normal;color:var(--sl-color-text-badge);background-color:var(--sl-color-bg-badge);overflow-wrap:anywhere}.sidebar-content .sl-badge:where(.astro-dr5neazu){line-height:1;font-size:var(--sl-text-xs);padding:.125rem .375rem}.sidebar-content a[aria-current=page]>.sl-badge:where(.astro-dr5neazu){--sl-color-bg-badge: transparent;--sl-color-border-badge: currentColor;color:inherit}.default:where(.astro-dr5neazu){--sl-color-bg-badge: var(--sl-badge-default-bg);--sl-color-border-badge: var(--sl-badge-default-border);--sl-color-text-badge: var(--sl-badge-default-text)}.note:where(.astro-dr5neazu){--sl-color-bg-badge: var(--sl-badge-note-bg);--sl-color-border-badge: var(--sl-badge-note-border);--sl-color-text-badge: var(--sl-badge-note-text)}.danger:where(.astro-dr5neazu){--sl-color-bg-badge: var(--sl-badge-danger-bg);--sl-color-border-badge: var(--sl-badge-danger-border);--sl-color-text-badge: var(--sl-badge-danger-text)}.success:where(.astro-dr5neazu){--sl-color-bg-badge: var(--sl-badge-success-bg);--sl-color-border-badge: var(--sl-badge-success-border);--sl-color-text-badge: var(--sl-badge-success-text)}.tip:where(.astro-dr5neazu){--sl-color-bg-badge: var(--sl-badge-tip-bg);--sl-color-border-badge: var(--sl-badge-tip-border);--sl-color-text-badge: var(--sl-badge-tip-text)}.caution:where(.astro-dr5neazu){--sl-color-bg-badge: var(--sl-badge-caution-bg);--sl-color-border-badge: var(--sl-badge-caution-border);--sl-color-text-badge: var(--sl-badge-caution-text)}.small:where(.astro-dr5neazu){font-size:var(--sl-text-xs);padding:.125rem .25rem}.medium:where(.astro-dr5neazu){font-size:var(--sl-text-sm);padding:.175rem .35rem}.large:where(.astro-dr5neazu){font-size:var(--sl-text-base);padding:.225rem .45rem}.sl-markdown-content :is(h1,h2,h3,h4,h5,h6) .sl-badge:where(.astro-dr5neazu){vertical-align:middle}}@layer starlight.components{starlight-tabs:where(.astro-njbqzmfe){display:block}.tablist-wrapper:where(.astro-njbqzmfe){overflow-x:auto}:where(.astro-njbqzmfe)[role=tablist]{display:flex;list-style:none;border-bottom:2px solid var(--sl-color-gray-5);padding:0}.tab:where(.astro-njbqzmfe){display:flex}.tab:where(.astro-njbqzmfe)>:where(.astro-njbqzmfe)[role=tab]{--sl-tab-color-border: var(--sl-color-gray-5);display:flex;align-items:center;gap:.5rem;line-height:var(--sl-line-height-headings);padding:.275rem 1.25rem;text-decoration:none;box-shadow:0 2px 0 var(--sl-tab-color-border);color:var(--sl-color-gray-3);outline-offset:var(--sl-outline-offset-inside);overflow-wrap:initial}.tab:where(.astro-njbqzmfe) :where(.astro-njbqzmfe)[role=tab][aria-selected=true]{--sl-tab-color-border: var(--sl-color-text-accent);color:var(--sl-color-white);font-weight:600}.tablist-wrapper:where(.astro-njbqzmfe)~[role=tabpanel]{margin-top:1rem}}@layer starlight.components{.sl-steps{--bullet-size: calc(var(--sl-line-height) * 1rem);--bullet-margin: .375rem;list-style:none;counter-reset:steps-counter var(--sl-steps-start, 0);padding-inline-start:0}.sl-steps>li{counter-increment:steps-counter;position:relative;padding-inline-start:calc(var(--bullet-size) + 1rem);padding-bottom:1px;min-height:calc(var(--bullet-size) + var(--bullet-margin))}.sl-steps>li+li{margin-top:0}.sl-steps>li:before{content:counter(steps-counter);position:absolute;top:0;inset-inline-start:0;width:var(--bullet-size);height:var(--bullet-size);line-height:var(--bullet-size);font-size:var(--sl-text-xs);font-weight:600;text-align:center;color:var(--sl-color-white);background-color:var(--sl-color-gray-6);border-radius:99rem;box-shadow:inset 0 0 0 1px var(--sl-color-gray-5)}.sl-steps>li:after{--guide-width: 1px;content:"";position:absolute;top:calc(var(--bullet-size) + var(--bullet-margin));bottom:var(--bullet-margin);inset-inline-start:calc((var(--bullet-size) - var(--guide-width)) / 2);width:var(--guide-width);background-color:var(--sl-color-hairline-light)}}@layer starlight.content{.sl-steps>li>:first-child{--lh: calc(1em * var(--sl-line-height));--shift-y: calc(.5 * (var(--bullet-size) - var(--lh)));transform:translateY(var(--shift-y));margin-bottom:var(--shift-y)}.sl-steps>li>:first-child:where(h1,h2,h3,h4,h5,h6){--lh: calc(1em * var(--sl-line-height-headings))}@supports (--prop: 1lh){.sl-steps>li>:first-child{--lh: 1lh}}}@layer starlight.components{.sl-link-button:where(.astro-yiov523n){align-items:center;border:1px solid transparent;border-radius:999rem;display:inline-flex;font-size:var(--sl-text-sm);gap:.5em;line-height:1.1875;outline-offset:.25rem;padding:.4375rem 1.125rem;text-decoration:none}.sl-link-button:where(.astro-yiov523n).primary{background:var(--sl-color-text-accent);border-color:var(--sl-color-text-accent);color:var(--sl-color-black)}.sl-link-button:where(.astro-yiov523n).primary:hover{color:var(--sl-color-black)}.sl-link-button:where(.astro-yiov523n).secondary{border-color:inherit;color:var(--sl-color-white)}.sl-link-button:where(.astro-yiov523n).minimal{color:var(--sl-color-white);padding-inline:0}.sl-link-button:where(.astro-yiov523n) svg{flex-shrink:0}@media(min-width:50rem){.sl-link-button:where(.astro-yiov523n){font-size:var(--sl-text-base);padding:.9375rem 1.25rem}}.sl-markdown-content .sl-link-button:where(.astro-yiov523n){margin-inline-end:1rem}.sl-markdown-content .sl-link-button:where(.astro-yiov523n):not(:where(p *)){margin-block:1rem}}
@@ -1 +0,0 @@
1
- import{S as r}from"./TableOfContents.astro_astro_type_script_index_0_lang.FuRcXuRY.js";class c extends r{set current(e){super.current=e;const t=this.querySelector(".display-current");t&&(t.textContent=e.textContent)}constructor(){super();const e=this.querySelector("details");if(!e)return;const t=()=>{e.open=!1};e.querySelectorAll("a").forEach(s=>{s.addEventListener("click",t)}),window.addEventListener("click",s=>{e.contains(s.target)||t()}),window.addEventListener("keydown",s=>{if(s.key==="Escape"&&e.open){const o=e.contains(document.activeElement);if(t(),o){const n=e.querySelector("summary");n&&n.focus()}}})}}customElements.define("mobile-starlight-toc",c);
@@ -1 +0,0 @@
1
- const y=(function(){const r=typeof document<"u"&&document.createElement("link").relList;return r&&r.supports&&r.supports("modulepreload")?"modulepreload":"preload"})(),S=function(m){return"/"+m},g={},w=function(r,c,d){let p=Promise.resolve();if(c&&c.length>0){let f=function(n){return Promise.all(n.map(i=>Promise.resolve(i).then(s=>({status:"fulfilled",value:s}),s=>({status:"rejected",reason:s}))))};document.getElementsByTagName("link");const o=document.querySelector("meta[property=csp-nonce]"),t=o?.nonce||o?.getAttribute("nonce");p=f(c.map(n=>{if(n=S(n),n in g)return;g[n]=!0;const i=n.endsWith(".css"),s=i?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${n}"]${s}`))return;const e=document.createElement("link");if(e.rel=i?"stylesheet":y,i||(e.as="script"),e.crossOrigin="",e.href=n,t&&e.setAttribute("nonce",t),document.head.appendChild(e),i)return new Promise((l,a)=>{e.addEventListener("load",l),e.addEventListener("error",()=>a(new Error(`Unable to preload CSS for ${n}`)))})}))}function u(o){const t=new Event("vite:preloadError",{cancelable:!0});if(t.payload=o,window.dispatchEvent(t),!t.defaultPrevented)throw o}return p.then(o=>{for(const t of o||[])t.status==="rejected"&&u(t.reason);return r().catch(u)})},E={ranking:{pageLength:.1,termFrequency:.1,termSaturation:2,termSimilarity:9,diacriticSimilarity:.8}};class v extends HTMLElement{constructor(){super();const r=this.querySelector("button[data-open-modal]"),c=this.querySelector("button[data-close-modal]"),d=this.querySelector("dialog"),p=this.querySelector(".dialog-frame"),u=e=>{("href"in(e.target||{})||document.body.contains(e.target)&&!p.contains(e.target))&&t()},o=e=>{d.showModal(),document.body.toggleAttribute("data-search-modal-open",!0),this.querySelector("input")?.focus(),e?.stopPropagation(),window.addEventListener("click",u)},t=()=>d.close();r.addEventListener("click",o),r.disabled=!1,c.addEventListener("click",t),d.addEventListener("close",()=>{document.body.toggleAttribute("data-search-modal-open",!1),window.removeEventListener("click",u)}),window.addEventListener("keydown",e=>{(e.metaKey===!0||e.ctrlKey===!0)&&e.key==="k"&&(d.open?t():o(),e.preventDefault())});let f={};try{f=JSON.parse(this.dataset.translations||"{}")}catch{}const s=this.dataset.stripTrailingSlash!==void 0?e=>e.replace(/(.)\/(#.*)?$/,"$1$2"):e=>e;window.addEventListener("DOMContentLoaded",()=>{(window.requestIdleCallback||(l=>setTimeout(l,1)))(async()=>{const{PagefindUI:l}=await w(async()=>{const{PagefindUI:a}=await import("./ui-core.C8327L_n.js");return{PagefindUI:a}},[]);new l({...E,element:"#starlight__search",baseUrl:"/",bundlePath:"/".replace(/\/$/,"")+"/pagefind/",showImages:!1,translations:f,showSubResults:!0,processResult:a=>{a.url=s(a.url),a.sub_results=a.sub_results.map(h=>(h.url=s(h.url),h))}})})})}}customElements.define("site-search",v);export{w as _};
@@ -1 +0,0 @@
1
- const m="_top";class H extends HTMLElement{constructor(){super(),this._current=this.querySelector('a[aria-current="true"]'),this.minH=parseInt(this.dataset.minH||"2",10),this.maxH=parseInt(this.dataset.maxH||"3",10),this.tocHeadingSelector=`h1#${m},:where(${[...Array.from({length:1+this.maxH-this.minH}).map((e,n)=>`h${this.minH+n}`)].join()})[id]`,this.onIdle=e=>(window.requestIdleCallback||(n=>setTimeout(n,1)))(e),this.init=()=>{const e=[...this.querySelectorAll("a")],n=t=>t.matches(this.tocHeadingSelector),i=t=>{if(!t)return null;const h=t;for(;t;){if(t.matches(".sl-markdown-content, main > *"))return document.getElementById(m);if(n(t))return t;const s=t.querySelector(this.tocHeadingSelector);if(s)return s;for(t=t.previousElementSibling;t?.lastElementChild;)t=t.lastElementChild;const o=i(t);if(o)return o}return i(h.parentElement)},c=t=>{for(const{isIntersecting:h,target:s}of t){if(!h)continue;const o=i(s);if(!o)continue;const l=e.find(g=>g.hash==="#"+encodeURIComponent(o.id));if(l){this.current=l;break}}},a=document.querySelectorAll([`main :where(${this.tocHeadingSelector})`,`main :where(${this.tocHeadingSelector}, .sl-heading-wrapper) ~ *:not(:has(${this.tocHeadingSelector}))`,`main .sl-markdown-content > *:not(:has(${this.tocHeadingSelector}))`,`main > *:not(:has(${this.tocHeadingSelector}))`].join());let r;const u=()=>{r||(r=new IntersectionObserver(c,{rootMargin:this.getRootMargin()}),a.forEach(t=>r.observe(t)))};u();let d;window.addEventListener("resize",()=>{r&&(r.disconnect(),r=void 0),clearTimeout(d),d=setTimeout(()=>this.onIdle(u),200)})},this.onIdle(()=>this.init())}set current(e){e!==this._current&&(this._current&&this._current.removeAttribute("aria-current"),e.setAttribute("aria-current","true"),this._current=e)}getRootMargin(){const e=document.querySelector("header")?.getBoundingClientRect().height||0,n=this.querySelector("summary")?.getBoundingClientRect().height||0,i=e+n+32,c=i+53,a=document.documentElement.clientHeight;return`-${i}px 0% ${c-a}px`}}customElements.define("starlight-toc",H);export{H as S};