@oevery/rsp 4.0.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/dist/chunk-AUZYUJ46.mjs +418 -0
  2. package/dist/chunk-DNRGBQS7.mjs +108 -0
  3. package/dist/chunk-LGDCIQYL.mjs +725 -0
  4. package/dist/chunk-P3KO3GZW.mjs +1023 -0
  5. package/dist/{chunk-HFSTLCCK.mjs → chunk-VDRXFKBP.mjs} +133 -1800
  6. package/dist/cli.mjs +286 -294
  7. package/dist/client-U2PN3VS4.mjs +108 -0
  8. package/dist/{entry-RIJQ2X46.mjs → entry-NDASTAI5.mjs} +13 -9
  9. package/dist/viewer/NOTICE.txt +299 -0
  10. package/dist/viewer/app.css +2 -0
  11. package/dist/viewer/app.js +670 -0
  12. package/dist/viewer/chunks/abnfDiagram-S7VZAUFO-K5EOZOEF.js +1 -0
  13. package/dist/viewer/chunks/architecture-WOLXFQ4H-UAT5D5BF.js +1 -0
  14. package/dist/viewer/chunks/architectureDiagram-YGSQEFNW-UNPSYM2L.js +36 -0
  15. package/dist/viewer/chunks/blockDiagram-APKKGZFW-2F7TTPAV.js +140 -0
  16. package/dist/viewer/chunks/bpmnIcons-APHXDYXF-ZWPFFN3O.js +1 -0
  17. package/dist/viewer/chunks/c4Diagram-KJ2TOQU5-KNLN6CME.js +38 -0
  18. package/dist/viewer/chunks/chunk-274PZN6B.js +1 -0
  19. package/dist/viewer/chunks/chunk-2VGDQLQD.js +1 -0
  20. package/dist/viewer/chunks/chunk-33XESOCS.js +1 -0
  21. package/dist/viewer/chunks/chunk-3ZSL2TGQ.js +1 -0
  22. package/dist/viewer/chunks/chunk-432I4U3B.js +1 -0
  23. package/dist/viewer/chunks/chunk-4PJNGS3V.js +1 -0
  24. package/dist/viewer/chunks/chunk-54NLSLTF.js +9 -0
  25. package/dist/viewer/chunks/chunk-6QRHYJ5Q.js +1 -0
  26. package/dist/viewer/chunks/chunk-6RCFKBVR.js +1 -0
  27. package/dist/viewer/chunks/chunk-6Y2HP7DC.js +213 -0
  28. package/dist/viewer/chunks/chunk-7MAT7NHP.js +1 -0
  29. package/dist/viewer/chunks/chunk-AKHBD6LT.js +2 -0
  30. package/dist/viewer/chunks/chunk-CZIM56BV.js +1 -0
  31. package/dist/viewer/chunks/chunk-DN4Y7LXJ.js +1 -0
  32. package/dist/viewer/chunks/chunk-E2UQN267.js +1 -0
  33. package/dist/viewer/chunks/chunk-GBARLDDT.js +1 -0
  34. package/dist/viewer/chunks/chunk-H4YEV7VR.js +1 -0
  35. package/dist/viewer/chunks/chunk-HUVVDQHZ.js +1 -0
  36. package/dist/viewer/chunks/chunk-IAKC32WV.js +72 -0
  37. package/dist/viewer/chunks/chunk-JTW4SY4L.js +1 -0
  38. package/dist/viewer/chunks/chunk-JZV675LF.js +1 -0
  39. package/dist/viewer/chunks/chunk-KDUDYTWO.js +1 -0
  40. package/dist/viewer/chunks/chunk-KHX3A5RZ.js +331 -0
  41. package/dist/viewer/chunks/chunk-KPXJJSUR.js +15 -0
  42. package/dist/viewer/chunks/chunk-LEGNM6PH.js +1 -0
  43. package/dist/viewer/chunks/chunk-LYY4ZY72.js +1 -0
  44. package/dist/viewer/chunks/chunk-MKDEK2LU.js +1 -0
  45. package/dist/viewer/chunks/chunk-MYJRFART.js +129 -0
  46. package/dist/viewer/chunks/chunk-NUXZBURC.js +1 -0
  47. package/dist/viewer/chunks/chunk-NXHT5I2L.js +1 -0
  48. package/dist/viewer/chunks/chunk-OF62XRVM.js +62 -0
  49. package/dist/viewer/chunks/chunk-ONCOEVBS.js +1 -0
  50. package/dist/viewer/chunks/chunk-P44SH3OO.js +1 -0
  51. package/dist/viewer/chunks/chunk-PDXKDBIX.js +1 -0
  52. package/dist/viewer/chunks/chunk-PHKIAKHS.js +1 -0
  53. package/dist/viewer/chunks/chunk-PJDMAGPE.js +2 -0
  54. package/dist/viewer/chunks/chunk-RC2NPHSX.js +88 -0
  55. package/dist/viewer/chunks/chunk-T6DM5RSK.js +10 -0
  56. package/dist/viewer/chunks/chunk-U5STX66M.js +2 -0
  57. package/dist/viewer/chunks/chunk-UIAJQPOF.js +1 -0
  58. package/dist/viewer/chunks/chunk-UX2S3LP3.js +1 -0
  59. package/dist/viewer/chunks/chunk-VEMTQQ7Y.js +2 -0
  60. package/dist/viewer/chunks/chunk-WKCPBR3C.js +1 -0
  61. package/dist/viewer/chunks/chunk-WMQ4Y2MS.js +32 -0
  62. package/dist/viewer/chunks/chunk-YPWIQAHX.js +3 -0
  63. package/dist/viewer/chunks/chunk-Z4ZDIMO2.js +136 -0
  64. package/dist/viewer/chunks/chunk-ZATQCNXH.js +1 -0
  65. package/dist/viewer/chunks/chunk-ZE5RDJX6.js +1 -0
  66. package/dist/viewer/chunks/chunk-ZO62R4L2.js +1 -0
  67. package/dist/viewer/chunks/classDiagram-v2-L25IHNXF-5F2M2CZ4.js +217 -0
  68. package/dist/viewer/chunks/cose-bilkent-JH36ORCC-S56HJ5WN.js +1 -0
  69. package/dist/viewer/chunks/cynefin-EF2NZ3EQ-6WY2NCLE.js +1 -0
  70. package/dist/viewer/chunks/cynefinDiagram-L36ESFBY-DE6PZNQA.js +62 -0
  71. package/dist/viewer/chunks/dagre-SPEB5RXH-QEOTKFV2.js +4 -0
  72. package/dist/viewer/chunks/diagram-3B7GL73J-RZPIU7FY.js +200 -0
  73. package/dist/viewer/chunks/diagram-DNEMPZML-YD26TJKC.js +24 -0
  74. package/dist/viewer/chunks/diagram-L3MUGCDN-TJUEX4QI.js +41 -0
  75. package/dist/viewer/chunks/diagram-LA4NIPWU-3PUQS6UW.js +24 -0
  76. package/dist/viewer/chunks/diagram-MHCOC4E2-YBTBD4PG.js +3 -0
  77. package/dist/viewer/chunks/diagram-XPFHEONP-VF2TQBNR.js +30 -0
  78. package/dist/viewer/chunks/ebnfDiagram-UZSYDL2S-QE7DIEME.js +1 -0
  79. package/dist/viewer/chunks/elk-XV6XMXQE-X5OZRIL3.js +27 -0
  80. package/dist/viewer/chunks/erDiagram-EYBAPDEH-5B2S7VB4.js +99 -0
  81. package/dist/viewer/chunks/eventmodeling-K75KTNOO-CHCQYZBI.js +1 -0
  82. package/dist/viewer/chunks/flowDiagram-MALGAFMO-XJFC5NTN.js +1 -0
  83. package/dist/viewer/chunks/ganttDiagram-23REXITY-TUUZ3QYO.js +292 -0
  84. package/dist/viewer/chunks/gitGraph-VSP46ZUC-VFZPJQ6A.js +1 -0
  85. package/dist/viewer/chunks/gitGraphDiagram-STJD7YCW-4CVV5LSH.js +106 -0
  86. package/dist/viewer/chunks/info-OHQRW6UA-P4744SJ6.js +1 -0
  87. package/dist/viewer/chunks/infoDiagram-KBXO4MBG-Q2WYYNNN.js +2 -0
  88. package/dist/viewer/chunks/ishikawaDiagram-M35NCXHK-PLQPTX2U.js +70 -0
  89. package/dist/viewer/chunks/journeyDiagram-OPLOXDXO-FB73RE6F.js +139 -0
  90. package/dist/viewer/chunks/kanban-definition-BZKZHSQY-6FU4TXYJ.js +89 -0
  91. package/dist/viewer/chunks/katex-IN4S3RHS.js +257 -0
  92. package/dist/viewer/chunks/mindmap-definition-I2QAS2VK-ZQ36ABVW.js +98 -0
  93. package/dist/viewer/chunks/packet-JDAUHWVQ-NYXIQQVA.js +1 -0
  94. package/dist/viewer/chunks/pegDiagram-FD4XPPHP-5XXVT6XN.js +1 -0
  95. package/dist/viewer/chunks/pie-XZMESJXO-32CUE6KZ.js +1 -0
  96. package/dist/viewer/chunks/pieDiagram-J4AKIIQI-WRTBDFM5.js +39 -0
  97. package/dist/viewer/chunks/quadrantDiagram-XTYBLID2-YPBDQIAK.js +7 -0
  98. package/dist/viewer/chunks/radar-ABXABTNO-272FILRO.js +1 -0
  99. package/dist/viewer/chunks/railroad-I3PHUGI6-6EGAH4XA.js +1 -0
  100. package/dist/viewer/chunks/railroad-abnf-LNEFI6M7-G6E7ATER.js +1 -0
  101. package/dist/viewer/chunks/railroad-ebnf-DWYD2UWJ-MR4P6BDR.js +1 -0
  102. package/dist/viewer/chunks/railroad-peg-JR7BVNR7-WXLQUGUO.js +1 -0
  103. package/dist/viewer/chunks/railroadDiagram-MRQ6NCDL-XHZQ2BLY.js +1 -0
  104. package/dist/viewer/chunks/requirementDiagram-Y7LWUY3S-W5FHRRML.js +84 -0
  105. package/dist/viewer/chunks/sankeyDiagram-WGJFYUXA-OJJIRVTI.js +40 -0
  106. package/dist/viewer/chunks/sequenceDiagram-5NDRBG5M-D6T4DRNG.js +169 -0
  107. package/dist/viewer/chunks/sizeCapture-INFHLROL-WL3FTUJX.js +1 -0
  108. package/dist/viewer/chunks/stateDiagram-v2-FJ4FJNEA-LAXL7KGG.js +291 -0
  109. package/dist/viewer/chunks/swimlanes-UMQ2AFYF-XIJBD726.js +1 -0
  110. package/dist/viewer/chunks/swimlanesDiagram-OMPLPG4O-7O7MHFW7.js +8 -0
  111. package/dist/viewer/chunks/timeline-definition-SEUHKTQE-BAZQA47I.js +120 -0
  112. package/dist/viewer/chunks/treeView-D4JQ5SDB-6YMDAAPA.js +1 -0
  113. package/dist/viewer/chunks/treemap-SAJKECNS-2X573PPS.js +1 -0
  114. package/dist/viewer/chunks/usecaseDiagram-AFA6YXN4-4YCEMWJB.js +370 -0
  115. package/dist/viewer/chunks/vennDiagram-NNMJR3RB-GEMROLAE.js +34 -0
  116. package/dist/viewer/chunks/wardley-7MLQ67FV-DGTGHYYC.js +1 -0
  117. package/dist/viewer/chunks/wardleyDiagram-S7YFUSEH-K4HVD3SS.js +78 -0
  118. package/dist/viewer/chunks/xychartDiagram-GGNKY7IX-HVLWCUEO.js +7 -0
  119. package/dist/viewer/mermaid.js +12 -0
  120. package/dist/viewer-build.json +1 -0
  121. package/dist/viewer-service.mjs +607 -0
  122. package/package.json +27 -8
  123. package/skills/rsp/SKILL.md +13 -10
  124. package/skills/rsp/references/architecture.md +39 -0
  125. package/skills/rsp/references/closeout.md +4 -4
  126. package/skills/rsp/references/durable-review.md +6 -9
  127. package/skills/rsp/references/focus-continuation.md +6 -2
  128. package/skills/rsp/references/setup-repair.md +11 -6
  129. package/skills/rsp/references/work-model.md +14 -4
  130. package/skills/rsp-commit/SKILL.md +2 -2
  131. package/skills/rsp-doc/SKILL.md +13 -10
  132. package/skills/rsp-doc/references/skill-authoring.md +2 -2
  133. package/skills/rsp-implement/SKILL.md +16 -12
  134. package/skills/rsp-review/SKILL.md +12 -12
  135. package/skills/rsp-review/references/code-review.md +11 -5
  136. package/skills/rsp-review/references/document-review.md +8 -10
  137. package/skills/rsp-shape/SKILL.md +3 -1
  138. package/skills/rsp-shape/references/planning.md +4 -4
  139. package/skills/rsp-verify/SKILL.md +9 -14
  140. package/skills/rsp-doc/references/artifact-lenses.md +0 -43
  141. package/skills/rsp-review/references/structural-review.md +0 -41
@@ -1,1094 +1,62 @@
1
1
  #!/usr/bin/env node
