@oevery/rsp 3.3.0 → 4.0.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 (67) hide show
  1. package/README.md +24 -17
  2. package/README.zh-CN.md +25 -18
  3. package/dist/{chunk-TSUDEX5Y.mjs → chunk-HFSTLCCK.mjs} +1261 -319
  4. package/dist/cli.mjs +1129 -2350
  5. package/dist/{entry-GIDNR7CJ.mjs → entry-RIJQ2X46.mjs} +122 -36
  6. package/package.json +32 -24
  7. package/skills/rsp/SKILL.md +43 -38
  8. package/skills/rsp/references/closeout.md +38 -0
  9. package/skills/rsp/references/context-migration.md +22 -0
  10. package/skills/rsp/references/control-outcome.md +3 -29
  11. package/skills/rsp/references/coordination-recovery.md +25 -0
  12. package/skills/rsp/references/coordination-review.md +15 -0
  13. package/skills/rsp/references/coordination.md +59 -0
  14. package/skills/rsp/references/durable-review.md +60 -12
  15. package/skills/rsp/references/focus-continuation.md +8 -4
  16. package/skills/rsp/references/groups-dependencies.md +6 -4
  17. package/skills/rsp/references/response-language.md +2 -0
  18. package/skills/rsp/references/setup-repair.md +37 -7
  19. package/skills/rsp/references/work-model.md +27 -0
  20. package/skills/rsp-commit/SKILL.md +76 -31
  21. package/skills/rsp-commit/references/message.md +34 -0
  22. package/skills/rsp-doc/SKILL.md +65 -0
  23. package/skills/rsp-doc/agents/openai.yaml +3 -0
  24. package/skills/rsp-doc/references/artifact-lenses.md +43 -0
  25. package/skills/rsp-doc/references/release-communication.md +39 -0
  26. package/skills/rsp-doc/references/skill-authoring.md +36 -0
  27. package/skills/rsp-implement/SKILL.md +56 -25
  28. package/skills/rsp-implement/references/diagnosis.md +10 -0
  29. package/skills/rsp-implement/references/finding-resolution.md +21 -0
  30. package/skills/rsp-implement/references/test-first.md +7 -0
  31. package/skills/rsp-review/SKILL.md +46 -45
  32. package/skills/rsp-review/references/code-review.md +23 -8
  33. package/skills/rsp-review/references/document-review.md +25 -8
  34. package/skills/rsp-review/references/release-communication.md +21 -0
  35. package/skills/{rsp-structural-audit/references/structural-lenses.md → rsp-review/references/structural-review.md} +5 -5
  36. package/skills/rsp-shape/SKILL.md +24 -45
  37. package/skills/rsp-shape/references/complex-shaping.md +3 -1
  38. package/skills/rsp-shape/references/deep-clarification.md +4 -4
  39. package/skills/rsp-shape/references/domain-modeling.md +13 -0
  40. package/skills/rsp-shape/references/module-seams.md +13 -0
  41. package/skills/rsp-shape/references/planning.md +42 -0
  42. package/skills/{rsp-design → rsp-shape}/references/reversible-exploration.md +2 -2
  43. package/skills/rsp-verify/SKILL.md +44 -12
  44. package/dist/chunk-7HXQNBIG.mjs +0 -871
  45. package/dist/chunk-JOMFFQRG.mjs +0 -99
  46. package/dist/chunk-QM2UVPQK.mjs +0 -375
  47. package/dist/entry-YAW3PYFA.mjs +0 -174
  48. package/rules/rsp-rules.md +0 -48
  49. package/skills/rsp/references/implementation-evidence.md +0 -12
  50. package/skills/rsp/references/managed-routing.md +0 -45
  51. package/skills/rsp-design/SKILL.md +0 -53
  52. package/skills/rsp-design/references/domain-modeling.md +0 -11
  53. package/skills/rsp-design/references/module-seams.md +0 -11
  54. package/skills/rsp-diagnose/SKILL.md +0 -48
  55. package/skills/rsp-manage/SKILL.md +0 -101
  56. package/skills/rsp-manage/references/closeout.md +0 -13
  57. package/skills/rsp-manage/references/delegation.md +0 -47
  58. package/skills/rsp-manage/references/interruption-recovery.md +0 -19
  59. package/skills/rsp-manage/references/review-convergence.md +0 -9
  60. package/skills/rsp-release-docs/SKILL.md +0 -77
  61. package/skills/rsp-release-docs/references/convention-discovery.md +0 -65
  62. package/skills/rsp-release-docs/references/evidence-and-surfaces.md +0 -21
  63. package/skills/rsp-release-docs/references/output-contracts.md +0 -97
  64. package/skills/rsp-release-docs/references/publication-lifecycle.md +0 -28
  65. package/skills/rsp-resolve-findings/SKILL.md +0 -61
  66. package/skills/rsp-structural-audit/SKILL.md +0 -71
  67. package/skills/rsp-tdd/SKILL.md +0 -56
@@ -1,80 +1,1004 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- ARCHIVES_DIR,
4
- CHANGES_DIR,
5
- CHANGE_DOCUMENT_SCHEMA,
6
- CONFIG_PATH,
7
- DEFAULT_DECISION_RECORDS_PATH,
8
- FOCUS_DIR,
9
- GROUP_BRIEF_DOCUMENT_SCHEMA,
10
- ManagedPathError,
11
- OBSOLETE_RSP_RULES_PATH,
12
- RSP_DIR,
13
- RSP_RULES_PATH,
14
- classifyVerifyCheckboxes,
15
- collectVerifyCheckboxLines,
16
- countCheckboxes,
17
- extractBlockerLines,
18
- getCanonicalSectionHeadings,
19
- getDocumentSectionBody,
20
- getDocumentTitle,
21
- getDocumentTitles,
22
- getOpenCheckboxes,
23
- hasMeaningfulBlockerBody,
24
- hasMeaningfulBlockers,
25
- inspectManagedDirectory,
26
- inspectManagedFile,
27
- inspectManagedFileTree,
28
- inspectRspConfig,
29
- isEmptyBlockerLine,
30
- parseFrontmatter,
31
- parseRspDocument,
32
- parseScenarios,
33
- pc,
34
- renderDocumentSectionHeading,
35
- renderDocumentTitle,
36
- requireManagedDirectory,
37
- requireManagedFile,
38
- resolveDecisionRecordsPath,
39
- resolveLanguagePolicy,
40
- resolveManagePolicy,
41
- toErrorMessage,
42
- validateDecisionRecordsFilesystemPath
43
- } from "./chunk-7HXQNBIG.mjs";
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
+ }
44
956
 
45
957
  // src/status/inspect.ts
46
- import { readFile as readFile4, stat } from "fs/promises";
958
+ import { readFile as readFile5, stat as stat2 } from "fs/promises";
47
959
 
48
960
  // src/core/change-group.ts
49
- import { readFile as readFile2 } from "fs/promises";
50
- import { dirname as dirname2, join as join3, relative as relative3 } from "path";
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
+ }
51
979
 
52
980
  // src/core/filesystem.ts
53
- import { existsSync } from "fs";
54
- import { readdir, readFile, rmdir } from "fs/promises";
55
- import { basename, dirname, join, relative, sep } from "path";
56
981
  async function walkMarkdownFiles(dir, options = {}) {
57
982
  const all = await walkFiles(dir, options);
58
983
  return all.filter((f) => f.endsWith(".md"));
59
984
  }
60
985
  async function inspectUnsupportedRules() {
61
- const rulesDir = join(RSP_DIR, "rules");
986
+ const rulesDir = join4(RSP_DIR, "rules");
62
987
  const diagnostics = [];
63
988
  const rawEntries = await walkAllEntries(rulesDir, diagnostics);
64
- const entries = rawEntries.filter((entry) => entry.path !== OBSOLETE_RSP_RULES_PATH).map((entry) => ({ path: normalizeLogicalPath(relative(rulesDir, entry.path)), kind: entry.kind }));
65
- const obsoleteFallbackPresent = existsSync(OBSOLETE_RSP_RULES_PATH);
989
+ const entries = rawEntries.filter((entry) => entry.path !== OBSOLETE_RSP_RULES_PATH).map((entry) => ({ path: normalizeLogicalPath(relative2(rulesDir, entry.path)), kind: entry.kind }));
66
990
  return {
67
991
  entries,
68
992
  diagnostics,
69
- directoryRemaining: existsSync(rulesDir) && (!obsoleteFallbackPresent || entries.length > 0)
993
+ directoryRemaining: entries.length > 0
70
994
  };
71
995
  }
