@oevery/rsp 3.3.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -17
- package/README.zh-CN.md +25 -18
- package/dist/chunk-JO6NDJM4.mjs +360 -0
- package/dist/chunk-LGDCIQYL.mjs +725 -0
- package/dist/{chunk-7HXQNBIG.mjs → chunk-P3KO3GZW.mjs} +420 -268
- package/dist/chunk-U56XMIOO.mjs +66 -0
- package/dist/{chunk-TSUDEX5Y.mjs → chunk-VDRXFKBP.mjs} +142 -867
- package/dist/cli.mjs +1111 -2347
- package/dist/client-EHRWH5Q7.mjs +108 -0
- package/dist/{entry-GIDNR7CJ.mjs → entry-NDASTAI5.mjs} +128 -38
- package/dist/viewer/NOTICE.txt +299 -0
- package/dist/viewer/app.css +2 -0
- package/dist/viewer/app.js +670 -0
- package/dist/viewer-build.json +1 -0
- package/dist/viewer-service.mjs +551 -0
- package/package.json +51 -25
- package/skills/rsp/SKILL.md +44 -38
- package/skills/rsp/references/architecture.md +39 -0
- package/skills/rsp/references/closeout.md +38 -0
- package/skills/rsp/references/context-migration.md +22 -0
- package/skills/rsp/references/control-outcome.md +3 -29
- package/skills/rsp/references/coordination-recovery.md +25 -0
- package/skills/rsp/references/coordination-review.md +15 -0
- package/skills/rsp/references/coordination.md +59 -0
- package/skills/rsp/references/durable-review.md +60 -12
- package/skills/rsp/references/focus-continuation.md +12 -4
- package/skills/rsp/references/groups-dependencies.md +6 -4
- package/skills/rsp/references/response-language.md +2 -0
- package/skills/rsp/references/setup-repair.md +42 -7
- package/skills/rsp/references/work-model.md +37 -0
- package/skills/rsp-commit/SKILL.md +76 -31
- package/skills/rsp-commit/references/message.md +34 -0
- package/skills/rsp-doc/SKILL.md +68 -0
- package/skills/rsp-doc/agents/openai.yaml +3 -0
- package/skills/rsp-doc/references/release-communication.md +39 -0
- package/skills/rsp-doc/references/skill-authoring.md +36 -0
- package/skills/rsp-implement/SKILL.md +60 -25
- package/skills/rsp-implement/references/diagnosis.md +10 -0
- package/skills/rsp-implement/references/finding-resolution.md +21 -0
- package/skills/rsp-implement/references/test-first.md +7 -0
- package/skills/rsp-review/SKILL.md +46 -45
- package/skills/rsp-review/references/code-review.md +30 -9
- package/skills/rsp-review/references/document-review.md +24 -9
- package/skills/rsp-review/references/release-communication.md +21 -0
- package/skills/rsp-shape/SKILL.md +26 -45
- package/skills/rsp-shape/references/complex-shaping.md +3 -1
- package/skills/rsp-shape/references/deep-clarification.md +4 -4
- package/skills/rsp-shape/references/domain-modeling.md +13 -0
- package/skills/rsp-shape/references/module-seams.md +13 -0
- package/skills/rsp-shape/references/planning.md +42 -0
- package/skills/{rsp-design → rsp-shape}/references/reversible-exploration.md +2 -2
- package/skills/rsp-verify/SKILL.md +39 -12
- package/dist/chunk-JOMFFQRG.mjs +0 -99
- package/dist/chunk-QM2UVPQK.mjs +0 -375
- package/dist/entry-YAW3PYFA.mjs +0 -174
- package/rules/rsp-rules.md +0 -48
- package/skills/rsp/references/implementation-evidence.md +0 -12
- package/skills/rsp/references/managed-routing.md +0 -45
- package/skills/rsp-design/SKILL.md +0 -53
- package/skills/rsp-design/references/domain-modeling.md +0 -11
- package/skills/rsp-design/references/module-seams.md +0 -11
- package/skills/rsp-diagnose/SKILL.md +0 -48
- package/skills/rsp-manage/SKILL.md +0 -101
- package/skills/rsp-manage/references/closeout.md +0 -13
- package/skills/rsp-manage/references/delegation.md +0 -47
- package/skills/rsp-manage/references/interruption-recovery.md +0 -19
- package/skills/rsp-manage/references/review-convergence.md +0 -9
- package/skills/rsp-release-docs/SKILL.md +0 -77
- package/skills/rsp-release-docs/references/convention-discovery.md +0 -65
- package/skills/rsp-release-docs/references/evidence-and-surfaces.md +0 -21
- package/skills/rsp-release-docs/references/output-contracts.md +0 -97
- package/skills/rsp-release-docs/references/publication-lifecycle.md +0 -28
- package/skills/rsp-resolve-findings/SKILL.md +0 -61
- package/skills/rsp-structural-audit/SKILL.md +0 -71
- package/skills/rsp-structural-audit/references/structural-lenses.md +0 -41
- package/skills/rsp-tdd/SKILL.md +0 -56
|
@@ -5,10 +5,191 @@ import { readFile } from "fs/promises";
|
|
|
5
5
|
import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
import pc from "picocolors";
|
|
8
|
-
import {
|
|
8
|
+
import { stringify } from "yaml";
|
|
9
9
|
|
|
10
|
-
// src/core/
|
|
11
|
-
import {
|
|
10
|
+
// src/core/markdown.mjs
|
|
11
|
+
import { fromHtml } from "hast-util-from-html";
|
|
12
|
+
import { fromMarkdown } from "mdast-util-from-markdown";
|
|
13
|
+
import { frontmatterFromMarkdown } from "mdast-util-frontmatter";
|
|
14
|
+
import { gfmFromMarkdown } from "mdast-util-gfm";
|
|
15
|
+
import { frontmatter } from "micromark-extension-frontmatter";
|
|
16
|
+
import { gfm } from "micromark-extension-gfm";
|
|
17
|
+
var htmlTokenAnalyses = /* @__PURE__ */ new WeakMap();
|
|
18
|
+
var matters = ["yaml", { type: "yaml", fence: { open: "---", close: "..." } }];
|
|
19
|
+
var extensions = [gfm(), frontmatter(matters)];
|
|
20
|
+
var mdastExtensions = [gfmFromMarkdown(), frontmatterFromMarkdown(matters)];
|
|
21
|
+
function parseMarkdown(input, options = {}) {
|
|
22
|
+
if (typeof input !== "string")
|
|
23
|
+
return input;
|
|
24
|
+
const syntax = options.frontmatter === false ? { extensions: [gfm()], mdastExtensions: [gfmFromMarkdown()] } : { extensions, mdastExtensions };
|
|
25
|
+
return { source: input, root: fromMarkdown(input, syntax), start: 0, end: input.length };
|
|
26
|
+
}
|
|
27
|
+
function walkMarkdown(node, visit) {
|
|
28
|
+
if (visit(node) === false)
|
|
29
|
+
return;
|
|
30
|
+
if ("children" in node) {
|
|
31
|
+
for (const child of node.children)
|
|
32
|
+
walkMarkdown(child, visit);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function markdownNodeSource(input, node) {
|
|
36
|
+
const { source } = parseMarkdown(input);
|
|
37
|
+
return source.slice(node.position?.start.offset ?? 0, node.position?.end.offset ?? 0);
|
|
38
|
+
}
|
|
39
|
+
function isMarkdownComment(node) {
|
|
40
|
+
return node.type === "html" && !readHtmlToken(node, node.value).maskedSource.trim();
|
|
41
|
+
}
|
|
42
|
+
function htmlComments(value) {
|
|
43
|
+
if (!value.includes("<!--"))
|
|
44
|
+
return [];
|
|
45
|
+
const comments = [];
|
|
46
|
+
function visit(node, topLevel) {
|
|
47
|
+
if (node.type === "comment" && node.position)
|
|
48
|
+
comments.push({ start: node.position.start.offset ?? 0, end: node.position.end.offset ?? value.length, topLevel });
|
|
49
|
+
if ("children" in node)
|
|
50
|
+
node.children.forEach((child) => visit(child, node.type === "root"));
|
|
51
|
+
}
|
|
52
|
+
visit(fromHtml(value, { fragment: true }), false);
|
|
53
|
+
return comments;
|
|
54
|
+
}
|
|
55
|
+
function readHtmlToken(node, value) {
|
|
56
|
+
let analyses = htmlTokenAnalyses.get(node);
|
|
57
|
+
const cached = analyses?.get(value);
|
|
58
|
+
if (cached)
|
|
59
|
+
return cached;
|
|
60
|
+
const comments = htmlComments(value);
|
|
61
|
+
const analysis = {
|
|
62
|
+
comments,
|
|
63
|
+
maskedSource: maskSourceRanges(value, comments.map((comment) => [comment.start, comment.end]), 0, value.length)
|
|
64
|
+
};
|
|
65
|
+
if (!analyses) {
|
|
66
|
+
analyses = /* @__PURE__ */ new Map();
|
|
67
|
+
htmlTokenAnalyses.set(node, analyses);
|
|
68
|
+
}
|
|
69
|
+
analyses.set(value, analysis);
|
|
70
|
+
return analysis;
|
|
71
|
+
}
|
|
72
|
+
function markdownTopLevelComments(input) {
|
|
73
|
+
const document = parseMarkdown(input);
|
|
74
|
+
return document.root.children.flatMap((node) => {
|
|
75
|
+
if (node.type !== "html")
|
|
76
|
+
return [];
|
|
77
|
+
const source = markdownNodeSource(document, node);
|
|
78
|
+
const offset = node.position?.start.offset ?? document.start;
|
|
79
|
+
return readHtmlToken(node, source).comments.filter((comment) => comment.topLevel).map((comment) => ({
|
|
80
|
+
start: offset + comment.start,
|
|
81
|
+
end: offset + comment.end,
|
|
82
|
+
source: source.slice(comment.start, comment.end)
|
|
83
|
+
}));
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function maskSourceRanges(source, ranges, start, end) {
|
|
87
|
+
let result = "";
|
|
88
|
+
let offset = start;
|
|
89
|
+
for (const [from, until] of ranges.sort((a, b) => a[0] - b[0])) {
|
|
90
|
+
result += source.slice(offset, from);
|
|
91
|
+
result += source.slice(from, until).replace(/[^\r\n]/g, " ");
|
|
92
|
+
offset = until;
|
|
93
|
+
}
|
|
94
|
+
return result + source.slice(offset, end);
|
|
95
|
+
}
|
|
96
|
+
function markdownText(node) {
|
|
97
|
+
if (node.type === "yaml")
|
|
98
|
+
return "";
|
|
99
|
+
if (node.type === "html")
|
|
100
|
+
return readHtmlToken(node, node.value).maskedSource;
|
|
101
|
+
if (node.type === "image" || node.type === "imageReference")
|
|
102
|
+
return node.alt ?? "";
|
|
103
|
+
if ("value" in node)
|
|
104
|
+
return node.value;
|
|
105
|
+
if ("children" in node) {
|
|
106
|
+
const separator = ["root", "list", "listItem", "blockquote"].includes(node.type) ? "\n" : "";
|
|
107
|
+
return node.children.map((child) => markdownText(child)).filter(Boolean).join(separator);
|
|
108
|
+
}
|
|
109
|
+
return node.type === "break" ? "\n" : "";
|
|
110
|
+
}
|
|
111
|
+
function markdownSection(document, nodes, start, end) {
|
|
112
|
+
return { source: document.source, root: { type: "root", children: nodes }, start, end };
|
|
113
|
+
}
|
|
114
|
+
function visibleMarkdownSource(input, options = {}) {
|
|
115
|
+
const document = parseMarkdown(input);
|
|
116
|
+
const visibleNodes = /* @__PURE__ */ new Set();
|
|
117
|
+
function markVisibleContent(node) {
|
|
118
|
+
if (node.type === "yaml" || node.type === "code" && !options.includeCode)
|
|
119
|
+
return false;
|
|
120
|
+
let visible = false;
|
|
121
|
+
if (node.type === "image" || node.type === "imageReference" || node.type === "footnoteReference")
|
|
122
|
+
visible = true;
|
|
123
|
+
else if (node.type === "html")
|
|
124
|
+
visible = !isMarkdownComment(node);
|
|
125
|
+
else if ("value" in node)
|
|
126
|
+
visible = Boolean(node.value.trim());
|
|
127
|
+
else if ("children" in node)
|
|
128
|
+
visible = node.children.map(markVisibleContent).some(Boolean);
|
|
129
|
+
if (visible)
|
|
130
|
+
visibleNodes.add(node);
|
|
131
|
+
return visible;
|
|
132
|
+
}
|
|
133
|
+
markVisibleContent(document.root);
|
|
134
|
+
const ranges = [];
|
|
135
|
+
walkMarkdown(document.root, (node) => {
|
|
136
|
+
if (!visibleNodes.has(node)) {
|
|
137
|
+
const start = Math.max(document.start, node.position?.start.offset ?? document.start);
|
|
138
|
+
const end = Math.min(document.end, node.position?.end.offset ?? document.end);
|
|
139
|
+
if (end > start)
|
|
140
|
+
ranges.push([start, end]);
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
if (node.type === "html") {
|
|
144
|
+
const nodeStart = node.position?.start.offset ?? document.start;
|
|
145
|
+
for (const comment of readHtmlToken(node, markdownNodeSource(document, node)).comments)
|
|
146
|
+
ranges.push([nodeStart + comment.start, nodeStart + comment.end]);
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
return maskSourceRanges(document.source, ranges, document.start, document.end);
|
|
151
|
+
}
|
|
152
|
+
function visibleMarkdownLines(input) {
|
|
153
|
+
return visibleMarkdownSource(input).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
154
|
+
}
|
|
155
|
+
function markdownListItems(input) {
|
|
156
|
+
const document = parseMarkdown(input);
|
|
157
|
+
const items = [];
|
|
158
|
+
walkMarkdown(document.root, (node) => {
|
|
159
|
+
if (node.type === "blockquote" || node.type === "footnoteDefinition")
|
|
160
|
+
return false;
|
|
161
|
+
if (node.type === "listItem") {
|
|
162
|
+
const source = markdownNodeSource(document, node);
|
|
163
|
+
items.push({ node, line: source.split(/\r?\n/, 1)[0].trim() });
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
return items;
|
|
167
|
+
}
|
|
168
|
+
function markdownFrontmatter(input) {
|
|
169
|
+
const document = parseMarkdown(input);
|
|
170
|
+
const node = document.root.children[0];
|
|
171
|
+
if (node?.type !== "yaml" || node.position?.start.offset !== 0)
|
|
172
|
+
return null;
|
|
173
|
+
const raw = markdownNodeSource(document, node);
|
|
174
|
+
const lines = raw.split(/\r?\n/);
|
|
175
|
+
if (lines[0] !== "---" || !["---", "..."].includes(lines.at(-1) ?? ""))
|
|
176
|
+
return null;
|
|
177
|
+
let end = node.position.end.offset ?? raw.length;
|
|
178
|
+
if (document.source.slice(end, end + 2) === "\r\n")
|
|
179
|
+
end += 2;
|
|
180
|
+
else if (document.source[end] === "\n")
|
|
181
|
+
end++;
|
|
182
|
+
return { node, value: node.value, end };
|
|
183
|
+
}
|
|
184
|
+
function markdownProse(input) {
|
|
185
|
+
const document = parseMarkdown(input);
|
|
186
|
+
const fragments = [];
|
|
187
|
+
walkMarkdown(document.root, (node) => {
|
|
188
|
+
if (node.type === "text" || node.type === "html")
|
|
189
|
+
fragments.push({ value: markdownText(node), line: node.position?.start.line ?? 1 });
|
|
190
|
+
});
|
|
191
|
+
return fragments;
|
|
192
|
+
}
|
|
12
193
|
|
|
13
194
|
// src/core/document-model.ts
|
|
14
195
|
var CHANGE_DOCUMENT_SCHEMA = {
|
|
@@ -44,9 +225,11 @@ var DocumentSectionCardinalityError = class extends Error {
|
|
|
44
225
|
sectionId;
|
|
45
226
|
count;
|
|
46
227
|
};
|
|
47
|
-
function parseRspDocument(
|
|
228
|
+
function parseRspDocument(input, schema) {
|
|
229
|
+
const markdown = parseMarkdown(input);
|
|
230
|
+
const source = markdown.source;
|
|
48
231
|
const definitionsByHeading = new Map(schema.sections.map((definition) => [definition.heading, definition]));
|
|
49
|
-
const observedHeadings = collectLevelTwoHeadings(
|
|
232
|
+
const observedHeadings = collectLevelTwoHeadings(markdown);
|
|
50
233
|
const sections = /* @__PURE__ */ new Map();
|
|
51
234
|
const unknownHeadings = [];
|
|
52
235
|
for (let index = 0; index < observedHeadings.length; index++) {
|
|
@@ -62,11 +245,8 @@ function parseRspDocument(source, schema) {
|
|
|
62
245
|
id: definition.id,
|
|
63
246
|
heading: definition.heading,
|
|
64
247
|
canonical: observed.canonical,
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
headingEnd: observed.end,
|
|
68
|
-
bodyStart: observed.end,
|
|
69
|
-
bodyEnd
|
|
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)
|
|
70
250
|
};
|
|
71
251
|
const matches = sections.get(definition.id) ?? [];
|
|
72
252
|
matches.push(section);
|
|
@@ -74,10 +254,11 @@ function parseRspDocument(source, schema) {
|
|
|
74
254
|
}
|
|
75
255
|
const missingSections = schema.sections.filter((definition) => !sections.has(definition.id)).map((definition) => definition.id);
|
|
76
256
|
const duplicateSections = schema.sections.filter((definition) => (sections.get(definition.id)?.length ?? 0) > 1).map((definition) => definition.id);
|
|
77
|
-
const titleOccurrences = collectDocumentTitles(
|
|
257
|
+
const titleOccurrences = collectDocumentTitles(markdown, schema.titlePrefix);
|
|
78
258
|
const titles = titleOccurrences.map((title) => title.value);
|
|
79
259
|
return {
|
|
80
260
|
source,
|
|
261
|
+
markdown,
|
|
81
262
|
schema,
|
|
82
263
|
title: titles[0] ?? null,
|
|
83
264
|
titles,
|
|
@@ -102,8 +283,8 @@ function getDocumentSections(document, sectionId) {
|
|
|
102
283
|
function getDocumentSection(document, sectionId) {
|
|
103
284
|
return getDocumentSections(document, sectionId)[0];
|
|
104
285
|
}
|
|
105
|
-
function
|
|
106
|
-
return getDocumentSection(document, sectionId)?.
|
|
286
|
+
function getDocumentSectionMarkdown(document, sectionId) {
|
|
287
|
+
return getDocumentSection(document, sectionId)?.markdown ?? markdownSection(document.markdown, [], 0, 0);
|
|
107
288
|
}
|
|
108
289
|
function getDocumentSectionDefinitionByHeading(schema, heading) {
|
|
109
290
|
return schema.sections.find((candidate) => candidate.heading === heading);
|
|
@@ -130,24 +311,33 @@ function appendDocumentSectionItem(document, sectionId, item) {
|
|
|
130
311
|
return `${before}${separator}${item}
|
|
131
312
|
${document.source.slice(insertAt)}`;
|
|
132
313
|
}
|
|
133
|
-
function collectLevelTwoHeadings(
|
|
314
|
+
function collectLevelTwoHeadings(document) {
|
|
134
315
|
const headings = [];
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
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();
|
|
139
324
|
headings.push({
|
|
140
325
|
heading,
|
|
141
|
-
canonical: heading ===
|
|
142
|
-
start:
|
|
143
|
-
end:
|
|
326
|
+
canonical: heading === value,
|
|
327
|
+
start: node.position.start.offset,
|
|
328
|
+
end: node.position.end.offset
|
|
144
329
|
});
|
|
145
330
|
}
|
|
146
331
|
return headings;
|
|
147
332
|
}
|
|
148
|
-
function collectDocumentTitles(
|
|
333
|
+
function collectDocumentTitles(document, prefix) {
|
|
149
334
|
const titles = [];
|
|
150
|
-
for (const
|
|
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);
|
|
151
341
|
const trimmed = line.trim();
|
|
152
342
|
if (!trimmed.startsWith(prefix))
|
|
153
343
|
continue;
|
|
@@ -166,110 +356,185 @@ function lineBreakStart(source, headingStart) {
|
|
|
166
356
|
return headingStart;
|
|
167
357
|
}
|
|
168
358
|
|
|
169
|
-
// src/core/
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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)
|
|
173
370
|
return {};
|
|
174
|
-
if (typeof
|
|
371
|
+
if (typeof value !== "object" || Array.isArray(value))
|
|
175
372
|
throw new Error("YAML document must be a mapping/object");
|
|
176
|
-
return
|
|
373
|
+
return value;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/core/content.ts
|
|
377
|
+
function parseYamlText(text) {
|
|
378
|
+
return yamlMapping(parseYamlDocument(text));
|
|
177
379
|
}
|
|
178
380
|
function parseFrontmatter(content) {
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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;
|
|
183
407
|
}
|
|
184
408
|
function countCheckboxes(content) {
|
|
185
|
-
|
|
186
|
-
const progress = (content.match(/\[\/\]/g) || []).length;
|
|
187
|
-
const done = (content.match(/\[x\]/g) || []).length;
|
|
188
|
-
const dropped = (content.match(/\[-\]/g) || []).length;
|
|
189
|
-
return { todo, progress, done, dropped, total: todo + progress + done + dropped };
|
|
190
|
-
}
|
|
191
|
-
function classifyVerifyCheckboxes(sectionText) {
|
|
192
|
-
const buckets = collectVerifyCheckboxLines(sectionText);
|
|
193
|
-
return {
|
|
194
|
-
required: countCheckboxes([...buckets.required, ...buckets.unclassified].join("\n")),
|
|
195
|
-
optional: countCheckboxes(buckets.optional.join("\n")),
|
|
196
|
-
unclassified: countCheckboxes(buckets.unclassified.join("\n")),
|
|
197
|
-
legacy: buckets.legacy
|
|
198
|
-
};
|
|
409
|
+
return countCheckboxItems(collectCheckboxItems(content));
|
|
199
410
|
}
|
|
200
|
-
function
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
const unclassified = [];
|
|
411
|
+
function collectVerifyCheckboxes(content) {
|
|
412
|
+
const document = parseMarkdown(content);
|
|
413
|
+
const buckets = { required: [], optional: [], unclassified: [] };
|
|
204
414
|
let current = "unclassified";
|
|
205
415
|
let sawClassificationHeading = false;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
+
}
|
|
212
425
|
}
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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);
|
|
223
445
|
}
|
|
224
|
-
function getOpenCheckboxes(
|
|
225
|
-
return
|
|
446
|
+
function getOpenCheckboxes(content) {
|
|
447
|
+
return openCheckboxLines(collectCheckboxItems(content));
|
|
226
448
|
}
|
|
227
|
-
function
|
|
228
|
-
|
|
229
|
-
return normalizeBlockerBody(getDocumentSectionBody(document, "blockers"));
|
|
449
|
+
function changeDocument(content) {
|
|
450
|
+
return typeof content === "string" ? parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA) : content;
|
|
230
451
|
}
|
|
231
452
|
function hasMeaningfulBlockerBody(body) {
|
|
232
|
-
return
|
|
453
|
+
return readBlockerEntries(body).length > 0;
|
|
233
454
|
}
|
|
234
|
-
function
|
|
235
|
-
return body
|
|
455
|
+
function getVisibleBodyLines(body) {
|
|
456
|
+
return visibleMarkdownLines(body);
|
|
236
457
|
}
|
|
237
458
|
function hasMeaningfulBlockers(content) {
|
|
238
|
-
return
|
|
459
|
+
return hasMeaningfulBlockerBody(getDocumentSectionMarkdown(changeDocument(content), "blockers"));
|
|
239
460
|
}
|
|
240
461
|
function isEmptyBlockerLine(line) {
|
|
241
462
|
const normalized = line.trim();
|
|
242
463
|
return /^none[.。]?$/i.test(normalized) || /^[-*]\s*(?:none[.。]?)?$/i.test(normalized);
|
|
243
464
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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;
|
|
248
510
|
}
|
|
249
511
|
function detectDeltaSections(content) {
|
|
250
|
-
const
|
|
251
|
-
const
|
|
252
|
-
return {
|
|
253
|
-
added: /^###\s*ADDED/im.test(body),
|
|
254
|
-
modified: /^###\s*MODIFIED/im.test(body),
|
|
255
|
-
removed: /^###\s*REMOVED/im.test(body)
|
|
256
|
-
};
|
|
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") };
|
|
257
515
|
}
|
|
258
516
|
function parseScenarios(content) {
|
|
517
|
+
const document = parseMarkdown(content);
|
|
259
518
|
const scenarios = [];
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
if (
|
|
270
|
-
|
|
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
|
+
}
|
|
271
536
|
}
|
|
272
|
-
return scenarios;
|
|
537
|
+
return scenarios.filter((item) => item.steps.length > 0);
|
|
273
538
|
}
|
|
274
539
|
|
|
275
540
|
// src/core/decisions.ts
|
|
@@ -385,11 +650,11 @@ async function inspectManagedFileTree(root, label, options = {}) {
|
|
|
385
650
|
if (!rootInspection.exists)
|
|
386
651
|
return result;
|
|
387
652
|
result.rootExists = true;
|
|
388
|
-
await collectManagedFiles(root, label, result);
|
|
653
|
+
await collectManagedFiles(root, label, result, options.includeHidden);
|
|
389
654
|
result.files.sort();
|
|
390
655
|
return result;
|
|
391
656
|
}
|
|
392
|
-
async function collectManagedFiles(directory, label, result) {
|
|
657
|
+
async function collectManagedFiles(directory, label, result, includeHidden = false) {
|
|
393
658
|
let entries;
|
|
394
659
|
try {
|
|
395
660
|
entries = await readdir(directory, { withFileTypes: true });
|
|
@@ -399,11 +664,11 @@ async function collectManagedFiles(directory, label, result) {
|
|
|
399
664
|
return;
|
|
400
665
|
}
|
|
401
666
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
402
|
-
if (entry.name.startsWith("."))
|
|
667
|
+
if (!includeHidden && entry.name.startsWith("."))
|
|
403
668
|
continue;
|
|
404
669
|
const path = join(directory, entry.name);
|
|
405
670
|
if (entry.isDirectory()) {
|
|
406
|
-
await collectManagedFiles(path, label, result);
|
|
671
|
+
await collectManagedFiles(path, label, result, includeHidden);
|
|
407
672
|
continue;
|
|
408
673
|
}
|
|
409
674
|
if (entry.isFile()) {
|
|
@@ -448,21 +713,6 @@ function resolveDecisionRecordsPath(config) {
|
|
|
448
713
|
const configured = config.decisions?.path;
|
|
449
714
|
return configured && validateDecisionRecordsPath(configured) === null ? normalizeDecisionRecordsPath(configured) : DEFAULT_DECISION_RECORDS_PATH;
|
|
450
715
|
}
|
|
451
|
-
async function ensureDecisionRecordsDirectory(pathValue) {
|
|
452
|
-
const filesystemError = await validateDecisionRecordsFilesystemPath(pathValue);
|
|
453
|
-
if (filesystemError)
|
|
454
|
-
throw new Error(filesystemError);
|
|
455
|
-
const existed = existsSync(pathValue);
|
|
456
|
-
await mkdir(pathValue, { recursive: true });
|
|
457
|
-
const entries = await readdir2(pathValue);
|
|
458
|
-
if (entries.length === 0) {
|
|
459
|
-
await ensureManagedFile(join2(pathValue, ".gitkeep"), "", "Decision Record placeholder");
|
|
460
|
-
return true;
|
|
461
|
-
}
|
|
462
|
-
if (entries.includes(".gitkeep"))
|
|
463
|
-
requireManagedFile(join2(pathValue, ".gitkeep"), "Decision Record placeholder");
|
|
464
|
-
return !existed;
|
|
465
|
-
}
|
|
466
716
|
async function validateDecisionRecordsFilesystemPath(pathValue, cwd = process.cwd()) {
|
|
467
717
|
const projectRoot = await realpath(cwd);
|
|
468
718
|
const targetPath = resolve(projectRoot, pathValue);
|
|
@@ -496,18 +746,18 @@ var LOCK_PATH = join3(RSP_DIR, ".lock");
|
|
|
496
746
|
var CONFIG_PATH = join3(RSP_DIR, "config.yaml");
|
|
497
747
|
var VALID_KINDS = ["feature", "fix", "refactor", "docs", "ops", "research"];
|
|
498
748
|
var DEFAULT_REQUIRED_SECTIONS = getCanonicalSectionHeadings(CHANGE_DOCUMENT_SCHEMA);
|
|
499
|
-
var
|
|
500
|
-
var
|
|
749
|
+
var DEFAULT_WORKFLOW_POLICY = { delegation: "off", closeout: "ask" };
|
|
750
|
+
var INVALID_CONFIG_WORKFLOW_POLICY = { delegation: "off", closeout: "manual" };
|
|
501
751
|
var CONFIG_DEFAULTS = {
|
|
502
752
|
kinds: [],
|
|
503
753
|
decisions: { path: ".rsp/specs/decisions" },
|
|
504
754
|
language: { default: "en" },
|
|
505
|
-
|
|
755
|
+
workflow: { delegation: "off", closeout: "ask" }
|
|
506
756
|
};
|
|
507
757
|
function generateConfigTemplate(config = {}) {
|
|
508
758
|
const kinds = config.kinds ?? CONFIG_DEFAULTS.kinds;
|
|
509
759
|
const decisions = { ...CONFIG_DEFAULTS.decisions, ...config.decisions };
|
|
510
|
-
const
|
|
760
|
+
const workflow = { ...CONFIG_DEFAULTS.workflow, ...config.workflow };
|
|
511
761
|
const language = { ...CONFIG_DEFAULTS.language, ...config.language };
|
|
512
762
|
const kindsYaml = stringify(kinds).trimEnd();
|
|
513
763
|
const renderedKinds = kinds.length === 0 ? `kinds: ${kindsYaml}` : `kinds:
|
|
@@ -528,12 +778,12 @@ ${renderedKinds}
|
|
|
528
778
|
decisions:
|
|
529
779
|
path: ${stringify(decisions.path).trim()}
|
|
530
780
|
|
|
531
|
-
#
|
|
532
|
-
#
|
|
533
|
-
#
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
closeout: ${
|
|
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}
|
|
537
787
|
|
|
538
788
|
# Set one shared project default for durable artifact and commit prose.
|
|
539
789
|
# Response language remains user/session-owned and is never read from this project mapping.
|
|
@@ -543,102 +793,8 @@ ${artifactsLine}
|
|
|
543
793
|
${commitLine}
|
|
544
794
|
`;
|
|
545
795
|
}
|
|
546
|
-
var
|
|
547
|
-
var
|
|
548
|
-
function reconcileRspConfigDefaults(raw) {
|
|
549
|
-
const document = parseDocument(raw);
|
|
550
|
-
if (document.errors.length > 0)
|
|
551
|
-
throw new Error(document.errors.map((error) => error.message).join("; "));
|
|
552
|
-
const parsed = parseYamlText(raw);
|
|
553
|
-
const issues = validateRspConfig(parsed);
|
|
554
|
-
if (issues.length > 0)
|
|
555
|
-
throw new Error(issues.join("; "));
|
|
556
|
-
const added = collectMissingConfigDefaults(parsed);
|
|
557
|
-
const customComments = hasCustomConfigComments(document, parsed);
|
|
558
|
-
if (customComments) {
|
|
559
|
-
backfillConfigDocument(document, parsed);
|
|
560
|
-
}
|
|
561
|
-
if (customComments && added.length === 0)
|
|
562
|
-
return { content: raw, added, changed: false };
|
|
563
|
-
const content = customComments ? String(document) : generateConfigTemplate(parsed);
|
|
564
|
-
return { content, added, changed: content !== raw };
|
|
565
|
-
}
|
|
566
|
-
function collectMissingConfigDefaults(parsed) {
|
|
567
|
-
const added = [];
|
|
568
|
-
if (!("kinds" in parsed))
|
|
569
|
-
added.push("kinds");
|
|
570
|
-
if (!("decisions" in parsed) || !hasNestedKey(parsed.decisions, "path"))
|
|
571
|
-
added.push("decisions.path");
|
|
572
|
-
if (!("language" in parsed))
|
|
573
|
-
added.push("language.default");
|
|
574
|
-
if (!("manage" in parsed)) {
|
|
575
|
-
added.push("manage.activation", "manage.closeout");
|
|
576
|
-
} else {
|
|
577
|
-
if (!hasNestedKey(parsed.manage, "activation"))
|
|
578
|
-
added.push("manage.activation");
|
|
579
|
-
if (!hasNestedKey(parsed.manage, "closeout"))
|
|
580
|
-
added.push("manage.closeout");
|
|
581
|
-
}
|
|
582
|
-
return added;
|
|
583
|
-
}
|
|
584
|
-
function backfillConfigDocument(document, parsed) {
|
|
585
|
-
if (!("kinds" in parsed))
|
|
586
|
-
document.set("kinds", [...CONFIG_DEFAULTS.kinds]);
|
|
587
|
-
if (!("decisions" in parsed))
|
|
588
|
-
document.set("decisions", { path: CONFIG_DEFAULTS.decisions.path });
|
|
589
|
-
else if (!hasNestedKey(parsed.decisions, "path"))
|
|
590
|
-
document.setIn(["decisions", "path"], CONFIG_DEFAULTS.decisions.path);
|
|
591
|
-
if (!("language" in parsed))
|
|
592
|
-
document.set("language", { default: CONFIG_DEFAULTS.language.default });
|
|
593
|
-
if (!("manage" in parsed)) {
|
|
594
|
-
document.set("manage", { ...CONFIG_DEFAULTS.manage });
|
|
595
|
-
} else {
|
|
596
|
-
if (!hasNestedKey(parsed.manage, "activation"))
|
|
597
|
-
document.setIn(["manage", "activation"], CONFIG_DEFAULTS.manage.activation);
|
|
598
|
-
if (!hasNestedKey(parsed.manage, "closeout"))
|
|
599
|
-
document.setIn(["manage", "closeout"], CONFIG_DEFAULTS.manage.closeout);
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
function hasNestedKey(value, key) {
|
|
603
|
-
return Boolean(value && typeof value === "object" && !Array.isArray(value) && key in value);
|
|
604
|
-
}
|
|
605
|
-
var LEGACY_GENERATED_CONFIG_COMMENTS = /* @__PURE__ */ new Set([
|
|
606
|
-
"Omit kinds or use [] to retain the built-in defaults.",
|
|
607
|
-
"New projects default to automatic Manage routing with lifecycle-only closeout.",
|
|
608
|
-
"Self-host automatic Manage routing with bounded deterministic local commits; push and publication stay explicit.",
|
|
609
|
-
"Set exactly one project-relative authoritative directory when the Host Project already owns Decision Records elsewhere.",
|
|
610
|
-
"kinds:",
|
|
611
|
-
"decisions:",
|
|
612
|
-
"path: docs/adr",
|
|
613
|
-
...VALID_KINDS.map((kind) => `- ${kind}`)
|
|
614
|
-
]);
|
|
615
|
-
function hasCustomConfigComments(document, parsed) {
|
|
616
|
-
const generated = new Set(collectConfigComments(parseDocument(generateConfigTemplate(parsed))));
|
|
617
|
-
for (const comment of LEGACY_GENERATED_CONFIG_COMMENTS)
|
|
618
|
-
generated.add(comment);
|
|
619
|
-
return collectConfigComments(document).some((comment) => !generated.has(comment));
|
|
620
|
-
}
|
|
621
|
-
function collectConfigComments(document) {
|
|
622
|
-
const comments = [];
|
|
623
|
-
const append = (value) => {
|
|
624
|
-
if (!value)
|
|
625
|
-
return;
|
|
626
|
-
for (const line of value.split("\n")) {
|
|
627
|
-
const comment = line.trim();
|
|
628
|
-
if (comment)
|
|
629
|
-
comments.push(comment);
|
|
630
|
-
}
|
|
631
|
-
};
|
|
632
|
-
append(document.commentBefore);
|
|
633
|
-
append(document.comment);
|
|
634
|
-
visit(document, {
|
|
635
|
-
Node(_key, node) {
|
|
636
|
-
append(node.commentBefore);
|
|
637
|
-
append(node.comment);
|
|
638
|
-
}
|
|
639
|
-
});
|
|
640
|
-
return comments;
|
|
641
|
-
}
|
|
796
|
+
var DELEGATION_POLICIES = ["off", "auto"];
|
|
797
|
+
var CLOSEOUT_POLICIES = ["ask", "auto", "manual"];
|
|
642
798
|
var _configCache = null;
|
|
643
799
|
async function loadRspConfig() {
|
|
644
800
|
const inspection = await inspectRspConfig();
|
|
@@ -664,17 +820,17 @@ async function inspectRspConfig(cwd = process.cwd()) {
|
|
|
664
820
|
const issues = validateRspConfig(parsed);
|
|
665
821
|
const decisions = parsed.decisions && typeof parsed.decisions === "object" && !Array.isArray(parsed.decisions) ? parsed.decisions : void 0;
|
|
666
822
|
const decisionPath = decisions?.path;
|
|
667
|
-
const
|
|
823
|
+
const workflowValue = parsed.workflow && typeof parsed.workflow === "object" && !Array.isArray(parsed.workflow) ? parsed.workflow : void 0;
|
|
668
824
|
const languageValue = parsed.language && typeof parsed.language === "object" && !Array.isArray(parsed.language) ? parsed.language : void 0;
|
|
669
|
-
const
|
|
670
|
-
const closeout =
|
|
825
|
+
const delegation = DELEGATION_POLICIES.includes(workflowValue?.delegation) ? workflowValue?.delegation : void 0;
|
|
826
|
+
const closeout = CLOSEOUT_POLICIES.includes(workflowValue?.closeout) ? workflowValue?.closeout : void 0;
|
|
671
827
|
const kinds = Array.isArray(parsed.kinds) && parsed.kinds.every((value) => typeof value === "string" && value.trim() !== "") ? parsed.kinds.map((value) => value.trim()) : void 0;
|
|
672
828
|
const decisionRecordsIssue = getDecisionRecordsConfigIssue(parsed);
|
|
673
829
|
const inspection = {
|
|
674
830
|
config: {
|
|
675
831
|
kinds,
|
|
676
832
|
decisions: decisionRecordsIssue === null && validateDecisionRecordsPath(decisionPath) === null ? { path: normalizeDecisionRecordsPath(String(decisionPath)) } : void 0,
|
|
677
|
-
|
|
833
|
+
workflow: workflowValue ? { delegation, closeout } : void 0,
|
|
678
834
|
language: parseLanguageConfig(languageValue)
|
|
679
835
|
},
|
|
680
836
|
issues
|
|
@@ -684,9 +840,11 @@ async function inspectRspConfig(cwd = process.cwd()) {
|
|
|
684
840
|
}
|
|
685
841
|
function validateRspConfig(parsed) {
|
|
686
842
|
const issues = [];
|
|
687
|
-
const supported = /* @__PURE__ */ new Set(["kinds", "decisions", "
|
|
843
|
+
const supported = /* @__PURE__ */ new Set(["kinds", "decisions", "workflow", "language"]);
|
|
688
844
|
for (const key of Object.keys(parsed)) {
|
|
689
|
-
if (key === "
|
|
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") {
|
|
690
848
|
issues.push('config.yaml field "required_sections" is no longer supported');
|
|
691
849
|
} else if (!supported.has(key)) {
|
|
692
850
|
issues.push(`config.yaml field "${key}" is not supported`);
|
|
@@ -707,20 +865,20 @@ function validateRspConfig(parsed) {
|
|
|
707
865
|
const decisionRecordsIssue = getDecisionRecordsConfigIssue(parsed);
|
|
708
866
|
if (decisionRecordsIssue)
|
|
709
867
|
issues.push(decisionRecordsIssue);
|
|
710
|
-
if ("
|
|
711
|
-
if (!parsed.
|
|
712
|
-
issues.push('config.yaml field "
|
|
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');
|
|
713
871
|
} else {
|
|
714
|
-
const
|
|
715
|
-
const
|
|
716
|
-
for (const key of Object.keys(
|
|
717
|
-
if (!
|
|
718
|
-
issues.push(`config.yaml field "
|
|
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`);
|
|
719
877
|
}
|
|
720
|
-
if ("
|
|
721
|
-
issues.push('config.yaml field "
|
|
722
|
-
if ("closeout" in
|
|
723
|
-
issues.push('config.yaml field "
|
|
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');
|
|
724
882
|
}
|
|
725
883
|
}
|
|
726
884
|
if ("language" in parsed) {
|
|
@@ -752,12 +910,12 @@ function resolveKinds(config) {
|
|
|
752
910
|
function resolveRequiredSections(_config) {
|
|
753
911
|
return [...DEFAULT_REQUIRED_SECTIONS];
|
|
754
912
|
}
|
|
755
|
-
function
|
|
913
|
+
function resolveWorkflowPolicy(config, options = {}) {
|
|
756
914
|
if (options.configValid === false)
|
|
757
|
-
return { ...
|
|
915
|
+
return { ...INVALID_CONFIG_WORKFLOW_POLICY };
|
|
758
916
|
return {
|
|
759
|
-
|
|
760
|
-
closeout: config.
|
|
917
|
+
delegation: config.workflow?.delegation ?? DEFAULT_WORKFLOW_POLICY.delegation,
|
|
918
|
+
closeout: config.workflow?.closeout ?? DEFAULT_WORKFLOW_POLICY.closeout
|
|
761
919
|
};
|
|
762
920
|
}
|
|
763
921
|
function resolveLanguagePolicy(config, options = {}) {
|
|
@@ -796,18 +954,15 @@ async function getVersion() {
|
|
|
796
954
|
return version;
|
|
797
955
|
}
|
|
798
956
|
|
|
799
|
-
// src/core/output.ts
|
|
800
|
-
import pc2 from "picocolors";
|
|
801
|
-
function toErrorMessage(error) {
|
|
802
|
-
return error instanceof Error ? error.message : String(error);
|
|
803
|
-
}
|
|
804
|
-
function emitJson(data, options = {}) {
|
|
805
|
-
const indentation = options.compact ? void 0 : 2;
|
|
806
|
-
process.stdout.write(`${JSON.stringify(data, null, indentation)}
|
|
807
|
-
`);
|
|
808
|
-
}
|
|
809
|
-
|
|
810
957
|
export {
|
|
958
|
+
parseMarkdown,
|
|
959
|
+
walkMarkdown,
|
|
960
|
+
isMarkdownComment,
|
|
961
|
+
markdownTopLevelComments,
|
|
962
|
+
markdownText,
|
|
963
|
+
visibleMarkdownSource,
|
|
964
|
+
markdownListItems,
|
|
965
|
+
markdownProse,
|
|
811
966
|
CHANGE_DOCUMENT_SCHEMA,
|
|
812
967
|
GROUP_BRIEF_DOCUMENT_SCHEMA,
|
|
813
968
|
DocumentSectionCardinalityError,
|
|
@@ -815,7 +970,7 @@ export {
|
|
|
815
970
|
getDocumentTitles,
|
|
816
971
|
getDocumentTitle,
|
|
817
972
|
getDocumentSections,
|
|
818
|
-
|
|
973
|
+
getDocumentSectionMarkdown,
|
|
819
974
|
getDocumentSectionDefinitionByHeading,
|
|
820
975
|
getCanonicalSectionHeadings,
|
|
821
976
|
renderDocumentSectionHeading,
|
|
@@ -823,13 +978,14 @@ export {
|
|
|
823
978
|
appendDocumentSectionItem,
|
|
824
979
|
parseFrontmatter,
|
|
825
980
|
countCheckboxes,
|
|
826
|
-
|
|
827
|
-
|
|
981
|
+
collectVerifyCheckboxes,
|
|
982
|
+
summarizeVerifyCheckboxes,
|
|
983
|
+
openCheckboxLines,
|
|
828
984
|
getOpenCheckboxes,
|
|
829
|
-
extractBlockerLines,
|
|
830
985
|
hasMeaningfulBlockerBody,
|
|
986
|
+
getVisibleBodyLines,
|
|
831
987
|
hasMeaningfulBlockers,
|
|
832
|
-
|
|
988
|
+
readBlockerEntries,
|
|
833
989
|
detectDeltaSections,
|
|
834
990
|
parseScenarios,
|
|
835
991
|
ManagedPathError,
|
|
@@ -843,7 +999,6 @@ export {
|
|
|
843
999
|
inspectManagedFileTree,
|
|
844
1000
|
DEFAULT_DECISION_RECORDS_PATH,
|
|
845
1001
|
resolveDecisionRecordsPath,
|
|
846
|
-
ensureDecisionRecordsDirectory,
|
|
847
1002
|
validateDecisionRecordsFilesystemPath,
|
|
848
1003
|
pc,
|
|
849
1004
|
PKG_ROOT,
|
|
@@ -857,15 +1012,12 @@ export {
|
|
|
857
1012
|
LOCK_PATH,
|
|
858
1013
|
CONFIG_PATH,
|
|
859
1014
|
generateConfigTemplate,
|
|
860
|
-
reconcileRspConfigDefaults,
|
|
861
1015
|
loadRspConfig,
|
|
862
1016
|
inspectRspConfig,
|
|
863
1017
|
clearConfigCache,
|
|
864
1018
|
resolveKinds,
|
|
865
1019
|
resolveRequiredSections,
|
|
866
|
-
|
|
1020
|
+
resolveWorkflowPolicy,
|
|
867
1021
|
resolveLanguagePolicy,
|
|
868
|
-
getVersion
|
|
869
|
-
toErrorMessage,
|
|
870
|
-
emitJson
|
|
1022
|
+
getVersion
|
|
871
1023
|
};
|