2
-
3
- // src/core/config.ts
4
- import { readFile } from "fs/promises";
5
- import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
6
- import { fileURLToPath } from "url";
7
- import pc from "picocolors";
8
- import { stringify } from "yaml";
9
-
10
- // src/core/markdown.mjs
11
- import { fromHtml } from "hast-util-from-html";
12
- import { fromMarkdown } from "mdast-util-from-markdown";
13
- import { frontmatterFromMarkdown } from "mdast-util-frontmatter";
14
- import { gfmFromMarkdown } from "mdast-util-gfm";
15
- import { frontmatter } from "micromark-extension-frontmatter";
16
- import { gfm } from "micromark-extension-gfm";
17
- var htmlTokenAnalyses = /* @__PURE__ */ new WeakMap();
18
- var matters = ["yaml", { type: "yaml", fence: { open: "---", close: "..." } }];
19
- var extensions = [gfm(), frontmatter(matters)];
20
- var mdastExtensions = [gfmFromMarkdown(), frontmatterFromMarkdown(matters)];
21
- function parseMarkdown(input, options = {}) {
22
- if (typeof input !== "string")
23
- return input;
24
- const syntax = options.frontmatter === false ? { extensions: [gfm()], mdastExtensions: [gfmFromMarkdown()] } : { extensions, mdastExtensions };
25
- return { source: input, root: fromMarkdown(input, syntax), start: 0, end: input.length };
26
- }
27
- function walkMarkdown(node, visit) {
28
- if (visit(node) === false)
29
- return;
30
- if ("children" in node) {
31
- for (const child of node.children)
32
- walkMarkdown(child, visit);
33
- }
34
- }
35
- function markdownNodeSource(input, node) {
36
- const { source } = parseMarkdown(input);
37
- return source.slice(node.position?.start.offset ?? 0, node.position?.end.offset ?? 0);
38
- }
39
- function isMarkdownComment(node) {
40
- return node.type === "html" && !readHtmlToken(node, node.value).maskedSource.trim();
41
- }
42
- function htmlComments(value) {
43
- if (!value.includes("<!--"))
44
- return [];
45
- const comments = [];
46
- function visit(node, topLevel) {
47
- if (node.type === "comment" && node.position)
48
- comments.push({ start: node.position.start.offset ?? 0, end: node.position.end.offset ?? value.length, topLevel });
49
- if ("children" in node)
50
- node.children.forEach((child) => visit(child, node.type === "root"));
51
- }
52
- visit(fromHtml(value, { fragment: true }), false);
53
- return comments;
54
- }
55
- function readHtmlToken(node, value) {
56
- let analyses = htmlTokenAnalyses.get(node);
57
- const cached = analyses?.get(value);
58
- if (cached)
59
- return cached;
60
- const comments = htmlComments(value);
61
- const analysis = {
62
- comments,
63
- maskedSource: maskSourceRanges(value, comments.map((comment) => [comment.start, comment.end]), 0, value.length)
64
- };
65
- if (!analyses) {
66
- analyses = /* @__PURE__ */ new Map();
67
- htmlTokenAnalyses.set(node, analyses);
68
- }
69
- analyses.set(value, analysis);
70
- return analysis;
71
- }
72
- function markdownTopLevelComments(input) {
73
- const document = parseMarkdown(input);
74
- return document.root.children.flatMap((node) => {
75
- if (node.type !== "html")
76
- return [];
77
- const source = markdownNodeSource(document, node);
78
- const offset = node.position?.start.offset ?? document.start;
79
- return readHtmlToken(node, source).comments.filter((comment) => comment.topLevel).map((comment) => ({
80
- start: offset + comment.start,
81
- end: offset + comment.end,
82
- source: source.slice(comment.start, comment.end)
83
- }));
84
- });
85
- }
86
- function maskSourceRanges(source, ranges, start, end) {
87
- let result = "";
88
- let offset = start;
89
- for (const [from, until] of ranges.sort((a, b) => a[0] - b[0])) {
90
- result += source.slice(offset, from);
91
- result += source.slice(from, until).replace(/[^\r\n]/g, " ");
92
- offset = until;
93
- }
94
- return result + source.slice(offset, end);
95
- }
96
- function markdownText(node) {
97
- if (node.type === "yaml")
98
- return "";
99
- if (node.type === "html")
100
- return readHtmlToken(node, node.value).maskedSource;
101
- if (node.type === "image" || node.type === "imageReference")
102
- return node.alt ?? "";
103
- if ("value" in node)
104
- return node.value;
105
- if ("children" in node) {
106
- const separator = ["root", "list", "listItem", "blockquote"].includes(node.type) ? "\n" : "";
107
- return node.children.map((child) => markdownText(child)).filter(Boolean).join(separator);
108
- }
109
- return node.type === "break" ? "\n" : "";
110
- }
111
- function markdownSection(document, nodes, start, end) {
112
- return { source: document.source, root: { type: "root", children: nodes }, start, end };
113
- }
114
- function visibleMarkdownSource(input, options = {}) {
115
- const document = parseMarkdown(input);
116
- const visibleNodes = /* @__PURE__ */ new Set();
117
- function markVisibleContent(node) {
118
- if (node.type === "yaml" || node.type === "code" && !options.includeCode)
119
- return false;
120
- let visible = false;
121
- if (node.type === "image" || node.type === "imageReference" || node.type === "footnoteReference")
122
- visible = true;
123
- else if (node.type === "html")
124
- visible = !isMarkdownComment(node);
125
- else if ("value" in node)
126
- visible = Boolean(node.value.trim());
127
- else if ("children" in node)
128
- visible = node.children.map(markVisibleContent).some(Boolean);
129
- if (visible)
130
- visibleNodes.add(node);
131
- return visible;
132
- }
133
- markVisibleContent(document.root);
134
- const ranges = [];
135
- walkMarkdown(document.root, (node) => {
136
- if (!visibleNodes.has(node)) {
137
- const start = Math.max(document.start, node.position?.start.offset ?? document.start);
138
- const end = Math.min(document.end, node.position?.end.offset ?? document.end);
139
- if (end > start)
140
- ranges.push([start, end]);
141
- return false;
142
- }
143
- if (node.type === "html") {
144
- const nodeStart = node.position?.start.offset ?? document.start;
145
- for (const comment of readHtmlToken(node, markdownNodeSource(document, node)).comments)
146
- ranges.push([nodeStart + comment.start, nodeStart + comment.end]);
147
- return false;
148
- }
149
- });
150
- return maskSourceRanges(document.source, ranges, document.start, document.end);
151
- }
152
- function visibleMarkdownLines(input) {
153
- return visibleMarkdownSource(input).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
154
- }
155
- function markdownListItems(input) {
156
- const document = parseMarkdown(input);
157
- const items = [];
158
- walkMarkdown(document.root, (node) => {
159
- if (node.type === "blockquote" || node.type === "footnoteDefinition")
160
- return false;
161
- if (node.type === "listItem") {
162
- const source = markdownNodeSource(document, node);
163
- items.push({ node, line: source.split(/\r?\n/, 1)[0].trim() });
164
- }
165
- });
166
- return items;
167
- }
168
- function markdownFrontmatter(input) {
169
- const document = parseMarkdown(input);
170
- const node = document.root.children[0];
171
- if (node?.type !== "yaml" || node.position?.start.offset !== 0)
172
- return null;
173
- const raw = markdownNodeSource(document, node);
174
- const lines = raw.split(/\r?\n/);
175
- if (lines[0] !== "---" || !["---", "..."].includes(lines.at(-1) ?? ""))
176
- return null;
177
- let end = node.position.end.offset ?? raw.length;
178
- if (document.source.slice(end, end + 2) === "\r\n")
179
- end += 2;
180
- else if (document.source[end] === "\n")
181
- end++;
182
- return { node, value: node.value, end };
183
- }
184
- function markdownProse(input) {
185
- const document = parseMarkdown(input);
186
- const fragments = [];
187
- walkMarkdown(document.root, (node) => {
188
- if (node.type === "text" || node.type === "html")
189
- fragments.push({ value: markdownText(node), line: node.position?.start.line ?? 1 });
190
- });
191
- return fragments;
192
- }
193
-
194
- // src/core/document-model.ts
195
- var CHANGE_DOCUMENT_SCHEMA = {
196
- titlePrefix: "# Change:",
197
- sections: [
198
- { id: "proposal", heading: "Proposal" },
199
- { id: "spec", heading: "Spec" },
200
- { id: "design", heading: "Design" },
201
- { id: "tasks", heading: "Tasks" },
202
- { id: "verify", heading: "Verify" },
203
- { id: "blockers", heading: "Blockers" }
204
- ]
205
- };
206
- var GROUP_BRIEF_DOCUMENT_SCHEMA = {
207
- titlePrefix: "# Change Group:",
208
- sections: [
209
- { id: "goal", heading: "Goal" },
210
- { id: "scope", heading: "Scope" },
211
- { id: "sharedConstraints", heading: "Shared Constraints" },
212
- { id: "slices", heading: "Slices" },
213
- { id: "completionConditions", heading: "Completion Conditions" },
214
- { id: "durableOutcomes", heading: "Durable Outcomes" },
215
- { id: "blockers", heading: "Blockers" }
216
- ]
217
- };
218
- var DocumentSectionCardinalityError = class extends Error {
219
- constructor(sectionId, count) {
220
- super(`document must contain exactly one ${sectionId} section; found ${count}`);
221
- this.sectionId = sectionId;
222
- this.count = count;
223
- this.name = "DocumentSectionCardinalityError";
224
- }
225
- sectionId;
226
- count;
227
- };
228
- function parseRspDocument(input, schema) {
229
- const markdown = parseMarkdown(input);
230
- const source = markdown.source;
231
- const definitionsByHeading = new Map(schema.sections.map((definition) => [definition.heading, definition]));
232
- const observedHeadings = collectLevelTwoHeadings(markdown);
233
- const sections = /* @__PURE__ */ new Map();
234
- const unknownHeadings = [];
235
- for (let index = 0; index < observedHeadings.length; index++) {
236
- const observed = observedHeadings[index];
237
- const definition = definitionsByHeading.get(observed.heading);
238
- if (!definition) {
239
- unknownHeadings.push(observed.heading);
240
- continue;
241
- }
242
- const nextHeading = observedHeadings[index + 1];
243
- const bodyEnd = nextHeading ? lineBreakStart(source, nextHeading.start) : source.length;
244
- const section = {
245
- id: definition.id,
246
- heading: definition.heading,
247
- canonical: observed.canonical,
248
- bodyEnd,
249
- markdown: markdownSection(markdown, markdown.root.children.filter((node) => (node.position?.start.offset ?? 0) >= observed.end && (node.position?.end.offset ?? source.length) <= bodyEnd), observed.end, bodyEnd)
250
- };
251
- const matches = sections.get(definition.id) ?? [];
252
- matches.push(section);
253
- sections.set(definition.id, matches);
254
- }
255
- const missingSections = schema.sections.filter((definition) => !sections.has(definition.id)).map((definition) => definition.id);
256
- const duplicateSections = schema.sections.filter((definition) => (sections.get(definition.id)?.length ?? 0) > 1).map((definition) => definition.id);
257
- const titleOccurrences = collectDocumentTitles(markdown, schema.titlePrefix);
258
- const titles = titleOccurrences.map((title) => title.value);
259
- return {
260
- source,
261
- markdown,
262
- schema,
263
- title: titles[0] ?? null,
264
- titles,
265
- titleOccurrences,
266
- sections,
267
- unknownHeadings,
268
- missingSections,
269
- duplicateSections
270
- };
271
- }
272
- function getDocumentTitles(document, compatibility = "broad") {
273
- if (compatibility === "broad")
274
- return document.titles;
275
- return document.titleOccurrences.filter((title) => compatibility === "identity" ? title.identityCompatible : title.spaced).map((title) => title.value);
276
- }
277
- function getDocumentTitle(document, compatibility = "broad") {
278
- return getDocumentTitles(document, compatibility)[0] ?? null;
279
- }
280
- function getDocumentSections(document, sectionId) {
281
- return document.sections.get(sectionId) ?? [];
282
- }
283
- function getDocumentSection(document, sectionId) {
284
- return getDocumentSections(document, sectionId)[0];
285
- }
286
- function getDocumentSectionMarkdown(document, sectionId) {
287
- return getDocumentSection(document, sectionId)?.markdown ?? markdownSection(document.markdown, [], 0, 0);
288
- }
289
- function getDocumentSectionDefinitionByHeading(schema, heading) {
290
- return schema.sections.find((candidate) => candidate.heading === heading);
291
- }
292
- function getCanonicalSectionHeadings(schema) {
293
- return schema.sections.map((section) => section.heading);
294
- }
295
- function renderDocumentSectionHeading(schema, sectionId) {
296
- const definition = schema.sections.find((candidate) => candidate.id === sectionId);
297
- if (!definition)
298
- throw new Error(`unknown document section: ${sectionId}`);
299
- return `## ${definition.heading}`;
300
- }
301
- function renderDocumentTitle(schema, title) {
302
- return `${schema.titlePrefix} ${title}`;
303
- }
304
- function appendDocumentSectionItem(document, sectionId, item) {
305
- const matches = getDocumentSections(document, sectionId);
306
- if (matches.length !== 1)
307
- throw new DocumentSectionCardinalityError(sectionId, matches.length);
308
- const insertAt = matches[0].bodyEnd;
309
- const before = document.source.slice(0, insertAt);
310
- const separator = before.endsWith("\n\n") ? "" : before.endsWith("\n") ? "\n" : "\n\n";
311
- return `${before}${separator}${item}
312
- ${document.source.slice(insertAt)}`;
313
- }
314
- function collectLevelTwoHeadings(document) {
315
- const headings = [];
316
- for (const node of document.root.children) {
317
- if (node.type !== "heading" || node.depth !== 2 || node.position?.start.column !== 1)
318
- continue;
319
- const raw = markdownNodeSource(document, node);
320
- if (!raw.startsWith("## ") || raw.includes("\n"))
321
- continue;
322
- const value = raw.slice(3);
323
- const heading = value.trimEnd();
324
- headings.push({
325
- heading,
326
- canonical: heading === value,
327
- start: node.position.start.offset,
328
- end: node.position.end.offset
329
- });
330
- }
331
- return headings;
332
- }
333
- function collectDocumentTitles(document, prefix) {
334
- const titles = [];
335
- for (const node of document.root.children) {
336
- if (node.type !== "heading" || node.depth !== 1)
337
- continue;
338
- const start = node.position.start.offset;
339
- const lineStart = document.source.lastIndexOf("\n", start - 1) + 1;
340
- const line = document.source.slice(lineStart, node.position.end.offset);
341
- const trimmed = line.trim();
342
- if (!trimmed.startsWith(prefix))
343
- continue;
344
- const identity = line.startsWith(prefix) ? line.slice(prefix.length).match(/^\s+(\S+)\s*$/) : null;
345
- titles.push({
346
- value: trimmed.slice(prefix.length).trim(),
347
- identityCompatible: identity !== null,
348
- spaced: line.startsWith(`${prefix} `)
349
- });
350
- }
351
- return titles;
352
- }
353
- function lineBreakStart(source, headingStart) {
354
- if (headingStart > 0 && source[headingStart - 1] === "\n")
355
- return headingStart - 1;
356
- return headingStart;
357
- }
358
-
359
- // src/core/yaml.mjs
360
- import { parseDocument } from "yaml";
361
- function parseYamlDocument(source) {
362
- const document = parseDocument(source, { keepSourceTokens: true });
363
- if (document.errors.length)
364
- throw new Error(document.errors.map((error) => error.message).join("; "));
365
- return document;
366
- }
367
- function yamlMapping(document) {
368
- const value = document.toJS();
369
- if (value === null || value === void 0)
370
- return {};
371
- if (typeof value !== "object" || Array.isArray(value))
372
- throw new Error("YAML document must be a mapping/object");
373
- return value;
374
- }
375
-
376
- // src/core/content.ts
377
- function parseYamlText(text) {
378
- return yamlMapping(parseYamlDocument(text));
379
- }
380
- function parseFrontmatter(content) {
381
- const matter = markdownFrontmatter(content);
382
- return matter ? parseYamlText(matter.value) : null;
383
- }
384
- function checkboxItem(line) {
385
- const marker = line.match(/^[-*+]\s+\[([ /x-])\]/i);
386
- return marker ? { state: marker[1].toLowerCase(), line } : null;
387
- }
388
- function collectCheckboxItems(content) {
389
- return markdownListItems(content).flatMap(({ line }) => {
390
- const item = checkboxItem(line);
391
- return item ? [item] : [];
392
- });
393
- }
394
- function countCheckboxItems(items) {
395
- const count = { todo: 0, progress: 0, done: 0, dropped: 0, total: items.length };
396
- for (const { state } of items) {
397
- if (state === " ")
398
- count.todo++;
399
- else if (state === "/")
400
- count.progress++;
401
- else if (state === "x")
402
- count.done++;
403
- else
404
- count.dropped++;
405
- }
406
- return count;
407
- }
408
- function countCheckboxes(content) {
409
- return countCheckboxItems(collectCheckboxItems(content));
410
- }
411
- function collectVerifyCheckboxes(content) {
412
- const document = parseMarkdown(content);
413
- const buckets = { required: [], optional: [], unclassified: [] };
414
- let current = "unclassified";
415
- let sawClassificationHeading = false;
416
- walkMarkdown(document.root, (node) => {
417
- if (node.type === "blockquote" || node.type === "footnoteDefinition")
418
- return false;
419
- if (node.type === "heading" && node.depth === 3 && node.position?.start.column === 1) {
420
- const heading = markdownNodeSource(document, node).match(/^###\s+(Required|Optional)\s*$/i);
421
- if (heading) {
422
- current = heading[1].toLowerCase();
423
- sawClassificationHeading = true;
424
- }
425
- }
426
- if (node.type === "listItem") {
427
- const line = markdownNodeSource(document, node).split(/\r?\n/, 1)[0].trim();
428
- const item = checkboxItem(line);
429
- if (item)
430
- buckets[current].push(item);
431
- }
432
- });
433
- return { ...buckets, legacy: buckets.unclassified.length > 0 || !sawClassificationHeading };
434
- }
435
- function summarizeVerifyCheckboxes(buckets) {
436
- return {
437
- required: countCheckboxItems([...buckets.required, ...buckets.unclassified]),
438
- optional: countCheckboxItems(buckets.optional),
439
- unclassified: countCheckboxItems(buckets.unclassified),
440
- legacy: buckets.legacy
441
- };
442
- }
443
- function openCheckboxLines(items) {
444
- return items.filter((item) => item.state === " " || item.state === "/").map((item) => item.line);
445
- }
446
- function getOpenCheckboxes(content) {
447
- return openCheckboxLines(collectCheckboxItems(content));
448
- }
449
- function changeDocument(content) {
450
- return typeof content === "string" ? parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA) : content;
451
- }
452
- function hasMeaningfulBlockerBody(body) {
453
- return readBlockerEntries(body).length > 0;
454
- }
455
- function getVisibleBodyLines(body) {
456
- return visibleMarkdownLines(body);
457
- }
458
- function hasMeaningfulBlockers(content) {
459
- return hasMeaningfulBlockerBody(getDocumentSectionMarkdown(changeDocument(content), "blockers"));
460
- }
461
- function isEmptyBlockerLine(line) {
462
- const normalized = line.trim();
463
- return /^none[.。]?$/i.test(normalized) || /^[-*]\s*(?:none[.。]?)?$/i.test(normalized);
464
- }
465
- var REQUIRES_RE = /^[-*]\s+requires\s+`([^`]+)`:[ \t]*(\S.*)$/i;
466
- function readBlockerEntries(input) {
467
- const document = parseMarkdown(input);
468
- const entries = [];
469
- const nodeLines = (node, start = node.position.start.offset) => getVisibleBodyLines(
470
- markdownSection(document, [node], start, node.position.end.offset)
471
- );
472
- const text = (line) => line.replace(/^[-*]\s+/, "").trim();
473
- const appendExternal = (lines) => {
474
- for (const line of lines) {
475
- if (!isEmptyBlockerLine(line))
476
- entries.push({ kind: "external", line, text: text(line) });
477
- }
478
- };
479
- const visit = (node) => {
480
- if (node.type === "code")
481
- return;
482
- if (node.type !== "list") {
483
- appendExternal(nodeLines(node));
484
- return;
485
- }
486
- for (const item of node.children) {
487
- for (const [index, child] of item.children.entries()) {
488
- if (child.type === "list" || child.type === "code") {
489
- visit(child);
490
- continue;
491
- }
492
- const lines = nodeLines(child, index === 0 ? item.position.start.offset : child.position.start.offset);
493
- if (index === 0 && child.type === "paragraph") {
494
- const line = lines[0] ?? "";
495
- const dependency = line.match(REQUIRES_RE);
496
- if (dependency) {
497
- entries.push({ kind: "dependency", line, text: text(line), name: dependency[1], reason: dependency[2] });
498
- lines.shift();
499
- } else if (/^[-*]\s+requires\b/i.test(line)) {
500
- entries.push({ kind: "malformed-dependency", line, text: text(line) });
501
- lines.shift();
502
- }
503
- }
504
- appendExternal(lines);
505
- }
506
- }
507
- };
508
- document.root.children.forEach(visit);
509
- return entries;
510
- }
511
- function detectDeltaSections(content) {
512
- const body = getDocumentSectionMarkdown(changeDocument(content), "spec");
513
- const labels = new Set(body.root.children.filter((node) => node.type === "heading" && node.depth === 3).map((node) => markdownText(node).trim().toUpperCase()));
514
- return { added: labels.has("ADDED"), modified: labels.has("MODIFIED"), removed: labels.has("REMOVED") };
515
- }
516
- function parseScenarios(content) {
517
- const document = parseMarkdown(content);
518
- const scenarios = [];
519
- let scenario = null;
520
- for (const node of document.root.children) {
521
- if (node.type === "heading" && node.depth <= 4) {
522
- const title = markdownText(node);
523
- scenario = (node.depth === 3 || node.depth === 4) && /^Scenario:\s*\S/i.test(title) ? { heading: title.slice(title.indexOf(":") + 1).trim(), steps: [] } : null;
524
- if (scenario)
525
- scenarios.push(scenario);
526
- continue;
527
- }
528
- if (scenario && node.type === "list") {
529
- for (const item of node.children) {
530
- const line = markdownNodeSource(document, item).split(/\r?\n/, 1)[0].trim();
531
- const step = line.replace(/^[-*+]\s+/, "");
532
- if (/^(?:GIVEN|WHEN|THEN|AND|BUT)\b/i.test(step))
533
- scenario.steps.push(step);
534
- }
535
- }
536
- }
537
- return scenarios.filter((item) => item.steps.length > 0);
538
- }
539
-
540
- // src/core/decisions.ts
541
- import { existsSync } from "fs";
542
- import { mkdir, readdir as readdir2, realpath, stat } from "fs/promises";
543
- import { dirname, isAbsolute, join as join2, posix, relative, resolve, sep } from "path";
544
-
545
- // src/core/managed-path.ts
546
- import { lstatSync } from "fs";
547
- import { readdir, writeFile } from "fs/promises";
548
- import { join } from "path";
549
- var ManagedPathError = class extends Error {
550
- constructor(path, message) {
551
- super(message);
552
- this.path = path;
553
- this.name = "ManagedPathError";
554
- }
555
- path;
556
- };
557
- function inspectManagedDirectory(path, label, options = {}) {
558
- try {
559
- const stats = lstatSync(path);
560
- if (stats.isDirectory())
561
- return { exists: true, issue: null };
562
- return {
563
- exists: true,
564
- issue: new ManagedPathError(path, `${label} must be a real directory: ${path}`)
565
- };
566
- } catch (error) {
567
- if (error.code === "ENOENT") {
568
- if (options.allowMissing)
569
- return { exists: false, issue: null };
570
- return {
571
- exists: false,
572
- issue: new ManagedPathError(path, `${label} must be a real directory: ${path}`)
573
- };
574
- }
575
- const message = error instanceof Error ? error.message : String(error);
576
- return {
577
- exists: false,
578
- issue: new ManagedPathError(path, `unable to inspect ${label} ${path}: ${message}`)
579
- };
580
- }
581
- }
582
- function requireManagedDirectory(path, label, options = {}) {
583
- const inspection = inspectManagedDirectory(path, label, options);
584
- if (inspection.issue)
585
- throw inspection.issue;
586
- return inspection.exists;
587
- }
588
- function inspectManagedFile(path, label, options = {}) {
589
- try {
590
- const stats = lstatSync(path);
591
- if (stats.isFile())
592
- return { exists: true, issue: null };
593
- return {
594
- exists: true,
595
- issue: new ManagedPathError(path, `${label} must be a regular file: ${path}`)
596
- };
597
- } catch (error) {
598
- if (error.code === "ENOENT") {
599
- if (options.allowMissing)
600
- return { exists: false, issue: null };
601
- return {
602
- exists: false,
603
- issue: new ManagedPathError(path, `${label} must be a regular file: ${path}`)
604
- };
605
- }
606
- const message = error instanceof Error ? error.message : String(error);
607
- return {
608
- exists: false,
609
- issue: new ManagedPathError(path, `unable to inspect ${label} ${path}: ${message}`)
610
- };
611
- }
612
- }
613
- function requireManagedFile(path, label, options = {}) {
614
- const inspection = inspectManagedFile(path, label, options);
615
- if (inspection.issue)
616
- throw inspection.issue;
617
- return inspection.exists;
618
- }
619
- async function ensureManagedFile(path, content, label) {
620
- if (requireManagedFile(path, label, { allowMissing: true }))
621
- return false;
622
- await writeFile(path, content, { flag: "wx" });
623
- return true;
624
- }
625
- async function writeManagedFile(path, content, label) {
626
- const exists = requireManagedFile(path, label, { allowMissing: true });
627
- if (exists)
628
- await writeFile(path, content);
629
- else
630
- await writeFile(path, content, { flag: "wx" });
631
- }
632
- function resolveManagedDirectoryChain(root, segments, label) {
633
- requireManagedDirectory(root, `${label} root`);
634
- let current = root;
635
- for (const segment of segments) {
636
- if (!segment || segment === "." || segment === ".." || segment.includes("/") || segment.includes("\\"))
637
- throw new ManagedPathError(current, `${label} contains an invalid directory segment: ${segment}`);
638
- current = join(current, segment);
639
- requireManagedDirectory(current, label, { allowMissing: true });
640
- }
641
- return current;
642
- }
643
- async function inspectManagedFileTree(root, label, options = {}) {
644
- const result = { rootExists: false, files: [], issues: [] };
645
- const rootInspection = inspectManagedDirectory(root, `${label} root`, options);
646
- if (rootInspection.issue) {
647
- result.issues.push(rootInspection.issue);
648
- return result;
649
- }
650
- if (!rootInspection.exists)
651
- return result;
652
- result.rootExists = true;
653
- await collectManagedFiles(root, label, result, options.includeHidden);
654
- result.files.sort();
655
- return result;
656
- }
657
- async function collectManagedFiles(directory, label, result, includeHidden = false) {
658
- let entries;
659
- try {
660
- entries = await readdir(directory, { withFileTypes: true });
661
- } catch (error) {
662
- const message = error instanceof Error ? error.message : String(error);
663
- result.issues.push(new ManagedPathError(directory, `unable to inspect the ${label} tree at ${directory}: ${message}`));
664
- return;
665
- }
666
- for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
667
- if (!includeHidden && entry.name.startsWith("."))
668
- continue;
669
- const path = join(directory, entry.name);
670
- if (entry.isDirectory()) {
671
- await collectManagedFiles(path, label, result, includeHidden);
672
- continue;
673
- }
674
- if (entry.isFile()) {
675
- result.files.push(path);
676
- continue;
677
- }
678
- result.issues.push(new ManagedPathError(path, `unsupported entry in the ${label} tree: ${path}`));
679
- }
680
- }
681
-
682
- // src/core/decisions.ts
683
- var DEFAULT_DECISION_RECORDS_PATH = ".rsp/specs/decisions";
684
- function normalizeDecisionRecordsPath(pathValue) {
685
- return posix.normalize(pathValue.trim().replace(/\\/g, "/")).replace(/^\.\//, "").replace(/\/$/, "");
686
- }
687
- function validateDecisionRecordsPath(pathValue) {
688
- if (typeof pathValue !== "string" || pathValue.trim() === "")
689
- return 'config.yaml field "decisions.path" must be a non-empty string';
690
- const raw = pathValue.trim();
691
- const portable = raw.replace(/\\/g, "/");
692
- if (isAbsolute(raw) || posix.isAbsolute(portable) || /^[a-z]:[\\/]/i.test(raw))
693
- return 'config.yaml field "decisions.path" must be project-relative';
694
- const normalized = normalizeDecisionRecordsPath(raw);
695
- if (normalized === "." || normalized === ".." || normalized.startsWith("../"))
696
- return 'config.yaml field "decisions.path" must stay inside the Host Project';
697
- if ((normalized === ".rsp" || normalized.startsWith(".rsp/")) && normalized !== DEFAULT_DECISION_RECORDS_PATH)
698
- return `config.yaml field "decisions.path" must use ${DEFAULT_DECISION_RECORDS_PATH} or a path outside .rsp/`;
699
- return null;
700
- }
701
- function getDecisionRecordsConfigIssue(config) {
702
- if (!("decisions" in config))
703
- return null;
704
- const decisions = config.decisions;
705
- if (!decisions || typeof decisions !== "object" || Array.isArray(decisions))
706
- return 'config.yaml field "decisions" must be a mapping with one path';
707
- const keys = Object.keys(decisions);
708
- if (keys.length !== 1 || keys[0] !== "path")
709
- return 'config.yaml field "decisions" supports only "path"';
710
- return validateDecisionRecordsPath(decisions.path);
711
- }
712
- function resolveDecisionRecordsPath(config) {
713
- const configured = config.decisions?.path;
714
- return configured && validateDecisionRecordsPath(configured) === null ? normalizeDecisionRecordsPath(configured) : DEFAULT_DECISION_RECORDS_PATH;
715
- }
716
- async function validateDecisionRecordsFilesystemPath(pathValue, cwd = process.cwd()) {
717
- const projectRoot = await realpath(cwd);
718
- const targetPath = resolve(projectRoot, pathValue);
719
- let existingAncestor = targetPath;
720
- while (!existsSync(existingAncestor)) {
721
- const parent = dirname(existingAncestor);
722
- if (parent === existingAncestor)
723
- return `Decision Record path "${pathValue}" has no accessible Host Project ancestor`;
724
- existingAncestor = parent;
725
- }
726
- const resolvedAncestor = await realpath(existingAncestor);
727
- const rel = relative(projectRoot, resolvedAncestor);
728
- if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel))
729
- return `Decision Record path "${pathValue}" resolves outside the Host Project through ${existingAncestor}`;
730
- if (existsSync(targetPath) && !(await stat(targetPath)).isDirectory())
731
- return `Decision Record path "${pathValue}" must be a directory`;
732
- return null;
733
- }
734
-
735
- // src/core/config.ts
736
- var __dirname = dirname2(fileURLToPath(import.meta.url));
737
- var PKG_ROOT = join3(__dirname, "..");
738
- var RSP_DIR = ".rsp";
739
- var RSP_RULES_PATH = join3(RSP_DIR, "rsp-rules.md");
740
- var OBSOLETE_RSP_RULES_PATH = join3(RSP_DIR, "rules", "rsp-rules.md");
741
- var CHANGES_DIR = join3(RSP_DIR, "changes");
742
- var FOCUS_DIR = join3(RSP_DIR, "focus.d");
743
- var MAX_FOCUS_CAPSULE_BYTES = 4096;
744
- var ARCHIVES_DIR = join3(RSP_DIR, "archives");
745
- var LOCK_PATH = join3(RSP_DIR, ".lock");
746
- var CONFIG_PATH = join3(RSP_DIR, "config.yaml");
747
- var VALID_KINDS = ["feature", "fix", "refactor", "docs", "ops", "research"];
748
- var DEFAULT_REQUIRED_SECTIONS = getCanonicalSectionHeadings(CHANGE_DOCUMENT_SCHEMA);
749
- var DEFAULT_WORKFLOW_POLICY = { delegation: "off", closeout: "ask" };
750
- var INVALID_CONFIG_WORKFLOW_POLICY = { delegation: "off", closeout: "manual" };
751
- var CONFIG_DEFAULTS = {
752
- kinds: [],
753
- decisions: { path: ".rsp/specs/decisions" },
754
- language: { default: "en" },
755
- workflow: { delegation: "off", closeout: "ask" }
756
- };
757
- function generateConfigTemplate(config = {}) {
758
- const kinds = config.kinds ?? CONFIG_DEFAULTS.kinds;
759
- const decisions = { ...CONFIG_DEFAULTS.decisions, ...config.decisions };
760
- const workflow = { ...CONFIG_DEFAULTS.workflow, ...config.workflow };
761
- const language = { ...CONFIG_DEFAULTS.language, ...config.language };
762
- const kindsYaml = stringify(kinds).trimEnd();
763
- const renderedKinds = kinds.length === 0 ? `kinds: ${kindsYaml}` : `kinds:
764
- ${kindsYaml.split("\n").map((line) => ` ${line}`).join("\n")}`;
765
- const artifactsLine = language.artifacts ? ` artifacts: ${stringify(language.artifacts).trim()}` : " # artifacts: zh-CN";
766
- const commitLine = language.commit ? ` commit: ${stringify(language.commit).trim()}` : " # commit: zh-CN";
767
- return `# RSP project configuration
768
- # An empty kinds list retains the built-in defaults.
769
- # A non-empty kinds list replaces the built-in defaults; it does not extend them.
770
- # Every entry must be a unique non-empty string.
771
- #
772
- # Built-in defaults:
773
- # kinds: ${VALID_KINDS.join(", ")}
774
- #
775
- ${renderedKinds}
776
-
777
- # Decision Records default to .rsp/specs/decisions.
778
- decisions:
779
- path: ${stringify(decisions.path).trim()}
780
-
781
- # Automatic delegation is off; explicit worker requests remain allowed.
782
- # Closeout applies only to established final delivery: ask, auto, or manual.
783
- # Ordinary completion and checkpoint commits keep work open. No setting grants Git authority.
784
- workflow:
785
- delegation: ${workflow.delegation}
786
- closeout: ${workflow.closeout}
787
-
788
- # Set one shared project default for durable artifact and commit prose.
789
- # Response language remains user/session-owned and is never read from this project mapping.
790
- language:
791
- default: ${stringify(language.default).trim()}
792
- ${artifactsLine}
793
- ${commitLine}
794
- `;
795
- }
796
- var DELEGATION_POLICIES = ["off", "auto"];
797
- var CLOSEOUT_POLICIES = ["ask", "auto", "manual"];
798
- var _configCache = null;
799
- async function loadRspConfig() {
800
- const inspection = await inspectRspConfig();
801
- if (inspection.issues.length > 0)
802
- throw new Error(inspection.issues.join("; "));
803
- return inspection.config;
804
- }
805
- async function inspectRspConfig(cwd = process.cwd()) {
806
- cwd = resolve2(cwd);
807
- if (_configCache?.cwd === cwd)
808
- return _configCache.inspection;
809
- const configPath = join3(cwd, CONFIG_PATH);
810
- const configFile = inspectManagedFile(configPath, "config file", { allowMissing: true });
811
- if (configFile.issue)
812
- throw configFile.issue;
813
- if (!configFile.exists) {
814
- const inspection2 = { config: {}, issues: [] };
815
- _configCache = { cwd, inspection: inspection2 };
816
- return inspection2;
817
- }
818
- const raw = await readFile(configPath, "utf-8");
819
- const parsed = parseYamlText(raw);
820
- const issues = validateRspConfig(parsed);
821
- const decisions = parsed.decisions && typeof parsed.decisions === "object" && !Array.isArray(parsed.decisions) ? parsed.decisions : void 0;
822
- const decisionPath = decisions?.path;
823
- const workflowValue = parsed.workflow && typeof parsed.workflow === "object" && !Array.isArray(parsed.workflow) ? parsed.workflow : void 0;
824
- const languageValue = parsed.language && typeof parsed.language === "object" && !Array.isArray(parsed.language) ? parsed.language : void 0;
825
- const delegation = DELEGATION_POLICIES.includes(workflowValue?.delegation) ? workflowValue?.delegation : void 0;
826
- const closeout = CLOSEOUT_POLICIES.includes(workflowValue?.closeout) ? workflowValue?.closeout : void 0;
827
- const kinds = Array.isArray(parsed.kinds) && parsed.kinds.every((value) => typeof value === "string" && value.trim() !== "") ? parsed.kinds.map((value) => value.trim()) : void 0;
828
- const decisionRecordsIssue = getDecisionRecordsConfigIssue(parsed);
829
- const inspection = {
830
- config: {
831
- kinds,
832
- decisions: decisionRecordsIssue === null && validateDecisionRecordsPath(decisionPath) === null ? { path: normalizeDecisionRecordsPath(String(decisionPath)) } : void 0,
833
- workflow: workflowValue ? { delegation, closeout } : void 0,
834
- language: parseLanguageConfig(languageValue)
835
- },
836
- issues
837
- };
838
- _configCache = { cwd, inspection };
839
- return inspection;
840
- }
841
- function validateRspConfig(parsed) {
842
- const issues = [];
843
- const supported = /* @__PURE__ */ new Set(["kinds", "decisions", "workflow", "language"]);
844
- for (const key of Object.keys(parsed)) {
845
- if (key === "manage") {
846
- issues.push('config.yaml field "manage" requires explicit migration: remove manage and choose workflow.delegation (off | auto) and workflow.closeout (ask | auto | manual); recommended defaults are off/ask. Old local closeout no longer grants Git authority. rsp update will not convert it automatically');
847
- } else if (key === "required_sections") {
848
- issues.push('config.yaml field "required_sections" is no longer supported');
849
- } else if (!supported.has(key)) {
850
- issues.push(`config.yaml field "${key}" is not supported`);
851
- }
852
- }
853
- if ("kinds" in parsed) {
854
- if (!Array.isArray(parsed.kinds)) {
855
- issues.push('config.yaml field "kinds" must be a YAML list');
856
- } else if (!parsed.kinds.every((value) => typeof value === "string" && value.trim() !== "")) {
857
- issues.push('config.yaml field "kinds" must contain only non-empty strings');
858
- } else {
859
- const kinds = parsed.kinds.map((value) => value.trim());
860
- const duplicates = [...new Set(kinds.filter((value, index) => kinds.indexOf(value) !== index))];
861
- if (duplicates.length > 0)
862
- issues.push(`config.yaml field "kinds" contains duplicate entries: ${duplicates.join(", ")}`);
863
- }
864
- }
865
- const decisionRecordsIssue = getDecisionRecordsConfigIssue(parsed);
866
- if (decisionRecordsIssue)
867
- issues.push(decisionRecordsIssue);
868
- if ("workflow" in parsed) {
869
- if (!parsed.workflow || typeof parsed.workflow !== "object" || Array.isArray(parsed.workflow)) {
870
- issues.push('config.yaml field "workflow" must be a YAML mapping');
871
- } else {
872
- const workflow = parsed.workflow;
873
- const supportedWorkflowFields = /* @__PURE__ */ new Set(["delegation", "closeout"]);
874
- for (const key of Object.keys(workflow)) {
875
- if (!supportedWorkflowFields.has(key))
876
- issues.push(`config.yaml field "workflow.${key}" is not supported`);
877
- }
878
- if ("delegation" in workflow && !DELEGATION_POLICIES.includes(workflow.delegation))
879
- issues.push('config.yaml field "workflow.delegation" must be one of: off, auto');
880
- if ("closeout" in workflow && !CLOSEOUT_POLICIES.includes(workflow.closeout))
881
- issues.push('config.yaml field "workflow.closeout" must be one of: ask, auto, manual');
882
- }
883
- }
884
- if ("language" in parsed) {
885
- if (!parsed.language || typeof parsed.language !== "object" || Array.isArray(parsed.language)) {
886
- issues.push('config.yaml field "language" must be a YAML mapping');
887
- } else {
888
- const language = parsed.language;
889
- const supportedLanguageFields = /* @__PURE__ */ new Set(["default", "artifacts", "commit"]);
890
- for (const key of Object.keys(language)) {
891
- if (!supportedLanguageFields.has(key))
892
- issues.push(`config.yaml field "language.${key}" is not supported`);
893
- }
894
- if (!("default" in language))
895
- issues.push('config.yaml field "language.default" is required when language is configured');
896
- for (const key of supportedLanguageFields) {
897
- if (key in language && canonicalizeLanguageTag(language[key]) === null)
898
- issues.push(`config.yaml field "language.${key}" must be a non-empty valid BCP 47 language tag`);
899
- }
900
- }
901
- }
902
- return issues;
903
- }
904
- function clearConfigCache() {
905
- _configCache = null;
906
- }
907
- function resolveKinds(config) {
908
- return config.kinds && config.kinds.length > 0 ? config.kinds : [...VALID_KINDS];
909
- }
910
- function resolveRequiredSections(_config) {
911
- return [...DEFAULT_REQUIRED_SECTIONS];
912
- }
913
- function resolveWorkflowPolicy(config, options = {}) {
914
- if (options.configValid === false)
915
- return { ...INVALID_CONFIG_WORKFLOW_POLICY };
916
- return {
917
- delegation: config.workflow?.delegation ?? DEFAULT_WORKFLOW_POLICY.delegation,
918
- closeout: config.workflow?.closeout ?? DEFAULT_WORKFLOW_POLICY.closeout
919
- };
920
- }
921
- function resolveLanguagePolicy(config, options = {}) {
922
- if (options.configValid === false || !config.language)
923
- return { artifacts: null, commit: null };
924
- return {
925
- artifacts: config.language.artifacts ?? config.language.default,
926
- commit: config.language.commit ?? config.language.default
927
- };
928
- }
929
- function parseLanguageConfig(value) {
930
- if (!value)
931
- return void 0;
932
- const defaultLanguage = canonicalizeLanguageTag(value.default);
933
- if (defaultLanguage === null)
934
- return void 0;
935
- const artifacts = canonicalizeLanguageTag(value.artifacts);
936
- const commit = canonicalizeLanguageTag(value.commit);
937
- return {
938
- default: defaultLanguage,
939
- artifacts: artifacts ?? void 0,
940
- commit: commit ?? void 0
941
- };
942
- }
943
- function canonicalizeLanguageTag(value) {
944
- if (typeof value !== "string" || value.trim() === "")
945
- return null;
946
- try {
947
- return Intl.getCanonicalLocales(value.trim())[0] ?? null;
948
- } catch {
949
- return null;
950
- }
951
- }
952
- async function getVersion() {
953
- const { version } = JSON.parse(await readFile(join3(PKG_ROOT, "package.json"), "utf-8"));
954
- return version;
955
- }
2
+ import {
3
+ SpecsQueryError,
4
+ normalizeLogicalPath,
5
+ readSpecsDetail,
6
+ searchSpecs,
7
+ specsInspectionComplete,
8
+ toErrorMessage
9
+ } from "./chunk-LGDCIQYL.mjs";
10
+ import {
11
+ ARCHIVES_DIR,
12
+ CHANGES_DIR,
13
+ CHANGE_DOCUMENT_SCHEMA,
14
+ CONFIG_PATH,
15
+ FOCUS_DIR,
16
+ GROUP_BRIEF_DOCUMENT_SCHEMA,
17
+ ManagedPathError,
18
+ collectVerifyCheckboxes,
19
+ countCheckboxes,
20
+ getCanonicalSectionHeadings,
21
+ getDocumentSectionMarkdown,
22
+ getDocumentTitle,
23
+ getDocumentTitles,
24
+ getOpenCheckboxes,
25
+ getVisibleBodyLines,
26
+ hasMeaningfulBlockerBody,
27
+ hasMeaningfulBlockers,
28
+ inspectManagedDirectory,
29
+ inspectRspConfig,
30
+ markdownListItems,
31
+ markdownText,
32
+ openCheckboxLines,
33
+ parseFrontmatter,
34
+ parseMarkdown,
35
+ parseRspDocument,
36
+ parseScenarios,
37
+ readBlockerEntries,
38
+ renderDocumentSectionHeading,
39
+ renderDocumentTitle,
40
+ requireManagedDirectory,
41
+ requireManagedFile,
42
+ resolveLanguagePolicy,
43
+ resolveWorkflowPolicy,
44
+ summarizeVerifyCheckboxes,
45
+ visibleMarkdownSource
46
+ } from "./chunk-P3KO3GZW.mjs";
956
47
 
957
48
  // src/status/inspect.ts
958
- import { readFile as readFile5, stat as stat2 } from "fs/promises";
49
+ import { readFile as readFile3, stat } from "fs/promises";
959
50
 
960
51
  // src/core/change-group.ts
961
- import { readFile as readFile3 } from "fs/promises";
962
- import { dirname as dirname4, join as join6, relative as relative4 } from "path";
963
-
964
- // src/core/filesystem.ts
965
- import { existsSync as existsSync2 } from "fs";
966
- import { readdir as readdir3, readFile as readFile2, rmdir } from "fs/promises";
967
- import { basename, dirname as dirname3, join as join4, relative as relative2, sep as sep2 } from "path";
968
-
969
- // src/core/output.ts
970
- import pc2 from "picocolors";
971
- function toErrorMessage(error) {
972
- return error instanceof Error ? error.message : String(error);
973
- }
974
- function emitJson(data, options = {}) {
975
- const indentation = options.compact ? void 0 : 2;
976
- process.stdout.write(`${JSON.stringify(data, null, indentation)}
977
- `);
978
- }
979
-
980
- // src/core/filesystem.ts
981
- async function walkMarkdownFiles(dir, options = {}) {
982
- const all = await walkFiles(dir, options);
983
- return all.filter((f) => f.endsWith(".md"));
984
- }
985
- async function inspectUnsupportedRules() {
986
- const rulesDir = join4(RSP_DIR, "rules");
987
- const diagnostics = [];
988
- const rawEntries = await walkAllEntries(rulesDir, diagnostics);
989
- const entries = rawEntries.filter((entry) => entry.path !== OBSOLETE_RSP_RULES_PATH).map((entry) => ({ path: normalizeLogicalPath(relative2(rulesDir, entry.path)), kind: entry.kind }));
990
- return {
991
- entries,
992
- diagnostics,
993
- directoryRemaining: entries.length > 0
994
- };
995
- }
996
- async function walkAllEntries(dir, diagnostics) {
997
- const entries = [];
998
- try {
999
- const items = await readdir3(dir, { withFileTypes: true });
1000
- for (const item of items) {
1001
- const path = join4(dir, item.name);
1002
- const kind = item.isDirectory() ? "directory" : "file";
1003
- entries.push({ path, kind });
1004
- if (kind === "directory")
1005
- entries.push(...await walkAllEntries(path, diagnostics));
1006
- }
1007
- } catch (error) {
1008
- if (error.code !== "ENOENT") {
1009
- diagnostics.push({ code: "walk_failed", operation: "readdir", path: dir, message: toErrorMessage(error) });
1010
- }
1011
- }
1012
- return entries;
1013
- }
1014
- async function walkFiles(dir, options = {}) {
1015
- const files = [];
1016
- try {
1017
- const items = await readdir3(dir, { withFileTypes: true });
1018
- for (const item of items) {
1019
- if (item.name.startsWith("."))
1020
- continue;
1021
- const filePath = join4(dir, item.name);
1022
- if (item.isDirectory())
1023
- files.push(...await walkFiles(filePath, options));
1024
- else
1025
- files.push(filePath);
1026
- }
1027
- } catch (error) {
1028
- if (error.code !== "ENOENT") {
1029
- options.onError?.({ code: "walk_failed", operation: "readdir", path: dir, message: toErrorMessage(error) });
1030
- }
1031
- }
1032
- return files;
1033
- }
1034
- function normalizeLogicalPath(pathValue) {
1035
- return pathValue.split(sep2).join("/").replace(/\\/g, "/");
1036
- }
1037
- var SPEC_NAME_RE = /^[a-z0-9-]+(?:\/[a-z0-9-]+)*$/;
1038
- function isValidSpecName(name) {
1039
- return SPEC_NAME_RE.test(name);
1040
- }
1041
- function inspectRspInitialization() {
1042
- const designPath = join4(RSP_DIR, "specs", "design.md");
1043
- const design = inspectManagedFile(designPath, "design Spec", { allowMissing: true });
1044
- if (design.issue)
1045
- return `${design.issue.message}
1046
- Check path types and permissions; init/update do not repair existing user content.`;
1047
- if (design.exists)
1048
- return void 0;
1049
- return existsSync2(RSP_DIR) ? `${designPath} is missing.
1050
- Restore your design file, or run rsp init to create missing scaffolding.` : "RSP is not initialized in this project\n Run: rsp init";
1051
- }
1052
- function guardRspInitialized() {
1053
- const issue = inspectRspInitialization();
1054
- if (issue) {
1055
- console.error(` ${pc.red("Error:")} ${issue}`);
1056
- process.exit(1);
1057
- }
1058
- }
1059
- async function cleanupEmptyParentDirs(path, stopDir) {
1060
- let parent = dirname3(path);
1061
- while (parent !== stopDir) {
1062
- try {
1063
- const remaining = await readdir3(parent);
1064
- if (remaining.length === 0)
1065
- await rmdir(parent);
1066
- else
1067
- break;
1068
- } catch {
1069
- break;
1070
- }
1071
- parent = dirname3(parent);
1072
- }
1073
- }
1074
- async function detectProjectName() {
1075
- try {
1076
- if (existsSync2("package.json")) {
1077
- const raw = await readFile2("package.json", "utf-8");
1078
- const pkg = JSON.parse(raw);
1079
- if (pkg.name && pkg.name !== "")
1080
- return pkg.name;
1081
- }
1082
- } catch {
1083
- }
1084
- return basename(process.cwd());
1085
- }
52
+ import { readFile } from "fs/promises";
53
+ import { dirname, join as join2, relative as relative2 } from "path";
1086
54
 
1087
55
  // src/core/work-ref.ts
1088
56
  import { Buffer } from "buffer";
1089
- import { lstatSync as lstatSync2, readdirSync } from "fs";
1090
- import { readdir as readdir4 } from "fs/promises";
1091
- import { isAbsolute as isAbsolute2, join as join5, relative as relative3 } from "path";
57
+ import { lstatSync, readdirSync } from "fs";
58
+ import { readdir } from "fs/promises";
59
+ import { isAbsolute, join, relative } from "path";
1092
60
  var WORK_SEGMENT_RE = /^[\p{Ll}\p{Lo}\p{Lm}\p{M}\p{Nd}]+(?:-[\p{Ll}\p{Lo}\p{Lm}\p{M}\p{Nd}]+)*$/u;
1093
61
  var WORK_SEGMENT_MAX_CODE_POINTS = 100;
1094
62
  var WORK_SEGMENT_MAX_UTF8_BYTES = 200;
@@ -1146,7 +114,7 @@ function resolveWorkRef(name, options = {}) {
1146
114
  assertValidWorkRoot(changesDir, normalizedName);
1147
115
  assertNoNormalizationCollision(changesDir, segments, normalizedName, isGroupBrief);
1148
116
  assertNoIdentityCollision(changesDir, segments, normalizedName, isGroupBrief ? GROUP_BRIEF_FILENAME : void 0);
1149
- const path = join5(changesDir, ...segments.slice(0, -1), isGroupBrief ? GROUP_BRIEF_FILENAME : `${segments.at(-1)}.md`);
117
+ const path = join(changesDir, ...segments.slice(0, -1), isGroupBrief ? GROUP_BRIEF_FILENAME : `${segments.at(-1)}.md`);
1150
118
  const ref = segments.length === 1 ? { kind: "change", name: normalizedName, path, group: null } : isGroupBrief ? { kind: "group-brief", name: normalizedName, path, group: segments[0] } : { kind: "group-change", name: normalizedName, path, group: segments[0] };
1151
119
  if (ref.kind === "group-change" && options.executable !== false)
1152
120
  assertGroupBrief(changesDir, ref);
@@ -1161,7 +129,7 @@ function resolveWorkRef(name, options = {}) {
1161
129
  return ref;
1162
130
  }
1163
131
  function assertGroupBrief(changesDir, ref) {
1164
- const briefPath = join5(changesDir, ref.group, GROUP_BRIEF_FILENAME);
132
+ const briefPath = join(changesDir, ref.group, GROUP_BRIEF_FILENAME);
1165
133
  const kind = getPathKind(briefPath, ref.name);
1166
134
  if (kind === "missing") {
1167
135
  throw new WorkRefError(
@@ -1181,7 +149,7 @@ function assertGroupBrief(changesDir, ref) {
1181
149
  function assertRegularWorkFile(ref, mustExist) {
1182
150
  let stats;
1183
151
  try {
1184
- stats = lstatSync2(ref.path);
152
+ stats = lstatSync(ref.path);
1185
153
  } catch (error) {
1186
154
  if (error.code === "ENOENT") {
1187
155
  if (mustExist) {
@@ -1205,8 +173,8 @@ function assertRegularWorkFile(ref, mustExist) {
1205
173
  }
1206
174
  function resolveWorkRefPath(path, options = {}) {
1207
175
  const changesDir = options.changesDir ?? CHANGES_DIR;
1208
- const logicalPath = normalizeLogicalPath2(relative3(changesDir, path));
1209
- if (isAbsolute2(logicalPath) || logicalPath === ".." || logicalPath.startsWith("../") || !logicalPath.endsWith(".md")) {
176
+ const logicalPath = normalizeLogicalPath2(relative(changesDir, path));
177
+ if (isAbsolute(logicalPath) || logicalPath === ".." || logicalPath.startsWith("../") || !logicalPath.endsWith(".md")) {
1210
178
  throw new WorkRefError(
1211
179
  "invalid_work_ref_path",
1212
180
  `work path must be a Markdown file inside ${changesDir}: ${path}`,
@@ -1233,13 +201,13 @@ function resolveFocusMarkerPath(ref, options = {}) {
1233
201
  assertNoManagedNormalizationCollision(focusDir, ref.name.split("/"), ref.name, "invalid_focus_path");
1234
202
  if (ref.group) {
1235
203
  assertOptionalManagedDirectory(
1236
- join5(focusDir, ref.group),
204
+ join(focusDir, ref.group),
1237
205
  ref.name,
1238
206
  "invalid_focus_path",
1239
207
  "focus path"
1240
208
  );
1241
209
  }
1242
- const markerPath = join5(focusDir, ...ref.name.split("/"));
210
+ const markerPath = join(focusDir, ...ref.name.split("/"));
1243
211
  try {
1244
212
  requireManagedFile(markerPath, "focus marker", { allowMissing: true });
1245
213
  } catch (error) {
@@ -1255,7 +223,7 @@ function resolveArchiveDirectory(ref, options = {}) {
1255
223
  if (!ref.group)
1256
224
  return archivesDir;
1257
225
  assertNoManagedNormalizationCollision(archivesDir, [ref.group], ref.name, "invalid_archive_path");
1258
- const groupPath = join5(archivesDir, ref.group);
226
+ const groupPath = join(archivesDir, ref.group);
1259
227
  assertOptionalManagedDirectory(groupPath, ref.name, "invalid_archive_path", "archive path");
1260
228
  return groupPath;
1261
229
  }
@@ -1274,7 +242,7 @@ async function inspectFocusTree(options = {}) {
1274
242
  if (!rootEntries)
1275
243
  return result;
1276
244
  for (const entry of rootEntries) {
1277
- const path = join5(focusDir, entry.name);
245
+ const path = join(focusDir, entry.name);
1278
246
  if (entry.name === ".gitkeep" && entry.isFile())
1279
247
  continue;
1280
248
  if (entry.isDirectory()) {
@@ -1317,7 +285,7 @@ async function inspectArchiveTree(options = {}) {
1317
285
  if (!rootEntries)
1318
286
  return result;
1319
287
  for (const entry of rootEntries) {
1320
- const path = join5(archivesDir, entry.name);
288
+ const path = join(archivesDir, entry.name);
1321
289
  if (entry.name === "INDEX.md" && entry.isFile())
1322
290
  continue;
1323
291
  if (entry.isDirectory()) {
@@ -1355,7 +323,7 @@ async function inspectWorkTree(options = {}) {
1355
323
  const rootMarkdownNames = new Set(rootEntries.filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name.slice(0, -3)));
1356
324
  const collidingGroups = new Set([...rootDirectories].filter((name) => rootMarkdownNames.has(name)));
1357
325
  for (const entry of rootEntries) {
1358
- const path = join5(changesDir, entry.name);
326
+ const path = join(changesDir, entry.name);
1359
327
  if (entry.name === ".gitkeep" && entry.isFile())
1360
328
  continue;
1361
329
  if (entry.isDirectory()) {
@@ -1402,17 +370,17 @@ async function inspectGroupDirectory(group, groupPath, changesDir, result, colli
1402
370
  "group_brief_missing",
1403
371
  `Change Group "${group}" requires a Group Brief; run: rsp group create ${group}`,
1404
372
  group
1405
- ), join5(groupPath, GROUP_BRIEF_FILENAME));
373
+ ), join(groupPath, GROUP_BRIEF_FILENAME));
1406
374
  } else if (!briefEntry.isFile()) {
1407
375
  addDiagnostic(result, new WorkRefError(
1408
376
  "work_ref_not_file",
1409
- `Group Brief must be a regular file: ${join5(groupPath, GROUP_BRIEF_FILENAME)}`,
377
+ `Group Brief must be a regular file: ${join(groupPath, GROUP_BRIEF_FILENAME)}`,
1410
378
  group
1411
- ), join5(groupPath, GROUP_BRIEF_FILENAME));
379
+ ), join(groupPath, GROUP_BRIEF_FILENAME));
1412
380
  }
1413
381
  for (const entry of entries) {
1414
382
  const input = `${group}/${entry.name}`;
1415
- const path = join5(groupPath, entry.name);
383
+ const path = join(groupPath, entry.name);
1416
384
  if (entry.name === GROUP_BRIEF_FILENAME && !entry.isFile())
1417
385
  continue;
1418
386
  if (entry.name === "brief.md") {
@@ -1441,7 +409,7 @@ async function inspectFocusGroup(group, groupPath, changesDir, result) {
1441
409
  return;
1442
410
  for (const entry of entries) {
1443
411
  const input = `${group}/${entry.name}`;
1444
- const path = join5(groupPath, entry.name);
412
+ const path = join(groupPath, entry.name);
1445
413
  if (entry.isDirectory()) {
1446
414
  addDiagnostic(result, new WorkRefError(
1447
415
  "unsupported_work_depth",
@@ -1467,7 +435,7 @@ async function inspectArchiveGroup(group, groupPath, result) {
1467
435
  return;
1468
436
  for (const entry of entries) {
1469
437
  const input = `${group}/${entry.name}`;
1470
- const path = join5(groupPath, entry.name);
438
+ const path = join(groupPath, entry.name);
1471
439
  if (!entry.isFile() || !entry.name.endsWith(".md")) {
1472
440
  addArchivePathDiagnostic(result, input, path);
1473
441
  continue;
@@ -1500,7 +468,7 @@ function collectResolvedPath(path, changesDir, result) {
1500
468
  }
1501
469
  async function readDirectory(path, input, result) {
1502
470
  try {
1503
- return (await readdir4(path, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
471
+ return (await readdir(path, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
1504
472
  } catch (error) {
1505
473
  addDiagnostic(result, asReadFailure(path, input, error), path);
1506
474
  return null;
@@ -1561,7 +529,7 @@ function normalizeWorkRefName(name, allowGroupBrief) {
1561
529
  }
1562
530
  function assertNoNormalizationCollision(changesDir, segments, input, isGroupBrief) {
1563
531
  for (let depth = 0; depth < segments.length; depth++) {
1564
- const parent = join5(changesDir, ...segments.slice(0, depth));
532
+ const parent = join(changesDir, ...segments.slice(0, depth));
1565
533
  const parentKind = getPathKind(parent, input);
1566
534
  if (parentKind === "missing")
1567
535
  return;
@@ -1594,7 +562,7 @@ function assertNoNormalizationCollision(changesDir, segments, input, isGroupBrie
1594
562
  }
1595
563
  function assertNoManagedNormalizationCollision(root, segments, input, code) {
1596
564
  for (let depth = 0; depth < segments.length; depth++) {
1597
- const parent = join5(root, ...segments.slice(0, depth));
565
+ const parent = join(root, ...segments.slice(0, depth));
1598
566
  const parentKind = getPathKind(parent, input);
1599
567
  if (parentKind === "missing")
1600
568
  return;
@@ -1619,8 +587,8 @@ function assertNoManagedNormalizationCollision(root, segments, input, code) {
1619
587
  }
1620
588
  function assertNoIdentityCollision(changesDir, segments, input, finalFilename) {
1621
589
  for (let depth = 1; depth <= segments.length; depth++) {
1622
- const identityPath = join5(changesDir, ...segments.slice(0, depth));
1623
- const filePath = finalFilename && depth === segments.length ? join5(changesDir, ...segments.slice(0, -1), finalFilename) : `${identityPath}.md`;
590
+ const identityPath = join(changesDir, ...segments.slice(0, depth));
591
+ const filePath = finalFilename && depth === segments.length ? join(changesDir, ...segments.slice(0, -1), finalFilename) : `${identityPath}.md`;
1624
592
  const fileKind = getPathKind(filePath, input);
1625
593
  const identityKind = getPathKind(identityPath, input);
1626
594
  const isPrefix = depth < segments.length;
@@ -1686,7 +654,7 @@ function assertOptionalManagedDirectory(path, input, code, label) {
1686
654
  }
1687
655
  function getPathKind(path, input) {
1688
656
  try {
1689
- const stats = lstatSync2(path);
657
+ const stats = lstatSync(path);
1690
658
  if (stats.isFile())
1691
659
  return "file";
1692
660
  if (stats.isDirectory())
@@ -1714,8 +682,8 @@ function normalizeLogicalPath2(path) {
1714
682
  var PLACEHOLDER_SUMMARY = "<\u2026>";
1715
683
  function extractChangeSummary(content, document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA), options = {}) {
1716
684
  try {
1717
- const frontmatter2 = parseFrontmatter(document.markdown);
1718
- const frontmatterSummary = meaningfulSummary(frontmatter2?.summary, options);
685
+ const frontmatter = parseFrontmatter(document.markdown);
686
+ const frontmatterSummary = meaningfulSummary(frontmatter?.summary, options);
1719
687
  if (frontmatterSummary)
1720
688
  return frontmatterSummary;
1721
689
  } catch {
@@ -1806,12 +774,12 @@ async function inspectChangeGroups(options = {}) {
1806
774
  const archivedGroupBriefPaths = /* @__PURE__ */ new Map();
1807
775
  const openGroupNames = new Set(workTree.briefs.map((brief) => brief.group));
1808
776
  for (const path of archiveTree.files) {
1809
- const archivePath = normalizeLogicalPath(relative4(ARCHIVES_DIR, path));
777
+ const archivePath = normalizeLogicalPath(relative2(ARCHIVES_DIR, path));
1810
778
  const archiveGroup = archivePath.includes("/") ? archivePath.split("/")[0] : null;
1811
779
  if (!archiveGroup || !openGroupNames.has(archiveGroup))
1812
780
  continue;
1813
781
  try {
1814
- const content = await readFile3(path, "utf-8");
782
+ const content = await readFile(path, "utf-8");
1815
783
  const archivedChange = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
1816
784
  const identity = getDocumentTitle(archivedChange, "identity");
1817
785
  if (identity?.includes("/")) {
@@ -1853,7 +821,7 @@ async function inspectChangeGroups(options = {}) {
1853
821
  for (const brief of workTree.briefs) {
1854
822
  let content;
1855
823
  try {
1856
- content = await readFile3(brief.path, "utf-8");
824
+ content = await readFile(brief.path, "utf-8");
1857
825
  } catch (error) {
1858
826
  diagnostics.push({
1859
827
  severity: "error",
@@ -1883,7 +851,7 @@ async function inspectChangeGroups(options = {}) {
1883
851
  for (const diagnostic of archiveTree.diagnostics.filter((item) => {
1884
852
  if (item.code === "invalid_archive_root")
1885
853
  return true;
1886
- const archivePath = normalizeLogicalPath(relative4(ARCHIVES_DIR, item.path));
854
+ const archivePath = normalizeLogicalPath(relative2(ARCHIVES_DIR, item.path));
1887
855
  return archivePath === brief.group || archivePath.startsWith(`${brief.group}/`);
1888
856
  })) {
1889
857
  diagnostics.push({
@@ -1976,10 +944,10 @@ function hasExplicitGroupReopenEvidence(content, retainedEvidence, retainedGroup
1976
944
  return getGroupReopenEvidence(content).some((item) => item.active && retainedGroupBriefPaths.has(item.sourcePath) && !retainedEvidence.has(item.key));
1977
945
  }
1978
946
  async function requireDeclaredGroupSlice(ref) {
1979
- const briefPath = join6(dirname4(ref.path), GROUP_BRIEF_FILENAME);
947
+ const briefPath = join2(dirname(ref.path), GROUP_BRIEF_FILENAME);
1980
948
  let content;
1981
949
  try {
1982
- content = await readFile3(briefPath, "utf-8");
950
+ content = await readFile(briefPath, "utf-8");
1983
951
  } catch (error) {
1984
952
  const message = error instanceof Error ? error.message : String(error);
1985
953
  throw new WorkRefError("work_tree_read_failed", `unable to read Group Brief ${briefPath}: ${message}`, ref.name);
@@ -2009,11 +977,11 @@ async function hasArchivedGroupBrief(group) {
2009
977
  }
2010
978
  const groupPrefix = `${group}/`;
2011
979
  for (const path of archiveTree.files) {
2012
- if (!normalizeLogicalPath(relative4(ARCHIVES_DIR, path)).startsWith(groupPrefix))
980
+ if (!normalizeLogicalPath(relative2(ARCHIVES_DIR, path)).startsWith(groupPrefix))
2013
981
  continue;
2014
982
  let content;
2015
983
  try {
2016
- content = await readFile3(path, "utf-8");
984
+ content = await readFile(path, "utf-8");
2017
985
  } catch (error) {
2018
986
  const message = error instanceof Error ? error.message : String(error);
2019
987
  throw new WorkRefError("work_tree_read_failed", `unable to inspect archived Change Group identity at ${path}: ${message}`, group);
@@ -2025,13 +993,13 @@ async function hasArchivedGroupBrief(group) {
2025
993
  }
2026
994
  function parseGroupBrief(group, document) {
2027
995
  const diagnostics = [];
2028
- let frontmatter2;
996
+ let frontmatter;
2029
997
  try {
2030
- frontmatter2 = parseFrontmatter(document.markdown);
998
+ frontmatter = parseFrontmatter(document.markdown);
2031
999
  } catch (error) {
2032
1000
  diagnostics.push({ severity: "error", code: "group_frontmatter_invalid", change: group, message: error instanceof Error ? error.message : String(error) });
2033
1001
  }
2034
- if (frontmatter2?.kind !== "group")
1002
+ if (frontmatter?.kind !== "group")
2035
1003
  diagnostics.push({ severity: "error", code: "group_kind_invalid", change: group, message: "Group Brief frontmatter must declare kind: group" });
2036
1004
  if (getDocumentTitle(document, "identity") !== group)
2037
1005
  diagnostics.push({ severity: "error", code: "group_heading_mismatch", change: group, message: `Group Brief heading must be: # Change Group: ${group}` });
