@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.
- package/README.md +24 -17
- package/README.zh-CN.md +25 -18
- package/dist/{chunk-TSUDEX5Y.mjs → chunk-HFSTLCCK.mjs} +1261 -319
- package/dist/cli.mjs +1129 -2350
- package/dist/{entry-GIDNR7CJ.mjs → entry-RIJQ2X46.mjs} +122 -36
- package/package.json +32 -24
- package/skills/rsp/SKILL.md +43 -38
- package/skills/rsp/references/closeout.md +38 -0
- package/skills/rsp/references/context-migration.md +22 -0
- package/skills/rsp/references/control-outcome.md +3 -29
- package/skills/rsp/references/coordination-recovery.md +25 -0
- package/skills/rsp/references/coordination-review.md +15 -0
- package/skills/rsp/references/coordination.md +59 -0
- package/skills/rsp/references/durable-review.md +60 -12
- package/skills/rsp/references/focus-continuation.md +8 -4
- package/skills/rsp/references/groups-dependencies.md +6 -4
- package/skills/rsp/references/response-language.md +2 -0
- package/skills/rsp/references/setup-repair.md +37 -7
- package/skills/rsp/references/work-model.md +27 -0
- package/skills/rsp-commit/SKILL.md +76 -31
- package/skills/rsp-commit/references/message.md +34 -0
- package/skills/rsp-doc/SKILL.md +65 -0
- package/skills/rsp-doc/agents/openai.yaml +3 -0
- package/skills/rsp-doc/references/artifact-lenses.md +43 -0
- package/skills/rsp-doc/references/release-communication.md +39 -0
- package/skills/rsp-doc/references/skill-authoring.md +36 -0
- package/skills/rsp-implement/SKILL.md +56 -25
- package/skills/rsp-implement/references/diagnosis.md +10 -0
- package/skills/rsp-implement/references/finding-resolution.md +21 -0
- package/skills/rsp-implement/references/test-first.md +7 -0
- package/skills/rsp-review/SKILL.md +46 -45
- package/skills/rsp-review/references/code-review.md +23 -8
- package/skills/rsp-review/references/document-review.md +25 -8
- package/skills/rsp-review/references/release-communication.md +21 -0
- package/skills/{rsp-structural-audit/references/structural-lenses.md → rsp-review/references/structural-review.md} +5 -5
- package/skills/rsp-shape/SKILL.md +24 -45
- package/skills/rsp-shape/references/complex-shaping.md +3 -1
- package/skills/rsp-shape/references/deep-clarification.md +4 -4
- package/skills/rsp-shape/references/domain-modeling.md +13 -0
- package/skills/rsp-shape/references/module-seams.md +13 -0
- package/skills/rsp-shape/references/planning.md +42 -0
- package/skills/{rsp-design → rsp-shape}/references/reversible-exploration.md +2 -2
- package/skills/rsp-verify/SKILL.md +44 -12
- package/dist/chunk-7HXQNBIG.mjs +0 -871
- package/dist/chunk-JOMFFQRG.mjs +0 -99
- package/dist/chunk-QM2UVPQK.mjs +0 -375
- package/dist/entry-YAW3PYFA.mjs +0 -174
- package/rules/rsp-rules.md +0 -48
- package/skills/rsp/references/implementation-evidence.md +0 -12
- package/skills/rsp/references/managed-routing.md +0 -45
- package/skills/rsp-design/SKILL.md +0 -53
- package/skills/rsp-design/references/domain-modeling.md +0 -11
- package/skills/rsp-design/references/module-seams.md +0 -11
- package/skills/rsp-diagnose/SKILL.md +0 -48
- package/skills/rsp-manage/SKILL.md +0 -101
- package/skills/rsp-manage/references/closeout.md +0 -13
- package/skills/rsp-manage/references/delegation.md +0 -47
- package/skills/rsp-manage/references/interruption-recovery.md +0 -19
- package/skills/rsp-manage/references/review-convergence.md +0 -9
- package/skills/rsp-release-docs/SKILL.md +0 -77
- package/skills/rsp-release-docs/references/convention-discovery.md +0 -65
- package/skills/rsp-release-docs/references/evidence-and-surfaces.md +0 -21
- package/skills/rsp-release-docs/references/output-contracts.md +0 -97
- package/skills/rsp-release-docs/references/publication-lifecycle.md +0 -28
- package/skills/rsp-resolve-findings/SKILL.md +0 -61
- package/skills/rsp-structural-audit/SKILL.md +0 -71
- package/skills/rsp-tdd/SKILL.md +0 -56
|
@@ -1,80 +1,1004 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
958
|
+
import { readFile as readFile5, stat as stat2 } from "fs/promises";
|
|
47
959
|
|
|
48
960
|
// src/core/change-group.ts
|
|
49
|
-
import { readFile as
|
|
50
|
-
import { dirname as
|
|
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 =
|
|
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(
|
|
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:
|
|
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
|
|
999
|
+
const items = await readdir3(dir, { withFileTypes: true });
|
|
76
1000
|
for (const item of items) {
|
|
77
|
-
const path =
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
118
|
-
const designPath =
|
|
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 (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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 =
|
|
1060
|
+
let parent = dirname3(path);
|
|
130
1061
|
while (parent !== stopDir) {
|
|
131
1062
|
try {
|
|
132
|
-
const remaining = await
|
|
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 =
|
|
1071
|
+
parent = dirname3(parent);
|
|
141
1072
|
}
|
|
142
1073
|
}
|
|
143
1074
|
async function detectProjectName() {
|
|
144
1075
|
try {
|
|
145
|
-
if (
|
|
146
|
-
const raw = await
|
|
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
|
|
160
|
-
import { isAbsolute, join as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
278
|
-
if (
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
),
|
|
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: ${
|
|
1409
|
+
`Group Brief must be a regular file: ${join5(groupPath, GROUP_BRIEF_FILENAME)}`,
|
|
479
1410
|
group
|
|
480
|
-
),
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
692
|
-
const filePath = finalFilename && depth === segments.length ?
|
|
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 =
|
|
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
|
|
787
|
-
const frontmatterSummary = meaningfulSummary(
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
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 =
|
|
1979
|
+
const briefPath = join6(dirname4(ref.path), GROUP_BRIEF_FILENAME);
|
|
1038
1980
|
let content;
|
|
1039
1981
|
try {
|
|
1040
|
-
content = await
|
|
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(
|
|
2012
|
+
if (!normalizeLogicalPath(relative4(ARCHIVES_DIR, path)).startsWith(groupPrefix))
|
|
1071
2013
|
continue;
|
|
1072
2014
|
let content;
|
|
1073
2015
|
try {
|
|
1074
|
-
content = await
|
|
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,
|
|
2026
|
+
function parseGroupBrief(group, document) {
|
|
1085
2027
|
const diagnostics = [];
|
|
1086
|
-
|
|
1087
|
-
let frontmatter;
|
|
2028
|
+
let frontmatter2;
|
|
1088
2029
|
try {
|
|
1089
|
-
|
|
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 (
|
|
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 (
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
1183
|
-
const archivesDir =
|
|
1184
|
-
const relativePath =
|
|
1185
|
-
if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${
|
|
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
|
|
1197
|
-
const resolvedSource = await
|
|
1198
|
-
const expectedSource =
|
|
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
|
|
1220
|
-
const currentSource = await
|
|
1221
|
-
if (!opened.isFile() || currentRoot !== realArchivesDir || currentSource !==
|
|
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
|
|
1285
|
-
const realCurrentSource = await
|
|
1286
|
-
if (!currentRoot.isDirectory() || !currentSource.isFile() || !sameIdentity(options.inspectedRoot, currentRoot) || realCurrentRoot !== options.realArchivesDir || realCurrentSource !==
|
|
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 =
|
|
1301
|
-
return candidate !== "" && candidate !== ".." && !candidate.startsWith(`..${
|
|
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
|
|
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(
|
|
1341
|
-
if (!
|
|
2281
|
+
function parseIssueRelationships(frontmatter2) {
|
|
2282
|
+
if (!frontmatter2 || !("issues" in frontmatter2))
|
|
1342
2283
|
return [];
|
|
1343
|
-
if (!Array.isArray(
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
1470
|
-
const groupDocument = parseRspDocument(
|
|
1471
|
-
const changeHeadings = getDocumentTitles(
|
|
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
|
|
2417
|
+
let frontmatter2;
|
|
1477
2418
|
try {
|
|
1478
|
-
|
|
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
|
|
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 ||
|
|
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
|
|
2433
|
+
if ("issues" in frontmatter2) {
|
|
1493
2434
|
try {
|
|
1494
|
-
issues = parseIssueRelationships(
|
|
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,
|
|
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:
|
|
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(
|
|
2484
|
+
return `.rsp/archives/${normalizeLogicalPath(relative6(resolve4(archivesDir), resolve4(sourcePath)))}`;
|
|
1544
2485
|
}
|
|
1545
2486
|
function normalizeOutputPath(path, archivesDir) {
|
|
1546
|
-
const relativePath = normalizeLogicalPath(
|
|
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 =
|
|
1646
|
-
const verify =
|
|
1647
|
-
const 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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
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
|
|
1952
|
-
|
|
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
|
|
1957
|
-
const
|
|
1958
|
-
const requiredVerifyTodos =
|
|
1959
|
-
const optionalVerifyTodos =
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
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
|
|
2125
|
-
kind =
|
|
2126
|
-
issues = parseIssueRelationships(
|
|
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(
|
|
2147
|
-
const details = collectArchiveReadiness(
|
|
2148
|
-
const
|
|
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
|
|
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
|
-
|
|
3160
|
+
externalBlockers,
|
|
2221
3161
|
readiness
|
|
2222
3162
|
});
|
|
2223
3163
|
}
|
|
2224
3164
|
return {
|
|
2225
|
-
|
|
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
|
|
2284
|
-
import { basename as basename3, dirname as
|
|
2285
|
-
var SPECS_ROOT =
|
|
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
|
|
2299
|
-
const specsRoot =
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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 =
|
|
2603
|
-
if (rel === ".." || rel.startsWith(`..${
|
|
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(
|
|
2627
|
-
|
|
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
|
|
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
|
|
2678
|
-
|
|
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
|
-
|
|
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 (
|
|
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 ===
|
|
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 =
|
|
2734
|
-
const sourceRoot =
|
|
2735
|
-
const relativePath =
|
|
2736
|
-
if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${
|
|
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
|
|
2749
|
-
const resolvedPath = await
|
|
2750
|
-
if (!inspectedRoot.isDirectory() || !inspected.isFile() || 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
|
|
2767
|
-
const currentPath = await
|
|
2768
|
-
if (!opened.isFile() || currentRoot !== realRoot || currentPath !==
|
|
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
|
|
2822
|
-
const realCurrentPath = await
|
|
2823
|
-
if (!currentRoot.isDirectory() || !currentPath.isFile() || !sameIdentity2(options.inspectedRoot, currentRoot) || realCurrentRoot !== options.realRoot || realCurrentPath !==
|
|
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 =
|
|
2840
|
-
return candidate !== "" && candidate !== ".." && !candidate.startsWith(`..${
|
|
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(
|
|
2854
|
-
const
|
|
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(
|
|
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 =
|
|
2874
|
-
return rel === "" || !
|
|
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(
|
|
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,
|