@oevery/rsp 3.1.1 → 3.3.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 +4 -3
- package/README.zh-CN.md +3 -2
- package/dist/{chunk-WJXKLLIF.mjs → chunk-7HXQNBIG.mjs} +447 -734
- package/dist/{chunk-3Z4IO2TD.mjs → chunk-QM2UVPQK.mjs} +47 -30
- package/dist/{chunk-MUNLLVVH.mjs → chunk-TSUDEX5Y.mjs} +1205 -117
- package/dist/cli.mjs +5336 -3045
- package/dist/entry-GIDNR7CJ.mjs +1846 -0
- package/dist/{entry-KAWA2AS4.mjs → entry-YAW3PYFA.mjs} +4 -4
- package/package.json +12 -7
- package/rules/rsp-rules.md +30 -58
- package/skills/rsp/SKILL.md +28 -45
- package/skills/rsp/references/control-outcome.md +31 -0
- package/skills/rsp/references/durable-review.md +23 -4
- package/skills/rsp/references/focus-continuation.md +9 -0
- package/skills/rsp/references/implementation-evidence.md +12 -0
- package/skills/rsp/references/managed-routing.md +23 -18
- package/skills/rsp/references/response-language.md +11 -0
- package/skills/rsp/references/setup-repair.md +3 -3
- package/skills/rsp-commit/SKILL.md +26 -9
- package/skills/rsp-design/SKILL.md +1 -1
- package/skills/rsp-diagnose/SKILL.md +4 -2
- package/skills/rsp-implement/SKILL.md +18 -6
- package/skills/rsp-manage/SKILL.md +61 -37
- package/skills/rsp-manage/references/closeout.md +13 -0
- package/skills/rsp-manage/references/delegation.md +47 -0
- package/skills/rsp-manage/references/interruption-recovery.md +19 -0
- package/skills/rsp-manage/references/review-convergence.md +9 -0
- package/skills/rsp-release-docs/SKILL.md +15 -13
- package/skills/rsp-release-docs/references/publication-lifecycle.md +4 -0
- package/skills/rsp-resolve-findings/SKILL.md +3 -3
- package/skills/rsp-review/SKILL.md +10 -36
- package/skills/rsp-review/references/code-review.md +15 -0
- package/skills/rsp-review/references/document-review.md +15 -0
- package/skills/rsp-shape/SKILL.md +14 -4
- package/skills/rsp-shape/references/deep-clarification.md +2 -0
- package/skills/rsp-structural-audit/SKILL.md +1 -1
- package/skills/rsp-tdd/SKILL.md +11 -3
- package/skills/rsp-verify/SKILL.md +40 -0
- package/dist/entry-IPHJBI53.mjs +0 -882
- package/skills/rsp/references/release-operations.md +0 -9
|
@@ -1,219 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/core/config.ts
|
|
4
|
-
import { readFile
|
|
5
|
-
import { dirname as
|
|
4
|
+
import { readFile } from "fs/promises";
|
|
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 { parseDocument, stringify, visit } from "yaml";
|
|
8
9
|
|
|
9
|
-
// src/core/
|
|
10
|
-
import {
|
|
11
|
-
import { mkdir, readdir as readdir2, realpath, stat } from "fs/promises";
|
|
12
|
-
import { dirname, isAbsolute, join as join2, posix, relative, resolve, sep } from "path";
|
|
13
|
-
|
|
14
|
-
// src/core/managed-path.ts
|
|
15
|
-
import { lstatSync } from "fs";
|
|
16
|
-
import { readdir, writeFile } from "fs/promises";
|
|
17
|
-
import { join } from "path";
|
|
18
|
-
var ManagedPathError = class extends Error {
|
|
19
|
-
constructor(path, message) {
|
|
20
|
-
super(message);
|
|
21
|
-
this.path = path;
|
|
22
|
-
this.name = "ManagedPathError";
|
|
23
|
-
}
|
|
24
|
-
path;
|
|
25
|
-
};
|
|
26
|
-
function inspectManagedDirectory(path, label, options = {}) {
|
|
27
|
-
try {
|
|
28
|
-
const stats = lstatSync(path);
|
|
29
|
-
if (stats.isDirectory())
|
|
30
|
-
return { exists: true, issue: null };
|
|
31
|
-
return {
|
|
32
|
-
exists: true,
|
|
33
|
-
issue: new ManagedPathError(path, `${label} must be a real directory: ${path}`)
|
|
34
|
-
};
|
|
35
|
-
} catch (error) {
|
|
36
|
-
if (error.code === "ENOENT") {
|
|
37
|
-
if (options.allowMissing)
|
|
38
|
-
return { exists: false, issue: null };
|
|
39
|
-
return {
|
|
40
|
-
exists: false,
|
|
41
|
-
issue: new ManagedPathError(path, `${label} must be a real directory: ${path}`)
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
45
|
-
return {
|
|
46
|
-
exists: false,
|
|
47
|
-
issue: new ManagedPathError(path, `unable to inspect ${label} ${path}: ${message}`)
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
function requireManagedDirectory(path, label, options = {}) {
|
|
52
|
-
const inspection = inspectManagedDirectory(path, label, options);
|
|
53
|
-
if (inspection.issue)
|
|
54
|
-
throw inspection.issue;
|
|
55
|
-
return inspection.exists;
|
|
56
|
-
}
|
|
57
|
-
function inspectManagedFile(path, label, options = {}) {
|
|
58
|
-
try {
|
|
59
|
-
const stats = lstatSync(path);
|
|
60
|
-
if (stats.isFile())
|
|
61
|
-
return { exists: true, issue: null };
|
|
62
|
-
return {
|
|
63
|
-
exists: true,
|
|
64
|
-
issue: new ManagedPathError(path, `${label} must be a regular file: ${path}`)
|
|
65
|
-
};
|
|
66
|
-
} catch (error) {
|
|
67
|
-
if (error.code === "ENOENT") {
|
|
68
|
-
if (options.allowMissing)
|
|
69
|
-
return { exists: false, issue: null };
|
|
70
|
-
return {
|
|
71
|
-
exists: false,
|
|
72
|
-
issue: new ManagedPathError(path, `${label} must be a regular file: ${path}`)
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
76
|
-
return {
|
|
77
|
-
exists: false,
|
|
78
|
-
issue: new ManagedPathError(path, `unable to inspect ${label} ${path}: ${message}`)
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
function requireManagedFile(path, label, options = {}) {
|
|
83
|
-
const inspection = inspectManagedFile(path, label, options);
|
|
84
|
-
if (inspection.issue)
|
|
85
|
-
throw inspection.issue;
|
|
86
|
-
return inspection.exists;
|
|
87
|
-
}
|
|
88
|
-
async function ensureManagedFile(path, content, label) {
|
|
89
|
-
if (requireManagedFile(path, label, { allowMissing: true }))
|
|
90
|
-
return false;
|
|
91
|
-
await writeFile(path, content, { flag: "wx" });
|
|
92
|
-
return true;
|
|
93
|
-
}
|
|
94
|
-
async function writeManagedFile(path, content, label) {
|
|
95
|
-
const exists = requireManagedFile(path, label, { allowMissing: true });
|
|
96
|
-
if (exists)
|
|
97
|
-
await writeFile(path, content);
|
|
98
|
-
else
|
|
99
|
-
await writeFile(path, content, { flag: "wx" });
|
|
100
|
-
}
|
|
101
|
-
function resolveManagedDirectoryChain(root, segments, label) {
|
|
102
|
-
requireManagedDirectory(root, `${label} root`);
|
|
103
|
-
let current = root;
|
|
104
|
-
for (const segment of segments) {
|
|
105
|
-
if (!segment || segment === "." || segment === ".." || segment.includes("/") || segment.includes("\\"))
|
|
106
|
-
throw new ManagedPathError(current, `${label} contains an invalid directory segment: ${segment}`);
|
|
107
|
-
current = join(current, segment);
|
|
108
|
-
requireManagedDirectory(current, label, { allowMissing: true });
|
|
109
|
-
}
|
|
110
|
-
return current;
|
|
111
|
-
}
|
|
112
|
-
async function inspectManagedFileTree(root, label, options = {}) {
|
|
113
|
-
const result = { rootExists: false, files: [], issues: [] };
|
|
114
|
-
const rootInspection = inspectManagedDirectory(root, `${label} root`, options);
|
|
115
|
-
if (rootInspection.issue) {
|
|
116
|
-
result.issues.push(rootInspection.issue);
|
|
117
|
-
return result;
|
|
118
|
-
}
|
|
119
|
-
if (!rootInspection.exists)
|
|
120
|
-
return result;
|
|
121
|
-
result.rootExists = true;
|
|
122
|
-
await collectManagedFiles(root, label, result);
|
|
123
|
-
result.files.sort();
|
|
124
|
-
return result;
|
|
125
|
-
}
|
|
126
|
-
async function collectManagedFiles(directory, label, result) {
|
|
127
|
-
let entries;
|
|
128
|
-
try {
|
|
129
|
-
entries = await readdir(directory, { withFileTypes: true });
|
|
130
|
-
} catch (error) {
|
|
131
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
132
|
-
result.issues.push(new ManagedPathError(directory, `unable to inspect the ${label} tree at ${directory}: ${message}`));
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
136
|
-
if (entry.name.startsWith("."))
|
|
137
|
-
continue;
|
|
138
|
-
const path = join(directory, entry.name);
|
|
139
|
-
if (entry.isDirectory()) {
|
|
140
|
-
await collectManagedFiles(path, label, result);
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
if (entry.isFile()) {
|
|
144
|
-
result.files.push(path);
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
result.issues.push(new ManagedPathError(path, `unsupported entry in the ${label} tree: ${path}`));
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// src/core/decisions.ts
|
|
152
|
-
var DEFAULT_DECISION_RECORDS_PATH = ".rsp/specs/decisions";
|
|
153
|
-
function normalizeDecisionRecordsPath(pathValue) {
|
|
154
|
-
return posix.normalize(pathValue.trim().replace(/\\/g, "/")).replace(/^\.\//, "").replace(/\/$/, "");
|
|
155
|
-
}
|
|
156
|
-
function validateDecisionRecordsPath(pathValue) {
|
|
157
|
-
if (typeof pathValue !== "string" || pathValue.trim() === "")
|
|
158
|
-
return 'config.yaml field "decisions.path" must be a non-empty string';
|
|
159
|
-
const raw = pathValue.trim();
|
|
160
|
-
const portable = raw.replace(/\\/g, "/");
|
|
161
|
-
if (isAbsolute(raw) || posix.isAbsolute(portable) || /^[a-z]:[\\/]/i.test(raw))
|
|
162
|
-
return 'config.yaml field "decisions.path" must be project-relative';
|
|
163
|
-
const normalized = normalizeDecisionRecordsPath(raw);
|
|
164
|
-
if (normalized === "." || normalized === ".." || normalized.startsWith("../"))
|
|
165
|
-
return 'config.yaml field "decisions.path" must stay inside the Host Project';
|
|
166
|
-
if ((normalized === ".rsp" || normalized.startsWith(".rsp/")) && normalized !== DEFAULT_DECISION_RECORDS_PATH)
|
|
167
|
-
return `config.yaml field "decisions.path" must use ${DEFAULT_DECISION_RECORDS_PATH} or a path outside .rsp/`;
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
function getDecisionRecordsConfigIssue(config) {
|
|
171
|
-
if (!("decisions" in config))
|
|
172
|
-
return null;
|
|
173
|
-
const decisions = config.decisions;
|
|
174
|
-
if (!decisions || typeof decisions !== "object" || Array.isArray(decisions))
|
|
175
|
-
return 'config.yaml field "decisions" must be a mapping with one path';
|
|
176
|
-
const keys = Object.keys(decisions);
|
|
177
|
-
if (keys.length !== 1 || keys[0] !== "path")
|
|
178
|
-
return 'config.yaml field "decisions" supports only "path"';
|
|
179
|
-
return validateDecisionRecordsPath(decisions.path);
|
|
180
|
-
}
|
|
181
|
-
function resolveDecisionRecordsPath(config) {
|
|
182
|
-
const configured = config.decisions?.path;
|
|
183
|
-
return configured && validateDecisionRecordsPath(configured) === null ? normalizeDecisionRecordsPath(configured) : DEFAULT_DECISION_RECORDS_PATH;
|
|
184
|
-
}
|
|
185
|
-
async function ensureDecisionRecordsDirectory(pathValue) {
|
|
186
|
-
const filesystemError = await validateDecisionRecordsFilesystemPath(pathValue);
|
|
187
|
-
if (filesystemError)
|
|
188
|
-
throw new Error(filesystemError);
|
|
189
|
-
const existed = existsSync(pathValue);
|
|
190
|
-
await mkdir(pathValue, { recursive: true });
|
|
191
|
-
const entries = await readdir2(pathValue);
|
|
192
|
-
if (entries.length === 0) {
|
|
193
|
-
await ensureManagedFile(join2(pathValue, ".gitkeep"), "", "Decision Record placeholder");
|
|
194
|
-
return true;
|
|
195
|
-
}
|
|
196
|
-
if (entries.includes(".gitkeep"))
|
|
197
|
-
requireManagedFile(join2(pathValue, ".gitkeep"), "Decision Record placeholder");
|
|
198
|
-
return !existed;
|
|
199
|
-
}
|
|
200
|
-
async function validateDecisionRecordsFilesystemPath(pathValue) {
|
|
201
|
-
const projectRoot = await realpath(".");
|
|
202
|
-
let existingAncestor = resolve(pathValue);
|
|
203
|
-
while (!existsSync(existingAncestor)) {
|
|
204
|
-
const parent = dirname(existingAncestor);
|
|
205
|
-
if (parent === existingAncestor)
|
|
206
|
-
return `Decision Record path "${pathValue}" has no accessible Host Project ancestor`;
|
|
207
|
-
existingAncestor = parent;
|
|
208
|
-
}
|
|
209
|
-
const resolvedAncestor = await realpath(existingAncestor);
|
|
210
|
-
const rel = relative(projectRoot, resolvedAncestor);
|
|
211
|
-
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel))
|
|
212
|
-
return `Decision Record path "${pathValue}" resolves outside the Host Project through ${existingAncestor}`;
|
|
213
|
-
if (existsSync(resolve(pathValue)) && !(await stat(resolve(pathValue))).isDirectory())
|
|
214
|
-
return `Decision Record path "${pathValue}" must be a directory`;
|
|
215
|
-
return null;
|
|
216
|
-
}
|
|
10
|
+
// src/core/content.ts
|
|
11
|
+
import { parse } from "yaml";
|
|
217
12
|
|
|
218
13
|
// src/core/document-model.ts
|
|
219
14
|
var CHANGE_DOCUMENT_SCHEMA = {
|
|
@@ -371,13 +166,7 @@ function lineBreakStart(source, headingStart) {
|
|
|
371
166
|
return headingStart;
|
|
372
167
|
}
|
|
373
168
|
|
|
374
|
-
// src/core/
|
|
375
|
-
import { existsSync as existsSync2 } from "fs";
|
|
376
|
-
import { readdir as readdir3, readFile, rmdir } from "fs/promises";
|
|
377
|
-
import { basename, dirname as dirname2, join as join3, relative as relative2, sep as sep2 } from "path";
|
|
378
|
-
import { parse } from "yaml";
|
|
379
|
-
var RSP_AGENTS_BEGIN = "<!-- rsp:begin -->";
|
|
380
|
-
var RSP_AGENTS_END = "<!-- rsp:end -->";
|
|
169
|
+
// src/core/content.ts
|
|
381
170
|
function parseYamlText(text) {
|
|
382
171
|
const parsed = parse(text);
|
|
383
172
|
if (parsed === null || parsed === void 0)
|
|
@@ -399,518 +188,457 @@ function countCheckboxes(content) {
|
|
|
399
188
|
const dropped = (content.match(/\[-\]/g) || []).length;
|
|
400
189
|
return { todo, progress, done, dropped, total: todo + progress + done + dropped };
|
|
401
190
|
}
|
|
402
|
-
|
|
403
|
-
const
|
|
404
|
-
return all.filter((f) => f.endsWith(".md"));
|
|
405
|
-
}
|
|
406
|
-
async function inspectUnsupportedRules() {
|
|
407
|
-
const rulesDir = join3(RSP_DIR, "rules");
|
|
408
|
-
const diagnostics = [];
|
|
409
|
-
const rawEntries = await walkAllEntries(rulesDir, diagnostics);
|
|
410
|
-
const entries = rawEntries.filter((entry) => entry.path !== OBSOLETE_RSP_RULES_PATH).map((entry) => ({
|
|
411
|
-
path: normalizeLogicalPath(relative2(rulesDir, entry.path)),
|
|
412
|
-
kind: entry.kind
|
|
413
|
-
}));
|
|
414
|
-
const obsoleteFallbackPresent = existsSync2(OBSOLETE_RSP_RULES_PATH);
|
|
191
|
+
function classifyVerifyCheckboxes(sectionText) {
|
|
192
|
+
const buckets = collectVerifyCheckboxLines(sectionText);
|
|
415
193
|
return {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
|
419
198
|
};
|
|
420
199
|
}
|
|
421
|
-
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
if (error.code !== "ENOENT") {
|
|
434
|
-
diagnostics.push({
|
|
435
|
-
code: "walk_failed",
|
|
436
|
-
operation: "readdir",
|
|
437
|
-
path: dir,
|
|
438
|
-
message: toErrorMessage(error)
|
|
439
|
-
});
|
|
200
|
+
function collectVerifyCheckboxLines(sectionText) {
|
|
201
|
+
const required = [];
|
|
202
|
+
const optional = [];
|
|
203
|
+
const unclassified = [];
|
|
204
|
+
let current = "unclassified";
|
|
205
|
+
let sawClassificationHeading = false;
|
|
206
|
+
for (const line of sectionText.split("\n")) {
|
|
207
|
+
const heading = line.match(/^###\s+(Required|Optional)\s*$/i);
|
|
208
|
+
if (heading) {
|
|
209
|
+
current = heading[1].toLowerCase();
|
|
210
|
+
sawClassificationHeading = true;
|
|
211
|
+
continue;
|
|
440
212
|
}
|
|
213
|
+
if (!/^- \[[ /x-]\]/i.test(line.trim()))
|
|
214
|
+
continue;
|
|
215
|
+
if (current === "required")
|
|
216
|
+
required.push(line);
|
|
217
|
+
else if (current === "optional")
|
|
218
|
+
optional.push(line);
|
|
219
|
+
else
|
|
220
|
+
unclassified.push(line);
|
|
441
221
|
}
|
|
442
|
-
return
|
|
222
|
+
return { required, optional, unclassified, legacy: unclassified.length > 0 || !sawClassificationHeading };
|
|
443
223
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
try {
|
|
447
|
-
const items = await readdir3(dir, { withFileTypes: true });
|
|
448
|
-
for (const item of items) {
|
|
449
|
-
if (item.name.startsWith("."))
|
|
450
|
-
continue;
|
|
451
|
-
const filePath = join3(dir, item.name);
|
|
452
|
-
if (item.isDirectory())
|
|
453
|
-
files.push(...await walkFiles(filePath, options));
|
|
454
|
-
else
|
|
455
|
-
files.push(filePath);
|
|
456
|
-
}
|
|
457
|
-
} catch (error) {
|
|
458
|
-
if (error.code !== "ENOENT") {
|
|
459
|
-
options.onError?.({
|
|
460
|
-
code: "walk_failed",
|
|
461
|
-
operation: "readdir",
|
|
462
|
-
path: dir,
|
|
463
|
-
message: toErrorMessage(error)
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
return files;
|
|
224
|
+
function getOpenCheckboxes(sectionText) {
|
|
225
|
+
return sectionText.split("\n").map((line) => line.trim()).filter((line) => /^- \[[ /]\]/.test(line));
|
|
468
226
|
}
|
|
469
|
-
function
|
|
470
|
-
|
|
227
|
+
function extractBlockerLines(content) {
|
|
228
|
+
const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
|
|
229
|
+
return normalizeBlockerBody(getDocumentSectionBody(document, "blockers"));
|
|
471
230
|
}
|
|
472
|
-
function
|
|
473
|
-
|
|
474
|
-
const proposalSummary = summary || placeholder;
|
|
475
|
-
const issueFrontmatter = renderIssueFrontmatter(options.issues ?? []);
|
|
476
|
-
if (name === "project-setup") {
|
|
477
|
-
return `---
|
|
478
|
-
kind: ops
|
|
479
|
-
${issueFrontmatter}---
|
|
480
|
-
|
|
481
|
-
${renderDocumentTitle(CHANGE_DOCUMENT_SCHEMA, "project-setup")}
|
|
482
|
-
|
|
483
|
-
${changeSectionHeading("proposal")}
|
|
484
|
-
- Outcome: ${proposalSummary}
|
|
485
|
-
- Why:
|
|
486
|
-
- ${placeholder}
|
|
487
|
-
- Scope:
|
|
488
|
-
- ${placeholder}
|
|
489
|
-
- Non-goals:
|
|
490
|
-
- ${placeholder}
|
|
491
|
-
|
|
492
|
-
${changeSectionHeading("spec")}
|
|
493
|
-
### ADDED
|
|
494
|
-
- Requirement: ${placeholder}
|
|
495
|
-
- ${placeholder}
|
|
496
|
-
|
|
497
|
-
### Acceptance
|
|
498
|
-
#### Scenario: ${placeholder}
|
|
499
|
-
- GIVEN ${placeholder}
|
|
500
|
-
- WHEN ${placeholder}
|
|
501
|
-
- THEN ${placeholder}
|
|
502
|
-
|
|
503
|
-
${changeSectionHeading("design")}
|
|
504
|
-
- Approach:
|
|
505
|
-
- ${placeholder}
|
|
506
|
-
- Boundaries:
|
|
507
|
-
- ${placeholder}
|
|
508
|
-
- Affected areas:
|
|
509
|
-
- .rsp/specs/design.md
|
|
510
|
-
- CONTEXT.md
|
|
511
|
-
- AGENTS.md
|
|
512
|
-
- Durable outcome targets:
|
|
513
|
-
- Current facts: ${placeholder}
|
|
514
|
-
- Lasting rationale: ${placeholder}
|
|
515
|
-
- Constraints:
|
|
516
|
-
- ${placeholder}
|
|
517
|
-
|
|
518
|
-
${changeSectionHeading("tasks")}
|
|
519
|
-
- [ ] .rsp/specs/design.md: ${placeholder}
|
|
520
|
-
- [ ] CONTEXT.md: ${placeholder}
|
|
521
|
-
- [ ] AGENTS.md: ${placeholder}
|
|
522
|
-
|
|
523
|
-
${changeSectionHeading("verify")}
|
|
524
|
-
- Automated:
|
|
525
|
-
- [ ] rsp doctor \u2014 proves: ${placeholder}
|
|
526
|
-
- Manual or environment:
|
|
527
|
-
- [ ] ${placeholder}
|
|
528
|
-
- Coverage:
|
|
529
|
-
- ${placeholder}
|
|
530
|
-
|
|
531
|
-
${changeSectionHeading("blockers")}
|
|
532
|
-
- none
|
|
533
|
-
`;
|
|
534
|
-
}
|
|
535
|
-
const frontmatterKind = kind ?? "<choose: feature | fix | refactor | docs | ops | research>";
|
|
536
|
-
if (options.lite)
|
|
537
|
-
return generateLiteChangeContent(name, proposalSummary, frontmatterKind, issueFrontmatter);
|
|
538
|
-
const template = getChangeTemplateByKind(kind);
|
|
539
|
-
return `---
|
|
540
|
-
kind: "${frontmatterKind}"
|
|
541
|
-
${issueFrontmatter}---
|
|
542
|
-
|
|
543
|
-
${renderDocumentTitle(CHANGE_DOCUMENT_SCHEMA, name)}
|
|
544
|
-
|
|
545
|
-
${changeSectionHeading("proposal")}
|
|
546
|
-
- Outcome: ${proposalSummary}
|
|
547
|
-
- Why:
|
|
548
|
-
- ${template.why}
|
|
549
|
-
- Scope:
|
|
550
|
-
- ${template.scope}
|
|
551
|
-
- Non-goals:
|
|
552
|
-
- ${template.nonGoals}
|
|
553
|
-
|
|
554
|
-
${changeSectionHeading("spec")}
|
|
555
|
-
${template.specSection}
|
|
556
|
-
|
|
557
|
-
### Acceptance
|
|
558
|
-
${template.acceptanceSection}
|
|
559
|
-
|
|
560
|
-
${changeSectionHeading("design")}
|
|
561
|
-
- Approach:
|
|
562
|
-
- ${template.approach}
|
|
563
|
-
- Boundaries:
|
|
564
|
-
- ${placeholder}
|
|
565
|
-
- Affected areas:
|
|
566
|
-
- ${template.affectedArea1}
|
|
567
|
-
- ${template.affectedArea2}
|
|
568
|
-
- Constraints:
|
|
569
|
-
- ${template.constraints}
|
|
570
|
-
|
|
571
|
-
${changeSectionHeading("tasks")}
|
|
572
|
-
- [ ] ${template.task}
|
|
573
|
-
|
|
574
|
-
${changeSectionHeading("verify")}
|
|
575
|
-
- Automated:
|
|
576
|
-
- [ ] ${template.automatedVerify} \u2014 proves: ${placeholder}
|
|
577
|
-
- Manual or environment:
|
|
578
|
-
- [ ] ${template.manualVerify}
|
|
579
|
-
- Coverage:
|
|
580
|
-
- ${placeholder}
|
|
581
|
-
|
|
582
|
-
${changeSectionHeading("blockers")}
|
|
583
|
-
- none
|
|
584
|
-
`;
|
|
231
|
+
function hasMeaningfulBlockerBody(body) {
|
|
232
|
+
return hasMeaningfulBlockerLines(normalizeBlockerBody(body));
|
|
585
233
|
}
|
|
586
|
-
function
|
|
587
|
-
|
|
588
|
-
return `---
|
|
589
|
-
kind: "${frontmatterKind}"
|
|
590
|
-
${issueFrontmatter}---
|
|
591
|
-
|
|
592
|
-
${renderDocumentTitle(CHANGE_DOCUMENT_SCHEMA, name)}
|
|
593
|
-
|
|
594
|
-
${changeSectionHeading("proposal")}
|
|
595
|
-
- Outcome: ${proposalSummary}
|
|
596
|
-
- Why:
|
|
597
|
-
- ${placeholder}
|
|
598
|
-
- Scope:
|
|
599
|
-
- ${placeholder}
|
|
600
|
-
- Non-goals:
|
|
601
|
-
- none
|
|
602
|
-
|
|
603
|
-
${changeSectionHeading("spec")}
|
|
604
|
-
### MODIFIED
|
|
605
|
-
- Requirement: ${placeholder}
|
|
606
|
-
- ${placeholder}
|
|
607
|
-
|
|
608
|
-
### Acceptance
|
|
609
|
-
#### Scenario: ${placeholder}
|
|
610
|
-
- GIVEN ${placeholder}
|
|
611
|
-
- WHEN ${placeholder}
|
|
612
|
-
- THEN ${placeholder}
|
|
613
|
-
|
|
614
|
-
${changeSectionHeading("design")}
|
|
615
|
-
- Approach:
|
|
616
|
-
- ${placeholder}
|
|
617
|
-
- Boundaries:
|
|
618
|
-
- ${placeholder}
|
|
619
|
-
- Affected areas:
|
|
620
|
-
- ${placeholder}
|
|
621
|
-
- Constraints:
|
|
622
|
-
- ${placeholder}
|
|
623
|
-
|
|
624
|
-
${changeSectionHeading("tasks")}
|
|
625
|
-
- [ ] ${placeholder}
|
|
626
|
-
|
|
627
|
-
${changeSectionHeading("verify")}
|
|
628
|
-
- Automated:
|
|
629
|
-
- [ ] ${placeholder} \u2014 proves: ${placeholder}
|
|
630
|
-
- Manual or environment:
|
|
631
|
-
- [ ] ${placeholder}
|
|
632
|
-
- Coverage:
|
|
633
|
-
- ${placeholder}
|
|
634
|
-
|
|
635
|
-
${changeSectionHeading("blockers")}
|
|
636
|
-
- none
|
|
637
|
-
`;
|
|
234
|
+
function normalizeBlockerBody(body) {
|
|
235
|
+
return body.replace(/<!--[\s\S]*?-->/g, "").split("\n").map((line) => line.trim()).filter(Boolean);
|
|
638
236
|
}
|
|
639
|
-
function
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
237
|
+
function hasMeaningfulBlockers(content) {
|
|
238
|
+
return hasMeaningfulBlockerLines(extractBlockerLines(content));
|
|
239
|
+
}
|
|
240
|
+
function isEmptyBlockerLine(line) {
|
|
241
|
+
const normalized = line.trim();
|
|
242
|
+
return /^none[.。]?$/i.test(normalized) || /^[-*]\s*(?:none[.。]?)?$/i.test(normalized);
|
|
243
|
+
}
|
|
244
|
+
function hasMeaningfulBlockerLines(lines) {
|
|
245
|
+
if (lines.length === 0)
|
|
246
|
+
return false;
|
|
247
|
+
return lines.some((line) => !isEmptyBlockerLine(line));
|
|
646
248
|
}
|
|
647
|
-
function
|
|
648
|
-
const
|
|
649
|
-
const
|
|
249
|
+
function detectDeltaSections(content) {
|
|
250
|
+
const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
|
|
251
|
+
const body = getDocumentSectionBody(document, "spec");
|
|
650
252
|
return {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
specSection: `### ${delta}
|
|
655
|
-
- Requirement: ${placeholder}
|
|
656
|
-
- ${placeholder}`,
|
|
657
|
-
acceptanceSection: `#### Scenario: ${placeholder}
|
|
658
|
-
- GIVEN ${placeholder}
|
|
659
|
-
- WHEN ${placeholder}
|
|
660
|
-
- THEN ${placeholder}`,
|
|
661
|
-
approach: placeholder,
|
|
662
|
-
affectedArea1: placeholder,
|
|
663
|
-
affectedArea2: placeholder,
|
|
664
|
-
constraints: placeholder,
|
|
665
|
-
task: placeholder,
|
|
666
|
-
automatedVerify: placeholder,
|
|
667
|
-
manualVerify: placeholder
|
|
253
|
+
added: /^###\s*ADDED/im.test(body),
|
|
254
|
+
modified: /^###\s*MODIFIED/im.test(body),
|
|
255
|
+
removed: /^###\s*REMOVED/im.test(body)
|
|
668
256
|
};
|
|
669
257
|
}
|
|
670
|
-
function
|
|
671
|
-
|
|
258
|
+
function parseScenarios(content) {
|
|
259
|
+
const scenarios = [];
|
|
260
|
+
const regex = /^#{3,4}\s*Scenario:\s*(.+)$/gim;
|
|
261
|
+
let match;
|
|
262
|
+
while ((match = regex.exec(content)) !== null) {
|
|
263
|
+
const heading = match[1].trim();
|
|
264
|
+
const startPos = match.index + match[0].length;
|
|
265
|
+
const restContent = content.slice(startPos);
|
|
266
|
+
const nextSection = restContent.match(/^#{3,4}\s/m);
|
|
267
|
+
const block = nextSection ? restContent.slice(0, nextSection.index) : restContent;
|
|
268
|
+
const steps = block.split("\n").map((s) => s.trim()).filter((s) => s.startsWith("-") && /GIVEN|WHEN|THEN|AND|BUT/i.test(s)).map((s) => s.replace(/^[-*]\s*/, ""));
|
|
269
|
+
if (steps.length > 0)
|
|
270
|
+
scenarios.push({ heading, steps });
|
|
271
|
+
}
|
|
272
|
+
return scenarios;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/core/decisions.ts
|
|
276
|
+
import { existsSync } from "fs";
|
|
277
|
+
import { mkdir, readdir as readdir2, realpath, stat } from "fs/promises";
|
|
278
|
+
import { dirname, isAbsolute, join as join2, posix, relative, resolve, sep } from "path";
|
|
279
|
+
|
|
280
|
+
// src/core/managed-path.ts
|
|
281
|
+
import { lstatSync } from "fs";
|
|
282
|
+
import { readdir, writeFile } from "fs/promises";
|
|
283
|
+
import { join } from "path";
|
|
284
|
+
var ManagedPathError = class extends Error {
|
|
285
|
+
constructor(path, message) {
|
|
286
|
+
super(message);
|
|
287
|
+
this.path = path;
|
|
288
|
+
this.name = "ManagedPathError";
|
|
289
|
+
}
|
|
290
|
+
path;
|
|
291
|
+
};
|
|
292
|
+
function inspectManagedDirectory(path, label, options = {}) {
|
|
293
|
+
try {
|
|
294
|
+
const stats = lstatSync(path);
|
|
295
|
+
if (stats.isDirectory())
|
|
296
|
+
return { exists: true, issue: null };
|
|
297
|
+
return {
|
|
298
|
+
exists: true,
|
|
299
|
+
issue: new ManagedPathError(path, `${label} must be a real directory: ${path}`)
|
|
300
|
+
};
|
|
301
|
+
} catch (error) {
|
|
302
|
+
if (error.code === "ENOENT") {
|
|
303
|
+
if (options.allowMissing)
|
|
304
|
+
return { exists: false, issue: null };
|
|
305
|
+
return {
|
|
306
|
+
exists: false,
|
|
307
|
+
issue: new ManagedPathError(path, `${label} must be a real directory: ${path}`)
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
311
|
+
return {
|
|
312
|
+
exists: false,
|
|
313
|
+
issue: new ManagedPathError(path, `unable to inspect ${label} ${path}: ${message}`)
|
|
314
|
+
};
|
|
315
|
+
}
|
|
672
316
|
}
|
|
673
|
-
function
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
Read in order:
|
|
680
|
-
1. Nearest \`AGENTS.md\` for project or module instructions.
|
|
681
|
-
2. Root \`CONTEXT-MAP.md\` if present, then the relevant nearest \`CONTEXT.md\`.
|
|
682
|
-
3. The \`rsp\` skill; if unavailable, read \`.rsp/rsp-rules.md\` as the fallback protocol.
|
|
683
|
-
4. \`.rsp/focus.d/\`; for grouped work read the sibling Group Brief, then the explicitly selected focused Change.
|
|
684
|
-
5. Only the relevant Specs and Decision Records under the configured authoritative path.
|
|
685
|
-
|
|
686
|
-
If \`.rsp/focus.d/\` is empty and the user has not provided a concrete task, ask what to work on or suggest \`npx -y @oevery/rsp create <name>\` for tracked work.
|
|
687
|
-
Do not treat \`.rsp/specs/\` or \`.rsp/changes/\` as replacements for nearest \`AGENTS.md\` or \`CONTEXT.md\`.
|
|
688
|
-
${RSP_AGENTS_END}`;
|
|
317
|
+
function requireManagedDirectory(path, label, options = {}) {
|
|
318
|
+
const inspection = inspectManagedDirectory(path, label, options);
|
|
319
|
+
if (inspection.issue)
|
|
320
|
+
throw inspection.issue;
|
|
321
|
+
return inspection.exists;
|
|
689
322
|
}
|
|
690
|
-
function
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
return {
|
|
323
|
+
function inspectManagedFile(path, label, options = {}) {
|
|
324
|
+
try {
|
|
325
|
+
const stats = lstatSync(path);
|
|
326
|
+
if (stats.isFile())
|
|
327
|
+
return { exists: true, issue: null };
|
|
328
|
+
return {
|
|
329
|
+
exists: true,
|
|
330
|
+
issue: new ManagedPathError(path, `${label} must be a regular file: ${path}`)
|
|
331
|
+
};
|
|
332
|
+
} catch (error) {
|
|
333
|
+
if (error.code === "ENOENT") {
|
|
334
|
+
if (options.allowMissing)
|
|
335
|
+
return { exists: false, issue: null };
|
|
336
|
+
return {
|
|
337
|
+
exists: false,
|
|
338
|
+
issue: new ManagedPathError(path, `${label} must be a regular file: ${path}`)
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
342
|
+
return {
|
|
343
|
+
exists: false,
|
|
344
|
+
issue: new ManagedPathError(path, `unable to inspect ${label} ${path}: ${message}`)
|
|
345
|
+
};
|
|
696
346
|
}
|
|
697
|
-
const trimmed = content.trimStart();
|
|
698
|
-
const next = trimmed ? `${block}
|
|
699
|
-
|
|
700
|
-
${trimmed}` : `${block}
|
|
701
|
-
`;
|
|
702
|
-
return { content: next, changed: true };
|
|
703
347
|
}
|
|
704
|
-
function
|
|
705
|
-
|
|
348
|
+
function requireManagedFile(path, label, options = {}) {
|
|
349
|
+
const inspection = inspectManagedFile(path, label, options);
|
|
350
|
+
if (inspection.issue)
|
|
351
|
+
throw inspection.issue;
|
|
352
|
+
return inspection.exists;
|
|
706
353
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
354
|
+
async function ensureManagedFile(path, content, label) {
|
|
355
|
+
if (requireManagedFile(path, label, { allowMissing: true }))
|
|
356
|
+
return false;
|
|
357
|
+
await writeFile(path, content, { flag: "wx" });
|
|
358
|
+
return true;
|
|
710
359
|
}
|
|
711
|
-
function
|
|
712
|
-
const
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
console.error(` ${pc.red("Error:")} ${initialized ? "RSP project requires an update" : "RSP is not initialized in this project"}`);
|
|
718
|
-
console.error(` ${pc.dim(initialized ? "Run: rsp update" : "Run: rsp init")}`);
|
|
719
|
-
process.exit(1);
|
|
720
|
-
}
|
|
360
|
+
async function writeManagedFile(path, content, label) {
|
|
361
|
+
const exists = requireManagedFile(path, label, { allowMissing: true });
|
|
362
|
+
if (exists)
|
|
363
|
+
await writeFile(path, content);
|
|
364
|
+
else
|
|
365
|
+
await writeFile(path, content, { flag: "wx" });
|
|
721
366
|
}
|
|
722
|
-
function
|
|
723
|
-
|
|
367
|
+
function resolveManagedDirectoryChain(root, segments, label) {
|
|
368
|
+
requireManagedDirectory(root, `${label} root`);
|
|
369
|
+
let current = root;
|
|
370
|
+
for (const segment of segments) {
|
|
371
|
+
if (!segment || segment === "." || segment === ".." || segment.includes("/") || segment.includes("\\"))
|
|
372
|
+
throw new ManagedPathError(current, `${label} contains an invalid directory segment: ${segment}`);
|
|
373
|
+
current = join(current, segment);
|
|
374
|
+
requireManagedDirectory(current, label, { allowMissing: true });
|
|
375
|
+
}
|
|
376
|
+
return current;
|
|
724
377
|
}
|
|
725
|
-
async function
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
await rmdir(parent);
|
|
732
|
-
else
|
|
733
|
-
break;
|
|
734
|
-
} catch {
|
|
735
|
-
break;
|
|
736
|
-
}
|
|
737
|
-
parent = dirname2(parent);
|
|
378
|
+
async function inspectManagedFileTree(root, label, options = {}) {
|
|
379
|
+
const result = { rootExists: false, files: [], issues: [] };
|
|
380
|
+
const rootInspection = inspectManagedDirectory(root, `${label} root`, options);
|
|
381
|
+
if (rootInspection.issue) {
|
|
382
|
+
result.issues.push(rootInspection.issue);
|
|
383
|
+
return result;
|
|
738
384
|
}
|
|
385
|
+
if (!rootInspection.exists)
|
|
386
|
+
return result;
|
|
387
|
+
result.rootExists = true;
|
|
388
|
+
await collectManagedFiles(root, label, result);
|
|
389
|
+
result.files.sort();
|
|
390
|
+
return result;
|
|
739
391
|
}
|
|
740
|
-
async function
|
|
392
|
+
async function collectManagedFiles(directory, label, result) {
|
|
393
|
+
let entries;
|
|
741
394
|
try {
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
395
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
396
|
+
} catch (error) {
|
|
397
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
398
|
+
result.issues.push(new ManagedPathError(directory, `unable to inspect the ${label} tree at ${directory}: ${message}`));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
402
|
+
if (entry.name.startsWith("."))
|
|
403
|
+
continue;
|
|
404
|
+
const path = join(directory, entry.name);
|
|
405
|
+
if (entry.isDirectory()) {
|
|
406
|
+
await collectManagedFiles(path, label, result);
|
|
407
|
+
continue;
|
|
747
408
|
}
|
|
748
|
-
|
|
409
|
+
if (entry.isFile()) {
|
|
410
|
+
result.files.push(path);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
result.issues.push(new ManagedPathError(path, `unsupported entry in the ${label} tree: ${path}`));
|
|
749
414
|
}
|
|
750
|
-
return basename(process.cwd());
|
|
751
|
-
}
|
|
752
|
-
function generateDesignContent(projectName) {
|
|
753
|
-
const placeholder = "<\u2026>";
|
|
754
|
-
return `# Project Design: ${projectName}
|
|
755
|
-
|
|
756
|
-
## Purpose
|
|
757
|
-
- ${placeholder}
|
|
758
|
-
|
|
759
|
-
## Stable Facts
|
|
760
|
-
- ${placeholder}
|
|
761
|
-
|
|
762
|
-
## Boundaries
|
|
763
|
-
- In scope:
|
|
764
|
-
- ${placeholder}
|
|
765
|
-
- Out of scope:
|
|
766
|
-
- ${placeholder}
|
|
767
|
-
|
|
768
|
-
## Structure
|
|
769
|
-
- ${placeholder}
|
|
770
|
-
|
|
771
|
-
## Constraints
|
|
772
|
-
- ${placeholder}
|
|
773
|
-
`;
|
|
774
415
|
}
|
|
775
|
-
function generateSpecContent(name) {
|
|
776
|
-
const title = toTitleCase(name);
|
|
777
|
-
const placeholder = "<\u2026>";
|
|
778
|
-
return `# ${title}
|
|
779
416
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
- ${placeholder}
|
|
785
|
-
|
|
786
|
-
## Boundaries
|
|
787
|
-
- In scope:
|
|
788
|
-
- ${placeholder}
|
|
789
|
-
- Out of scope:
|
|
790
|
-
- ${placeholder}
|
|
791
|
-
|
|
792
|
-
## Constraints
|
|
793
|
-
- ${placeholder}
|
|
794
|
-
`;
|
|
795
|
-
}
|
|
796
|
-
function toTitleCase(value) {
|
|
797
|
-
return value.split(/[-/]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
798
|
-
}
|
|
799
|
-
function extractBlockerLines(content) {
|
|
800
|
-
const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
|
|
801
|
-
return normalizeBlockerBody(getDocumentSectionBody(document, "blockers"));
|
|
802
|
-
}
|
|
803
|
-
function hasMeaningfulBlockerBody(body) {
|
|
804
|
-
return hasMeaningfulBlockerLines(normalizeBlockerBody(body));
|
|
805
|
-
}
|
|
806
|
-
function normalizeBlockerBody(body) {
|
|
807
|
-
return body.replace(/<!--[\s\S]*?-->/g, "").split("\n").map((line) => line.trim()).filter(Boolean);
|
|
808
|
-
}
|
|
809
|
-
function hasMeaningfulBlockers(content) {
|
|
810
|
-
return hasMeaningfulBlockerLines(extractBlockerLines(content));
|
|
811
|
-
}
|
|
812
|
-
function hasMeaningfulBlockerLines(lines) {
|
|
813
|
-
if (lines.length === 0)
|
|
814
|
-
return false;
|
|
815
|
-
return lines.some((line) => !/^[-*]\s*(?:none)?$/i.test(line) && !/^none$/i.test(line));
|
|
816
|
-
}
|
|
817
|
-
function detectDeltaSections(content) {
|
|
818
|
-
const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
|
|
819
|
-
const body = getDocumentSectionBody(document, "spec");
|
|
820
|
-
return {
|
|
821
|
-
added: /^###\s*ADDED/im.test(body),
|
|
822
|
-
modified: /^###\s*MODIFIED/im.test(body),
|
|
823
|
-
removed: /^###\s*REMOVED/im.test(body)
|
|
824
|
-
};
|
|
417
|
+
// src/core/decisions.ts
|
|
418
|
+
var DEFAULT_DECISION_RECORDS_PATH = ".rsp/specs/decisions";
|
|
419
|
+
function normalizeDecisionRecordsPath(pathValue) {
|
|
420
|
+
return posix.normalize(pathValue.trim().replace(/\\/g, "/")).replace(/^\.\//, "").replace(/\/$/, "");
|
|
825
421
|
}
|
|
826
|
-
function
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
const
|
|
830
|
-
const
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
warnings.push("active blockers are present in the change file");
|
|
840
|
-
const scenarios = parseScenarios(content);
|
|
841
|
-
const missingScenarios = scenarios.length === 0;
|
|
842
|
-
if (missingScenarios)
|
|
843
|
-
warnings.push("no Scenario blocks found (some changes do not need them)");
|
|
844
|
-
const archiveReady = activeBlockers ? "no" : warnings.length === 0 ? "yes" : "judgment";
|
|
845
|
-
return {
|
|
846
|
-
taskTodos,
|
|
847
|
-
verifyTodos,
|
|
848
|
-
activeBlockers,
|
|
849
|
-
scenarioCount: scenarios.length,
|
|
850
|
-
missingScenarios,
|
|
851
|
-
deterministic: warnings.length === 0 ? "pass" : "warnings",
|
|
852
|
-
semantic: "needs-review",
|
|
853
|
-
archiveReady,
|
|
854
|
-
warnings
|
|
855
|
-
};
|
|
422
|
+
function validateDecisionRecordsPath(pathValue) {
|
|
423
|
+
if (typeof pathValue !== "string" || pathValue.trim() === "")
|
|
424
|
+
return 'config.yaml field "decisions.path" must be a non-empty string';
|
|
425
|
+
const raw = pathValue.trim();
|
|
426
|
+
const portable = raw.replace(/\\/g, "/");
|
|
427
|
+
if (isAbsolute(raw) || posix.isAbsolute(portable) || /^[a-z]:[\\/]/i.test(raw))
|
|
428
|
+
return 'config.yaml field "decisions.path" must be project-relative';
|
|
429
|
+
const normalized = normalizeDecisionRecordsPath(raw);
|
|
430
|
+
if (normalized === "." || normalized === ".." || normalized.startsWith("../"))
|
|
431
|
+
return 'config.yaml field "decisions.path" must stay inside the Host Project';
|
|
432
|
+
if ((normalized === ".rsp" || normalized.startsWith(".rsp/")) && normalized !== DEFAULT_DECISION_RECORDS_PATH)
|
|
433
|
+
return `config.yaml field "decisions.path" must use ${DEFAULT_DECISION_RECORDS_PATH} or a path outside .rsp/`;
|
|
434
|
+
return null;
|
|
856
435
|
}
|
|
857
|
-
function
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
"Create or update a Decision Record"
|
|
868
|
-
],
|
|
869
|
-
factCandidateTargets,
|
|
870
|
-
decisionRecordsPath,
|
|
871
|
-
note: "Semantic review decides current-fact and lasting-rationale updates independently. The CLI never infers scoped instruction or Decision Record filenames and never promotes Change content automatically."
|
|
872
|
-
};
|
|
436
|
+
function getDecisionRecordsConfigIssue(config) {
|
|
437
|
+
if (!("decisions" in config))
|
|
438
|
+
return null;
|
|
439
|
+
const decisions = config.decisions;
|
|
440
|
+
if (!decisions || typeof decisions !== "object" || Array.isArray(decisions))
|
|
441
|
+
return 'config.yaml field "decisions" must be a mapping with one path';
|
|
442
|
+
const keys = Object.keys(decisions);
|
|
443
|
+
if (keys.length !== 1 || keys[0] !== "path")
|
|
444
|
+
return 'config.yaml field "decisions" supports only "path"';
|
|
445
|
+
return validateDecisionRecordsPath(decisions.path);
|
|
873
446
|
}
|
|
874
|
-
function
|
|
875
|
-
|
|
447
|
+
function resolveDecisionRecordsPath(config) {
|
|
448
|
+
const configured = config.decisions?.path;
|
|
449
|
+
return configured && validateDecisionRecordsPath(configured) === null ? normalizeDecisionRecordsPath(configured) : DEFAULT_DECISION_RECORDS_PATH;
|
|
876
450
|
}
|
|
877
|
-
function
|
|
878
|
-
|
|
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;
|
|
879
465
|
}
|
|
880
|
-
function
|
|
881
|
-
const
|
|
882
|
-
const
|
|
883
|
-
let
|
|
884
|
-
while ((
|
|
885
|
-
const
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
const block = nextSection ? restContent.slice(0, nextSection.index) : restContent;
|
|
890
|
-
const steps = block.split("\n").map((s) => s.trim()).filter((s) => s.startsWith("-") && /GIVEN|WHEN|THEN|AND|BUT/i.test(s)).map((s) => s.replace(/^[-*]\s*/, ""));
|
|
891
|
-
if (steps.length > 0)
|
|
892
|
-
scenarios.push({ heading, steps });
|
|
466
|
+
async function validateDecisionRecordsFilesystemPath(pathValue, cwd = process.cwd()) {
|
|
467
|
+
const projectRoot = await realpath(cwd);
|
|
468
|
+
const targetPath = resolve(projectRoot, pathValue);
|
|
469
|
+
let existingAncestor = targetPath;
|
|
470
|
+
while (!existsSync(existingAncestor)) {
|
|
471
|
+
const parent = dirname(existingAncestor);
|
|
472
|
+
if (parent === existingAncestor)
|
|
473
|
+
return `Decision Record path "${pathValue}" has no accessible Host Project ancestor`;
|
|
474
|
+
existingAncestor = parent;
|
|
893
475
|
}
|
|
894
|
-
|
|
476
|
+
const resolvedAncestor = await realpath(existingAncestor);
|
|
477
|
+
const rel = relative(projectRoot, resolvedAncestor);
|
|
478
|
+
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel))
|
|
479
|
+
return `Decision Record path "${pathValue}" resolves outside the Host Project through ${existingAncestor}`;
|
|
480
|
+
if (existsSync(targetPath) && !(await stat(targetPath)).isDirectory())
|
|
481
|
+
return `Decision Record path "${pathValue}" must be a directory`;
|
|
482
|
+
return null;
|
|
895
483
|
}
|
|
896
484
|
|
|
897
485
|
// src/core/config.ts
|
|
898
|
-
var __dirname =
|
|
899
|
-
var PKG_ROOT =
|
|
486
|
+
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
487
|
+
var PKG_ROOT = join3(__dirname, "..");
|
|
900
488
|
var RSP_DIR = ".rsp";
|
|
901
|
-
var RSP_RULES_PATH =
|
|
902
|
-
var OBSOLETE_RSP_RULES_PATH =
|
|
903
|
-
var CHANGES_DIR =
|
|
904
|
-
var FOCUS_DIR =
|
|
905
|
-
var
|
|
906
|
-
var
|
|
907
|
-
var
|
|
489
|
+
var RSP_RULES_PATH = join3(RSP_DIR, "rsp-rules.md");
|
|
490
|
+
var OBSOLETE_RSP_RULES_PATH = join3(RSP_DIR, "rules", "rsp-rules.md");
|
|
491
|
+
var CHANGES_DIR = join3(RSP_DIR, "changes");
|
|
492
|
+
var FOCUS_DIR = join3(RSP_DIR, "focus.d");
|
|
493
|
+
var MAX_FOCUS_CAPSULE_BYTES = 4096;
|
|
494
|
+
var ARCHIVES_DIR = join3(RSP_DIR, "archives");
|
|
495
|
+
var LOCK_PATH = join3(RSP_DIR, ".lock");
|
|
496
|
+
var CONFIG_PATH = join3(RSP_DIR, "config.yaml");
|
|
908
497
|
var VALID_KINDS = ["feature", "fix", "refactor", "docs", "ops", "research"];
|
|
909
498
|
var DEFAULT_REQUIRED_SECTIONS = getCanonicalSectionHeadings(CHANGE_DOCUMENT_SCHEMA);
|
|
910
|
-
var
|
|
911
|
-
var
|
|
499
|
+
var LEGACY_MISSING_CONFIG_MANAGE_POLICY = { activation: "explicit", closeout: "local" };
|
|
500
|
+
var INVALID_CONFIG_MANAGE_POLICY = { activation: "explicit", closeout: "manual" };
|
|
501
|
+
var CONFIG_DEFAULTS = {
|
|
502
|
+
kinds: [],
|
|
503
|
+
decisions: { path: ".rsp/specs/decisions" },
|
|
504
|
+
language: { default: "en" },
|
|
505
|
+
manage: { activation: "auto", closeout: "local" }
|
|
506
|
+
};
|
|
507
|
+
function generateConfigTemplate(config = {}) {
|
|
508
|
+
const kinds = config.kinds ?? CONFIG_DEFAULTS.kinds;
|
|
509
|
+
const decisions = { ...CONFIG_DEFAULTS.decisions, ...config.decisions };
|
|
510
|
+
const manage = { ...CONFIG_DEFAULTS.manage, ...config.manage };
|
|
511
|
+
const language = { ...CONFIG_DEFAULTS.language, ...config.language };
|
|
512
|
+
const kindsYaml = stringify(kinds).trimEnd();
|
|
513
|
+
const renderedKinds = kinds.length === 0 ? `kinds: ${kindsYaml}` : `kinds:
|
|
514
|
+
${kindsYaml.split("\n").map((line) => ` ${line}`).join("\n")}`;
|
|
515
|
+
const artifactsLine = language.artifacts ? ` artifacts: ${stringify(language.artifacts).trim()}` : " # artifacts: zh-CN";
|
|
516
|
+
const commitLine = language.commit ? ` commit: ${stringify(language.commit).trim()}` : " # commit: zh-CN";
|
|
517
|
+
return `# RSP project configuration
|
|
518
|
+
# An empty kinds list retains the built-in defaults.
|
|
519
|
+
# A non-empty kinds list replaces the built-in defaults; it does not extend them.
|
|
520
|
+
# Every entry must be a unique non-empty string.
|
|
521
|
+
#
|
|
522
|
+
# Built-in defaults:
|
|
523
|
+
# kinds: ${VALID_KINDS.join(", ")}
|
|
524
|
+
#
|
|
525
|
+
${renderedKinds}
|
|
526
|
+
|
|
527
|
+
# Decision Records default to .rsp/specs/decisions.
|
|
528
|
+
decisions:
|
|
529
|
+
path: ${stringify(decisions.path).trim()}
|
|
530
|
+
|
|
531
|
+
# New projects default to automatic Manage routing with local closeout.
|
|
532
|
+
# Use activation: explicit to require a named managed request.
|
|
533
|
+
# closeout accepts manual, lifecycle, or local; local routes a qualified clean terminal non-small boundary to one local commit but never push or publication.
|
|
534
|
+
manage:
|
|
535
|
+
activation: ${manage.activation}
|
|
536
|
+
closeout: ${manage.closeout}
|
|
537
|
+
|
|
538
|
+
# Set one shared project default for durable artifact and commit prose.
|
|
539
|
+
# Response language remains user/session-owned and is never read from this project mapping.
|
|
540
|
+
language:
|
|
541
|
+
default: ${stringify(language.default).trim()}
|
|
542
|
+
${artifactsLine}
|
|
543
|
+
${commitLine}
|
|
544
|
+
`;
|
|
545
|
+
}
|
|
912
546
|
var MANAGE_ACTIVATIONS = ["explicit", "auto"];
|
|
913
547
|
var MANAGE_CLOSEOUTS = ["manual", "lifecycle", "local"];
|
|
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
|
+
}
|
|
914
642
|
var _configCache = null;
|
|
915
643
|
async function loadRspConfig() {
|
|
916
644
|
const inspection = await inspectRspConfig();
|
|
@@ -918,11 +646,12 @@ async function loadRspConfig() {
|
|
|
918
646
|
throw new Error(inspection.issues.join("; "));
|
|
919
647
|
return inspection.config;
|
|
920
648
|
}
|
|
921
|
-
async function inspectRspConfig() {
|
|
922
|
-
|
|
649
|
+
async function inspectRspConfig(cwd = process.cwd()) {
|
|
650
|
+
cwd = resolve2(cwd);
|
|
923
651
|
if (_configCache?.cwd === cwd)
|
|
924
652
|
return _configCache.inspection;
|
|
925
|
-
const
|
|
653
|
+
const configPath = join3(cwd, CONFIG_PATH);
|
|
654
|
+
const configFile = inspectManagedFile(configPath, "config file", { allowMissing: true });
|
|
926
655
|
if (configFile.issue)
|
|
927
656
|
throw configFile.issue;
|
|
928
657
|
if (!configFile.exists) {
|
|
@@ -930,7 +659,7 @@ async function inspectRspConfig() {
|
|
|
930
659
|
_configCache = { cwd, inspection: inspection2 };
|
|
931
660
|
return inspection2;
|
|
932
661
|
}
|
|
933
|
-
const raw = await
|
|
662
|
+
const raw = await readFile(configPath, "utf-8");
|
|
934
663
|
const parsed = parseYamlText(raw);
|
|
935
664
|
const issues = validateRspConfig(parsed);
|
|
936
665
|
const decisions = parsed.decisions && typeof parsed.decisions === "object" && !Array.isArray(parsed.decisions) ? parsed.decisions : void 0;
|
|
@@ -1025,10 +754,10 @@ function resolveRequiredSections(_config) {
|
|
|
1025
754
|
}
|
|
1026
755
|
function resolveManagePolicy(config, options = {}) {
|
|
1027
756
|
if (options.configValid === false)
|
|
1028
|
-
return { ...
|
|
757
|
+
return { ...INVALID_CONFIG_MANAGE_POLICY };
|
|
1029
758
|
return {
|
|
1030
|
-
activation: config.manage?.activation ??
|
|
1031
|
-
closeout: config.manage?.closeout ??
|
|
759
|
+
activation: config.manage?.activation ?? LEGACY_MISSING_CONFIG_MANAGE_POLICY.activation,
|
|
760
|
+
closeout: config.manage?.closeout ?? LEGACY_MISSING_CONFIG_MANAGE_POLICY.closeout
|
|
1032
761
|
};
|
|
1033
762
|
}
|
|
1034
763
|
function resolveLanguagePolicy(config, options = {}) {
|
|
@@ -1063,11 +792,12 @@ function canonicalizeLanguageTag(value) {
|
|
|
1063
792
|
}
|
|
1064
793
|
}
|
|
1065
794
|
async function getVersion() {
|
|
1066
|
-
const { version } = JSON.parse(await
|
|
795
|
+
const { version } = JSON.parse(await readFile(join3(PKG_ROOT, "package.json"), "utf-8"));
|
|
1067
796
|
return version;
|
|
1068
797
|
}
|
|
1069
798
|
|
|
1070
799
|
// src/core/output.ts
|
|
800
|
+
import pc2 from "picocolors";
|
|
1071
801
|
function toErrorMessage(error) {
|
|
1072
802
|
return error instanceof Error ? error.message : String(error);
|
|
1073
803
|
}
|
|
@@ -1076,27 +806,8 @@ function emitJson(data, options = {}) {
|
|
|
1076
806
|
process.stdout.write(`${JSON.stringify(data, null, indentation)}
|
|
1077
807
|
`);
|
|
1078
808
|
}
|
|
1079
|
-
function recordRuntimeDiagnostic(runtime, diagnostic, verbose = false) {
|
|
1080
|
-
runtime.push(diagnostic);
|
|
1081
|
-
if (verbose) {
|
|
1082
|
-
console.error(` ${pc.dim(`[verbose] ${diagnostic.operation} ${diagnostic.path}: ${diagnostic.message}`)}`);
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
809
|
|
|
1086
810
|
export {
|
|
1087
|
-
ManagedPathError,
|
|
1088
|
-
inspectManagedDirectory,
|
|
1089
|
-
requireManagedDirectory,
|
|
1090
|
-
inspectManagedFile,
|
|
1091
|
-
requireManagedFile,
|
|
1092
|
-
ensureManagedFile,
|
|
1093
|
-
writeManagedFile,
|
|
1094
|
-
resolveManagedDirectoryChain,
|
|
1095
|
-
inspectManagedFileTree,
|
|
1096
|
-
DEFAULT_DECISION_RECORDS_PATH,
|
|
1097
|
-
resolveDecisionRecordsPath,
|
|
1098
|
-
ensureDecisionRecordsDirectory,
|
|
1099
|
-
validateDecisionRecordsFilesystemPath,
|
|
1100
811
|
CHANGE_DOCUMENT_SCHEMA,
|
|
1101
812
|
GROUP_BRIEF_DOCUMENT_SCHEMA,
|
|
1102
813
|
DocumentSectionCardinalityError,
|
|
@@ -1110,32 +821,30 @@ export {
|
|
|
1110
821
|
renderDocumentSectionHeading,
|
|
1111
822
|
renderDocumentTitle,
|
|
1112
823
|
appendDocumentSectionItem,
|
|
1113
|
-
toErrorMessage,
|
|
1114
|
-
emitJson,
|
|
1115
|
-
recordRuntimeDiagnostic,
|
|
1116
824
|
parseFrontmatter,
|
|
1117
825
|
countCheckboxes,
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
generateChangeContent,
|
|
1122
|
-
upsertRspAgentsBlock,
|
|
1123
|
-
hasRspAgentsBlock,
|
|
1124
|
-
isValidSpecName,
|
|
1125
|
-
guardRspInitialized,
|
|
1126
|
-
cleanupEmptyParentDirs,
|
|
1127
|
-
detectProjectName,
|
|
1128
|
-
generateDesignContent,
|
|
1129
|
-
generateSpecContent,
|
|
826
|
+
classifyVerifyCheckboxes,
|
|
827
|
+
collectVerifyCheckboxLines,
|
|
828
|
+
getOpenCheckboxes,
|
|
1130
829
|
extractBlockerLines,
|
|
1131
830
|
hasMeaningfulBlockerBody,
|
|
1132
831
|
hasMeaningfulBlockers,
|
|
832
|
+
isEmptyBlockerLine,
|
|
1133
833
|
detectDeltaSections,
|
|
1134
|
-
collectArchiveReadiness,
|
|
1135
|
-
buildDurableReviewGuidance,
|
|
1136
|
-
getDurableReviewCandidateTargets,
|
|
1137
|
-
collectArchiveChecklist,
|
|
1138
834
|
parseScenarios,
|
|
835
|
+
ManagedPathError,
|
|
836
|
+
inspectManagedDirectory,
|
|
837
|
+
requireManagedDirectory,
|
|
838
|
+
inspectManagedFile,
|
|
839
|
+
requireManagedFile,
|
|
840
|
+
ensureManagedFile,
|
|
841
|
+
writeManagedFile,
|
|
842
|
+
resolveManagedDirectoryChain,
|
|
843
|
+
inspectManagedFileTree,
|
|
844
|
+
DEFAULT_DECISION_RECORDS_PATH,
|
|
845
|
+
resolveDecisionRecordsPath,
|
|
846
|
+
ensureDecisionRecordsDirectory,
|
|
847
|
+
validateDecisionRecordsFilesystemPath,
|
|
1139
848
|
pc,
|
|
1140
849
|
PKG_ROOT,
|
|
1141
850
|
RSP_DIR,
|
|
@@ -1143,10 +852,12 @@ export {
|
|
|
1143
852
|
OBSOLETE_RSP_RULES_PATH,
|
|
1144
853
|
CHANGES_DIR,
|
|
1145
854
|
FOCUS_DIR,
|
|
855
|
+
MAX_FOCUS_CAPSULE_BYTES,
|
|
1146
856
|
ARCHIVES_DIR,
|
|
1147
857
|
LOCK_PATH,
|
|
1148
858
|
CONFIG_PATH,
|
|
1149
|
-
|
|
859
|
+
generateConfigTemplate,
|
|
860
|
+
reconcileRspConfigDefaults,
|
|
1150
861
|
loadRspConfig,
|
|
1151
862
|
inspectRspConfig,
|
|
1152
863
|
clearConfigCache,
|
|
@@ -1154,5 +865,7 @@ export {
|
|
|
1154
865
|
resolveRequiredSections,
|
|
1155
866
|
resolveManagePolicy,
|
|
1156
867
|
resolveLanguagePolicy,
|
|
1157
|
-
getVersion
|
|
868
|
+
getVersion,
|
|
869
|
+
toErrorMessage,
|
|
870
|
+
emitJson
|
|
1158
871
|
};
|