@@ -2085,13 +1053,13 @@ function groupSectionHeading(sectionId) {
2085
1053
  }
2086
1054
 
2087
1055
  // src/core/dependency-plan.ts
2088
- import { readFile as readFile4 } from "fs/promises";
1056
+ import { readFile as readFile2 } from "fs/promises";
2089
1057
 
2090
1058
  // src/history/current-file.ts
2091
1059
  import { Buffer as Buffer2 } from "buffer";
2092
1060
  import { constants } from "fs";
2093
- import { lstat, open, realpath as realpath2 } from "fs/promises";
2094
- import { isAbsolute as isAbsolute3, relative as relative5, resolve as resolve3, sep as sep3 } from "path";
1061
+ import { lstat, open, realpath } from "fs/promises";
1062
+ import { isAbsolute as isAbsolute2, relative as relative3, resolve, sep } from "path";
2095
1063
 
2096
1064
  // src/history/model.ts
2097
1065
  var HISTORY_DEFAULT_LIMIT = 20;
@@ -2120,10 +1088,10 @@ var ArchiveHistoryError = class extends Error {
2120
1088
  // src/history/current-file.ts
2121
1089
  var READ_NO_FOLLOW_FLAGS = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0);
2122
1090
  async function readCurrentArchiveFile(options) {
2123
- const sourcePath = resolve3(options.sourcePath);
2124
- const archivesDir = resolve3(options.archivesDir);
2125
- const relativePath = relative5(archivesDir, sourcePath);
2126
- if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${sep3}`) || isAbsolute3(relativePath)) {
1091
+ const sourcePath = resolve(options.sourcePath);
1092
+ const archivesDir = resolve(options.archivesDir);
1093
+ const relativePath = relative3(archivesDir, sourcePath);
1094
+ if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute2(relativePath)) {
2127
1095
  throw changed(options.projectPath);
2128
1096
  }
2129
1097
  let inspectedRoot;
@@ -2134,9 +1102,9 @@ async function readCurrentArchiveFile(options) {
2134
1102
  if (!inspectedRoot.isDirectory())
2135
1103
  throw changed(options.projectPath);
2136
1104
  inspected = await lstat(sourcePath, { bigint: true });
2137
- realArchivesDir = await realpath2(archivesDir);
2138
- const resolvedSource = await realpath2(sourcePath);
2139
- const expectedSource = resolve3(realArchivesDir, relativePath);
1105
+ realArchivesDir = await realpath(archivesDir);
1106
+ const resolvedSource = await realpath(sourcePath);
1107
+ const expectedSource = resolve(realArchivesDir, relativePath);
2140
1108
  if (!inspected.isFile() || resolvedSource !== expectedSource || !isContained(realArchivesDir, resolvedSource)) {
2141
1109
  throw changed(options.projectPath);
2142
1110
  }
@@ -2157,9 +1125,9 @@ async function readCurrentArchiveFile(options) {
2157
1125
  }
2158
1126
  try {
2159
1127
  const opened = await handle.stat({ bigint: true });
2160
- const currentRoot = await realpath2(archivesDir);
2161
- const currentSource = await realpath2(sourcePath);
2162
- if (!opened.isFile() || currentRoot !== realArchivesDir || currentSource !== resolve3(currentRoot, relativePath) || !isContained(currentRoot, currentSource) || !sameIdentity(inspected, opened) || !sameSnapshot(inspected, opened)) {
1128
+ const currentRoot = await realpath(archivesDir);
1129
+ const currentSource = await realpath(sourcePath);
1130
+ if (!opened.isFile() || currentRoot !== realArchivesDir || currentSource !== resolve(currentRoot, relativePath) || !isContained(currentRoot, currentSource) || !sameIdentity(inspected, opened) || !sameSnapshot(inspected, opened)) {
2163
1131
  throw changed(options.projectPath);
2164
1132
  }
2165
1133
  if (opened.size > BigInt(options.maxFileBytes))
@@ -2222,9 +1190,9 @@ async function readBoundedArchive(handle, opened, projectPath, maxFileBytes) {
2222
1190
  async function validateCurrentArchivePath(options) {
2223
1191
  const currentRoot = await lstat(options.archivesDir, { bigint: true });
2224
1192
  const currentSource = await lstat(options.sourcePath, { bigint: true });
2225
- const realCurrentRoot = await realpath2(options.archivesDir);
2226
- const realCurrentSource = await realpath2(options.sourcePath);
2227
- if (!currentRoot.isDirectory() || !currentSource.isFile() || !sameIdentity(options.inspectedRoot, currentRoot) || realCurrentRoot !== options.realArchivesDir || realCurrentSource !== resolve3(realCurrentRoot, options.relativePath) || !isContained(realCurrentRoot, realCurrentSource) || !sameIdentity(currentSource, options.finalHandle) || !sameSnapshot(currentSource, options.finalHandle)) {
1193
+ const realCurrentRoot = await realpath(options.archivesDir);
1194
+ const realCurrentSource = await realpath(options.sourcePath);
1195
+ if (!currentRoot.isDirectory() || !currentSource.isFile() || !sameIdentity(options.inspectedRoot, currentRoot) || realCurrentRoot !== options.realArchivesDir || realCurrentSource !== resolve(realCurrentRoot, options.relativePath) || !isContained(realCurrentRoot, realCurrentSource) || !sameIdentity(currentSource, options.finalHandle) || !sameSnapshot(currentSource, options.finalHandle)) {
2228
1196
  throw changed(options.projectPath);
2229
1197
  }
2230
1198
  }
@@ -2238,8 +1206,8 @@ function sameFileSnapshot(left, right) {
2238
1206
  return left.device === right.dev && left.inode === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
2239
1207
  }
2240
1208
  function isContained(root, path) {
2241
- const candidate = relative5(root, path);
2242
- return candidate !== "" && candidate !== ".." && !candidate.startsWith(`..${sep3}`) && !isAbsolute3(candidate);
1209
+ const candidate = relative3(root, path);
1210
+ return candidate !== "" && candidate !== ".." && !candidate.startsWith(`..${sep}`) && !isAbsolute2(candidate);
2243
1211
  }
2244
1212
  function changed(projectPath) {
2245
1213
  return new ArchiveHistoryError(
@@ -2265,7 +1233,7 @@ function isPermissionError(error) {
2265
1233
  }
2266
1234
 
2267
1235
  // src/history/inspect.ts
2268
- import { basename as basename2, dirname as dirname5, relative as relative6, resolve as resolve4 } from "path";
1236
+ import { basename, dirname as dirname2, relative as relative4, resolve as resolve2 } from "path";
2269
1237
 
2270
1238
  // src/core/issue-relationship.ts
2271
1239
  var ISSUE_RELATIONS = /* @__PURE__ */ new Set(["relates", "closes"]);
@@ -2278,13 +1246,13 @@ var IssueRelationshipError = class extends Error {
2278
1246
  }
2279
1247
  code;
2280
1248
  };
2281
- function parseIssueRelationships(frontmatter2) {
2282
- if (!frontmatter2 || !("issues" in frontmatter2))
1249
+ function parseIssueRelationships(frontmatter) {
1250
+ if (!frontmatter || !("issues" in frontmatter))
2283
1251
  return [];
2284
- if (!Array.isArray(frontmatter2.issues))
1252
+ if (!Array.isArray(frontmatter.issues))
2285
1253
  throw new IssueRelationshipError("invalid_issue_shape", 'frontmatter field "issues" must be a list');
2286
1254
  const seen = /* @__PURE__ */ new Set();
2287
- return frontmatter2.issues.map((value, index) => {
1255
+ return frontmatter.issues.map((value, index) => {
2288
1256
  if (!isPlainObject(value))
2289
1257
  throw new IssueRelationshipError("invalid_issue_shape", `issues[${index}] must be a mapping with url and relation`);
2290
1258
  const fields = Object.keys(value);
@@ -2333,7 +1301,7 @@ function isPlainObject(value) {
2333
1301
  // src/history/inspect.ts
2334
1302
  var ARCHIVE_NAME_RE = /^(\d{4}-\d{2}-\d{2})_(.+)\.md$/;
2335
1303
  async function inspectArchiveHistory(options = {}) {
2336
- const archivesDir = resolve4(options.archivesDir ?? ARCHIVES_DIR);
1304
+ const archivesDir = resolve2(options.archivesDir ?? ARCHIVES_DIR);
2337
1305
  const maxFileBytes = options.maxFileBytes ?? HISTORY_MAX_FILE_BYTES;
2338
1306
  const tree = options.archiveTree ?? await inspectArchiveTree({ archivesDir });
2339
1307
  const diagnostics = tree.diagnostics.map((diagnostic) => ({
@@ -2397,42 +1365,42 @@ async function inspectArchiveHistory(options = {}) {
2397
1365
  }
2398
1366
  function parseArchiveEntry(current, path, maxFileBytes) {
2399
1367
  const { sourcePath, archivesDir, content } = current;
2400
- const nameMatch = basename2(sourcePath).match(ARCHIVE_NAME_RE);
1368
+ const nameMatch = basename(sourcePath).match(ARCHIVE_NAME_RE);
2401
1369
  if (!nameMatch || !isCalendarDate(nameMatch[1])) {
2402
1370
  return { diagnostic: errorDiagnostic("archive_name_invalid", path, "archive filename must use a valid YYYY-MM-DD_name.md date") };
2403
1371
  }
2404
1372
  const date = nameMatch[1];
2405
1373
  const archiveStem = nameMatch[2];
2406
- const relativeDirectory = normalizeLogicalPath(relative6(archivesDir, dirname5(sourcePath)));
1374
+ const relativeDirectory = normalizeLogicalPath(relative4(archivesDir, dirname2(sourcePath)));
2407
1375
  const archiveGroup = relativeDirectory === "." || relativeDirectory === "" ? null : relativeDirectory;
2408
1376
  if (archiveGroup && !isCanonicalWorkRefSegment(archiveGroup))
2409
1377
  return { diagnostic: errorDiagnostic("archive_identity_mismatch", path, "archive group path is not a valid one-segment Group identity") };
2410
- const changeDocument2 = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
2411
- const groupDocument = parseRspDocument(changeDocument2.markdown, GROUP_BRIEF_DOCUMENT_SCHEMA);
2412
- const changeHeadings = getDocumentTitles(changeDocument2, "identity");
1378
+ const changeDocument = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
1379
+ const groupDocument = parseRspDocument(changeDocument.markdown, GROUP_BRIEF_DOCUMENT_SCHEMA);
1380
+ const changeHeadings = getDocumentTitles(changeDocument, "identity");
2413
1381
  const groupHeadings = getDocumentTitles(groupDocument, "identity");
2414
1382
  if (changeHeadings.length + groupHeadings.length !== 1) {
2415
1383
  return { diagnostic: errorDiagnostic("archive_heading_invalid", path, "archive must contain exactly one Change or Change Group identity heading") };
2416
1384
  }
2417
- let frontmatter2;
1385
+ let frontmatter;
2418
1386
  try {
2419
- frontmatter2 = parseFrontmatter(changeDocument2.markdown);
1387
+ frontmatter = parseFrontmatter(changeDocument.markdown);
2420
1388
  } catch (error) {
2421
1389
  return { diagnostic: errorDiagnostic("archive_frontmatter_invalid", path, `archive frontmatter could not be parsed: ${toErrorMessage(error)}`) };
2422
1390
  }
2423
- if (typeof frontmatter2?.kind !== "string" || frontmatter2.kind.trim() === "")
1391
+ if (typeof frontmatter?.kind !== "string" || frontmatter.kind.trim() === "")
2424
1392
  return { diagnostic: errorDiagnostic("archive_kind_invalid", path, "archive frontmatter must declare a non-empty kind") };
2425
1393
  if (groupHeadings.length === 1) {
2426
1394
  const group = groupHeadings[0];
2427
- if (!archiveGroup || group !== archiveGroup || frontmatter2.kind !== "group" || !matchesArchiveStem(archiveStem, "brief")) {
1395
+ if (!archiveGroup || group !== archiveGroup || frontmatter.kind !== "group" || !matchesArchiveStem(archiveStem, "brief")) {
2428
1396
  return { diagnostic: errorDiagnostic("archive_identity_mismatch", path, `archived Change Group identity ${group} does not match its archive path`) };
2429
1397
  }
2430
1398
  return { groupBrief: { date, group, path, sourcePath } };
2431
1399
  }
2432
1400
  let issues;
2433
- if ("issues" in frontmatter2) {
1401
+ if ("issues" in frontmatter) {
2434
1402
  try {
2435
- issues = parseIssueRelationships(frontmatter2);
1403
+ issues = parseIssueRelationships(frontmatter);
2436
1404
  } catch {
2437
1405
  issues = void 0;
2438
1406
  }
@@ -2447,7 +1415,7 @@ function parseArchiveEntry(current, path, maxFileBytes) {
2447
1415
  if (!isCanonicalExecutableWorkRef(workRef) || expectedGroup !== archiveGroup || !matchesArchiveStem(archiveStem, expectedBase)) {
2448
1416
  return { diagnostic: errorDiagnostic("archive_identity_mismatch", path, `archived Change identity ${workRef} does not match its archive path`) };
2449
1417
  }
2450
- const rawSummary = extractChangeSummary(content, changeDocument2, { preservePlaceholder: true });
1418
+ const rawSummary = extractChangeSummary(content, changeDocument, { preservePlaceholder: true });
2451
1419
  if (!rawSummary)
2452
1420
  return { diagnostic: errorDiagnostic("archive_summary_missing", path, "archived Change must contain frontmatter summary, Proposal Outcome, or Proposal Summary") };
2453
1421
  const summary = boundText(rawSummary);
@@ -2456,7 +1424,7 @@ function parseArchiveEntry(current, path, maxFileBytes) {
2456
1424
  date,
2457
1425
  workRef,
2458
1426
  group: expectedGroup,
2459
- kind: frontmatter2.kind.trim(),
1427
+ kind: frontmatter.kind.trim(),
2460
1428
  summary: summary.value,
2461
1429
  summaryTruncated: summary.truncated,
2462
1430
  path,
@@ -2481,10 +1449,10 @@ function isCalendarDate(value) {
2481
1449
  return Number.isFinite(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value;
2482
1450
  }
2483
1451
  function archiveOutputPath(sourcePath, archivesDir) {
2484
- return `.rsp/archives/${normalizeLogicalPath(relative6(resolve4(archivesDir), resolve4(sourcePath)))}`;
1452
+ return `.rsp/archives/${normalizeLogicalPath(relative4(resolve2(archivesDir), resolve2(sourcePath)))}`;
2485
1453
  }
2486
1454
  function normalizeOutputPath(path, archivesDir) {
2487
- const relativePath = normalizeLogicalPath(relative6(resolve4(archivesDir), resolve4(path)));
1455
+ const relativePath = normalizeLogicalPath(relative4(resolve2(archivesDir), resolve2(path)));
2488
1456
  return relativePath === "" ? ".rsp/archives" : `.rsp/archives/${relativePath}`;
2489
1457
  }
2490
1458
  function errorDiagnostic(code, path, message) {
@@ -2653,7 +1621,7 @@ async function inspectChangeDependencies(options = {}) {
2653
1621
  diagnostics.push(...archiveHistory.diagnostics);
2654
1622
  for (const brief of workTree.briefs) {
2655
1623
  try {
2656
- if (hasMeaningfulBlockers(await readFile4(brief.path, "utf-8")))
1624
+ if (hasMeaningfulBlockers(await readFile2(brief.path, "utf-8")))
2657
1625
  blockedGroups.add(brief.group);
2658
1626
  } catch (error) {
2659
1627
  diagnostics.push({
@@ -2667,7 +1635,7 @@ async function inspectChangeDependencies(options = {}) {
2667
1635
  }
2668
1636
  for (const ref of workTree.changes) {
2669
1637
  try {
2670
- const parsed = parseDependencyBlockers(await readFile4(ref.path, "utf-8"));
1638
+ const parsed = parseDependencyBlockers(await readFile2(ref.path, "utf-8"));
2671
1639
  if (ref.group && blockedGroups.has(ref.group))
2672
1640
  parsed.external = true;
2673
1641
  parsedByName.set(ref.name, parsed);
@@ -3056,15 +2024,15 @@ async function inspectProjectStatus(options = {}) {
3056
2024
  let issues = [];
3057
2025
  if (path) {
3058
2026
  try {
3059
- const content = await readFile5(path, "utf-8");
2027
+ const content = await readFile3(path, "utf-8");
3060
2028
  const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
3061
2029
  title = getDocumentTitle(document, "spaced") ?? name;
3062
2030
  summary = extractChangeSummary(content, document);
3063
2031
  externalBlockers = readBlockerEntries(getDocumentSectionMarkdown(document, "blockers")).filter((entry) => entry.kind !== "dependency").map((entry) => entry.text);
3064
2032
  try {
3065
- const frontmatter2 = parseFrontmatter(document.markdown);
3066
- kind = frontmatter2?.kind ? String(frontmatter2.kind) : "\u2014";
3067
- issues = parseIssueRelationships(frontmatter2);
2033
+ const frontmatter = parseFrontmatter(document.markdown);
2034
+ kind = frontmatter?.kind ? String(frontmatter.kind) : "\u2014";
2035
+ issues = parseIssueRelationships(frontmatter);
3068
2036
  } catch (error) {
3069
2037
  kind = "(invalid)";
3070
2038
  const issueError = error instanceof IssueRelationshipError;
@@ -3108,7 +2076,7 @@ async function inspectProjectStatus(options = {}) {
3108
2076
  });
3109
2077
  }
3110
2078
  try {
3111
- const metadata = await stat2(path);
2079
+ const metadata = await stat(path);
3112
2080
  ageDays = Math.floor((nowMs - metadata.mtime.getTime()) / (1e3 * 60 * 60 * 24));
3113
2081
  } catch (error) {
3114
2082
  diagnostics.push({
@@ -3200,569 +2168,6 @@ function deriveArchiveTrend(records) {
3200
2168
  return [...counts].sort(([left], [right]) => left.localeCompare(right)).map(([month, count]) => ({ month, count }));
3201
2169
  }
3202
2170
 
3203
- // src/specs/model.ts
3204
- var SPECS_DEFAULT_RESULT_LIMIT = 20;
3205
- var SPECS_MAX_RESULT_LIMIT = 100;
3206
- var SPECS_MAX_CANDIDATES = 1e3;
3207
- var SPECS_MAX_FILE_BYTES = 512 * 1024;
3208
- var SPECS_SEARCH_EXCERPT_CODE_POINTS = 240;
3209
- var SPECS_DETAIL_CONTENT_CODE_POINTS = 12e3;
3210
- var SPECS_MAX_DIAGNOSTICS = 20;
3211
-
3212
- // src/specs/query.ts
3213
- import { Buffer as Buffer3 } from "buffer";
3214
- import { spawnSync } from "child_process";
3215
- import { constants as constants2 } from "fs";
3216
- import { lstat as lstat2, open as open2, realpath as realpath3 } from "fs/promises";
3217
- import { basename as basename3, dirname as dirname6, isAbsolute as isAbsolute4, join as join7, relative as relative7, resolve as resolve5, sep as sep4 } from "path";
3218
- var SPECS_ROOT = join7(RSP_DIR, "specs");
3219
- var GENERATED_INDEX_FILENAME = "00-index.md";
3220
- var LEGACY_GENERATED_INDEX_FILENAME = "INDEX.md";
3221
- var READ_NO_FOLLOW_FLAGS2 = constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0);
3222
- var SpecsQueryError = class extends Error {
3223
- constructor(code, message) {
3224
- super(message);
3225
- this.code = code;
3226
- this.name = "SpecsQueryError";
3227
- }
3228
- code;
3229
- };
3230
- async function inspectSpecs(options = {}) {
3231
- const projectRoot = await realpath3(resolve5(options.cwd ?? process.cwd()));
3232
- const specsRoot = resolve5(projectRoot, SPECS_ROOT);
3233
- const maxCandidates = options.maxCandidates ?? SPECS_MAX_CANDIDATES;
3234
- const maxFileBytes = options.maxFileBytes ?? SPECS_MAX_FILE_BYTES;
3235
- validatePositiveBound(maxCandidates, "maxCandidates");
3236
- validatePositiveBound(maxFileBytes, "maxFileBytes");
3237
- const configInspection = await inspectRspConfig(projectRoot);
3238
- if (configInspection.issues.length > 0)
3239
- throw new SpecsQueryError("invalid_config", configInspection.issues.join("; "));
3240
- const decisionRecordsPath = resolveDecisionRecordsPath(configInspection.config);
3241
- const decisionPathIssue = await validateDecisionRecordsFilesystemPath(decisionRecordsPath, projectRoot);
3242
- if (decisionPathIssue)
3243
- throw new SpecsQueryError("invalid_decision_records_path", decisionPathIssue);
3244
- const decisionRecordsRoot = resolve5(projectRoot, decisionRecordsPath);
3245
- const accumulator = {
3246
- documents: [],
3247
- generatedIndexes: [],
3248
- diagnostics: [],
3249
- candidateCount: 0,
3250
- candidateLimitReported: false
3251
- };
3252
- const excludedSpecDirectories = [DEFAULT_DECISION_RECORDS_PATH, decisionRecordsPath].map((path) => resolve5(projectRoot, path)).filter((path) => isInside(specsRoot, path));
3253
- await inspectDocumentRoot({
3254
- projectRoot,
3255
- root: specsRoot,
3256
- label: "Specs",
3257
- kind: "spec",
3258
- excludedDirectories: excludedSpecDirectories,
3259
- accumulator,
3260
- maxCandidates,
3261
- maxFileBytes,
3262
- allowMissing: false
3263
- });
3264
- await inspectDocumentRoot({
3265
- projectRoot,
3266
- root: decisionRecordsRoot,
3267
- label: "Decision Records",
3268
- kind: "decision-record",
3269
- excludedDirectories: [],
3270
- accumulator,
3271
- maxCandidates,
3272
- maxFileBytes,
3273
- allowMissing: options.allowMissingDecisionRecords === true
3274
- });
3275
- detectPathCollisions(accumulator);
3276
- accumulator.documents.sort(compareDocument);
3277
- accumulator.generatedIndexes.sort((left, right) => left.path.localeCompare(right.path));
3278
- const boundedDiagnostics = accumulator.diagnostics.slice(0, SPECS_MAX_DIAGNOSTICS);
3279
- return {
3280
- source: await inspectSourceIdentity(projectRoot),
3281
- roots: {
3282
- specs: toProjectPath(projectRoot, specsRoot),
3283
- decisions: toProjectPath(projectRoot, decisionRecordsRoot)
3284
- },
3285
- tree: buildDirectoryTree(projectRoot, specsRoot, "spec", accumulator.documents),
3286
- decisionRecords: buildDirectoryTree(projectRoot, decisionRecordsRoot, "decision-record", accumulator.documents),
3287
- documents: accumulator.documents.map(stripSource),
3288
- generatedIndexes: accumulator.generatedIndexes,
3289
- diagnostics: boundedDiagnostics,
3290
- diagnosticSummary: {
3291
- total: accumulator.diagnostics.length,
3292
- returned: boundedDiagnostics.length,
3293
- hasMore: accumulator.diagnostics.length > boundedDiagnostics.length
3294
- },
3295
- runtime: [],
3296
- limits: {
3297
- candidates: maxCandidates,
3298
- fileBytes: maxFileBytes,
3299
- results: SPECS_MAX_RESULT_LIMIT,
3300
- searchExcerptCodePoints: SPECS_SEARCH_EXCERPT_CODE_POINTS,
3301
- detailContentCodePoints: SPECS_DETAIL_CONTENT_CODE_POINTS
3302
- }
3303
- };
3304
- }
3305
- function specsInspectionComplete(inspection) {
3306
- return inspection.diagnostics.every((diagnostic) => diagnostic.severity !== "error");
3307
- }
3308
- async function readSpecsDetail(inspection, projectPath) {
3309
- const current = await readCurrentSpecsDocument(inspection, projectPath);
3310
- const bounded = boundCodePoints(current.content, SPECS_DETAIL_CONTENT_CODE_POINTS);
3311
- return {
3312
- ...current,
3313
- content: bounded.value,
3314
- contentTruncated: bounded.truncated
3315
- };
3316
- }
3317
- async function readCurrentSpecsDocument(inspection, projectPath) {
3318
- const normalized = normalizeRequestedPath(projectPath);
3319
- const document = inspection.documents.find((candidate) => candidate.path === normalized);
3320
- if (!document)
3321
- throw new SpecsQueryError("specs_document_not_found", `Specs document not found: ${projectPath}`);
3322
- const sourcePath = resolve5(inspection.source.root, normalized);
3323
- const current = await readCurrentDocument(
3324
- sourcePath,
3325
- inspection.source.root,
3326
- document.path,
3327
- inspection.limits.fileBytes
3328
- );
3329
- const currentDocument = parseDocumentRecord(parseMarkdown(current.content), current.bytes, document.path, document.kind);
3330
- return {
3331
- ...currentDocument,
3332
- content: current.content
3333
- };
3334
- }
3335
- async function searchSpecs(inspection, literal, options = {}) {
3336
- return (await searchSpecsWithSources(inspection, literal, options)).result;
3337
- }
3338
- async function searchSpecsWithSources(inspection, literal, options = {}) {
3339
- const query = literal.trim();
3340
- if (query === "")
3341
- throw new SpecsQueryError("invalid_specs_search", "Specs search literal must be non-empty");
3342
- const limit = options.limit ?? 20;
3343
- if (!Number.isInteger(limit) || limit < 1 || limit > SPECS_MAX_RESULT_LIMIT)
3344
- throw new SpecsQueryError("invalid_specs_limit", `Specs search limit must be an integer from 1 through ${SPECS_MAX_RESULT_LIMIT}`);
3345
- const excerptCodePoints = options.excerptCodePoints ?? SPECS_SEARCH_EXCERPT_CODE_POINTS;
3346
- if (!Number.isInteger(excerptCodePoints) || excerptCodePoints < 40 || excerptCodePoints > 1e3)
3347
- throw new SpecsQueryError("invalid_specs_excerpt", "Specs search excerpt must be an integer from 40 through 1000 code points");
3348
- const lowered = query.toLowerCase();
3349
- const matches = [];
3350
- const documents = /* @__PURE__ */ new Map();
3351
- let matched = 0;
3352
- for (const document of inspection.documents) {
3353
- const current = await readCurrentDocument(
3354
- resolve5(inspection.source.root, document.path),
3355
- inspection.source.root,
3356
- document.path,
3357
- inspection.limits.fileBytes
3358
- );
3359
- const currentDocument = parseDocumentRecord(parseMarkdown(current.content), current.bytes, document.path, document.kind);
3360
- const lines = current.content.split(/\r?\n/);
3361
- const headingsByLine = new Map(currentDocument.headings.map((item) => [item.line, item.title]));
3362
- let heading = null;
3363
- for (let index = 0; index < lines.length; index++) {
3364
- heading = headingsByLine.get(index + 1) ?? heading;
3365
- const position = lines[index].toLowerCase().indexOf(lowered);
3366
- if (position < 0)
3367
- continue;
3368
- matched += 1;
3369
- if (matches.length < limit) {
3370
- if (!documents.has(document.path)) {
3371
- documents.set(document.path, {
3372
- ...currentDocument,
3373
- content: current.content
3374
- });
3375
- }
3376
- matches.push({
3377
- path: document.path,
3378
- kind: document.kind,
3379
- title: currentDocument.title,
3380
- heading,
3381
- line: index + 1,
3382
- excerpt: excerptAround(
3383
- lines[index],
3384
- [...lines[index].slice(0, position)].length,
3385
- [...query].length,
3386
- excerptCodePoints
3387
- )
3388
- });
3389
- }
3390
- }
3391
- }
3392
- return {
3393
- result: {
3394
- matches,
3395
- summary: {
3396
- candidates: inspection.documents.length,
3397
- searched: inspection.documents.length,
3398
- matched,
3399
- returned: matches.length,
3400
- hasMore: matched > matches.length
3401
- }
3402
- },
3403
- documents
3404
- };
3405
- }
3406
- async function inspectDocumentRoot(options) {
3407
- const inspection = await inspectManagedFileTree(options.root, options.label, { allowMissing: options.allowMissing });
3408
- for (const issue of inspection.issues) {
3409
- options.accumulator.diagnostics.push({
3410
- severity: "error",
3411
- code: "specs_tree_invalid",
3412
- path: toProjectPath(options.projectRoot, issue.path),
3413
- message: issue.message
3414
- });
3415
- }
3416
- if (!inspection.rootExists)
3417
- return;
3418
- for (const path of inspection.files) {
3419
- if (options.excludedDirectories.some((directory) => isInside(directory, path)))
3420
- continue;
3421
- const projectPath = toProjectPath(options.projectRoot, path);
3422
- const reserved = reservedIndexKind(options.root, path);
3423
- if (reserved) {
3424
- await classifyGeneratedIndex(path, options.projectRoot, projectPath, options.maxFileBytes, options.accumulator);
3425
- continue;
3426
- }
3427
- if (!path.endsWith(".md"))
3428
- continue;
3429
- options.accumulator.candidateCount += 1;
3430
- if (options.accumulator.candidateCount > options.maxCandidates) {
3431
- if (!options.accumulator.candidateLimitReported) {
3432
- options.accumulator.diagnostics.push({
3433
- severity: "error",
3434
- code: "specs_candidate_limit_exceeded",
3435
- path: projectPath,
3436
- message: `Specs candidate limit exceeded (${options.maxCandidates})`
3437
- });
3438
- options.accumulator.candidateLimitReported = true;
3439
- }
3440
- continue;
3441
- }
3442
- await inspectDocument(path, options.projectRoot, projectPath, options.kind, options.maxFileBytes, options.accumulator);
3443
- }
3444
- }
3445
- async function classifyGeneratedIndex(path, projectRoot, projectPath, maxFileBytes, accumulator) {
3446
- try {
3447
- const current = await readCurrentDocument(path, projectRoot, projectPath, maxFileBytes);
3448
- const recognized = isRecognizedGeneratedSpecsIndex(current.content, projectPath);
3449
- accumulator.generatedIndexes.push({
3450
- path: projectPath,
3451
- classification: recognized ? "safe-removal" : "owner-controlled",
3452
- reason: recognized ? "metadata recognizes the reserved path as a generated Specs index" : "reserved path content is not recognized generated Specs-index output"
3453
- });
3454
- if (!recognized) {
3455
- accumulator.diagnostics.push({
3456
- severity: "error",
3457
- code: "unrecognized_reserved_specs_index",
3458
- path: projectPath,
3459
- message: `reserved Specs index is owner-controlled and cannot be treated as generated content: ${projectPath}`
3460
- });
3461
- }
3462
- } catch (error) {
3463
- accumulator.generatedIndexes.push({
3464
- path: projectPath,
3465
- classification: "owner-controlled",
3466
- reason: "reserved path could not be boundedly inspected as generated Specs-index output"
3467
- });
3468
- accumulator.diagnostics.push({
3469
- severity: "error",
3470
- code: error instanceof SpecsQueryError ? error.code : "specs_index_read_failed",
3471
- path: projectPath,
3472
- message: `unable to inspect reserved Specs index ${projectPath}: ${error instanceof Error ? error.message : String(error)}`
3473
- });
3474
- }
3475
- }
3476
- function isRecognizedGeneratedSpecsIndex(content, projectPath) {
3477
- const metadata = parseFrontmatter(content);
3478
- const expectedSourceDirectory = normalizeLogicalPath(dirname6(projectPath));
3479
- return metadata?.kind === "generated-index" && metadata.index_type === "specs" && metadata.source_dir === expectedSourceDirectory;
3480
- }
3481
- async function inspectDocument(path, projectRoot, projectPath, kind, maxFileBytes, accumulator) {
3482
- try {
3483
- const current = await readCurrentDocument(path, projectRoot, projectPath, maxFileBytes);
3484
- const document = parseMarkdown(current.content);
3485
- const metadata = parseFrontmatter(document);
3486
- if (metadata?.kind === "generated-index")
3487
- return;
3488
- accumulator.documents.push({
3489
- sourcePath: path,
3490
- ...parseDocumentRecord(document, current.bytes, projectPath, kind, metadata)
3491
- });
3492
- } catch (error) {
3493
- accumulator.diagnostics.push({
3494
- severity: "error",
3495
- code: error instanceof SpecsQueryError ? error.code : "specs_file_read_failed",
3496
- path: projectPath,
3497
- message: `unable to read Specs document ${projectPath}: ${error instanceof Error ? error.message : String(error)}`
3498
- });
3499
- }
3500
- }
3501
- function detectPathCollisions(accumulator) {
3502
- const seen = /* @__PURE__ */ new Map();
3503
- for (const document of accumulator.documents) {
3504
- const canonical = document.path.normalize("NFC");
3505
- const existing = seen.get(canonical);
3506
- if (existing && existing !== document.path) {
3507
- accumulator.diagnostics.push({
3508
- severity: "error",
3509
- code: "specs_path_collision",
3510
- path: document.path,
3511
- message: `Specs path normalization collision: ${existing} and ${document.path}`
3512
- });
3513
- } else {
3514
- seen.set(canonical, document.path);
3515
- }
3516
- if (canonical !== document.path) {
3517
- accumulator.diagnostics.push({
3518
- severity: "error",
3519
- code: "specs_path_not_canonical",
3520
- path: document.path,
3521
- message: `Specs path must use canonical NFC form: ${document.path}`
3522
- });
3523
- }
3524
- }
3525
- }
3526
- function buildDirectoryTree(projectRoot, root, kind, documents) {
3527
- const rootPath = toProjectPath(projectRoot, root);
3528
- const tree = {
3529
- name: basename3(root),
3530
- path: rootPath,
3531
- directories: [],
3532
- documents: []
3533
- };
3534
- for (const document of documents.filter((candidate) => candidate.kind === kind)) {
3535
- const rel = relative7(root, document.sourcePath);
3536
- if (rel === ".." || rel.startsWith(`..${sep4}`) || isAbsolute4(rel))
3537
- continue;
3538
- const segments = normalizeLogicalPath(rel).split("/");
3539
- let current = tree;
3540
- for (const segment of segments.slice(0, -1)) {
3541
- let child = current.directories.find((directory) => directory.name === segment);
3542
- if (!child) {
3543
- child = {
3544
- name: segment,
3545
- path: `${current.path}/${segment}`,
3546
- directories: [],
3547
- documents: []
3548
- };
3549
- current.directories.push(child);
3550
- current.directories.sort((left, right) => left.name.localeCompare(right.name));
3551
- }
3552
- current = child;
3553
- }
3554
- current.documents.push(stripSource(document));
3555
- current.documents.sort(compareDocument);
3556
- }
3557
- return tree;
3558
- }
3559
- function extractHeadings(document) {
3560
- return document.root.children.flatMap((node) => node.type === "heading" ? [{ depth: node.depth, title: markdownText(node), line: node.position.start.line }] : []);
3561
- }
3562
- function extractSummary(document, frontmatterSummary) {
3563
- if (typeof frontmatterSummary === "string" && frontmatterSummary.trim() !== "")
3564
- return frontmatterSummary.trim();
3565
- for (const node of document.root.children) {
3566
- if (node.type !== "paragraph" && node.type !== "list")
3567
- continue;
3568
- const text = markdownText(node).split("\n", 1)[0].trim();
3569
- if (text)
3570
- return text;
3571
- }
3572
- return null;
3573
- }
3574
- async function inspectSourceIdentity(projectRoot) {
3575
- const root = normalizeLogicalPath(projectRoot);
3576
- const head = runGit(["rev-parse", "HEAD"], projectRoot);
3577
- const branch = runGit(["symbolic-ref", "--quiet", "--short", "HEAD"], projectRoot);
3578
- const status = spawnSync("git", ["status", "--porcelain=v1", "--untracked-files=normal"], {
3579
- cwd: root,
3580
- encoding: "utf-8",
3581
- maxBuffer: 256 * 1024
3582
- });
3583
- return {
3584
- root,
3585
- gitHead: head,
3586
- gitBranch: branch,
3587
- dirty: status.status === 0 ? status.stdout.trim() !== "" : status.error && status.error.code === "ENOBUFS" ? true : null
3588
- };
3589
- }
3590
- function runGit(args, cwd) {
3591
- const result = spawnSync("git", args, { cwd, encoding: "utf-8", maxBuffer: 64 * 1024 });
3592
- return result.status === 0 ? result.stdout.trim() || null : null;
3593
- }
3594
- function normalizeRequestedPath(pathValue) {
3595
- const trimmed = pathValue.trim();
3596
- if (trimmed === "")
3597
- throw new SpecsQueryError("invalid_specs_path", "Specs detail path must be non-empty");
3598
- const normalized = normalizeLogicalPath(trimmed.replace(/^\.\//, ""));
3599
- if (isAbsolute4(trimmed) || normalized === ".." || normalized.startsWith("../"))
3600
- throw new SpecsQueryError("invalid_specs_path", "Specs detail path must be one project-relative path returned by rsp specs");
3601
- return normalized;
3602
- }
3603
- function reservedIndexKind(root, path) {
3604
- if (basename3(path) === GENERATED_INDEX_FILENAME)
3605
- return "current";
3606
- if (path === join7(root, LEGACY_GENERATED_INDEX_FILENAME))
3607
- return "legacy";
3608
- return null;
3609
- }
3610
- function excerptAround(line, matchStart, matchLength, limit) {
3611
- const codePoints = [...line];
3612
- if (codePoints.length <= limit)
3613
- return codePoints.join("").trim();
3614
- const before = Math.max(0, matchStart - Math.floor((limit - matchLength) / 2));
3615
- const value = codePoints.slice(before, before + limit).join("").trim();
3616
- return `${before > 0 ? "\u2026" : ""}${value}${before + limit < codePoints.length ? "\u2026" : ""}`;
3617
- }
3618
- async function readCurrentDocument(path, root, projectPath, maxFileBytes) {
3619
- const changed2 = () => new SpecsQueryError("specs_file_changed", `Specs document is no longer a readable regular file: ${projectPath}`);
3620
- const sourcePath = resolve5(path);
3621
- const sourceRoot = resolve5(root);
3622
- const relativePath = relative7(sourceRoot, sourcePath);
3623
- if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${sep4}`) || isAbsolute4(relativePath)) {
3624
- throw changed2();
3625
- }
3626
- const inspection = inspectManagedFile(sourcePath, "Specs document");
3627
- if (inspection.issue)
3628
- throw changed2();
3629
- let inspectedRoot;
3630
- let inspected;
3631
- let realRoot;
3632
- try {
3633
- inspectedRoot = await lstat2(sourceRoot, { bigint: true });
3634
- inspected = await lstat2(sourcePath, { bigint: true });
3635
- realRoot = await realpath3(sourceRoot);
3636
- const resolvedPath = await realpath3(sourcePath);
3637
- if (!inspectedRoot.isDirectory() || !inspected.isFile() || resolvedPath !== resolve5(realRoot, relativePath) || !isContained2(realRoot, resolvedPath)) {
3638
- throw changed2();
3639
- }
3640
- } catch (error) {
3641
- if (error instanceof SpecsQueryError)
3642
- throw error;
3643
- throw changed2();
3644
- }
3645
- let handle;
3646
- try {
3647
- handle = await open2(sourcePath, READ_NO_FOLLOW_FLAGS2);
3648
- } catch {
3649
- throw changed2();
3650
- }
3651
- try {
3652
- const opened = await handle.stat({ bigint: true });
3653
- const currentRoot = await realpath3(sourceRoot);
3654
- const currentPath = await realpath3(sourcePath);
3655
- if (!opened.isFile() || currentRoot !== realRoot || currentPath !== resolve5(currentRoot, relativePath) || !isContained2(currentRoot, currentPath) || !sameIdentity2(inspected, opened) || !sameSnapshot2(inspected, opened)) {
3656
- throw changed2();
3657
- }
3658
- if (opened.size > BigInt(maxFileBytes))
3659
- throw new SpecsQueryError("specs_file_too_large", `Specs document exceeds the ${maxFileBytes}-byte inspection limit: ${projectPath}`);
3660
- const current = await readBoundedDocument(handle, opened, projectPath, maxFileBytes);
3661
- await validateCurrentSpecsPath({
3662
- sourcePath,
3663
- sourceRoot,
3664
- relativePath,
3665
- realRoot,
3666
- inspectedRoot,
3667
- finalHandle: current.final,
3668
- projectPath
3669
- });
3670
- return {
3671
- content: current.content,
3672
- bytes: current.bytes
3673
- };
3674
- } catch (error) {
3675
- if (error instanceof SpecsQueryError)
3676
- throw error;
3677
- throw changed2();
3678
- } finally {
3679
- await handle.close();
3680
- }
3681
- }
3682
- async function readBoundedDocument(handle, opened, projectPath, maxFileBytes) {
3683
- const buffer = Buffer3.allocUnsafe(maxFileBytes + 1);
3684
- let bytes = 0;
3685
- while (bytes < buffer.length) {
3686
- const result = await handle.read(buffer, bytes, buffer.length - bytes, bytes);
3687
- if (result.bytesRead === 0)
3688
- break;
3689
- bytes += result.bytesRead;
3690
- }
3691
- const final = await handle.stat({ bigint: true });
3692
- if (bytes > maxFileBytes || final.size > BigInt(maxFileBytes))
3693
- throw new SpecsQueryError("specs_file_too_large", `Specs document exceeds the ${maxFileBytes}-byte inspection limit: ${projectPath}`);
3694
- if (!sameIdentity2(opened, final) || !sameSnapshot2(opened, final) || final.size !== BigInt(bytes)) {
3695
- throw new SpecsQueryError("specs_file_changed", `Specs document is no longer a readable regular file: ${projectPath}`);
3696
- }
3697
- return {
3698
- content: buffer.subarray(0, bytes).toString("utf-8"),
3699
- bytes,
3700
- final
3701
- };
3702
- }
3703
- async function validateCurrentSpecsPath(options) {
3704
- const changed2 = () => new SpecsQueryError("specs_file_changed", `Specs document is no longer a readable regular file: ${options.projectPath}`);
3705
- try {
3706
- const currentRoot = await lstat2(options.sourceRoot, { bigint: true });
3707
- const currentPath = await lstat2(options.sourcePath, { bigint: true });
3708
- const realCurrentRoot = await realpath3(options.sourceRoot);
3709
- const realCurrentPath = await realpath3(options.sourcePath);
3710
- if (!currentRoot.isDirectory() || !currentPath.isFile() || !sameIdentity2(options.inspectedRoot, currentRoot) || realCurrentRoot !== options.realRoot || realCurrentPath !== resolve5(realCurrentRoot, options.relativePath) || !isContained2(realCurrentRoot, realCurrentPath) || !sameIdentity2(currentPath, options.finalHandle) || !sameSnapshot2(currentPath, options.finalHandle)) {
3711
- throw changed2();
3712
- }
3713
- } catch (error) {
3714
- if (error instanceof SpecsQueryError)
3715
- throw error;
3716
- throw changed2();
3717
- }
3718
- }
3719
- function sameIdentity2(left, right) {
3720
- return left.dev === right.dev && left.ino === right.ino;
3721
- }
3722
- function sameSnapshot2(left, right) {
3723
- return left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
3724
- }
3725
- function isContained2(root, path) {
3726
- const candidate = relative7(root, path);
3727
- return candidate !== "" && candidate !== ".." && !candidate.startsWith(`..${sep4}`) && !isAbsolute4(candidate);
3728
- }
3729
- function boundCodePoints(value, limit) {
3730
- const codePoints = [...value];
3731
- return {
3732
- value: codePoints.slice(0, limit).join(""),
3733
- truncated: codePoints.length > limit
3734
- };
3735
- }
3736
- function stripSource(document) {
3737
- const { sourcePath: _sourcePath, ...record } = document;
3738
- return record;
3739
- }
3740
- function parseDocumentRecord(document, bytes, path, kind, metadata = parseFrontmatter(document)) {
3741
- const headings = extractHeadings(document);
3742
- return {
3743
- path,
3744
- kind,
3745
- title: typeof metadata?.title === "string" && metadata.title.trim() !== "" ? metadata.title.trim() : headings.find((heading) => heading.depth === 1)?.title ?? basename3(path, ".md"),
3746
- summary: extractSummary(document, metadata?.summary),
3747
- bytes,
3748
- headings
3749
- };
3750
- }
3751
- function compareDocument(left, right) {
3752
- return left.path.localeCompare(right.path);
3753
- }
3754
- function validatePositiveBound(value, label) {
3755
- if (!Number.isInteger(value) || value < 1)
3756
- throw new SpecsQueryError("invalid_specs_bound", `${label} must be a positive integer`);
3757
- }
3758
- function isInside(directory, path) {
3759
- const rel = relative7(directory, path);
3760
- return rel === "" || !isAbsolute4(rel) && rel !== ".." && !rel.startsWith(`..${sep4}`);
3761
- }
3762
- function toProjectPath(projectRoot, path) {
3763
- return normalizeLogicalPath(relative7(projectRoot, path)) || ".";
3764
- }
3765
-
3766
2171
  // src/specs/projection.ts
