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