72
996
  async function walkAllEntries(dir, diagnostics) {
73
997
  const entries = [];
74
998
  try {
75
- const items = await readdir(dir, { withFileTypes: true });
999
+ const items = await readdir3(dir, { withFileTypes: true });
76
1000
  for (const item of items) {
77
- const path = join(dir, item.name);
1001
+ const path = join4(dir, item.name);
78
1002
  const kind = item.isDirectory() ? "directory" : "file";
79
1003
  entries.push({ path, kind });
80
1004
  if (kind === "directory")
@@ -90,11 +1014,11 @@ async function walkAllEntries(dir, diagnostics) {
90
1014
  async function walkFiles(dir, options = {}) {
91
1015
  const files = [];
92
1016
  try {
93
- const items = await readdir(dir, { withFileTypes: true });
1017
+ const items = await readdir3(dir, { withFileTypes: true });
94
1018
  for (const item of items) {
95
1019
  if (item.name.startsWith("."))
96
1020
  continue;
97
- const filePath = join(dir, item.name);
1021
+ const filePath = join4(dir, item.name);
98
1022
  if (item.isDirectory())
99
1023
  files.push(...await walkFiles(filePath, options));
100
1024
  else
@@ -108,28 +1032,35 @@ async function walkFiles(dir, options = {}) {
108
1032
  return files;
109
1033
  }
110
1034
  function normalizeLogicalPath(pathValue) {
111
- return pathValue.split(sep).join("/").replace(/\\/g, "/");
1035
+ return pathValue.split(sep2).join("/").replace(/\\/g, "/");
112
1036
  }
113
1037
  var SPEC_NAME_RE = /^[a-z0-9-]+(?:\/[a-z0-9-]+)*$/;
114
1038
  function isValidSpecName(name) {
115
1039
  return SPEC_NAME_RE.test(name);
116
1040
  }
117
- function guardRspInitialized() {
118
- const designPath = join(RSP_DIR, "specs", "design.md");
119
- const rules = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
1041
+ function inspectRspInitialization() {
1042
+ const designPath = join4(RSP_DIR, "specs", "design.md");
120
1043
  const design = inspectManagedFile(designPath, "design Spec", { allowMissing: true });
121
- if (rules.issue || design.issue || !rules.exists || !design.exists) {
122
- const initialized = existsSync(RSP_DIR);
123
- console.error(` ${pc.red("Error:")} ${initialized ? "RSP project requires an update" : "RSP is not initialized in this project"}`);
124
- console.error(` ${pc.dim(initialized ? "Run: rsp update" : "Run: rsp init")}`);
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}`);
125
1056
  process.exit(1);
126
1057
  }
127
1058
  }
128
1059
  async function cleanupEmptyParentDirs(path, stopDir) {
129
- let parent = dirname(path);
1060
+ let parent = dirname3(path);
130
1061
  while (parent !== stopDir) {
131
1062
  try {
132
- const remaining = await readdir(parent);
1063
+ const remaining = await readdir3(parent);
133
1064
  if (remaining.length === 0)
134
1065
  await rmdir(parent);
135
1066
  else
@@ -137,13 +1068,13 @@ async function cleanupEmptyParentDirs(path, stopDir) {
137
1068
  } catch {
138
1069
  break;
139
1070
  }
140
- parent = dirname(parent);
1071
+ parent = dirname3(parent);
141
1072
  }
142
1073
  }
143
1074
  async function detectProjectName() {
144
1075
  try {
145
- if (existsSync("package.json")) {
146
- const raw = await readFile("package.json", "utf-8");
1076
+ if (existsSync2("package.json")) {
1077
+ const raw = await readFile2("package.json", "utf-8");
147
1078
  const pkg = JSON.parse(raw);
148
1079
  if (pkg.name && pkg.name !== "")
149
1080
  return pkg.name;
@@ -155,9 +1086,9 @@ async function detectProjectName() {
155
1086
 
156
1087
  // src/core/work-ref.ts
157
1088
  import { Buffer } from "buffer";
158
- import { lstatSync, readdirSync } from "fs";
159
- import { readdir as readdir2 } from "fs/promises";
160
- import { isAbsolute, join as join2, relative as relative2 } from "path";
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";
161
1092
  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;
162
1093
  var WORK_SEGMENT_MAX_CODE_POINTS = 100;
163
1094
  var WORK_SEGMENT_MAX_UTF8_BYTES = 200;
@@ -215,7 +1146,7 @@ function resolveWorkRef(name, options = {}) {
215
1146
  assertValidWorkRoot(changesDir, normalizedName);
216
1147
  assertNoNormalizationCollision(changesDir, segments, normalizedName, isGroupBrief);
217
1148
  assertNoIdentityCollision(changesDir, segments, normalizedName, isGroupBrief ? GROUP_BRIEF_FILENAME : void 0);
218
- const path = join2(changesDir, ...segments.slice(0, -1), isGroupBrief ? GROUP_BRIEF_FILENAME : `${segments.at(-1)}.md`);
1149
+ const path = join5(changesDir, ...segments.slice(0, -1), isGroupBrief ? GROUP_BRIEF_FILENAME : `${segments.at(-1)}.md`);
219
1150
  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] };
220
1151
  if (ref.kind === "group-change" && options.executable !== false)
221
1152
  assertGroupBrief(changesDir, ref);
@@ -230,7 +1161,7 @@ function resolveWorkRef(name, options = {}) {
230
1161
  return ref;
231
1162
  }
232
1163
  function assertGroupBrief(changesDir, ref) {
233
- const briefPath = join2(changesDir, ref.group, GROUP_BRIEF_FILENAME);
1164
+ const briefPath = join5(changesDir, ref.group, GROUP_BRIEF_FILENAME);
234
1165
  const kind = getPathKind(briefPath, ref.name);
235
1166
  if (kind === "missing") {
236
1167
  throw new WorkRefError(
@@ -250,7 +1181,7 @@ function assertGroupBrief(changesDir, ref) {
250
1181
  function assertRegularWorkFile(ref, mustExist) {
251
1182
  let stats;
252
1183
  try {
253
- stats = lstatSync(ref.path);
1184
+ stats = lstatSync2(ref.path);
254
1185
  } catch (error) {
255
1186
  if (error.code === "ENOENT") {
256
1187
  if (mustExist) {
@@ -274,8 +1205,8 @@ function assertRegularWorkFile(ref, mustExist) {
274
1205
  }
275
1206
  function resolveWorkRefPath(path, options = {}) {
276
1207
  const changesDir = options.changesDir ?? CHANGES_DIR;
277
- const logicalPath = normalizeLogicalPath2(relative2(changesDir, path));
278
- if (isAbsolute(logicalPath) || logicalPath === ".." || logicalPath.startsWith("../") || !logicalPath.endsWith(".md")) {
1208
+ const logicalPath = normalizeLogicalPath2(relative3(changesDir, path));
1209
+ if (isAbsolute2(logicalPath) || logicalPath === ".." || logicalPath.startsWith("../") || !logicalPath.endsWith(".md")) {
279
1210
  throw new WorkRefError(
280
1211
  "invalid_work_ref_path",
281
1212
  `work path must be a Markdown file inside ${changesDir}: ${path}`,
@@ -302,13 +1233,13 @@ function resolveFocusMarkerPath(ref, options = {}) {
302
1233
  assertNoManagedNormalizationCollision(focusDir, ref.name.split("/"), ref.name, "invalid_focus_path");
303
1234
  if (ref.group) {
304
1235
  assertOptionalManagedDirectory(
305
- join2(focusDir, ref.group),
1236
+ join5(focusDir, ref.group),
306
1237
  ref.name,
307
1238
  "invalid_focus_path",
308
1239
  "focus path"
309
1240
  );
310
1241
  }
311
- const markerPath = join2(focusDir, ...ref.name.split("/"));
1242
+ const markerPath = join5(focusDir, ...ref.name.split("/"));
312
1243
  try {
313
1244
  requireManagedFile(markerPath, "focus marker", { allowMissing: true });
314
1245
  } catch (error) {
@@ -324,7 +1255,7 @@ function resolveArchiveDirectory(ref, options = {}) {
324
1255
  if (!ref.group)
325
1256
  return archivesDir;
326
1257
  assertNoManagedNormalizationCollision(archivesDir, [ref.group], ref.name, "invalid_archive_path");
327
- const groupPath = join2(archivesDir, ref.group);
1258
+ const groupPath = join5(archivesDir, ref.group);
328
1259
  assertOptionalManagedDirectory(groupPath, ref.name, "invalid_archive_path", "archive path");
329
1260
  return groupPath;
330
1261
  }
@@ -343,7 +1274,7 @@ async function inspectFocusTree(options = {}) {
343
1274
  if (!rootEntries)
344
1275
  return result;
345
1276
  for (const entry of rootEntries) {
346
- const path = join2(focusDir, entry.name);
1277
+ const path = join5(focusDir, entry.name);
347
1278
  if (entry.name === ".gitkeep" && entry.isFile())
348
1279
  continue;
349
1280
  if (entry.isDirectory()) {
@@ -386,7 +1317,7 @@ async function inspectArchiveTree(options = {}) {
386
1317
  if (!rootEntries)
387
1318
  return result;
388
1319
  for (const entry of rootEntries) {
389
- const path = join2(archivesDir, entry.name);
1320
+ const path = join5(archivesDir, entry.name);
390
1321
  if (entry.name === "INDEX.md" && entry.isFile())
391
1322
  continue;
392
1323
  if (entry.isDirectory()) {
@@ -424,7 +1355,7 @@ async function inspectWorkTree(options = {}) {
424
1355
  const rootMarkdownNames = new Set(rootEntries.filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name.slice(0, -3)));
425
1356
  const collidingGroups = new Set([...rootDirectories].filter((name) => rootMarkdownNames.has(name)));
426
1357
  for (const entry of rootEntries) {
427
- const path = join2(changesDir, entry.name);
1358
+ const path = join5(changesDir, entry.name);
428
1359
  if (entry.name === ".gitkeep" && entry.isFile())
429
1360
  continue;
430
1361
  if (entry.isDirectory()) {
@@ -471,17 +1402,17 @@ async function inspectGroupDirectory(group, groupPath, changesDir, result, colli
471
1402
  "group_brief_missing",
472
1403
  `Change Group "${group}" requires a Group Brief; run: rsp group create ${group}`,
473
1404
  group
474
- ), join2(groupPath, GROUP_BRIEF_FILENAME));
1405
+ ), join5(groupPath, GROUP_BRIEF_FILENAME));
475
1406
  } else if (!briefEntry.isFile()) {
476
1407
  addDiagnostic(result, new WorkRefError(
477
1408
  "work_ref_not_file",
478
- `Group Brief must be a regular file: ${join2(groupPath, GROUP_BRIEF_FILENAME)}`,
1409
+ `Group Brief must be a regular file: ${join5(groupPath, GROUP_BRIEF_FILENAME)}`,
479
1410
  group
480
- ), join2(groupPath, GROUP_BRIEF_FILENAME));
1411
+ ), join5(groupPath, GROUP_BRIEF_FILENAME));
481
1412
  }
482
1413
  for (const entry of entries) {
483
1414
  const input = `${group}/${entry.name}`;
484
- const path = join2(groupPath, entry.name);
1415
+ const path = join5(groupPath, entry.name);
485
1416
  if (entry.name === GROUP_BRIEF_FILENAME && !entry.isFile())
486
1417
  continue;
487
1418
  if (entry.name === "brief.md") {
@@ -510,7 +1441,7 @@ async function inspectFocusGroup(group, groupPath, changesDir, result) {
510
1441
  return;
511
1442
  for (const entry of entries) {
512
1443
  const input = `${group}/${entry.name}`;
513
- const path = join2(groupPath, entry.name);
1444
+ const path = join5(groupPath, entry.name);
514
1445
  if (entry.isDirectory()) {
515
1446
  addDiagnostic(result, new WorkRefError(
516
1447
  "unsupported_work_depth",
@@ -536,7 +1467,7 @@ async function inspectArchiveGroup(group, groupPath, result) {
536
1467
  return;
537
1468
  for (const entry of entries) {
538
1469
  const input = `${group}/${entry.name}`;
539
- const path = join2(groupPath, entry.name);
1470
+ const path = join5(groupPath, entry.name);
540
1471
  if (!entry.isFile() || !entry.name.endsWith(".md")) {
541
1472
  addArchivePathDiagnostic(result, input, path);
542
1473
  continue;
@@ -569,7 +1500,7 @@ function collectResolvedPath(path, changesDir, result) {
569
1500
  }
570
1501
  async function readDirectory(path, input, result) {
571
1502
  try {
572
- return (await readdir2(path, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
1503
+ return (await readdir4(path, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
573
1504
  } catch (error) {
574
1505
  addDiagnostic(result, asReadFailure(path, input, error), path);
575
1506
  return null;
@@ -630,7 +1561,7 @@ function normalizeWorkRefName(name, allowGroupBrief) {
630
1561
  }
631
1562
  function assertNoNormalizationCollision(changesDir, segments, input, isGroupBrief) {
632
1563
  for (let depth = 0; depth < segments.length; depth++) {
633
- const parent = join2(changesDir, ...segments.slice(0, depth));
1564
+ const parent = join5(changesDir, ...segments.slice(0, depth));
634
1565
  const parentKind = getPathKind(parent, input);
635
1566
  if (parentKind === "missing")
636
1567
  return;
@@ -663,7 +1594,7 @@ function assertNoNormalizationCollision(changesDir, segments, input, isGroupBrie
663
1594
  }
664
1595
  function assertNoManagedNormalizationCollision(root, segments, input, code) {
665
1596
  for (let depth = 0; depth < segments.length; depth++) {
666
- const parent = join2(root, ...segments.slice(0, depth));
1597
+ const parent = join5(root, ...segments.slice(0, depth));
667
1598
  const parentKind = getPathKind(parent, input);
668
1599
  if (parentKind === "missing")
669
1600
  return;
@@ -688,8 +1619,8 @@ function assertNoManagedNormalizationCollision(root, segments, input, code) {
688
1619
  }
689
1620
  function assertNoIdentityCollision(changesDir, segments, input, finalFilename) {
690
1621
  for (let depth = 1; depth <= segments.length; depth++) {
691
- const identityPath = join2(changesDir, ...segments.slice(0, depth));
692
- const filePath = finalFilename && depth === segments.length ? join2(changesDir, ...segments.slice(0, -1), finalFilename) : `${identityPath}.md`;
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`;
693
1624
  const fileKind = getPathKind(filePath, input);
694
1625
  const identityKind = getPathKind(identityPath, input);
695
1626
  const isPrefix = depth < segments.length;
@@ -755,7 +1686,7 @@ function assertOptionalManagedDirectory(path, input, code, label) {
755
1686
  }
756
1687
  function getPathKind(path, input) {
757
1688
  try {
758
- const stats = lstatSync(path);
1689
+ const stats = lstatSync2(path);
759
1690
  if (stats.isFile())
760
1691
  return "file";
761
1692
  if (stats.isDirectory())
@@ -783,13 +1714,13 @@ function normalizeLogicalPath2(path) {
783
1714
  var PLACEHOLDER_SUMMARY = "<\u2026>";
784
1715
  function extractChangeSummary(content, document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA), options = {}) {
785
1716
  try {
786
- const frontmatter = parseFrontmatter(content);
787
- const frontmatterSummary = meaningfulSummary(frontmatter?.summary, options);
1717
+ const frontmatter2 = parseFrontmatter(document.markdown);
1718
+ const frontmatterSummary = meaningfulSummary(frontmatter2?.summary, options);
788
1719
  if (frontmatterSummary)
789
1720
  return frontmatterSummary;
790
1721
  } catch {
791
1722
  }
792
- const proposalLines = getDocumentSectionBody(document, "proposal").split("\n").map((line) => line.trim());
1723
+ const proposalLines = visibleSummaryItemLines(getDocumentSectionMarkdown(document, "proposal"));
793
1724
  for (const field of ["Outcome", "Summary"]) {
794
1725
  const prefix = `- ${field}:`;
795
1726
  for (const line of proposalLines) {
@@ -801,7 +1732,7 @@ function extractChangeSummary(content, document = parseRspDocument(content, CHAN
801
1732
  return null;
802
1733
  }
803
1734
  function extractGroupSummary(content, document = parseRspDocument(content, GROUP_BRIEF_DOCUMENT_SCHEMA)) {
804
- for (const line of getDocumentSectionBody(document, "goal").split("\n")) {
1735
+ for (const line of visibleSummaryItemLines(getDocumentSectionMarkdown(document, "goal"))) {
805
1736
  const trimmed = line.trim();
806
1737
  const summary = trimmed.startsWith("- ") || trimmed.startsWith("* ") ? meaningfulSummary(trimmed.slice(2)) : null;
807
1738
  if (summary)
@@ -809,6 +1740,10 @@ function extractGroupSummary(content, document = parseRspDocument(content, GROUP
809
1740
  }
810
1741
  return null;
811
1742
  }
1743
+ function visibleSummaryItemLines(document) {
1744
+ const source = visibleMarkdownSource(document);
1745
+ return markdownListItems(document).map(({ node }) => source.slice(node.position.start.offset - document.start, node.position.end.offset - document.start).split(/\r?\n/, 1)[0].trim());
1746
+ }
812
1747
  function meaningfulSummary(value, options = {}) {
813
1748
  if (typeof value !== "string")
814
1749
  return null;
@@ -843,12 +1778,15 @@ ${groupSectionHeading("sharedConstraints")}
843
1778
  - ${placeholder}
844
1779
 
845
1780
  ${groupSectionHeading("slices")}
1781
+ <!-- Declare at least two independently executable direct child WorkRefs before creating them; keep their Tasks and evidence in each Change. -->
846
1782
  - \`${name}/<change>\`: ${placeholder}
847
1783
 
848
1784
  ${groupSectionHeading("completionConditions")}
1785
+ <!-- State shared completion conditions. For Group-level verification, name an Integration: condition and retain its acceptance and evidence here; child passes alone do not prove it. -->
849
1786
  - [ ] ${placeholder}
850
1787
 
851
1788
  ${groupSectionHeading("durableOutcomes")}
1789
+ <!-- Identify current-fact and lasting-rationale owners only when an update is needed; do not duplicate child results, current Specs or execution history. -->
852
1790
  - Current facts: ${placeholder}
853
1791
  - Lasting rationale: ${placeholder}
854
1792
 
@@ -868,12 +1806,12 @@ async function inspectChangeGroups(options = {}) {
868
1806
  const archivedGroupBriefPaths = /* @__PURE__ */ new Map();
869
1807
  const openGroupNames = new Set(workTree.briefs.map((brief) => brief.group));
870
1808
  for (const path of archiveTree.files) {
871
- const archivePath = normalizeLogicalPath(relative3(ARCHIVES_DIR, path));
1809
+ const archivePath = normalizeLogicalPath(relative4(ARCHIVES_DIR, path));
872
1810
  const archiveGroup = archivePath.includes("/") ? archivePath.split("/")[0] : null;
873
1811
  if (!archiveGroup || !openGroupNames.has(archiveGroup))
874
1812
  continue;
875
1813
  try {
876
- const content = await readFile2(path, "utf-8");
1814
+ const content = await readFile3(path, "utf-8");
877
1815
  const archivedChange = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
878
1816
  const identity = getDocumentTitle(archivedChange, "identity");
879
1817
  if (identity?.includes("/")) {
@@ -891,10 +1829,11 @@ async function inspectChangeGroups(options = {}) {
891
1829
  archivedSlicesByGroup.set(archiveGroup, groupSlices);
892
1830
  }
893
1831
  }
894
- if (getDocumentTitle(parseRspDocument(content, GROUP_BRIEF_DOCUMENT_SCHEMA), "identity") === archiveGroup) {
1832
+ const archivedBrief = parseRspDocument(archivedChange.markdown, GROUP_BRIEF_DOCUMENT_SCHEMA);
1833
+ if (getDocumentTitle(archivedBrief, "identity") === archiveGroup) {
895
1834
  archivedGroups.add(archiveGroup);
896
1835
  const evidence = archivedGroupReopenEvidence.get(archiveGroup) ?? /* @__PURE__ */ new Set();
897
- for (const item of getGroupReopenEvidence(content))
1836
+ for (const item of getGroupReopenEvidence(archivedBrief))
898
1837
  evidence.add(item.key);
899
1838
  archivedGroupReopenEvidence.set(archiveGroup, evidence);
900
1839
  const retainedPaths = archivedGroupBriefPaths.get(archiveGroup) ?? /* @__PURE__ */ new Set();
@@ -914,7 +1853,7 @@ async function inspectChangeGroups(options = {}) {
914
1853
  for (const brief of workTree.briefs) {
915
1854
  let content;
916
1855
  try {
917
- content = await readFile2(brief.path, "utf-8");
1856
+ content = await readFile3(brief.path, "utf-8");
918
1857
  } catch (error) {
919
1858
  diagnostics.push({
920
1859
  severity: "error",
@@ -925,10 +1864,11 @@ async function inspectChangeGroups(options = {}) {
925
1864
  });
926
1865
  continue;
927
1866
  }
928
- const parsed = parseGroupBrief(brief.group, content);
1867
+ const document = parseRspDocument(content, GROUP_BRIEF_DOCUMENT_SCHEMA);
1868
+ const parsed = parseGroupBrief(brief.group, document);
929
1869
  diagnostics.push(...parsed.diagnostics.map((diagnostic) => ({ ...diagnostic, path: brief.path })));
930
1870
  if (archivedGroups.has(brief.group) && !hasExplicitGroupReopenEvidence(
931
- content,
1871
+ document,
932
1872
  archivedGroupReopenEvidence.get(brief.group) ?? /* @__PURE__ */ new Set(),
933
1873
  archivedGroupBriefPaths.get(brief.group) ?? /* @__PURE__ */ new Set()
934
1874
  )) {
@@ -943,7 +1883,7 @@ async function inspectChangeGroups(options = {}) {
943
1883
  for (const diagnostic of archiveTree.diagnostics.filter((item) => {
944
1884
  if (item.code === "invalid_archive_root")
945
1885
  return true;
946
- const archivePath = normalizeLogicalPath(relative3(ARCHIVES_DIR, item.path));
1886
+ const archivePath = normalizeLogicalPath(relative4(ARCHIVES_DIR, item.path));
947
1887
  return archivePath === brief.group || archivePath.startsWith(`${brief.group}/`);
948
1888
  })) {
949
1889
  diagnostics.push({
@@ -1011,7 +1951,7 @@ async function inspectChangeGroups(options = {}) {
1011
1951
  const readyToClose = diagnostics.every((diagnostic) => diagnostic.change !== brief.group || diagnostic.severity !== "error") && slices.length >= 2 && slices.every((slice) => slice.state === "archived") && parsed.completion.total > 0 && parsed.completion.done === parsed.completion.total && !parsed.blockers;
1012
1952
  groups.push({
1013
1953
  name: brief.group,
1014
- summary: extractGroupSummary(content),
1954
+ summary: extractGroupSummary(content, document),
1015
1955
  path: normalizeLogicalPath(brief.path),
1016
1956
  slices,
1017
1957
  completion: parsed.completion,
@@ -1024,25 +1964,27 @@ async function inspectChangeGroups(options = {}) {
1024
1964
  return { groups, diagnostics };
1025
1965
  }
1026
1966
  function getGroupReopenEvidence(content) {
1027
- const completion = getDocumentSectionBody(parseRspDocument(content, GROUP_BRIEF_DOCUMENT_SCHEMA), "completionConditions");
1028
- return completion.split("\n").flatMap((line) => {
1029
- const match = line.trim().match(/^-\s+\[([ /xX-])\]\s+(Resolve reopened concern from `(\.rsp\/archives\/[^`]+)`: \S.*)$/);
1030
- return match ? [{ active: match[1] !== "-", key: match[2], sourcePath: match[3] }] : [];
1967
+ const document = typeof content === "string" ? parseRspDocument(content, GROUP_BRIEF_DOCUMENT_SCHEMA) : content;
1968
+ const completion = getDocumentSectionMarkdown(document, "completionConditions");
1969
+ return markdownListItems(completion).flatMap(({ line }) => {
1970
+ const match = line.trim().match(/^-\s+\[([ /xX-])\]\s+(Resolve reopened concern from `(\.rsp\/archives\/[^`]+)`: (\S.*))$/);
1971
+ const reason = match?.[4];
1972
+ return match && reason && markdownText(parseMarkdown(reason).root).trim() ? [{ active: match[1] !== "-", key: match[2], sourcePath: match[3] }] : [];
1031
1973
  });
1032
1974
  }
1033
1975
  function hasExplicitGroupReopenEvidence(content, retainedEvidence, retainedGroupBriefPaths) {
1034
1976
  return getGroupReopenEvidence(content).some((item) => item.active && retainedGroupBriefPaths.has(item.sourcePath) && !retainedEvidence.has(item.key));
1035
1977
  }
1036
1978
  async function requireDeclaredGroupSlice(ref) {
1037
- const briefPath = join3(dirname2(ref.path), GROUP_BRIEF_FILENAME);
1979
+ const briefPath = join6(dirname4(ref.path), GROUP_BRIEF_FILENAME);
1038
1980
  let content;
1039
1981
  try {
1040
- content = await readFile2(briefPath, "utf-8");
1982
+ content = await readFile3(briefPath, "utf-8");
1041
1983
  } catch (error) {
1042
1984
  const message = error instanceof Error ? error.message : String(error);
1043
1985
  throw new WorkRefError("work_tree_read_failed", `unable to read Group Brief ${briefPath}: ${message}`, ref.name);
1044
1986
  }
1045
- const parsed = parseGroupBrief(ref.group, content);
1987
+ const parsed = parseGroupBrief(ref.group, parseRspDocument(content, GROUP_BRIEF_DOCUMENT_SCHEMA));
1046
1988
  const errors = parsed.diagnostics.filter((diagnostic) => diagnostic.severity === "error");
1047
1989
  if (errors.length > 0) {
1048
1990
  throw new WorkRefError(
@@ -1067,11 +2009,11 @@ async function hasArchivedGroupBrief(group) {
1067
2009
  }
1068
2010
  const groupPrefix = `${group}/`;
1069
2011
  for (const path of archiveTree.files) {
1070
- if (!normalizeLogicalPath(relative3(ARCHIVES_DIR, path)).startsWith(groupPrefix))
2012
+ if (!normalizeLogicalPath(relative4(ARCHIVES_DIR, path)).startsWith(groupPrefix))
1071
2013
  continue;
1072
2014
  let content;
1073
2015
  try {
1074
- content = await readFile2(path, "utf-8");
2016
+ content = await readFile3(path, "utf-8");
1075
2017
  } catch (error) {
1076
2018
  const message = error instanceof Error ? error.message : String(error);
1077
2019
  throw new WorkRefError("work_tree_read_failed", `unable to inspect archived Change Group identity at ${path}: ${message}`, group);
@@ -1081,26 +2023,25 @@ async function hasArchivedGroupBrief(group) {
1081
2023
  }
1082
2024
  return false;
1083
2025
  }
1084
- function parseGroupBrief(group, content) {
2026
+ function parseGroupBrief(group, document) {
1085
2027
  const diagnostics = [];
1086
- const document = parseRspDocument(content, GROUP_BRIEF_DOCUMENT_SCHEMA);
1087
- let frontmatter;
2028
+ let frontmatter2;
1088
2029
  try {
1089
- frontmatter = parseFrontmatter(content);
2030
+ frontmatter2 = parseFrontmatter(document.markdown);
1090
2031
  } catch (error) {
1091
2032
  diagnostics.push({ severity: "error", code: "group_frontmatter_invalid", change: group, message: error instanceof Error ? error.message : String(error) });
1092
2033
  }
1093
- if (frontmatter?.kind !== "group")
2034
+ if (frontmatter2?.kind !== "group")
1094
2035
  diagnostics.push({ severity: "error", code: "group_kind_invalid", change: group, message: "Group Brief frontmatter must declare kind: group" });
1095
2036
  if (getDocumentTitle(document, "identity") !== group)
1096
2037
  diagnostics.push({ severity: "error", code: "group_heading_mismatch", change: group, message: `Group Brief heading must be: # Change Group: ${group}` });
1097
2038
  for (const definition of GROUP_BRIEF_DOCUMENT_SCHEMA.sections) {
1098
- if (!getDocumentSectionBody(document, definition.id))
2039
+ if (getVisibleBodyLines(getDocumentSectionMarkdown(document, definition.id)).length === 0)
1099
2040
  diagnostics.push({ severity: "error", code: "group_section_missing", change: group, message: `Group Brief missing required section: ${definition.heading}` });
1100
2041
  }
1101
2042
  const slices = [];
1102
2043
  const seen = /* @__PURE__ */ new Set();
1103
- for (const line of getDocumentSectionBody(document, "slices").split("\n")) {
2044
+ for (const { line } of markdownListItems(getDocumentSectionMarkdown(document, "slices"))) {
1104
2045
  const trimmed = line.trim();
1105
2046
  const identity = trimmed.match(/^-\s+`(\S+)`/);
1106
2047
  if (!identity || identity[1].includes("<"))
@@ -1129,13 +2070,13 @@ function parseGroupBrief(group, content) {
1129
2070
  }
1130
2071
  if (slices.length < 2)
1131
2072
  diagnostics.push({ severity: "warning", code: "group_slices_incomplete", change: group, message: "Change Group must declare at least two direct child slices" });
1132
- const completion = countCheckboxes(getDocumentSectionBody(document, "completionConditions"));
2073
+ const completion = countCheckboxes(getDocumentSectionMarkdown(document, "completionConditions"));
1133
2074
  if (completion.total === 0)
1134
2075
  diagnostics.push({ severity: "warning", code: "group_completion_missing", change: group, message: "Change Group must declare at least one completion condition" });
1135
2076
  return {
1136
2077
  slices,
1137
2078
  completion: { done: completion.done + completion.dropped, total: completion.total },
1138
- blockers: hasMeaningfulBlockerBody(getDocumentSectionBody(document, "blockers")),
2079
+ blockers: hasMeaningfulBlockerBody(getDocumentSectionMarkdown(document, "blockers")),
1139
2080
  diagnostics
1140
2081
  };
1141
2082
  }
@@ -1144,13 +2085,13 @@ function groupSectionHeading(sectionId) {
1144
2085
  }
1145
2086
 
1146
2087
  // src/core/dependency-plan.ts
1147
- import { readFile as readFile3 } from "fs/promises";
2088
+ import { readFile as readFile4 } from "fs/promises";
1148
2089
 
1149
2090
  // src/history/current-file.ts
1150
2091
  import { Buffer as Buffer2 } from "buffer";
1151
2092
  import { constants } from "fs";
1152
- import { lstat, open, realpath } from "fs/promises";
1153
- import { isAbsolute as isAbsolute2, relative as relative4, resolve, sep as sep2 } from "path";
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";
1154
2095
 
1155
2096
  // src/history/model.ts
1156
2097
  var HISTORY_DEFAULT_LIMIT = 20;
@@ -1179,10 +2120,10 @@ var ArchiveHistoryError = class extends Error {
1179
2120
  // src/history/current-file.ts
1180
2121
  var READ_NO_FOLLOW_FLAGS = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0);
1181
2122
  async function readCurrentArchiveFile(options) {
1182
- const sourcePath = resolve(options.sourcePath);
1183
- const archivesDir = resolve(options.archivesDir);
1184
- const relativePath = relative4(archivesDir, sourcePath);
1185
- if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${sep2}`) || isAbsolute2(relativePath)) {
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)) {
1186
2127
  throw changed(options.projectPath);
1187
2128
  }
1188
2129
  let inspectedRoot;
@@ -1193,9 +2134,9 @@ async function readCurrentArchiveFile(options) {
1193
2134
  if (!inspectedRoot.isDirectory())
1194
2135
  throw changed(options.projectPath);
1195
2136
  inspected = await lstat(sourcePath, { bigint: true });
1196
- realArchivesDir = await realpath(archivesDir);
1197
- const resolvedSource = await realpath(sourcePath);
1198
- const expectedSource = resolve(realArchivesDir, relativePath);
2137
+ realArchivesDir = await realpath2(archivesDir);
2138
+ const resolvedSource = await realpath2(sourcePath);
2139
+ const expectedSource = resolve3(realArchivesDir, relativePath);
1199
2140
  if (!inspected.isFile() || resolvedSource !== expectedSource || !isContained(realArchivesDir, resolvedSource)) {
1200
2141
  throw changed(options.projectPath);
1201
2142
  }
@@ -1216,9 +2157,9 @@ async function readCurrentArchiveFile(options) {
1216
2157
  }
1217
2158
  try {
1218
2159
  const opened = await handle.stat({ bigint: true });
1219
- const currentRoot = await realpath(archivesDir);
1220
- const currentSource = await realpath(sourcePath);
1221
- if (!opened.isFile() || currentRoot !== realArchivesDir || currentSource !== resolve(currentRoot, relativePath) || !isContained(currentRoot, currentSource) || !sameIdentity(inspected, opened) || !sameSnapshot(inspected, opened)) {
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)) {
1222
2163
  throw changed(options.projectPath);
1223
2164
  }
1224
2165
  if (opened.size > BigInt(options.maxFileBytes))
@@ -1281,9 +2222,9 @@ async function readBoundedArchive(handle, opened, projectPath, maxFileBytes) {
1281
2222
  async function validateCurrentArchivePath(options) {
1282
2223
  const currentRoot = await lstat(options.archivesDir, { bigint: true });
1283
2224
  const currentSource = await lstat(options.sourcePath, { bigint: true });
1284
- const realCurrentRoot = await realpath(options.archivesDir);
1285
- const realCurrentSource = await realpath(options.sourcePath);
1286
- 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)) {
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)) {
1287
2228
  throw changed(options.projectPath);
1288
2229
  }
1289
2230
  }
@@ -1297,8 +2238,8 @@ function sameFileSnapshot(left, right) {
1297
2238
  return left.device === right.dev && left.inode === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
1298
2239
  }
1299
2240
  function isContained(root, path) {
1300
- const candidate = relative4(root, path);
1301
- return candidate !== "" && candidate !== ".." && !candidate.startsWith(`..${sep2}`) && !isAbsolute2(candidate);
2241
+ const candidate = relative5(root, path);
2242
+ return candidate !== "" && candidate !== ".." && !candidate.startsWith(`..${sep3}`) && !isAbsolute3(candidate);
1302
2243
  }
1303
2244
  function changed(projectPath) {
1304
2245
  return new ArchiveHistoryError(
@@ -1324,7 +2265,7 @@ function isPermissionError(error) {
1324
2265
  }
1325
2266
 
1326
2267
  // src/history/inspect.ts
1327
- import { basename as basename2, dirname as dirname3, relative as relative5, resolve as resolve2 } from "path";
2268
+ import { basename as basename2, dirname as dirname5, relative as relative6, resolve as resolve4 } from "path";
1328
2269
 
1329
2270
  // src/core/issue-relationship.ts
1330
2271
  var ISSUE_RELATIONS = /* @__PURE__ */ new Set(["relates", "closes"]);
@@ -1337,13 +2278,13 @@ var IssueRelationshipError = class extends Error {
1337
2278
  }
1338
2279
  code;
1339
2280
  };
1340
- function parseIssueRelationships(frontmatter) {
1341
- if (!frontmatter || !("issues" in frontmatter))
2281
+ function parseIssueRelationships(frontmatter2) {
2282
+ if (!frontmatter2 || !("issues" in frontmatter2))
1342
2283
  return [];
1343
- if (!Array.isArray(frontmatter.issues))
2284
+ if (!Array.isArray(frontmatter2.issues))
1344
2285
  throw new IssueRelationshipError("invalid_issue_shape", 'frontmatter field "issues" must be a list');
1345
2286
  const seen = /* @__PURE__ */ new Set();
1346
- return frontmatter.issues.map((value, index) => {
2287
+ return frontmatter2.issues.map((value, index) => {
1347
2288
  if (!isPlainObject(value))
1348
2289
  throw new IssueRelationshipError("invalid_issue_shape", `issues[${index}] must be a mapping with url and relation`);
1349
2290
  const fields = Object.keys(value);
@@ -1392,7 +2333,7 @@ function isPlainObject(value) {
1392
2333
  // src/history/inspect.ts
1393
2334
  var ARCHIVE_NAME_RE = /^(\d{4}-\d{2}-\d{2})_(.+)\.md$/;
1394
2335
  async function inspectArchiveHistory(options = {}) {
1395
- const archivesDir = resolve2(options.archivesDir ?? ARCHIVES_DIR);
2336
+ const archivesDir = resolve4(options.archivesDir ?? ARCHIVES_DIR);
1396
2337
  const maxFileBytes = options.maxFileBytes ?? HISTORY_MAX_FILE_BYTES;
1397
2338
  const tree = options.archiveTree ?? await inspectArchiveTree({ archivesDir });
1398
2339
  const diagnostics = tree.diagnostics.map((diagnostic) => ({
@@ -1462,36 +2403,36 @@ function parseArchiveEntry(current, path, maxFileBytes) {
1462
2403
  }
1463
2404
  const date = nameMatch[1];
1464
2405
  const archiveStem = nameMatch[2];
1465
- const relativeDirectory = normalizeLogicalPath(relative5(archivesDir, dirname3(sourcePath)));
2406
+ const relativeDirectory = normalizeLogicalPath(relative6(archivesDir, dirname5(sourcePath)));
1466
2407
  const archiveGroup = relativeDirectory === "." || relativeDirectory === "" ? null : relativeDirectory;
1467
2408
  if (archiveGroup && !isCanonicalWorkRefSegment(archiveGroup))
1468
2409
  return { diagnostic: errorDiagnostic("archive_identity_mismatch", path, "archive group path is not a valid one-segment Group identity") };
1469
- const changeDocument = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
1470
- const groupDocument = parseRspDocument(content, GROUP_BRIEF_DOCUMENT_SCHEMA);
1471
- const changeHeadings = getDocumentTitles(changeDocument, "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");
1472
2413
  const groupHeadings = getDocumentTitles(groupDocument, "identity");
1473
2414
  if (changeHeadings.length + groupHeadings.length !== 1) {
1474
2415
  return { diagnostic: errorDiagnostic("archive_heading_invalid", path, "archive must contain exactly one Change or Change Group identity heading") };
1475
2416
  }
1476
- let frontmatter;
2417
+ let frontmatter2;
1477
2418
  try {
1478
- frontmatter = parseFrontmatter(content);
2419
+ frontmatter2 = parseFrontmatter(changeDocument2.markdown);
1479
2420
  } catch (error) {
1480
2421
  return { diagnostic: errorDiagnostic("archive_frontmatter_invalid", path, `archive frontmatter could not be parsed: ${toErrorMessage(error)}`) };
1481
2422
  }
1482
- if (typeof frontmatter?.kind !== "string" || frontmatter.kind.trim() === "")
2423
+ if (typeof frontmatter2?.kind !== "string" || frontmatter2.kind.trim() === "")
1483
2424
  return { diagnostic: errorDiagnostic("archive_kind_invalid", path, "archive frontmatter must declare a non-empty kind") };
1484
2425
  if (groupHeadings.length === 1) {
1485
2426
  const group = groupHeadings[0];
1486
- if (!archiveGroup || group !== archiveGroup || frontmatter.kind !== "group" || !matchesArchiveStem(archiveStem, "brief")) {
2427
+ if (!archiveGroup || group !== archiveGroup || frontmatter2.kind !== "group" || !matchesArchiveStem(archiveStem, "brief")) {
1487
2428
  return { diagnostic: errorDiagnostic("archive_identity_mismatch", path, `archived Change Group identity ${group} does not match its archive path`) };
1488
2429
  }
1489
2430
  return { groupBrief: { date, group, path, sourcePath } };
1490
2431
  }
1491
2432
  let issues;
1492
- if ("issues" in frontmatter) {
2433
+ if ("issues" in frontmatter2) {
1493
2434
  try {
1494
- issues = parseIssueRelationships(frontmatter);
2435
+ issues = parseIssueRelationships(frontmatter2);
1495
2436
  } catch {
1496
2437
  issues = void 0;
1497
2438
  }
@@ -1506,7 +2447,7 @@ function parseArchiveEntry(current, path, maxFileBytes) {
1506
2447
  if (!isCanonicalExecutableWorkRef(workRef) || expectedGroup !== archiveGroup || !matchesArchiveStem(archiveStem, expectedBase)) {
1507
2448
  return { diagnostic: errorDiagnostic("archive_identity_mismatch", path, `archived Change identity ${workRef} does not match its archive path`) };
1508
2449
  }
1509
- const rawSummary = extractChangeSummary(content, changeDocument, { preservePlaceholder: true });
2450
+ const rawSummary = extractChangeSummary(content, changeDocument2, { preservePlaceholder: true });
1510
2451
  if (!rawSummary)
1511
2452
  return { diagnostic: errorDiagnostic("archive_summary_missing", path, "archived Change must contain frontmatter summary, Proposal Outcome, or Proposal Summary") };
1512
2453
  const summary = boundText(rawSummary);
@@ -1515,7 +2456,7 @@ function parseArchiveEntry(current, path, maxFileBytes) {
1515
2456
  date,
1516
2457
  workRef,
1517
2458
  group: expectedGroup,
1518
- kind: frontmatter.kind.trim(),
2459
+ kind: frontmatter2.kind.trim(),
1519
2460
  summary: summary.value,
1520
2461
  summaryTruncated: summary.truncated,
1521
2462
  path,
@@ -1540,10 +2481,10 @@ function isCalendarDate(value) {
1540
2481
  return Number.isFinite(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value;
1541
2482
  }
1542
2483
  function archiveOutputPath(sourcePath, archivesDir) {
1543
- return `.rsp/archives/${normalizeLogicalPath(relative5(resolve2(archivesDir), resolve2(sourcePath)))}`;
2484
+ return `.rsp/archives/${normalizeLogicalPath(relative6(resolve4(archivesDir), resolve4(sourcePath)))}`;
1544
2485
  }
1545
2486
  function normalizeOutputPath(path, archivesDir) {
1546
- const relativePath = normalizeLogicalPath(relative5(resolve2(archivesDir), resolve2(path)));
2487
+ const relativePath = normalizeLogicalPath(relative6(resolve4(archivesDir), resolve4(path)));
1547
2488
  return relativePath === "" ? ".rsp/archives" : `.rsp/archives/${relativePath}`;
1548
2489
  }
1549
2490
  function errorDiagnostic(code, path, message) {
@@ -1642,20 +2583,20 @@ async function readArchiveHistoryDocument(record) {
1642
2583
  throw new ArchiveHistoryError("archive_read_failed", `unable to read archived Change ${record.path}: ${error instanceof Error ? error.message : String(error)}`);
1643
2584
  }
1644
2585
  const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
1645
- const tasks = getDocumentSectionBody(document, "tasks");
1646
- const verify = getDocumentSectionBody(document, "verify");
1647
- const blockers = getDocumentSectionBody(document, "blockers");
2586
+ const tasks = getDocumentSectionMarkdown(document, "tasks");
2587
+ const verify = getDocumentSectionMarkdown(document, "verify");
2588
+ const blockers = getDocumentSectionMarkdown(document, "blockers");
1648
2589
  const detail = {
1649
2590
  ...toOutputRecord(record),
1650
- scenarioCount: parseScenarios(content).length,
2591
+ scenarioCount: parseScenarios(document.markdown).length,
1651
2592
  checkboxes: {
1652
2593
  tasks: countCheckboxes(tasks),
1653
2594
  verify: countCheckboxes(verify)
1654
2595
  },
1655
2596
  evidence: {
1656
- tasks: boundEvidence(tasks),
1657
- verify: boundEvidence(verify),
1658
- blockers: boundEvidence(blockers, true)
2597
+ tasks: boundEvidence(getVisibleBodyLines(tasks)),
2598
+ verify: boundEvidence(getVisibleBodyLines(verify)),
2599
+ blockers: boundEvidence(readBlockerEntries(blockers).map((entry) => entry.line))
1659
2600
  }
1660
2601
  };
1661
2602
  return { content, detail };
@@ -1674,8 +2615,7 @@ function toOutputRecord(record) {
1674
2615
  } = record;
1675
2616
  return output;
1676
2617
  }
1677
- function boundEvidence(section, omitNone = false) {
1678
- const rawItems = section.split("\n").map((line) => line.trim()).filter(Boolean).filter((line) => !omitNone || !/^[-*]\s*(?:none)?$/i.test(line) && !/^none$/i.test(line));
2618
+ function boundEvidence(rawItems) {
1679
2619
  const boundedItems = rawItems.slice(0, HISTORY_MAX_EVIDENCE_ITEMS).map((item) => boundText(item));
1680
2620
  return {
1681
2621
  items: boundedItems.map((item) => item.value),
@@ -1693,7 +2633,6 @@ function compareHistoryRecords2(left, right) {
1693
2633
  }
1694
2634
 
1695
2635
  // src/core/dependency-plan.ts
1696
- var REQUIRES_RE = /^[-*]\s+requires\s+`([^`]+)`:[ \t]*(\S.*)$/i;
1697
2636
  async function inspectChangeDependencies(options = {}) {
1698
2637
  const workTree = options.workTree ?? await inspectWorkTree();
1699
2638
  const archiveTree = options.archiveTree ?? await inspectArchiveTree();
@@ -1714,7 +2653,7 @@ async function inspectChangeDependencies(options = {}) {
1714
2653
  diagnostics.push(...archiveHistory.diagnostics);
1715
2654
  for (const brief of workTree.briefs) {
1716
2655
  try {
1717
- if (hasMeaningfulBlockers(await readFile3(brief.path, "utf-8")))
2656
+ if (hasMeaningfulBlockers(await readFile4(brief.path, "utf-8")))
1718
2657
  blockedGroups.add(brief.group);
1719
2658
  } catch (error) {
1720
2659
  diagnostics.push({
@@ -1728,7 +2667,7 @@ async function inspectChangeDependencies(options = {}) {
1728
2667
  }
1729
2668
  for (const ref of workTree.changes) {
1730
2669
  try {
1731
- const parsed = parseDependencyBlockers(await readFile3(ref.path, "utf-8"));
2670
+ const parsed = parseDependencyBlockers(await readFile4(ref.path, "utf-8"));
1732
2671
  if (ref.group && blockedGroups.has(ref.group))
1733
2672
  parsed.external = true;
1734
2673
  parsedByName.set(ref.name, parsed);
@@ -1833,18 +2772,16 @@ function parseDependencyBlockers(content) {
1833
2772
  const requires = /* @__PURE__ */ new Map();
1834
2773
  let external = false;
1835
2774
  const malformed = [];
1836
- for (const line of extractBlockerLines(content)) {
1837
- if (!line || isEmptyBlockerLine(line))
1838
- continue;
1839
- const dependency = line.match(REQUIRES_RE);
1840
- if (dependency) {
1841
- if (!requires.has(dependency[1]))
1842
- requires.set(dependency[1], dependency[2]);
1843
- } else if (/^[-*]\s+requires\b/i.test(line)) {
1844
- malformed.push(line);
1845
- external = true;
2775
+ const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
2776
+ const blockers = getDocumentSectionMarkdown(document, "blockers");
2777
+ for (const entry of readBlockerEntries(blockers)) {
2778
+ if (entry.kind === "dependency") {
2779
+ if (!requires.has(entry.name))
2780
+ requires.set(entry.name, entry.reason);
1846
2781
  } else {
1847
2782
  external = true;
2783
+ if (entry.kind === "malformed-dependency")
2784
+ malformed.push(entry.line);
1848
2785
  }
1849
2786
  }
1850
2787
  return {
@@ -1947,29 +2884,32 @@ function collectArchiveReadiness(content, options = {}) {
1947
2884
  const warnings = [];
1948
2885
  const documentDiagnostics = options.documentDiagnostics ?? [];
1949
2886
  warnings.push(...documentDiagnostics.map((diagnostic) => diagnostic.message));
1950
- const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
1951
- const tasksSection = getDocumentSectionBody(document, "tasks");
1952
- const verifySection = getDocumentSectionBody(document, "verify");
2887
+ const document = typeof content === "string" ? parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA) : content;
2888
+ const missingSummary = !extractChangeSummary(document.source, document, { preservePlaceholder: true });
2889
+ if (missingSummary)
2890
+ warnings.push("archived Change must contain frontmatter summary, Proposal Outcome, or Proposal Summary");
2891
+ const tasksSection = getDocumentSectionMarkdown(document, "tasks");
2892
+ const verifySection = getDocumentSectionMarkdown(document, "verify");
1953
2893
  const taskTodos = getOpenCheckboxes(tasksSection);
1954
2894
  if (taskTodos.length > 0)
1955
2895
  warnings.push(`${taskTodos.length} task item(s) still incomplete`);
1956
- const verifyCriticality = classifyVerifyCheckboxes(verifySection);
1957
- const verifyLines = collectVerifyCheckboxLines(verifySection);
1958
- const requiredVerifyTodos = getOpenCheckboxes([...verifyLines.required, ...verifyLines.unclassified].join("\n"));
1959
- const optionalVerifyTodos = getOpenCheckboxes(verifyLines.optional.join("\n"));
2896
+ const verifyItems = collectVerifyCheckboxes(verifySection);
2897
+ const verifyCriticality = summarizeVerifyCheckboxes(verifyItems);
2898
+ const requiredVerifyTodos = openCheckboxLines([...verifyItems.required, ...verifyItems.unclassified]);
2899
+ const optionalVerifyTodos = openCheckboxLines(verifyItems.optional);
1960
2900
  const verifyTodos = [...requiredVerifyTodos, ...optionalVerifyTodos];
1961
2901
  if (requiredVerifyTodos.length > 0)
1962
2902
  warnings.push(`${requiredVerifyTodos.length} required Verify item(s) are still incomplete`);
1963
2903
  if (optionalVerifyTodos.length > 0)
1964
2904
  warnings.push(`${optionalVerifyTodos.length} optional Verify item(s) are still incomplete`);
1965
- const activeBlockers = options.activeBlockers ?? hasMeaningfulBlockers(content);
2905
+ const activeBlockers = options.activeBlockers ?? hasMeaningfulBlockers(document);
1966
2906
  if (activeBlockers)
1967
2907
  warnings.push("active blockers are present in the change file");
1968
- const scenarios = parseScenarios(content);
2908
+ const scenarios = parseScenarios(document.markdown);
1969
2909
  const missingScenarios = scenarios.length === 0;
1970
2910
  if (missingScenarios)
1971
2911
  warnings.push("no Scenario blocks found (some changes do not need them)");
1972
- const archiveReady = documentDiagnostics.some((diagnostic) => diagnostic.severity === "error") || activeBlockers || taskTodos.length > 0 || requiredVerifyTodos.length > 0 ? "no" : "yes";
2912
+ const archiveReady = documentDiagnostics.some((diagnostic) => diagnostic.severity === "error") || missingSummary || activeBlockers || taskTodos.length > 0 || requiredVerifyTodos.length > 0 ? "no" : "yes";
1973
2913
  return {
1974
2914
  taskTodos,
1975
2915
  verifyTodos,
@@ -2022,12 +2962,12 @@ function getDurableReviewCandidateTargets() {
2022
2962
  async function inspectProjectStatus(options = {}) {
2023
2963
  const runtime = [];
2024
2964
  const diagnostics = [];
2025
- let manage = resolveManagePolicy({});
2965
+ let workflow = resolveWorkflowPolicy({});
2026
2966
  let language = resolveLanguagePolicy({});
2027
2967
  try {
2028
2968
  const configInspection = await inspectRspConfig();
2029
2969
  const configValid = configInspection.issues.length === 0;
2030
- manage = resolveManagePolicy(configInspection.config, { configValid });
2970
+ workflow = resolveWorkflowPolicy(configInspection.config, { configValid });
2031
2971
  language = resolveLanguagePolicy(configInspection.config, { configValid });
2032
2972
  if (!configValid) {
2033
2973
  diagnostics.push({
@@ -2038,7 +2978,7 @@ async function inspectProjectStatus(options = {}) {
2038
2978
  });
2039
2979
  }
2040
2980
  } catch (error) {
2041
- manage = resolveManagePolicy({}, { configValid: false });
2981
+ workflow = resolveWorkflowPolicy({}, { configValid: false });
2042
2982
  language = resolveLanguagePolicy({}, { configValid: false });
2043
2983
  diagnostics.push({
2044
2984
  severity: "error",
@@ -2105,7 +3045,7 @@ async function inspectProjectStatus(options = {}) {
2105
3045
  let kind = "\u2014";
2106
3046
  let summary = null;
2107
3047
  let title = name;
2108
- let blockerEntries = [];
3048
+ let externalBlockers = [];
2109
3049
  let readiness = emptyReadiness(false);
2110
3050
  let ageDays = null;
2111
3051
  const isFocused = focusedSet.has(name);
@@ -2116,14 +3056,15 @@ async function inspectProjectStatus(options = {}) {
2116
3056
  let issues = [];
2117
3057
  if (path) {
2118
3058
  try {
2119
- const content = await readFile4(path, "utf-8");
2120
- title = extractChangeTitle(content) ?? name;
2121
- summary = extractChangeSummary(content);
2122
- blockerEntries = extractBlockerEntries(content);
3059
+ const content = await readFile5(path, "utf-8");
3060
+ const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
3061
+ title = getDocumentTitle(document, "spaced") ?? name;
3062
+ summary = extractChangeSummary(content, document);
3063
+ externalBlockers = readBlockerEntries(getDocumentSectionMarkdown(document, "blockers")).filter((entry) => entry.kind !== "dependency").map((entry) => entry.text);
2123
3064
  try {
2124
- const frontmatter = parseFrontmatter(content);
2125
- kind = frontmatter?.kind ? String(frontmatter.kind) : "\u2014";
2126
- issues = parseIssueRelationships(frontmatter);
3065
+ const frontmatter2 = parseFrontmatter(document.markdown);
3066
+ kind = frontmatter2?.kind ? String(frontmatter2.kind) : "\u2014";
3067
+ issues = parseIssueRelationships(frontmatter2);
2127
3068
  } catch (error) {
2128
3069
  kind = "(invalid)";
2129
3070
  const issueError = error instanceof IssueRelationshipError;
@@ -2143,10 +3084,9 @@ async function inspectProjectStatus(options = {}) {
2143
3084
  message: toErrorMessage(error)
2144
3085
  });
2145
3086
  }
2146
- isBlocked = dependencyInspection.activeBlockers.get(name) ?? hasMeaningfulBlockers(content);
2147
- const details = collectArchiveReadiness(content, { activeBlockers: isBlocked });
2148
- const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
2149
- const taskCheckboxes = countCheckboxes(getDocumentSectionBody(document, "tasks"));
3087
+ isBlocked = dependencyInspection.activeBlockers.get(name) ?? hasMeaningfulBlockers(document);
3088
+ const details = collectArchiveReadiness(document, { activeBlockers: isBlocked });
3089
+ const taskCheckboxes = countCheckboxes(getDocumentSectionMarkdown(document, "tasks"));
2150
3090
  done = taskCheckboxes.done + details.verifyCriticality.required.done;
2151
3091
  total = taskCheckboxes.total + details.verifyCriticality.required.total;
2152
3092
  progressKnown = true;
@@ -2168,7 +3108,7 @@ async function inspectProjectStatus(options = {}) {
2168
3108
  });
2169
3109
  }
2170
3110
  try {
2171
- const metadata = await stat(path);
3111
+ const metadata = await stat2(path);
2172
3112
  ageDays = Math.floor((nowMs - metadata.mtime.getTime()) / (1e3 * 60 * 60 * 24));
2173
3113
  } catch (error) {
2174
3114
  diagnostics.push({
@@ -2217,12 +3157,12 @@ async function inspectProjectStatus(options = {}) {
2217
3157
  },
2218
3158
  progressKnown,
2219
3159
  title,
2220
- blockerEntries,
3160
+ externalBlockers,
2221
3161
  readiness
2222
3162
  });
2223
3163
  }
2224
3164
  return {
2225
- manage,
3165
+ workflow,
2226
3166
  language,
2227
3167
  focused: [...focusedSet].sort(),
2228
3168
  records,
@@ -2251,13 +3191,6 @@ function emptyReadiness(activeBlockers) {
2251
3191
  archiveReady: activeBlockers ? "no" : "judgment"
2252
3192
  };
2253
3193
  }
2254
- function extractChangeTitle(content) {
2255
- return getDocumentTitle(parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA), "spaced");
2256
- }
2257
- function extractBlockerEntries(content) {
2258
- const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
2259
- return getDocumentSectionBody(document, "blockers").split("\n").map((line) => line.trim()).filter((line) => /^[-*]\s+/.test(line)).map((line) => line.replace(/^[-*]\s+/, "").trim()).filter((line) => line.toLowerCase() !== "none");
2260
- }
2261
3194
  function deriveArchiveTrend(records) {
2262
3195
  const counts = /* @__PURE__ */ new Map();
2263
3196
  for (const record of records) {
@@ -2280,9 +3213,9 @@ var SPECS_MAX_DIAGNOSTICS = 20;
2280
3213
  import { Buffer as Buffer3 } from "buffer";
2281
3214
  import { spawnSync } from "child_process";
2282
3215
  import { constants as constants2 } from "fs";
2283
- import { lstat as lstat2, open as open2, realpath as realpath2 } from "fs/promises";
2284
- import { basename as basename3, dirname as dirname4, isAbsolute as isAbsolute3, join as join4, relative as relative6, resolve as resolve3, sep as sep3 } from "path";
2285
- var SPECS_ROOT = join4(RSP_DIR, "specs");
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");
2286
3219
  var GENERATED_INDEX_FILENAME = "00-index.md";
2287
3220
  var LEGACY_GENERATED_INDEX_FILENAME = "INDEX.md";
2288
3221
  var READ_NO_FOLLOW_FLAGS2 = constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0);
@@ -2295,8 +3228,8 @@ var SpecsQueryError = class extends Error {
2295
3228
  code;
2296
3229
  };
2297
3230
  async function inspectSpecs(options = {}) {
2298
- const projectRoot = await realpath2(resolve3(options.cwd ?? process.cwd()));
2299
- const specsRoot = resolve3(projectRoot, SPECS_ROOT);
3231
+ const projectRoot = await realpath3(resolve5(options.cwd ?? process.cwd()));
3232
+ const specsRoot = resolve5(projectRoot, SPECS_ROOT);
2300
3233
  const maxCandidates = options.maxCandidates ?? SPECS_MAX_CANDIDATES;
2301
3234
  const maxFileBytes = options.maxFileBytes ?? SPECS_MAX_FILE_BYTES;
2302
3235
  validatePositiveBound(maxCandidates, "maxCandidates");
@@ -2308,7 +3241,7 @@ async function inspectSpecs(options = {}) {
2308
3241
  const decisionPathIssue = await validateDecisionRecordsFilesystemPath(decisionRecordsPath, projectRoot);
2309
3242
  if (decisionPathIssue)
2310
3243
  throw new SpecsQueryError("invalid_decision_records_path", decisionPathIssue);
2311
- const decisionRecordsRoot = resolve3(projectRoot, decisionRecordsPath);
3244
+ const decisionRecordsRoot = resolve5(projectRoot, decisionRecordsPath);
2312
3245
  const accumulator = {
2313
3246
  documents: [],
2314
3247
  generatedIndexes: [],
@@ -2316,7 +3249,7 @@ async function inspectSpecs(options = {}) {
2316
3249
  candidateCount: 0,
2317
3250
  candidateLimitReported: false
2318
3251
  };
2319
- const excludedSpecDirectories = [DEFAULT_DECISION_RECORDS_PATH, decisionRecordsPath].map((path) => resolve3(projectRoot, path)).filter((path) => isInside(specsRoot, path));
3252
+ const excludedSpecDirectories = [DEFAULT_DECISION_RECORDS_PATH, decisionRecordsPath].map((path) => resolve5(projectRoot, path)).filter((path) => isInside(specsRoot, path));
2320
3253
  await inspectDocumentRoot({
2321
3254
  projectRoot,
2322
3255
  root: specsRoot,
@@ -2386,14 +3319,14 @@ async function readCurrentSpecsDocument(inspection, projectPath) {
2386
3319
  const document = inspection.documents.find((candidate) => candidate.path === normalized);
2387
3320
  if (!document)
2388
3321
  throw new SpecsQueryError("specs_document_not_found", `Specs document not found: ${projectPath}`);
2389
- const sourcePath = resolve3(inspection.source.root, normalized);
3322
+ const sourcePath = resolve5(inspection.source.root, normalized);
2390
3323
  const current = await readCurrentDocument(
2391
3324
  sourcePath,
2392
3325
  inspection.source.root,
2393
3326
  document.path,
2394
3327
  inspection.limits.fileBytes
2395
3328
  );
2396
- const currentDocument = parseDocumentRecord(current.content, current.bytes, document.path, document.kind);
3329
+ const currentDocument = parseDocumentRecord(parseMarkdown(current.content), current.bytes, document.path, document.kind);
2397
3330
  return {
2398
3331
  ...currentDocument,
2399
3332
  content: current.content
@@ -2418,18 +3351,17 @@ async function searchSpecsWithSources(inspection, literal, options = {}) {
2418
3351
  let matched = 0;
2419
3352
  for (const document of inspection.documents) {
2420
3353
  const current = await readCurrentDocument(
2421
- resolve3(inspection.source.root, document.path),
3354
+ resolve5(inspection.source.root, document.path),
2422
3355
  inspection.source.root,
2423
3356
  document.path,
2424
3357
  inspection.limits.fileBytes
2425
3358
  );
2426
- const currentDocument = parseDocumentRecord(current.content, current.bytes, document.path, document.kind);
3359
+ const currentDocument = parseDocumentRecord(parseMarkdown(current.content), current.bytes, document.path, document.kind);
2427
3360
  const lines = current.content.split(/\r?\n/);
3361
+ const headingsByLine = new Map(currentDocument.headings.map((item) => [item.line, item.title]));
2428
3362
  let heading = null;
2429
3363
  for (let index = 0; index < lines.length; index++) {
2430
- const parsedHeading = parseMarkdownHeading(lines[index]);
2431
- if (parsedHeading)
2432
- heading = parsedHeading.title;
3364
+ heading = headingsByLine.get(index + 1) ?? heading;
2433
3365
  const position = lines[index].toLowerCase().indexOf(lowered);
2434
3366
  if (position < 0)
2435
3367
  continue;
@@ -2543,18 +3475,19 @@ async function classifyGeneratedIndex(path, projectRoot, projectPath, maxFileByt
2543
3475
  }
2544
3476
  function isRecognizedGeneratedSpecsIndex(content, projectPath) {
2545
3477
  const metadata = parseFrontmatter(content);
2546
- const expectedSourceDirectory = normalizeLogicalPath(dirname4(projectPath));
3478
+ const expectedSourceDirectory = normalizeLogicalPath(dirname6(projectPath));
2547
3479
  return metadata?.kind === "generated-index" && metadata.index_type === "specs" && metadata.source_dir === expectedSourceDirectory;
2548
3480
  }
2549
3481
  async function inspectDocument(path, projectRoot, projectPath, kind, maxFileBytes, accumulator) {
2550
3482
  try {
2551
3483
  const current = await readCurrentDocument(path, projectRoot, projectPath, maxFileBytes);
2552
- const metadata = parseFrontmatter(current.content);
3484
+ const document = parseMarkdown(current.content);
3485
+ const metadata = parseFrontmatter(document);
2553
3486
  if (metadata?.kind === "generated-index")
2554
3487
  return;
2555
3488
  accumulator.documents.push({
2556
3489
  sourcePath: path,
2557
- ...parseDocumentRecord(current.content, current.bytes, projectPath, kind)
3490
+ ...parseDocumentRecord(document, current.bytes, projectPath, kind, metadata)
2558
3491
  });
2559
3492
  } catch (error) {
2560
3493
  accumulator.diagnostics.push({
@@ -2599,8 +3532,8 @@ function buildDirectoryTree(projectRoot, root, kind, documents) {
2599
3532
  documents: []
2600
3533
  };
2601
3534
  for (const document of documents.filter((candidate) => candidate.kind === kind)) {
2602
- const rel = relative6(root, document.sourcePath);
2603
- if (rel === ".." || rel.startsWith(`..${sep3}`) || isAbsolute3(rel))
3535
+ const rel = relative7(root, document.sourcePath);
3536
+ if (rel === ".." || rel.startsWith(`..${sep4}`) || isAbsolute4(rel))
2604
3537
  continue;
2605
3538
  const segments = normalizeLogicalPath(rel).split("/");
2606
3539
  let current = tree;
@@ -2623,64 +3556,18 @@ function buildDirectoryTree(projectRoot, root, kind, documents) {
2623
3556
  }
2624
3557
  return tree;
2625
3558
  }
2626
- function extractHeadings(content) {
2627
- const headings = [];
2628
- const lines = content.split(/\r?\n/);
2629
- for (let index = 0; index < lines.length; index++) {
2630
- const heading = parseMarkdownHeading(lines[index]);
2631
- if (!heading)
2632
- continue;
2633
- headings.push({
2634
- depth: heading.depth,
2635
- title: heading.title,
2636
- line: index + 1
2637
- });
2638
- }
2639
- return headings;
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 }] : []);
2640
3561
  }
2641
- function parseMarkdownHeading(line) {
2642
- let depth = 0;
2643
- while (depth < 6 && line[depth] === "#")
2644
- depth += 1;
2645
- if (depth === 0 || line[depth] === "#" || !isWhitespace(line[depth]))
2646
- return null;
2647
- let contentStart = depth;
2648
- while (contentStart < line.length && isWhitespace(line[contentStart]))
2649
- contentStart += 1;
2650
- if (contentStart === line.length)
2651
- return null;
2652
- const rawTitle = line.slice(contentStart);
2653
- let titleEnd = rawTitle.length;
2654
- while (titleEnd > 0 && isSpaceOrTab(rawTitle[titleEnd - 1]))
2655
- titleEnd -= 1;
2656
- let closingStart = titleEnd;
2657
- while (closingStart > 0 && rawTitle[closingStart - 1] === "#")
2658
- closingStart -= 1;
2659
- const closingPredecessor = closingStart === 0 ? line[contentStart - 1] : rawTitle[closingStart - 1];
2660
- if (closingStart < titleEnd && isSpaceOrTab(closingPredecessor)) {
2661
- titleEnd = closingStart;
2662
- while (titleEnd > 0 && isSpaceOrTab(rawTitle[titleEnd - 1]))
2663
- titleEnd -= 1;
2664
- }
2665
- const title = rawTitle.slice(0, titleEnd).trim();
2666
- return { depth, title };
2667
- }
2668
- function isWhitespace(character) {
2669
- return character !== void 0 && character.trim() === "";
2670
- }
2671
- function isSpaceOrTab(character) {
2672
- return character === " " || character === " ";
2673
- }
2674
- function extractSummary(content, frontmatterSummary) {
3562
+ function extractSummary(document, frontmatterSummary) {
2675
3563
  if (typeof frontmatterSummary === "string" && frontmatterSummary.trim() !== "")
2676
3564
  return frontmatterSummary.trim();
2677
- const frontmatter = content.match(/^---\r?\n[\s\S]*?\r?\n(?:---|\.\.\.)\r?\n/);
2678
- const body = frontmatter ? content.slice(frontmatter[0].length) : content;
2679
- for (const rawLine of body.split(/\r?\n/)) {
2680
- const line = rawLine.trim();
2681
- if (line === "" || line.startsWith("#") || line.startsWith("<!--"))
3565
+ for (const node of document.root.children) {
3566
+ if (node.type !== "paragraph" && node.type !== "list")
2682
3567
  continue;
2683
- return line.replace(/^[-*]\s*/, "").trim() || null;
3568
+ const text = markdownText(node).split("\n", 1)[0].trim();
3569
+ if (text)
3570
+ return text;
2684
3571
  }
2685
3572
  return null;
2686
3573
  }
@@ -2709,14 +3596,14 @@ function normalizeRequestedPath(pathValue) {
2709
3596
  if (trimmed === "")
2710
3597
  throw new SpecsQueryError("invalid_specs_path", "Specs detail path must be non-empty");
2711
3598
  const normalized = normalizeLogicalPath(trimmed.replace(/^\.\//, ""));
2712
- if (isAbsolute3(trimmed) || normalized === ".." || normalized.startsWith("../"))
3599
+ if (isAbsolute4(trimmed) || normalized === ".." || normalized.startsWith("../"))
2713
3600
  throw new SpecsQueryError("invalid_specs_path", "Specs detail path must be one project-relative path returned by rsp specs");
2714
3601
  return normalized;
2715
3602
  }
2716
3603
  function reservedIndexKind(root, path) {
2717
3604
  if (basename3(path) === GENERATED_INDEX_FILENAME)
2718
3605
  return "current";
2719
- if (path === join4(root, LEGACY_GENERATED_INDEX_FILENAME))
3606
+ if (path === join7(root, LEGACY_GENERATED_INDEX_FILENAME))
2720
3607
  return "legacy";
2721
3608
  return null;
2722
3609
  }
@@ -2730,10 +3617,10 @@ function excerptAround(line, matchStart, matchLength, limit) {
2730
3617
  }
2731
3618
  async function readCurrentDocument(path, root, projectPath, maxFileBytes) {
2732
3619
  const changed2 = () => new SpecsQueryError("specs_file_changed", `Specs document is no longer a readable regular file: ${projectPath}`);
2733
- const sourcePath = resolve3(path);
2734
- const sourceRoot = resolve3(root);
2735
- const relativePath = relative6(sourceRoot, sourcePath);
2736
- if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${sep3}`) || isAbsolute3(relativePath)) {
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)) {
2737
3624
  throw changed2();
2738
3625
  }
2739
3626
  const inspection = inspectManagedFile(sourcePath, "Specs document");
@@ -2745,9 +3632,9 @@ async function readCurrentDocument(path, root, projectPath, maxFileBytes) {
2745
3632
  try {
2746
3633
  inspectedRoot = await lstat2(sourceRoot, { bigint: true });
2747
3634
  inspected = await lstat2(sourcePath, { bigint: true });
2748
- realRoot = await realpath2(sourceRoot);
2749
- const resolvedPath = await realpath2(sourcePath);
2750
- if (!inspectedRoot.isDirectory() || !inspected.isFile() || resolvedPath !== resolve3(realRoot, relativePath) || !isContained2(realRoot, resolvedPath)) {
3635
+ realRoot = await realpath3(sourceRoot);
3636
+ const resolvedPath = await realpath3(sourcePath);
3637
+ if (!inspectedRoot.isDirectory() || !inspected.isFile() || resolvedPath !== resolve5(realRoot, relativePath) || !isContained2(realRoot, resolvedPath)) {
2751
3638
  throw changed2();
2752
3639
  }
2753
3640
  } catch (error) {
@@ -2763,9 +3650,9 @@ async function readCurrentDocument(path, root, projectPath, maxFileBytes) {
2763
3650
  }
2764
3651
  try {
2765
3652
  const opened = await handle.stat({ bigint: true });
2766
- const currentRoot = await realpath2(sourceRoot);
2767
- const currentPath = await realpath2(sourcePath);
2768
- if (!opened.isFile() || currentRoot !== realRoot || currentPath !== resolve3(currentRoot, relativePath) || !isContained2(currentRoot, currentPath) || !sameIdentity2(inspected, opened) || !sameSnapshot2(inspected, opened)) {
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)) {
2769
3656
  throw changed2();
2770
3657
  }
2771
3658
  if (opened.size > BigInt(maxFileBytes))
@@ -2818,9 +3705,9 @@ async function validateCurrentSpecsPath(options) {
2818
3705
  try {
2819
3706
  const currentRoot = await lstat2(options.sourceRoot, { bigint: true });
2820
3707
  const currentPath = await lstat2(options.sourcePath, { bigint: true });
2821
- const realCurrentRoot = await realpath2(options.sourceRoot);
2822
- const realCurrentPath = await realpath2(options.sourcePath);
2823
- if (!currentRoot.isDirectory() || !currentPath.isFile() || !sameIdentity2(options.inspectedRoot, currentRoot) || realCurrentRoot !== options.realRoot || realCurrentPath !== resolve3(realCurrentRoot, options.relativePath) || !isContained2(realCurrentRoot, realCurrentPath) || !sameIdentity2(currentPath, options.finalHandle) || !sameSnapshot2(currentPath, options.finalHandle)) {
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)) {
2824
3711
  throw changed2();
2825
3712
  }
2826
3713
  } catch (error) {
@@ -2836,8 +3723,8 @@ function sameSnapshot2(left, right) {
2836
3723
  return left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
2837
3724
  }
2838
3725
  function isContained2(root, path) {
2839
- const candidate = relative6(root, path);
2840
- return candidate !== "" && candidate !== ".." && !candidate.startsWith(`..${sep3}`) && !isAbsolute3(candidate);
3726
+ const candidate = relative7(root, path);
3727
+ return candidate !== "" && candidate !== ".." && !candidate.startsWith(`..${sep4}`) && !isAbsolute4(candidate);
2841
3728
  }
2842
3729
  function boundCodePoints(value, limit) {
2843
3730
  const codePoints = [...value];
@@ -2850,14 +3737,13 @@ function stripSource(document) {
2850
3737
  const { sourcePath: _sourcePath, ...record } = document;
2851
3738
  return record;
2852
3739
  }
2853
- function parseDocumentRecord(content, bytes, path, kind) {
2854
- const metadata = parseFrontmatter(content);
2855
- const headings = extractHeadings(content);
3740
+ function parseDocumentRecord(document, bytes, path, kind, metadata = parseFrontmatter(document)) {
3741
+ const headings = extractHeadings(document);
2856
3742
  return {
2857
3743
  path,
2858
3744
  kind,
2859
3745
  title: typeof metadata?.title === "string" && metadata.title.trim() !== "" ? metadata.title.trim() : headings.find((heading) => heading.depth === 1)?.title ?? basename3(path, ".md"),
2860
- summary: extractSummary(content, metadata?.summary),
3746
+ summary: extractSummary(document, metadata?.summary),
2861
3747
  bytes,
2862
3748
  headings
2863
3749
  };
@@ -2870,11 +3756,11 @@ function validatePositiveBound(value, label) {
2870
3756
  throw new SpecsQueryError("invalid_specs_bound", `${label} must be a positive integer`);
2871
3757
  }
2872
3758
  function isInside(directory, path) {
2873
- const rel = relative6(directory, path);
2874
- return rel === "" || !isAbsolute3(rel) && rel !== ".." && !rel.startsWith(`..${sep3}`);
3759
+ const rel = relative7(directory, path);
3760
+ return rel === "" || !isAbsolute4(rel) && rel !== ".." && !rel.startsWith(`..${sep4}`);
2875
3761
  }
2876
3762
  function toProjectPath(projectRoot, path) {
2877
- return normalizeLogicalPath(relative6(projectRoot, path)) || ".";
3763
+ return normalizeLogicalPath(relative7(projectRoot, path)) || ".";
2878
3764
  }
2879
3765
 
2880
3766
  // src/specs/projection.ts
@@ -2967,13 +3853,70 @@ function projectDependencyForest(plan, requestedRoots = defaultRoots(plan)) {
2967
3853
  }
2968
3854
 
2969
3855
  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,
2970
3878
  IssueRelationshipError,
2971
3879
  parseIssueRelationships,
2972
3880
  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,
2973
3915
  walkMarkdownFiles,
2974
3916
  inspectUnsupportedRules,
2975
3917
  normalizeLogicalPath,
2976
3918
  isValidSpecName,
3919
+ inspectRspInitialization,
2977
3920
  guardRspInitialized,
2978
3921
  cleanupEmptyParentDirs,
2979
3922
  detectProjectName,
@@ -3007,7 +3950,6 @@ export {
3007
3950
  inspectChangeDependencies,
3008
3951
  SPECS_DEFAULT_RESULT_LIMIT,
3009
3952
  SPECS_MAX_RESULT_LIMIT,
3010
- SPECS_MAX_FILE_BYTES,
3011
3953
  SPECS_SEARCH_EXCERPT_CODE_POINTS,
3012
3954
  SPECS_DETAIL_CONTENT_CODE_POINTS,
3013
3955
  SpecsQueryError,