3767
2172
  function projectSpecsTree(inspection) {
3768
2173
  assertCompleteInspection(inspection);
@@ -3853,73 +2258,9 @@ function projectDependencyForest(plan, requestedRoots = defaultRoots(plan)) {
3853
2258
  }
3854
2259
 
3855
2260
  export {
3856
- parseMarkdown,
3857
- isMarkdownComment,
3858
- markdownTopLevelComments,
3859
- markdownText,
3860
- markdownProse,
3861
- CHANGE_DOCUMENT_SCHEMA,
3862
- GROUP_BRIEF_DOCUMENT_SCHEMA,
3863
- DocumentSectionCardinalityError,
3864
- parseRspDocument,
3865
- getDocumentTitles,
3866
- getDocumentSections,
3867
- getDocumentSectionMarkdown,
3868
- getDocumentSectionDefinitionByHeading,
3869
- renderDocumentSectionHeading,
3870
- renderDocumentTitle,
3871
- appendDocumentSectionItem,
3872
- parseFrontmatter,
3873
- countCheckboxes,
3874
- getVisibleBodyLines,
3875
- hasMeaningfulBlockers,
3876
- detectDeltaSections,
3877
- parseScenarios,
3878
2261
  IssueRelationshipError,
3879
2262
  parseIssueRelationships,
3880
2263
  createIssueRelationship,
3881
- ManagedPathError,
3882
- inspectManagedDirectory,
3883
- requireManagedDirectory,
3884
- inspectManagedFile,
3885
- requireManagedFile,
3886
- ensureManagedFile,
3887
- writeManagedFile,
3888
- resolveManagedDirectoryChain,
3889
- inspectManagedFileTree,
3890
- DEFAULT_DECISION_RECORDS_PATH,
3891
- resolveDecisionRecordsPath,
3892
- validateDecisionRecordsFilesystemPath,
3893
- pc,
3894
- PKG_ROOT,
3895
- RSP_DIR,
3896
- RSP_RULES_PATH,
3897
- OBSOLETE_RSP_RULES_PATH,
3898
- CHANGES_DIR,
3899
- FOCUS_DIR,
3900
- MAX_FOCUS_CAPSULE_BYTES,
3901
- ARCHIVES_DIR,
3902
- LOCK_PATH,
3903
- CONFIG_PATH,
3904
- generateConfigTemplate,
3905
- loadRspConfig,
3906
- inspectRspConfig,
3907
- clearConfigCache,
3908
- resolveKinds,
3909
- resolveRequiredSections,
3910
- resolveWorkflowPolicy,
3911
- resolveLanguagePolicy,
3912
- getVersion,
3913
- toErrorMessage,
3914
- emitJson,
3915
- walkMarkdownFiles,
3916
- inspectUnsupportedRules,
3917
- normalizeLogicalPath,
3918
- isValidSpecName,
3919
- inspectRspInitialization,
3920
- guardRspInitialized,
3921
- cleanupEmptyParentDirs,
3922
- detectProjectName,
3923
2264
  GROUP_BRIEF_FILENAME,
3924
2265
  WorkRefError,
3925
2266
  normalizeWorkRefSegment,
@@ -3948,14 +2289,6 @@ export {
3948
2289
  readArchiveHistoryDocument,
3949
2290
  historyInspectionComplete,
3950
2291
  inspectChangeDependencies,
3951
- SPECS_DEFAULT_RESULT_LIMIT,
3952
- SPECS_MAX_RESULT_LIMIT,
3953
- SPECS_SEARCH_EXCERPT_CODE_POINTS,
3954
- SPECS_DETAIL_CONTENT_CODE_POINTS,
3955
- SpecsQueryError,
3956
- inspectSpecs,
3957
- specsInspectionComplete,
3958
- isRecognizedGeneratedSpecsIndex,
3959
2292
  collectArchiveReadiness,
3960
2293
  toArchiveReadinessOutput,
3961
2294
  buildDurableReviewGuidance,