@oevery/rsp 3.2.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -17
- package/README.zh-CN.md +25 -17
- package/dist/chunk-HFSTLCCK.mjs +3968 -0
- package/dist/cli.mjs +3510 -2446
- package/dist/entry-RIJQ2X46.mjs +1932 -0
- package/package.json +32 -19
- package/skills/rsp/SKILL.md +42 -68
- package/skills/rsp/references/closeout.md +38 -0
- package/skills/rsp/references/context-migration.md +22 -0
- package/skills/rsp/references/control-outcome.md +5 -0
- 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 +74 -9
- package/skills/rsp/references/focus-continuation.md +13 -0
- package/skills/rsp/references/groups-dependencies.md +6 -4
- package/skills/rsp/references/response-language.md +13 -0
- package/skills/rsp/references/setup-repair.md +37 -7
- package/skills/rsp/references/work-model.md +27 -0
- package/skills/rsp-commit/SKILL.md +82 -22
- package/skills/rsp-commit/references/message.md +34 -0
- package/skills/rsp-doc/SKILL.md +65 -0
- package/skills/rsp-doc/agents/openai.yaml +3 -0
- package/skills/rsp-doc/references/artifact-lenses.md +43 -0
- package/skills/rsp-doc/references/release-communication.md +39 -0
- package/skills/rsp-doc/references/skill-authoring.md +36 -0
- package/skills/rsp-implement/SKILL.md +62 -21
- 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 +44 -46
- package/skills/rsp-review/references/code-review.md +23 -7
- package/skills/rsp-review/references/document-review.md +25 -8
- package/skills/rsp-review/references/release-communication.md +21 -0
- package/skills/{rsp-structural-audit/references/structural-lenses.md → rsp-review/references/structural-review.md} +5 -5
- package/skills/rsp-shape/SKILL.md +24 -45
- package/skills/rsp-shape/references/complex-shaping.md +3 -1
- package/skills/rsp-shape/references/deep-clarification.md +4 -4
- package/skills/rsp-shape/references/domain-modeling.md +13 -0
- package/skills/rsp-shape/references/module-seams.md +13 -0
- package/skills/rsp-shape/references/planning.md +42 -0
- package/skills/{rsp-design → rsp-shape}/references/reversible-exploration.md +2 -2
- package/skills/rsp-verify/SKILL.md +72 -0
- package/dist/chunk-JOMFFQRG.mjs +0 -99
- package/dist/chunk-JYDEJEM4.mjs +0 -358
- package/dist/chunk-SEGJLWRE.mjs +0 -1158
- package/dist/chunk-ZORUJTVS.mjs +0 -1967
- package/dist/entry-I22OUIW5.mjs +0 -888
- package/dist/entry-RG6HREUN.mjs +0 -174
- package/rules/rsp-rules.md +0 -47
- package/skills/rsp/references/managed-routing.md +0 -44
- package/skills/rsp/references/release-operations.md +0 -9
- 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 -46
- package/skills/rsp-manage/SKILL.md +0 -96
- package/skills/rsp-manage/references/closeout.md +0 -11
- package/skills/rsp-manage/references/interruption-recovery.md +0 -7
- package/skills/rsp-manage/references/review-convergence.md +0 -7
- 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 -24
- package/skills/rsp-resolve-findings/SKILL.md +0 -61
- package/skills/rsp-structural-audit/SKILL.md +0 -71
- package/skills/rsp-tdd/SKILL.md +0 -48
package/dist/chunk-ZORUJTVS.mjs
DELETED
|
@@ -1,1967 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
ARCHIVES_DIR,
|
|
4
|
-
CHANGES_DIR,
|
|
5
|
-
CHANGE_DOCUMENT_SCHEMA,
|
|
6
|
-
CONFIG_PATH,
|
|
7
|
-
FOCUS_DIR,
|
|
8
|
-
GROUP_BRIEF_DOCUMENT_SCHEMA,
|
|
9
|
-
ManagedPathError,
|
|
10
|
-
collectArchiveReadiness,
|
|
11
|
-
countCheckboxes,
|
|
12
|
-
extractBlockerLines,
|
|
13
|
-
getCanonicalSectionHeadings,
|
|
14
|
-
getDocumentSectionBody,
|
|
15
|
-
getDocumentTitle,
|
|
16
|
-
getDocumentTitles,
|
|
17
|
-
hasMeaningfulBlockerBody,
|
|
18
|
-
hasMeaningfulBlockers,
|
|
19
|
-
inspectManagedDirectory,
|
|
20
|
-
inspectRspConfig,
|
|
21
|
-
normalizeLogicalPath,
|
|
22
|
-
parseFrontmatter,
|
|
23
|
-
parseRspDocument,
|
|
24
|
-
parseScenarios,
|
|
25
|
-
renderDocumentSectionHeading,
|
|
26
|
-
renderDocumentTitle,
|
|
27
|
-
requireManagedDirectory,
|
|
28
|
-
requireManagedFile,
|
|
29
|
-
resolveLanguagePolicy,
|
|
30
|
-
resolveManagePolicy,
|
|
31
|
-
toErrorMessage
|
|
32
|
-
} from "./chunk-SEGJLWRE.mjs";
|
|
33
|
-
|
|
34
|
-
// src/status/inspect.ts
|
|
35
|
-
import { readFile as readFile5, stat } from "fs/promises";
|
|
36
|
-
|
|
37
|
-
// src/core/change-group.ts
|
|
38
|
-
import { readFile } from "fs/promises";
|
|
39
|
-
import { dirname, join as join2, relative as relative2 } from "path";
|
|
40
|
-
|
|
41
|
-
// src/core/work-ref.ts
|
|
42
|
-
import { Buffer } from "buffer";
|
|
43
|
-
import { lstatSync, readdirSync } from "fs";
|
|
44
|
-
import { readdir } from "fs/promises";
|
|
45
|
-
import { isAbsolute, join, relative } from "path";
|
|
46
|
-
var WORK_SEGMENT_RE = /^[\p{Ll}\p{Lo}\p{Lm}\p{M}\p{Nd}]+(?:-[\p{Ll}\p{Lo}\p{Lm}\p{M}\p{Nd}]+)*$/u;
|
|
47
|
-
var WORK_SEGMENT_MAX_CODE_POINTS = 100;
|
|
48
|
-
var WORK_SEGMENT_MAX_UTF8_BYTES = 200;
|
|
49
|
-
var GROUP_BRIEF_FILENAME = "00-brief.md";
|
|
50
|
-
var WorkRefError = class extends Error {
|
|
51
|
-
constructor(code, message, input) {
|
|
52
|
-
super(message);
|
|
53
|
-
this.code = code;
|
|
54
|
-
this.input = input;
|
|
55
|
-
this.name = "WorkRefError";
|
|
56
|
-
}
|
|
57
|
-
code;
|
|
58
|
-
input;
|
|
59
|
-
};
|
|
60
|
-
function normalizeWorkRefSegment(segment) {
|
|
61
|
-
const normalized = segment.normalize("NFC");
|
|
62
|
-
if (!WORK_SEGMENT_RE.test(normalized) || [...normalized].length > WORK_SEGMENT_MAX_CODE_POINTS || Buffer.byteLength(normalized, "utf8") > WORK_SEGMENT_MAX_UTF8_BYTES) {
|
|
63
|
-
throw new WorkRefError(
|
|
64
|
-
"invalid_work_ref",
|
|
65
|
-
`work identity segment "${segment}" must contain at most ${WORK_SEGMENT_MAX_CODE_POINTS} lowercase or caseless Unicode letters, marks, or decimal numbers separated only by internal hyphens and fit within ${WORK_SEGMENT_MAX_UTF8_BYTES} UTF-8 bytes`,
|
|
66
|
-
segment
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
return normalized;
|
|
70
|
-
}
|
|
71
|
-
function normalizeExecutableWorkRef(name) {
|
|
72
|
-
return normalizeWorkRefName(name, false);
|
|
73
|
-
}
|
|
74
|
-
function isCanonicalExecutableWorkRef(name) {
|
|
75
|
-
try {
|
|
76
|
-
return normalizeExecutableWorkRef(name) === name;
|
|
77
|
-
} catch {
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
function isCanonicalWorkRefSegment(segment) {
|
|
82
|
-
try {
|
|
83
|
-
return normalizeWorkRefSegment(segment) === segment;
|
|
84
|
-
} catch {
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
function resolveWorkRef(name, options = {}) {
|
|
89
|
-
const changesDir = options.changesDir ?? CHANGES_DIR;
|
|
90
|
-
const normalizedName = normalizeWorkRefName(name, true);
|
|
91
|
-
if (options.canonical && normalizedName !== name) {
|
|
92
|
-
throw new WorkRefError(
|
|
93
|
-
"invalid_work_ref",
|
|
94
|
-
`stored work identity "${name}" must use Unicode NFC normalization`,
|
|
95
|
-
name
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
const segments = normalizedName.split("/");
|
|
99
|
-
const isGroupBrief = segments.length === 2 && segments[1] === "brief";
|
|
100
|
-
assertValidWorkRoot(changesDir, normalizedName);
|
|
101
|
-
assertNoNormalizationCollision(changesDir, segments, normalizedName, isGroupBrief);
|
|
102
|
-
assertNoIdentityCollision(changesDir, segments, normalizedName, isGroupBrief ? GROUP_BRIEF_FILENAME : void 0);
|
|
103
|
-
const path = join(changesDir, ...segments.slice(0, -1), isGroupBrief ? GROUP_BRIEF_FILENAME : `${segments.at(-1)}.md`);
|
|
104
|
-
const ref = segments.length === 1 ? { kind: "change", name: normalizedName, path, group: null } : isGroupBrief ? { kind: "group-brief", name: normalizedName, path, group: segments[0] } : { kind: "group-change", name: normalizedName, path, group: segments[0] };
|
|
105
|
-
if (ref.kind === "group-change" && options.executable !== false)
|
|
106
|
-
assertGroupBrief(changesDir, ref);
|
|
107
|
-
if (options.executable && ref.kind === "group-brief") {
|
|
108
|
-
throw new WorkRefError(
|
|
109
|
-
"non_executable_work_ref",
|
|
110
|
-
`work identity "${name}" is a Group Brief and cannot be used as an executable Change`,
|
|
111
|
-
name
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
assertRegularWorkFile(ref, Boolean(options.mustExist));
|
|
115
|
-
return ref;
|
|
116
|
-
}
|
|
117
|
-
function assertGroupBrief(changesDir, ref) {
|
|
118
|
-
const briefPath = join(changesDir, ref.group, GROUP_BRIEF_FILENAME);
|
|
119
|
-
const kind = getPathKind(briefPath, ref.name);
|
|
120
|
-
if (kind === "missing") {
|
|
121
|
-
throw new WorkRefError(
|
|
122
|
-
"group_brief_missing",
|
|
123
|
-
`Change Group "${ref.group}" requires a Group Brief; run: rsp group create ${ref.group}`,
|
|
124
|
-
ref.name
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
if (kind !== "file") {
|
|
128
|
-
throw new WorkRefError(
|
|
129
|
-
"work_ref_not_file",
|
|
130
|
-
`Group Brief must be a regular file: ${briefPath}`,
|
|
131
|
-
ref.name
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
function assertRegularWorkFile(ref, mustExist) {
|
|
136
|
-
let stats;
|
|
137
|
-
try {
|
|
138
|
-
stats = lstatSync(ref.path);
|
|
139
|
-
} catch (error) {
|
|
140
|
-
if (error.code === "ENOENT") {
|
|
141
|
-
if (mustExist) {
|
|
142
|
-
throw new WorkRefError(
|
|
143
|
-
"work_ref_not_found",
|
|
144
|
-
`open work file not found: ${ref.path}`,
|
|
145
|
-
ref.name
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
throw asReadFailure(ref.path, ref.name, error);
|
|
151
|
-
}
|
|
152
|
-
if (!stats.isFile()) {
|
|
153
|
-
throw new WorkRefError(
|
|
154
|
-
"work_ref_not_file",
|
|
155
|
-
`open work path is not a regular file: ${ref.path}`,
|
|
156
|
-
ref.name
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
function resolveWorkRefPath(path, options = {}) {
|
|
161
|
-
const changesDir = options.changesDir ?? CHANGES_DIR;
|
|
162
|
-
const logicalPath = normalizeLogicalPath2(relative(changesDir, path));
|
|
163
|
-
if (isAbsolute(logicalPath) || logicalPath === ".." || logicalPath.startsWith("../") || !logicalPath.endsWith(".md")) {
|
|
164
|
-
throw new WorkRefError(
|
|
165
|
-
"invalid_work_ref_path",
|
|
166
|
-
`work path must be a Markdown file inside ${changesDir}: ${path}`,
|
|
167
|
-
path
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
const segments = logicalPath.split("/");
|
|
171
|
-
if (segments.length === 2 && segments[1] === "brief.md") {
|
|
172
|
-
throw new WorkRefError(
|
|
173
|
-
"invalid_work_ref_path",
|
|
174
|
-
`unsupported legacy Group Brief path: ${path}`,
|
|
175
|
-
path
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
const name = segments.length === 2 && segments[1] === GROUP_BRIEF_FILENAME ? `${segments[0]}/brief` : logicalPath.slice(0, -3);
|
|
179
|
-
return resolveWorkRef(name, { ...options, canonical: true });
|
|
180
|
-
}
|
|
181
|
-
function isExecutableWorkRef(ref) {
|
|
182
|
-
return ref.kind !== "group-brief";
|
|
183
|
-
}
|
|
184
|
-
function resolveFocusMarkerPath(ref, options = {}) {
|
|
185
|
-
const focusDir = options.focusDir ?? FOCUS_DIR;
|
|
186
|
-
assertManagedRoot(focusDir, ref.name, "invalid_focus_root", "focus root", true);
|
|
187
|
-
assertNoManagedNormalizationCollision(focusDir, ref.name.split("/"), ref.name, "invalid_focus_path");
|
|
188
|
-
if (ref.group) {
|
|
189
|
-
assertOptionalManagedDirectory(
|
|
190
|
-
join(focusDir, ref.group),
|
|
191
|
-
ref.name,
|
|
192
|
-
"invalid_focus_path",
|
|
193
|
-
"focus path"
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
const markerPath = join(focusDir, ...ref.name.split("/"));
|
|
197
|
-
try {
|
|
198
|
-
requireManagedFile(markerPath, "focus marker", { allowMissing: true });
|
|
199
|
-
} catch (error) {
|
|
200
|
-
if (error instanceof ManagedPathError)
|
|
201
|
-
throw new WorkRefError("invalid_focus_path", error.message, ref.name);
|
|
202
|
-
throw error;
|
|
203
|
-
}
|
|
204
|
-
return markerPath;
|
|
205
|
-
}
|
|
206
|
-
function resolveArchiveDirectory(ref, options = {}) {
|
|
207
|
-
const archivesDir = options.archivesDir ?? ARCHIVES_DIR;
|
|
208
|
-
assertManagedRoot(archivesDir, ref.name, "invalid_archive_root", "archive root", true);
|
|
209
|
-
if (!ref.group)
|
|
210
|
-
return archivesDir;
|
|
211
|
-
assertNoManagedNormalizationCollision(archivesDir, [ref.group], ref.name, "invalid_archive_path");
|
|
212
|
-
const groupPath = join(archivesDir, ref.group);
|
|
213
|
-
assertOptionalManagedDirectory(groupPath, ref.name, "invalid_archive_path", "archive path");
|
|
214
|
-
return groupPath;
|
|
215
|
-
}
|
|
216
|
-
async function inspectFocusTree(options = {}) {
|
|
217
|
-
const focusDir = options.focusDir ?? FOCUS_DIR;
|
|
218
|
-
const changesDir = options.changesDir ?? CHANGES_DIR;
|
|
219
|
-
const result = { markers: [], diagnostics: [] };
|
|
220
|
-
const rootInspection = inspectManagedDirectory(focusDir, "focus root", { allowMissing: true });
|
|
221
|
-
if (rootInspection.issue) {
|
|
222
|
-
addDiagnostic(result, new WorkRefError("invalid_focus_root", rootInspection.issue.message, focusDir), focusDir);
|
|
223
|
-
return result;
|
|
224
|
-
}
|
|
225
|
-
if (!rootInspection.exists)
|
|
226
|
-
return result;
|
|
227
|
-
const rootEntries = await readDirectory(focusDir, focusDir, result);
|
|
228
|
-
if (!rootEntries)
|
|
229
|
-
return result;
|
|
230
|
-
for (const entry of rootEntries) {
|
|
231
|
-
const path = join(focusDir, entry.name);
|
|
232
|
-
if (entry.name === ".gitkeep" && entry.isFile())
|
|
233
|
-
continue;
|
|
234
|
-
if (entry.isDirectory()) {
|
|
235
|
-
if (!isCanonicalWorkRefSegment(entry.name)) {
|
|
236
|
-
addDiagnostic(result, new WorkRefError(
|
|
237
|
-
"invalid_work_ref",
|
|
238
|
-
`stored work identity "${entry.name}" must be a safe Unicode NFC segment`,
|
|
239
|
-
entry.name
|
|
240
|
-
), path);
|
|
241
|
-
continue;
|
|
242
|
-
}
|
|
243
|
-
await inspectFocusGroup(entry.name, path, changesDir, result);
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
246
|
-
if (!entry.isFile()) {
|
|
247
|
-
addDiagnostic(result, new WorkRefError(
|
|
248
|
-
"invalid_focus_path",
|
|
249
|
-
`focus path must be a regular marker file or real directory: ${path}`,
|
|
250
|
-
entry.name
|
|
251
|
-
), path);
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
collectFocusMarker(entry.name, path, changesDir, result);
|
|
255
|
-
}
|
|
256
|
-
result.markers.sort((a, b) => a.name.localeCompare(b.name));
|
|
257
|
-
return result;
|
|
258
|
-
}
|
|
259
|
-
async function inspectArchiveTree(options = {}) {
|
|
260
|
-
const archivesDir = options.archivesDir ?? ARCHIVES_DIR;
|
|
261
|
-
const result = { rootExists: false, files: [], diagnostics: [] };
|
|
262
|
-
const rootInspection = inspectManagedDirectory(archivesDir, "archive root", { allowMissing: true });
|
|
263
|
-
if (rootInspection.issue) {
|
|
264
|
-
addDiagnostic(result, new WorkRefError("invalid_archive_root", rootInspection.issue.message, archivesDir), archivesDir);
|
|
265
|
-
return result;
|
|
266
|
-
}
|
|
267
|
-
if (!rootInspection.exists)
|
|
268
|
-
return result;
|
|
269
|
-
result.rootExists = true;
|
|
270
|
-
const rootEntries = await readDirectory(archivesDir, archivesDir, result);
|
|
271
|
-
if (!rootEntries)
|
|
272
|
-
return result;
|
|
273
|
-
for (const entry of rootEntries) {
|
|
274
|
-
const path = join(archivesDir, entry.name);
|
|
275
|
-
if (entry.name === "INDEX.md" && entry.isFile())
|
|
276
|
-
continue;
|
|
277
|
-
if (entry.isDirectory()) {
|
|
278
|
-
if (!isCanonicalWorkRefSegment(entry.name)) {
|
|
279
|
-
addArchivePathDiagnostic(result, entry.name, path);
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
await inspectArchiveGroup(entry.name, path, result);
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
if (!entry.isFile() || !entry.name.endsWith(".md")) {
|
|
286
|
-
addArchivePathDiagnostic(result, entry.name, path);
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
result.files.push(path);
|
|
290
|
-
}
|
|
291
|
-
result.files.sort();
|
|
292
|
-
return result;
|
|
293
|
-
}
|
|
294
|
-
async function inspectWorkTree(options = {}) {
|
|
295
|
-
const changesDir = options.changesDir ?? CHANGES_DIR;
|
|
296
|
-
const result = { changes: [], briefs: [], diagnostics: [] };
|
|
297
|
-
try {
|
|
298
|
-
assertValidWorkRoot(changesDir, changesDir);
|
|
299
|
-
} catch (error) {
|
|
300
|
-
if (!(error instanceof WorkRefError))
|
|
301
|
-
throw error;
|
|
302
|
-
addDiagnostic(result, error, changesDir);
|
|
303
|
-
return result;
|
|
304
|
-
}
|
|
305
|
-
const rootEntries = await readDirectory(changesDir, changesDir, result);
|
|
306
|
-
if (!rootEntries)
|
|
307
|
-
return result;
|
|
308
|
-
const rootDirectories = new Set(rootEntries.filter((entry) => entry.isDirectory()).map((entry) => entry.name));
|
|
309
|
-
const rootMarkdownNames = new Set(rootEntries.filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name.slice(0, -3)));
|
|
310
|
-
const collidingGroups = new Set([...rootDirectories].filter((name) => rootMarkdownNames.has(name)));
|
|
311
|
-
for (const entry of rootEntries) {
|
|
312
|
-
const path = join(changesDir, entry.name);
|
|
313
|
-
if (entry.name === ".gitkeep" && entry.isFile())
|
|
314
|
-
continue;
|
|
315
|
-
if (entry.isDirectory()) {
|
|
316
|
-
if (!isCanonicalWorkRefSegment(entry.name)) {
|
|
317
|
-
addDiagnostic(result, new WorkRefError(
|
|
318
|
-
"invalid_work_ref",
|
|
319
|
-
`stored work identity "${entry.name}" must be a safe Unicode NFC segment`,
|
|
320
|
-
entry.name
|
|
321
|
-
), path);
|
|
322
|
-
continue;
|
|
323
|
-
}
|
|
324
|
-
const colliding = collidingGroups.has(entry.name);
|
|
325
|
-
if (colliding) {
|
|
326
|
-
addDiagnostic(result, new WorkRefError(
|
|
327
|
-
"work_ref_collision",
|
|
328
|
-
`work identity "${entry.name}" is claimed by both a file and a directory`,
|
|
329
|
-
entry.name
|
|
330
|
-
), path);
|
|
331
|
-
}
|
|
332
|
-
await inspectGroupDirectory(entry.name, path, changesDir, result, colliding);
|
|
333
|
-
continue;
|
|
334
|
-
}
|
|
335
|
-
if (!entry.isFile() || !entry.name.endsWith(".md")) {
|
|
336
|
-
addInvalidEntryDiagnostic(result, entry.name, path);
|
|
337
|
-
continue;
|
|
338
|
-
}
|
|
339
|
-
const name = entry.name.slice(0, -3);
|
|
340
|
-
if (collidingGroups.has(name)) {
|
|
341
|
-
continue;
|
|
342
|
-
}
|
|
343
|
-
collectResolvedPath(path, changesDir, result);
|
|
344
|
-
}
|
|
345
|
-
result.changes.sort((a, b) => a.name.localeCompare(b.name));
|
|
346
|
-
result.briefs.sort((a, b) => a.name.localeCompare(b.name));
|
|
347
|
-
return result;
|
|
348
|
-
}
|
|
349
|
-
async function inspectGroupDirectory(group, groupPath, changesDir, result, colliding) {
|
|
350
|
-
const entries = await readDirectory(groupPath, group, result);
|
|
351
|
-
if (!entries)
|
|
352
|
-
return;
|
|
353
|
-
const briefEntry = entries.find((entry) => entry.name === GROUP_BRIEF_FILENAME);
|
|
354
|
-
if (!briefEntry) {
|
|
355
|
-
addDiagnostic(result, new WorkRefError(
|
|
356
|
-
"group_brief_missing",
|
|
357
|
-
`Change Group "${group}" requires a Group Brief; run: rsp group create ${group}`,
|
|
358
|
-
group
|
|
359
|
-
), join(groupPath, GROUP_BRIEF_FILENAME));
|
|
360
|
-
} else if (!briefEntry.isFile()) {
|
|
361
|
-
addDiagnostic(result, new WorkRefError(
|
|
362
|
-
"work_ref_not_file",
|
|
363
|
-
`Group Brief must be a regular file: ${join(groupPath, GROUP_BRIEF_FILENAME)}`,
|
|
364
|
-
group
|
|
365
|
-
), join(groupPath, GROUP_BRIEF_FILENAME));
|
|
366
|
-
}
|
|
367
|
-
for (const entry of entries) {
|
|
368
|
-
const input = `${group}/${entry.name}`;
|
|
369
|
-
const path = join(groupPath, entry.name);
|
|
370
|
-
if (entry.name === GROUP_BRIEF_FILENAME && !entry.isFile())
|
|
371
|
-
continue;
|
|
372
|
-
if (entry.name === "brief.md") {
|
|
373
|
-
addInvalidEntryDiagnostic(result, input, path);
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
if (entry.isDirectory()) {
|
|
377
|
-
addDiagnostic(result, new WorkRefError(
|
|
378
|
-
"unsupported_work_depth",
|
|
379
|
-
`work identity "${input}" exceeds the supported one-level Change Group depth`,
|
|
380
|
-
input
|
|
381
|
-
), path);
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
384
|
-
if (!entry.isFile() || !entry.name.endsWith(".md")) {
|
|
385
|
-
addInvalidEntryDiagnostic(result, input, path);
|
|
386
|
-
continue;
|
|
387
|
-
}
|
|
388
|
-
if (!colliding)
|
|
389
|
-
collectResolvedPath(path, changesDir, result);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
async function inspectFocusGroup(group, groupPath, changesDir, result) {
|
|
393
|
-
const entries = await readDirectory(groupPath, group, result);
|
|
394
|
-
if (!entries)
|
|
395
|
-
return;
|
|
396
|
-
for (const entry of entries) {
|
|
397
|
-
const input = `${group}/${entry.name}`;
|
|
398
|
-
const path = join(groupPath, entry.name);
|
|
399
|
-
if (entry.isDirectory()) {
|
|
400
|
-
addDiagnostic(result, new WorkRefError(
|
|
401
|
-
"unsupported_work_depth",
|
|
402
|
-
`work identity "${input}" exceeds the supported one-level Change Group depth`,
|
|
403
|
-
input
|
|
404
|
-
), path);
|
|
405
|
-
continue;
|
|
406
|
-
}
|
|
407
|
-
if (!entry.isFile()) {
|
|
408
|
-
addDiagnostic(result, new WorkRefError(
|
|
409
|
-
"invalid_focus_path",
|
|
410
|
-
`focus path must be a regular marker file: ${path}`,
|
|
411
|
-
input
|
|
412
|
-
), path);
|
|
413
|
-
continue;
|
|
414
|
-
}
|
|
415
|
-
collectFocusMarker(input, path, changesDir, result);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
async function inspectArchiveGroup(group, groupPath, result) {
|
|
419
|
-
const entries = await readDirectory(groupPath, group, result);
|
|
420
|
-
if (!entries)
|
|
421
|
-
return;
|
|
422
|
-
for (const entry of entries) {
|
|
423
|
-
const input = `${group}/${entry.name}`;
|
|
424
|
-
const path = join(groupPath, entry.name);
|
|
425
|
-
if (!entry.isFile() || !entry.name.endsWith(".md")) {
|
|
426
|
-
addArchivePathDiagnostic(result, input, path);
|
|
427
|
-
continue;
|
|
428
|
-
}
|
|
429
|
-
result.files.push(path);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
function collectFocusMarker(input, path, changesDir, result) {
|
|
433
|
-
try {
|
|
434
|
-
const ref = resolveWorkRef(input, { changesDir, canonical: true, executable: true });
|
|
435
|
-
result.markers.push({ name: ref.name, path });
|
|
436
|
-
} catch (error) {
|
|
437
|
-
if (!(error instanceof WorkRefError))
|
|
438
|
-
throw error;
|
|
439
|
-
addDiagnostic(result, error, path);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
function collectResolvedPath(path, changesDir, result) {
|
|
443
|
-
try {
|
|
444
|
-
const ref = resolveWorkRefPath(path, { changesDir });
|
|
445
|
-
if (isExecutableWorkRef(ref))
|
|
446
|
-
result.changes.push(ref);
|
|
447
|
-
else
|
|
448
|
-
result.briefs.push(ref);
|
|
449
|
-
} catch (error) {
|
|
450
|
-
if (!(error instanceof WorkRefError))
|
|
451
|
-
throw error;
|
|
452
|
-
addDiagnostic(result, error, path);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
async function readDirectory(path, input, result) {
|
|
456
|
-
try {
|
|
457
|
-
return (await readdir(path, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
458
|
-
} catch (error) {
|
|
459
|
-
addDiagnostic(result, asReadFailure(path, input, error), path);
|
|
460
|
-
return null;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
function addInvalidEntryDiagnostic(result, input, path) {
|
|
464
|
-
addDiagnostic(result, new WorkRefError(
|
|
465
|
-
"invalid_work_ref_path",
|
|
466
|
-
`unsupported entry in the open-work tree: ${path}`,
|
|
467
|
-
input
|
|
468
|
-
), path);
|
|
469
|
-
}
|
|
470
|
-
function addArchivePathDiagnostic(result, input, path) {
|
|
471
|
-
addDiagnostic(result, new WorkRefError(
|
|
472
|
-
"invalid_archive_path",
|
|
473
|
-
`unsupported entry in the archive tree: ${path}`,
|
|
474
|
-
input
|
|
475
|
-
), path);
|
|
476
|
-
}
|
|
477
|
-
function addDiagnostic(result, error, path) {
|
|
478
|
-
if (result.diagnostics.some((item) => item.code === error.code && item.input === error.input))
|
|
479
|
-
return;
|
|
480
|
-
result.diagnostics.push({ code: error.code, input: error.input, path, message: error.message });
|
|
481
|
-
}
|
|
482
|
-
function normalizeWorkRefName(name, allowGroupBrief) {
|
|
483
|
-
const segments = name.split("/");
|
|
484
|
-
if (segments.length > 2) {
|
|
485
|
-
throw new WorkRefError(
|
|
486
|
-
"unsupported_work_depth",
|
|
487
|
-
`work identity "${name}" exceeds the supported one-level Change Group depth`,
|
|
488
|
-
name
|
|
489
|
-
);
|
|
490
|
-
}
|
|
491
|
-
if (!name || segments.some((segment) => !segment)) {
|
|
492
|
-
throw new WorkRefError(
|
|
493
|
-
"invalid_work_ref",
|
|
494
|
-
`work identity "${name}" must contain one or two safe Unicode segments`,
|
|
495
|
-
name
|
|
496
|
-
);
|
|
497
|
-
}
|
|
498
|
-
const normalizedSegments = segments.map(normalizeWorkRefSegment);
|
|
499
|
-
const child = normalizedSegments[1];
|
|
500
|
-
if (segments.length === 2 && child === GROUP_BRIEF_FILENAME.slice(0, -3)) {
|
|
501
|
-
throw new WorkRefError(
|
|
502
|
-
"invalid_work_ref",
|
|
503
|
-
`work identity "${name}" uses the reserved Group Brief filename`,
|
|
504
|
-
name
|
|
505
|
-
);
|
|
506
|
-
}
|
|
507
|
-
if (segments.length === 2 && child === "brief" && !allowGroupBrief) {
|
|
508
|
-
throw new WorkRefError(
|
|
509
|
-
"non_executable_work_ref",
|
|
510
|
-
`work identity "${name}" is a Group Brief and cannot be used as an executable Change`,
|
|
511
|
-
name
|
|
512
|
-
);
|
|
513
|
-
}
|
|
514
|
-
return normalizedSegments.join("/");
|
|
515
|
-
}
|
|
516
|
-
function assertNoNormalizationCollision(changesDir, segments, input, isGroupBrief) {
|
|
517
|
-
for (let depth = 0; depth < segments.length; depth++) {
|
|
518
|
-
const parent = join(changesDir, ...segments.slice(0, depth));
|
|
519
|
-
const parentKind = getPathKind(parent, input);
|
|
520
|
-
if (parentKind === "missing")
|
|
521
|
-
return;
|
|
522
|
-
if (parentKind !== "directory")
|
|
523
|
-
continue;
|
|
524
|
-
let entries;
|
|
525
|
-
try {
|
|
526
|
-
entries = readdirSync(parent, { withFileTypes: true });
|
|
527
|
-
} catch (error) {
|
|
528
|
-
throw asReadFailure(parent, input, error);
|
|
529
|
-
}
|
|
530
|
-
const expected = segments[depth];
|
|
531
|
-
for (const entry of entries) {
|
|
532
|
-
let candidate = null;
|
|
533
|
-
if (entry.isDirectory())
|
|
534
|
-
candidate = entry.name;
|
|
535
|
-
else if (entry.isFile() && entry.name.endsWith(".md"))
|
|
536
|
-
candidate = entry.name.slice(0, -3);
|
|
537
|
-
if (depth === segments.length - 1 && isGroupBrief && entry.name === GROUP_BRIEF_FILENAME)
|
|
538
|
-
candidate = "brief";
|
|
539
|
-
if (candidate && candidate !== expected && candidate.normalize("NFC") === expected) {
|
|
540
|
-
throw new WorkRefError(
|
|
541
|
-
"work_ref_collision",
|
|
542
|
-
`work identity "${input}" collides with non-canonical stored identity "${candidate}" after Unicode NFC normalization`,
|
|
543
|
-
input
|
|
544
|
-
);
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
function assertNoManagedNormalizationCollision(root, segments, input, code) {
|
|
550
|
-
for (let depth = 0; depth < segments.length; depth++) {
|
|
551
|
-
const parent = join(root, ...segments.slice(0, depth));
|
|
552
|
-
const parentKind = getPathKind(parent, input);
|
|
553
|
-
if (parentKind === "missing")
|
|
554
|
-
return;
|
|
555
|
-
if (parentKind !== "directory")
|
|
556
|
-
continue;
|
|
557
|
-
let entries;
|
|
558
|
-
try {
|
|
559
|
-
entries = readdirSync(parent, { withFileTypes: true });
|
|
560
|
-
} catch (error) {
|
|
561
|
-
throw asReadFailure(parent, input, error);
|
|
562
|
-
}
|
|
563
|
-
const expected = segments[depth];
|
|
564
|
-
const alias = entries.find((entry) => entry.name !== expected && entry.name.normalize("NFC") === expected);
|
|
565
|
-
if (alias) {
|
|
566
|
-
throw new WorkRefError(
|
|
567
|
-
code,
|
|
568
|
-
`managed identity "${input}" collides with non-canonical stored entry "${alias.name}" after Unicode NFC normalization`,
|
|
569
|
-
input
|
|
570
|
-
);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
function assertNoIdentityCollision(changesDir, segments, input, finalFilename) {
|
|
575
|
-
for (let depth = 1; depth <= segments.length; depth++) {
|
|
576
|
-
const identityPath = join(changesDir, ...segments.slice(0, depth));
|
|
577
|
-
const filePath = finalFilename && depth === segments.length ? join(changesDir, ...segments.slice(0, -1), finalFilename) : `${identityPath}.md`;
|
|
578
|
-
const fileKind = getPathKind(filePath, input);
|
|
579
|
-
const identityKind = getPathKind(identityPath, input);
|
|
580
|
-
const isPrefix = depth < segments.length;
|
|
581
|
-
const isFlatIdentity = segments.length === 1;
|
|
582
|
-
const identity = segments.slice(0, depth).join("/");
|
|
583
|
-
if (fileKind !== "missing" && fileKind !== "file") {
|
|
584
|
-
throw new WorkRefError(
|
|
585
|
-
"work_ref_not_file",
|
|
586
|
-
`open work path is not a regular file: ${filePath}`,
|
|
587
|
-
input
|
|
588
|
-
);
|
|
589
|
-
}
|
|
590
|
-
if (fileKind === "file" && identityKind === "directory" || isPrefix && fileKind === "file" || isFlatIdentity && identityKind === "directory") {
|
|
591
|
-
throw new WorkRefError(
|
|
592
|
-
"work_ref_collision",
|
|
593
|
-
`work identity "${identity}" is or would be claimed by both a file and a directory`,
|
|
594
|
-
input
|
|
595
|
-
);
|
|
596
|
-
}
|
|
597
|
-
if (isPrefix && identityKind !== "missing" && identityKind !== "directory") {
|
|
598
|
-
throw new WorkRefError(
|
|
599
|
-
"invalid_work_ref_path",
|
|
600
|
-
`open work path must be a real directory: ${identityPath}`,
|
|
601
|
-
input
|
|
602
|
-
);
|
|
603
|
-
}
|
|
604
|
-
if (!isPrefix && !isFlatIdentity && identityKind === "directory") {
|
|
605
|
-
throw new WorkRefError(
|
|
606
|
-
"unsupported_work_depth",
|
|
607
|
-
`work identity "${identity}" exceeds the supported one-level Change Group depth`,
|
|
608
|
-
input
|
|
609
|
-
);
|
|
610
|
-
}
|
|
611
|
-
if (!isPrefix && identityKind !== "missing" && identityKind !== "directory") {
|
|
612
|
-
throw new WorkRefError(
|
|
613
|
-
"invalid_work_ref_path",
|
|
614
|
-
`unsupported entry in the open-work tree: ${identityPath}`,
|
|
615
|
-
input
|
|
616
|
-
);
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
function assertValidWorkRoot(changesDir, input) {
|
|
621
|
-
assertManagedRoot(changesDir, input, "invalid_work_root", "open work root");
|
|
622
|
-
}
|
|
623
|
-
function assertManagedRoot(path, input, code, label, allowMissing = false) {
|
|
624
|
-
try {
|
|
625
|
-
requireManagedDirectory(path, label, { allowMissing });
|
|
626
|
-
} catch (error) {
|
|
627
|
-
if (error instanceof ManagedPathError)
|
|
628
|
-
throw new WorkRefError(code, error.message, input);
|
|
629
|
-
throw error;
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
function assertOptionalManagedDirectory(path, input, code, label) {
|
|
633
|
-
try {
|
|
634
|
-
requireManagedDirectory(path, label, { allowMissing: true });
|
|
635
|
-
} catch (error) {
|
|
636
|
-
if (error instanceof ManagedPathError)
|
|
637
|
-
throw new WorkRefError(code, error.message, input);
|
|
638
|
-
throw error;
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
function getPathKind(path, input) {
|
|
642
|
-
try {
|
|
643
|
-
const stats = lstatSync(path);
|
|
644
|
-
if (stats.isFile())
|
|
645
|
-
return "file";
|
|
646
|
-
if (stats.isDirectory())
|
|
647
|
-
return "directory";
|
|
648
|
-
return "other";
|
|
649
|
-
} catch (error) {
|
|
650
|
-
if (error.code === "ENOENT")
|
|
651
|
-
return "missing";
|
|
652
|
-
throw asReadFailure(path, input, error);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
function asReadFailure(path, input, error) {
|
|
656
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
657
|
-
return new WorkRefError(
|
|
658
|
-
"work_tree_read_failed",
|
|
659
|
-
`unable to inspect open work path ${path}: ${message}`,
|
|
660
|
-
input
|
|
661
|
-
);
|
|
662
|
-
}
|
|
663
|
-
function normalizeLogicalPath2(path) {
|
|
664
|
-
return path.split("\\").join("/");
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// src/core/work-summary.ts
|
|
668
|
-
var PLACEHOLDER_SUMMARY = "<\u2026>";
|
|
669
|
-
function extractChangeSummary(content, document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA), options = {}) {
|
|
670
|
-
try {
|
|
671
|
-
const frontmatter = parseFrontmatter(content);
|
|
672
|
-
const frontmatterSummary = meaningfulSummary(frontmatter?.summary, options);
|
|
673
|
-
if (frontmatterSummary)
|
|
674
|
-
return frontmatterSummary;
|
|
675
|
-
} catch {
|
|
676
|
-
}
|
|
677
|
-
const proposalLines = getDocumentSectionBody(document, "proposal").split("\n").map((line) => line.trim());
|
|
678
|
-
for (const field of ["Outcome", "Summary"]) {
|
|
679
|
-
const prefix = `- ${field}:`;
|
|
680
|
-
for (const line of proposalLines) {
|
|
681
|
-
const summary = line.startsWith(prefix) ? meaningfulSummary(line.slice(prefix.length), options) : null;
|
|
682
|
-
if (summary)
|
|
683
|
-
return summary;
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
return null;
|
|
687
|
-
}
|
|
688
|
-
function extractGroupSummary(content, document = parseRspDocument(content, GROUP_BRIEF_DOCUMENT_SCHEMA)) {
|
|
689
|
-
for (const line of getDocumentSectionBody(document, "goal").split("\n")) {
|
|
690
|
-
const trimmed = line.trim();
|
|
691
|
-
const summary = trimmed.startsWith("- ") || trimmed.startsWith("* ") ? meaningfulSummary(trimmed.slice(2)) : null;
|
|
692
|
-
if (summary)
|
|
693
|
-
return summary;
|
|
694
|
-
}
|
|
695
|
-
return null;
|
|
696
|
-
}
|
|
697
|
-
function meaningfulSummary(value, options = {}) {
|
|
698
|
-
if (typeof value !== "string")
|
|
699
|
-
return null;
|
|
700
|
-
const summary = value.trim();
|
|
701
|
-
return summary && (options.preservePlaceholder || summary !== PLACEHOLDER_SUMMARY) ? summary : null;
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
// src/core/change-group.ts
|
|
705
|
-
var GROUP_BRIEF_SECTIONS = getCanonicalSectionHeadings(GROUP_BRIEF_DOCUMENT_SCHEMA);
|
|
706
|
-
var GROUP_REOPEN_COMPLETION_PREFIX = "Resolve reopened concern from";
|
|
707
|
-
async function resolveExecutableChange(name, options = {}) {
|
|
708
|
-
const ref = resolveWorkRef(name, { ...options, executable: true });
|
|
709
|
-
if (ref.kind === "group-change")
|
|
710
|
-
await requireDeclaredGroupSlice(ref);
|
|
711
|
-
return ref;
|
|
712
|
-
}
|
|
713
|
-
function generateGroupBriefContent(name, goal = "") {
|
|
714
|
-
const placeholder = "<\u2026>";
|
|
715
|
-
return `---
|
|
716
|
-
kind: group
|
|
717
|
-
---
|
|
718
|
-
|
|
719
|
-
${renderDocumentTitle(GROUP_BRIEF_DOCUMENT_SCHEMA, name)}
|
|
720
|
-
|
|
721
|
-
${groupSectionHeading("goal")}
|
|
722
|
-
- ${goal || placeholder}
|
|
723
|
-
|
|
724
|
-
${groupSectionHeading("scope")}
|
|
725
|
-
- ${placeholder}
|
|
726
|
-
|
|
727
|
-
${groupSectionHeading("sharedConstraints")}
|
|
728
|
-
- ${placeholder}
|
|
729
|
-
|
|
730
|
-
${groupSectionHeading("slices")}
|
|
731
|
-
- \`${name}/<change>\`: ${placeholder}
|
|
732
|
-
|
|
733
|
-
${groupSectionHeading("completionConditions")}
|
|
734
|
-
- [ ] ${placeholder}
|
|
735
|
-
|
|
736
|
-
${groupSectionHeading("durableOutcomes")}
|
|
737
|
-
- Current facts: ${placeholder}
|
|
738
|
-
- Lasting rationale: ${placeholder}
|
|
739
|
-
|
|
740
|
-
${groupSectionHeading("blockers")}
|
|
741
|
-
- none
|
|
742
|
-
`;
|
|
743
|
-
}
|
|
744
|
-
async function inspectChangeGroups(options = {}) {
|
|
745
|
-
const workTree = options.workTree ?? await inspectWorkTree();
|
|
746
|
-
const archiveTree = options.archiveTree ?? await inspectArchiveTree();
|
|
747
|
-
const focusTree = options.focusTree ?? await inspectFocusTree();
|
|
748
|
-
const diagnostics = [];
|
|
749
|
-
const groups = [];
|
|
750
|
-
const archivedSlicesByGroup = /* @__PURE__ */ new Map();
|
|
751
|
-
const archivedGroups = /* @__PURE__ */ new Set();
|
|
752
|
-
const archivedGroupReopenEvidence = /* @__PURE__ */ new Map();
|
|
753
|
-
const archivedGroupBriefPaths = /* @__PURE__ */ new Map();
|
|
754
|
-
const openGroupNames = new Set(workTree.briefs.map((brief) => brief.group));
|
|
755
|
-
for (const path of archiveTree.files) {
|
|
756
|
-
const archivePath = normalizeLogicalPath(relative2(ARCHIVES_DIR, path));
|
|
757
|
-
const archiveGroup = archivePath.includes("/") ? archivePath.split("/")[0] : null;
|
|
758
|
-
if (!archiveGroup || !openGroupNames.has(archiveGroup))
|
|
759
|
-
continue;
|
|
760
|
-
try {
|
|
761
|
-
const content = await readFile(path, "utf-8");
|
|
762
|
-
const archivedChange = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
|
|
763
|
-
const identity = getDocumentTitle(archivedChange, "identity");
|
|
764
|
-
if (identity?.includes("/")) {
|
|
765
|
-
if (!isCanonicalExecutableWorkRef(identity) || !identity.startsWith(`${archiveGroup}/`)) {
|
|
766
|
-
diagnostics.push({
|
|
767
|
-
severity: "error",
|
|
768
|
-
code: "group_archive_identity_mismatch",
|
|
769
|
-
change: archiveGroup,
|
|
770
|
-
path,
|
|
771
|
-
message: `archived Change identity ${identity} does not belong to archive group ${archiveGroup}`
|
|
772
|
-
});
|
|
773
|
-
} else {
|
|
774
|
-
const groupSlices = archivedSlicesByGroup.get(archiveGroup) ?? /* @__PURE__ */ new Set();
|
|
775
|
-
groupSlices.add(identity);
|
|
776
|
-
archivedSlicesByGroup.set(archiveGroup, groupSlices);
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
if (getDocumentTitle(parseRspDocument(content, GROUP_BRIEF_DOCUMENT_SCHEMA), "identity") === archiveGroup) {
|
|
780
|
-
archivedGroups.add(archiveGroup);
|
|
781
|
-
const evidence = archivedGroupReopenEvidence.get(archiveGroup) ?? /* @__PURE__ */ new Set();
|
|
782
|
-
for (const item of getGroupReopenEvidence(content))
|
|
783
|
-
evidence.add(item.key);
|
|
784
|
-
archivedGroupReopenEvidence.set(archiveGroup, evidence);
|
|
785
|
-
const retainedPaths = archivedGroupBriefPaths.get(archiveGroup) ?? /* @__PURE__ */ new Set();
|
|
786
|
-
retainedPaths.add(`.rsp/archives/${archivePath}`);
|
|
787
|
-
archivedGroupBriefPaths.set(archiveGroup, retainedPaths);
|
|
788
|
-
}
|
|
789
|
-
} catch (error) {
|
|
790
|
-
diagnostics.push({
|
|
791
|
-
severity: "error",
|
|
792
|
-
code: "group_archive_read_failed",
|
|
793
|
-
change: archiveGroup,
|
|
794
|
-
path,
|
|
795
|
-
message: error instanceof Error ? error.message : String(error)
|
|
796
|
-
});
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
for (const brief of workTree.briefs) {
|
|
800
|
-
let content;
|
|
801
|
-
try {
|
|
802
|
-
content = await readFile(brief.path, "utf-8");
|
|
803
|
-
} catch (error) {
|
|
804
|
-
diagnostics.push({
|
|
805
|
-
severity: "error",
|
|
806
|
-
code: "group_brief_read_failed",
|
|
807
|
-
change: brief.group,
|
|
808
|
-
path: brief.path,
|
|
809
|
-
message: error instanceof Error ? error.message : String(error)
|
|
810
|
-
});
|
|
811
|
-
continue;
|
|
812
|
-
}
|
|
813
|
-
const parsed = parseGroupBrief(brief.group, content);
|
|
814
|
-
diagnostics.push(...parsed.diagnostics.map((diagnostic) => ({ ...diagnostic, path: brief.path })));
|
|
815
|
-
if (archivedGroups.has(brief.group) && !hasExplicitGroupReopenEvidence(
|
|
816
|
-
content,
|
|
817
|
-
archivedGroupReopenEvidence.get(brief.group) ?? /* @__PURE__ */ new Set(),
|
|
818
|
-
archivedGroupBriefPaths.get(brief.group) ?? /* @__PURE__ */ new Set()
|
|
819
|
-
)) {
|
|
820
|
-
diagnostics.push({
|
|
821
|
-
severity: "error",
|
|
822
|
-
code: "group_identity_reopened",
|
|
823
|
-
change: brief.group,
|
|
824
|
-
path: brief.path,
|
|
825
|
-
message: `archived Change Group cannot be reopened: ${brief.group}`
|
|
826
|
-
});
|
|
827
|
-
}
|
|
828
|
-
for (const diagnostic of archiveTree.diagnostics.filter((item) => {
|
|
829
|
-
if (item.code === "invalid_archive_root")
|
|
830
|
-
return true;
|
|
831
|
-
const archivePath = normalizeLogicalPath(relative2(ARCHIVES_DIR, item.path));
|
|
832
|
-
return archivePath === brief.group || archivePath.startsWith(`${brief.group}/`);
|
|
833
|
-
})) {
|
|
834
|
-
diagnostics.push({
|
|
835
|
-
severity: "error",
|
|
836
|
-
code: diagnostic.code,
|
|
837
|
-
change: brief.group,
|
|
838
|
-
path: diagnostic.path,
|
|
839
|
-
message: diagnostic.message
|
|
840
|
-
});
|
|
841
|
-
}
|
|
842
|
-
for (const diagnostic of focusTree.diagnostics.filter((item) => item.input.startsWith(`${brief.group}/`))) {
|
|
843
|
-
diagnostics.push({
|
|
844
|
-
severity: "error",
|
|
845
|
-
code: "group_focus_invalid",
|
|
846
|
-
change: brief.group,
|
|
847
|
-
path: diagnostic.path,
|
|
848
|
-
message: `invalid focus for ${diagnostic.input}: ${diagnostic.message}`
|
|
849
|
-
});
|
|
850
|
-
}
|
|
851
|
-
const openChildren = new Set(workTree.changes.filter((ref) => ref.group === brief.group).map((ref) => ref.name));
|
|
852
|
-
const archivedSlices = archivedSlicesByGroup.get(brief.group) ?? /* @__PURE__ */ new Set();
|
|
853
|
-
const slices = parsed.slices.map((slice) => {
|
|
854
|
-
const state = openChildren.has(slice.name) ? "open" : archivedSlices.has(slice.name) ? "archived" : "missing";
|
|
855
|
-
return { ...slice, state };
|
|
856
|
-
});
|
|
857
|
-
const focusedNames = new Set(focusTree.markers.map((marker) => marker.name));
|
|
858
|
-
for (const slice of slices.filter((slice2) => slice2.state !== "open" && focusedNames.has(slice2.name))) {
|
|
859
|
-
diagnostics.push({
|
|
860
|
-
severity: "error",
|
|
861
|
-
code: "group_focus_invalid",
|
|
862
|
-
change: brief.group,
|
|
863
|
-
path: focusTree.markers.find((marker) => marker.name === slice.name)?.path,
|
|
864
|
-
message: `invalid focus for ${slice.name}: focused group slice is not open`
|
|
865
|
-
});
|
|
866
|
-
}
|
|
867
|
-
const declared = new Set(slices.map((slice) => slice.name));
|
|
868
|
-
for (const child of [...openChildren].filter((name) => !declared.has(name)).sort()) {
|
|
869
|
-
diagnostics.push({
|
|
870
|
-
severity: "error",
|
|
871
|
-
code: "group_child_undeclared",
|
|
872
|
-
change: brief.group,
|
|
873
|
-
path: brief.path,
|
|
874
|
-
message: `open child Change is not declared by the Group Brief: ${child}`
|
|
875
|
-
});
|
|
876
|
-
}
|
|
877
|
-
for (const child of [...archivedSlices].filter((name) => !declared.has(name)).sort()) {
|
|
878
|
-
diagnostics.push({
|
|
879
|
-
severity: "error",
|
|
880
|
-
code: "group_archived_child_undeclared",
|
|
881
|
-
change: brief.group,
|
|
882
|
-
path: brief.path,
|
|
883
|
-
message: `archived child Change is not declared by the Group Brief: ${child}`
|
|
884
|
-
});
|
|
885
|
-
}
|
|
886
|
-
for (const slice of slices.filter((slice2) => slice2.state === "missing")) {
|
|
887
|
-
diagnostics.push({
|
|
888
|
-
severity: "error",
|
|
889
|
-
code: "group_slice_missing",
|
|
890
|
-
change: brief.group,
|
|
891
|
-
path: brief.path,
|
|
892
|
-
message: `declared group slice is neither open nor archived: ${slice.name}`
|
|
893
|
-
});
|
|
894
|
-
}
|
|
895
|
-
const warnings = parsed.diagnostics.filter((diagnostic) => diagnostic.severity === "warning").map((diagnostic) => diagnostic.message);
|
|
896
|
-
const readyToClose = diagnostics.every((diagnostic) => diagnostic.change !== brief.group || diagnostic.severity !== "error") && slices.length >= 2 && slices.every((slice) => slice.state === "archived") && parsed.completion.total > 0 && parsed.completion.done === parsed.completion.total && !parsed.blockers;
|
|
897
|
-
groups.push({
|
|
898
|
-
name: brief.group,
|
|
899
|
-
summary: extractGroupSummary(content),
|
|
900
|
-
path: normalizeLogicalPath(brief.path),
|
|
901
|
-
slices,
|
|
902
|
-
completion: parsed.completion,
|
|
903
|
-
blockers: parsed.blockers,
|
|
904
|
-
readyToClose,
|
|
905
|
-
warnings
|
|
906
|
-
});
|
|
907
|
-
}
|
|
908
|
-
groups.sort((left, right) => left.name.localeCompare(right.name));
|
|
909
|
-
return { groups, diagnostics };
|
|
910
|
-
}
|
|
911
|
-
function getGroupReopenEvidence(content) {
|
|
912
|
-
const completion = getDocumentSectionBody(parseRspDocument(content, GROUP_BRIEF_DOCUMENT_SCHEMA), "completionConditions");
|
|
913
|
-
return completion.split("\n").flatMap((line) => {
|
|
914
|
-
const match = line.trim().match(/^-\s+\[([ /xX-])\]\s+(Resolve reopened concern from `(\.rsp\/archives\/[^`]+)`: \S.*)$/);
|
|
915
|
-
return match ? [{ active: match[1] !== "-", key: match[2], sourcePath: match[3] }] : [];
|
|
916
|
-
});
|
|
917
|
-
}
|
|
918
|
-
function hasExplicitGroupReopenEvidence(content, retainedEvidence, retainedGroupBriefPaths) {
|
|
919
|
-
return getGroupReopenEvidence(content).some((item) => item.active && retainedGroupBriefPaths.has(item.sourcePath) && !retainedEvidence.has(item.key));
|
|
920
|
-
}
|
|
921
|
-
async function requireDeclaredGroupSlice(ref) {
|
|
922
|
-
const briefPath = join2(dirname(ref.path), GROUP_BRIEF_FILENAME);
|
|
923
|
-
let content;
|
|
924
|
-
try {
|
|
925
|
-
content = await readFile(briefPath, "utf-8");
|
|
926
|
-
} catch (error) {
|
|
927
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
928
|
-
throw new WorkRefError("work_tree_read_failed", `unable to read Group Brief ${briefPath}: ${message}`, ref.name);
|
|
929
|
-
}
|
|
930
|
-
const parsed = parseGroupBrief(ref.group, content);
|
|
931
|
-
const errors = parsed.diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
932
|
-
if (errors.length > 0) {
|
|
933
|
-
throw new WorkRefError(
|
|
934
|
-
"invalid_group_brief",
|
|
935
|
-
`Change Group "${ref.group}" brief is invalid: ${errors.map((error) => error.message).join("; ")}`,
|
|
936
|
-
ref.name
|
|
937
|
-
);
|
|
938
|
-
}
|
|
939
|
-
if (!parsed.slices.some((slice) => slice.name === ref.name)) {
|
|
940
|
-
throw new WorkRefError(
|
|
941
|
-
"group_child_undeclared",
|
|
942
|
-
`grouped Change is not declared by ${ref.group}/brief: ${ref.name}`,
|
|
943
|
-
ref.name
|
|
944
|
-
);
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
async function hasArchivedGroupBrief(group) {
|
|
948
|
-
const archiveTree = await inspectArchiveTree();
|
|
949
|
-
if (archiveTree.diagnostics.length > 0) {
|
|
950
|
-
const diagnostic = archiveTree.diagnostics[0];
|
|
951
|
-
throw new WorkRefError(diagnostic.code, diagnostic.message, diagnostic.input);
|
|
952
|
-
}
|
|
953
|
-
const groupPrefix = `${group}/`;
|
|
954
|
-
for (const path of archiveTree.files) {
|
|
955
|
-
if (!normalizeLogicalPath(relative2(ARCHIVES_DIR, path)).startsWith(groupPrefix))
|
|
956
|
-
continue;
|
|
957
|
-
let content;
|
|
958
|
-
try {
|
|
959
|
-
content = await readFile(path, "utf-8");
|
|
960
|
-
} catch (error) {
|
|
961
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
962
|
-
throw new WorkRefError("work_tree_read_failed", `unable to inspect archived Change Group identity at ${path}: ${message}`, group);
|
|
963
|
-
}
|
|
964
|
-
if (getDocumentTitle(parseRspDocument(content, GROUP_BRIEF_DOCUMENT_SCHEMA), "identity") === group)
|
|
965
|
-
return true;
|
|
966
|
-
}
|
|
967
|
-
return false;
|
|
968
|
-
}
|
|
969
|
-
function parseGroupBrief(group, content) {
|
|
970
|
-
const diagnostics = [];
|
|
971
|
-
const document = parseRspDocument(content, GROUP_BRIEF_DOCUMENT_SCHEMA);
|
|
972
|
-
let frontmatter;
|
|
973
|
-
try {
|
|
974
|
-
frontmatter = parseFrontmatter(content);
|
|
975
|
-
} catch (error) {
|
|
976
|
-
diagnostics.push({ severity: "error", code: "group_frontmatter_invalid", change: group, message: error instanceof Error ? error.message : String(error) });
|
|
977
|
-
}
|
|
978
|
-
if (frontmatter?.kind !== "group")
|
|
979
|
-
diagnostics.push({ severity: "error", code: "group_kind_invalid", change: group, message: "Group Brief frontmatter must declare kind: group" });
|
|
980
|
-
if (getDocumentTitle(document, "identity") !== group)
|
|
981
|
-
diagnostics.push({ severity: "error", code: "group_heading_mismatch", change: group, message: `Group Brief heading must be: # Change Group: ${group}` });
|
|
982
|
-
for (const definition of GROUP_BRIEF_DOCUMENT_SCHEMA.sections) {
|
|
983
|
-
if (!getDocumentSectionBody(document, definition.id))
|
|
984
|
-
diagnostics.push({ severity: "error", code: "group_section_missing", change: group, message: `Group Brief missing required section: ${definition.heading}` });
|
|
985
|
-
}
|
|
986
|
-
const slices = [];
|
|
987
|
-
const seen = /* @__PURE__ */ new Set();
|
|
988
|
-
for (const line of getDocumentSectionBody(document, "slices").split("\n")) {
|
|
989
|
-
const trimmed = line.trim();
|
|
990
|
-
const identity = trimmed.match(/^-\s+`(\S+)`/);
|
|
991
|
-
if (!identity || identity[1].includes("<"))
|
|
992
|
-
continue;
|
|
993
|
-
const suffix = trimmed.slice(identity[0].length).trim();
|
|
994
|
-
if (![":", "\u2014", "-"].includes(suffix[0])) {
|
|
995
|
-
diagnostics.push({ severity: "error", code: "group_slice_invalid", change: group, message: `group slice requires an identity and boundary: ${trimmed}` });
|
|
996
|
-
continue;
|
|
997
|
-
}
|
|
998
|
-
const name = identity[1];
|
|
999
|
-
const boundary = suffix.slice(1).trim();
|
|
1000
|
-
if (!boundary) {
|
|
1001
|
-
diagnostics.push({ severity: "error", code: "group_slice_invalid", change: group, message: `group slice requires a boundary: ${name}` });
|
|
1002
|
-
continue;
|
|
1003
|
-
}
|
|
1004
|
-
if (!isCanonicalExecutableWorkRef(name) || !name.startsWith(`${group}/`)) {
|
|
1005
|
-
diagnostics.push({ severity: "error", code: "group_slice_invalid", change: group, message: `invalid direct child identity in Group Brief: ${name}` });
|
|
1006
|
-
continue;
|
|
1007
|
-
}
|
|
1008
|
-
if (seen.has(name)) {
|
|
1009
|
-
diagnostics.push({ severity: "error", code: "group_slice_duplicate", change: group, message: `duplicate group slice: ${name}` });
|
|
1010
|
-
continue;
|
|
1011
|
-
}
|
|
1012
|
-
seen.add(name);
|
|
1013
|
-
slices.push({ name, boundary: boundary.trim() });
|
|
1014
|
-
}
|
|
1015
|
-
if (slices.length < 2)
|
|
1016
|
-
diagnostics.push({ severity: "warning", code: "group_slices_incomplete", change: group, message: "Change Group must declare at least two direct child slices" });
|
|
1017
|
-
const completion = countCheckboxes(getDocumentSectionBody(document, "completionConditions"));
|
|
1018
|
-
if (completion.total === 0)
|
|
1019
|
-
diagnostics.push({ severity: "warning", code: "group_completion_missing", change: group, message: "Change Group must declare at least one completion condition" });
|
|
1020
|
-
return {
|
|
1021
|
-
slices,
|
|
1022
|
-
completion: { done: completion.done + completion.dropped, total: completion.total },
|
|
1023
|
-
blockers: hasMeaningfulBlockerBody(getDocumentSectionBody(document, "blockers")),
|
|
1024
|
-
diagnostics
|
|
1025
|
-
};
|
|
1026
|
-
}
|
|
1027
|
-
function groupSectionHeading(sectionId) {
|
|
1028
|
-
return renderDocumentSectionHeading(GROUP_BRIEF_DOCUMENT_SCHEMA, sectionId);
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
// src/core/dependency-plan.ts
|
|
1032
|
-
import { readFile as readFile4 } from "fs/promises";
|
|
1033
|
-
|
|
1034
|
-
// src/history/query.ts
|
|
1035
|
-
import { readFile as readFile3 } from "fs/promises";
|
|
1036
|
-
|
|
1037
|
-
// src/history/inspect.ts
|
|
1038
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
1039
|
-
import { basename, dirname as dirname2, relative as relative3 } from "path";
|
|
1040
|
-
|
|
1041
|
-
// src/core/issue-relationship.ts
|
|
1042
|
-
var ISSUE_RELATIONS = /* @__PURE__ */ new Set(["relates", "closes"]);
|
|
1043
|
-
var ISSUE_FIELDS = /* @__PURE__ */ new Set(["url", "relation"]);
|
|
1044
|
-
var IssueRelationshipError = class extends Error {
|
|
1045
|
-
constructor(code, message) {
|
|
1046
|
-
super(message);
|
|
1047
|
-
this.code = code;
|
|
1048
|
-
this.name = "IssueRelationshipError";
|
|
1049
|
-
}
|
|
1050
|
-
code;
|
|
1051
|
-
};
|
|
1052
|
-
function parseIssueRelationships(frontmatter) {
|
|
1053
|
-
if (!frontmatter || !("issues" in frontmatter))
|
|
1054
|
-
return [];
|
|
1055
|
-
if (!Array.isArray(frontmatter.issues))
|
|
1056
|
-
throw new IssueRelationshipError("invalid_issue_shape", 'frontmatter field "issues" must be a list');
|
|
1057
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1058
|
-
return frontmatter.issues.map((value, index) => {
|
|
1059
|
-
if (!isPlainObject(value))
|
|
1060
|
-
throw new IssueRelationshipError("invalid_issue_shape", `issues[${index}] must be a mapping with url and relation`);
|
|
1061
|
-
const fields = Object.keys(value);
|
|
1062
|
-
const unsupported = fields.filter((field) => !ISSUE_FIELDS.has(field));
|
|
1063
|
-
if (unsupported.length > 0)
|
|
1064
|
-
throw new IssueRelationshipError("unsupported_issue_field", `issues[${index}] contains unsupported field(s): ${unsupported.sort().join(", ")}`);
|
|
1065
|
-
if (fields.length !== 2 || !("url" in value) || !("relation" in value))
|
|
1066
|
-
throw new IssueRelationshipError("invalid_issue_shape", `issues[${index}] must contain exactly url and relation`);
|
|
1067
|
-
if (typeof value.url !== "string")
|
|
1068
|
-
throw new IssueRelationshipError("invalid_issue_url", `issues[${index}].url must be an absolute http or https URL`);
|
|
1069
|
-
if (typeof value.relation !== "string" || !ISSUE_RELATIONS.has(value.relation))
|
|
1070
|
-
throw new IssueRelationshipError("unsupported_issue_relation", `issues[${index}].relation must be relates or closes`);
|
|
1071
|
-
const url = normalizeIssueUrl(value.url, `issues[${index}].url`);
|
|
1072
|
-
if (seen.has(url))
|
|
1073
|
-
throw new IssueRelationshipError("duplicate_issue_url", `issues contains duplicate normalized URL: ${url}`);
|
|
1074
|
-
seen.add(url);
|
|
1075
|
-
return { url, relation: value.relation };
|
|
1076
|
-
});
|
|
1077
|
-
}
|
|
1078
|
-
function createIssueRelationship(url, relation = "relates") {
|
|
1079
|
-
return parseIssueRelationships({ issues: [{ url, relation }] })[0];
|
|
1080
|
-
}
|
|
1081
|
-
function normalizeIssueUrl(raw, label) {
|
|
1082
|
-
if (raw.trim() !== raw || raw === "")
|
|
1083
|
-
throw new IssueRelationshipError("invalid_issue_url", `${label} must be an absolute http or https URL without surrounding whitespace`);
|
|
1084
|
-
let parsed;
|
|
1085
|
-
try {
|
|
1086
|
-
parsed = new URL(raw);
|
|
1087
|
-
} catch {
|
|
1088
|
-
throw new IssueRelationshipError("invalid_issue_url", `${label} must be an absolute http or https URL`);
|
|
1089
|
-
}
|
|
1090
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
1091
|
-
throw new IssueRelationshipError("invalid_issue_url", `${label} must use http or https`);
|
|
1092
|
-
if (!parsed.hostname)
|
|
1093
|
-
throw new IssueRelationshipError("invalid_issue_url", `${label} must include a host`);
|
|
1094
|
-
if (parsed.username || parsed.password)
|
|
1095
|
-
throw new IssueRelationshipError("unsafe_issue_url", `${label} must not contain credentials`);
|
|
1096
|
-
if (parsed.hash)
|
|
1097
|
-
throw new IssueRelationshipError("unsafe_issue_url", `${label} must not contain a fragment`);
|
|
1098
|
-
return parsed.toString();
|
|
1099
|
-
}
|
|
1100
|
-
function isPlainObject(value) {
|
|
1101
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
// src/history/model.ts
|
|
1105
|
-
var HISTORY_DEFAULT_LIMIT = 20;
|
|
1106
|
-
var HISTORY_MAX_LIMIT = 100;
|
|
1107
|
-
var HISTORY_MAX_TEXT_CODE_POINTS = 500;
|
|
1108
|
-
var HISTORY_MAX_EVIDENCE_ITEMS = 20;
|
|
1109
|
-
var HISTORY_MAX_DIAGNOSTICS = 20;
|
|
1110
|
-
var HISTORY_MAX_CANDIDATES = 20;
|
|
1111
|
-
var ArchiveHistoryError = class extends Error {
|
|
1112
|
-
constructor(code, message, candidates = []) {
|
|
1113
|
-
super(message);
|
|
1114
|
-
this.code = code;
|
|
1115
|
-
this.name = "ArchiveHistoryError";
|
|
1116
|
-
const ordered = [...candidates].sort();
|
|
1117
|
-
this.candidateTotal = ordered.length;
|
|
1118
|
-
this.candidates = ordered.slice(0, HISTORY_MAX_CANDIDATES);
|
|
1119
|
-
this.candidatesTruncated = this.candidateTotal > this.candidates.length;
|
|
1120
|
-
}
|
|
1121
|
-
code;
|
|
1122
|
-
candidates;
|
|
1123
|
-
candidateTotal;
|
|
1124
|
-
candidatesTruncated;
|
|
1125
|
-
};
|
|
1126
|
-
|
|
1127
|
-
// src/history/inspect.ts
|
|
1128
|
-
var ARCHIVE_NAME_RE = /^(\d{4}-\d{2}-\d{2})_(.+)\.md$/;
|
|
1129
|
-
async function inspectArchiveHistory(options = {}) {
|
|
1130
|
-
const archivesDir = options.archivesDir ?? ARCHIVES_DIR;
|
|
1131
|
-
const tree = options.archiveTree ?? await inspectArchiveTree({ archivesDir });
|
|
1132
|
-
const diagnostics = tree.diagnostics.map((diagnostic) => ({
|
|
1133
|
-
severity: "error",
|
|
1134
|
-
code: diagnostic.code,
|
|
1135
|
-
path: normalizeOutputPath(diagnostic.path, archivesDir),
|
|
1136
|
-
message: diagnostic.message
|
|
1137
|
-
}));
|
|
1138
|
-
const runtime = [];
|
|
1139
|
-
const records = [];
|
|
1140
|
-
const groupBriefs = [];
|
|
1141
|
-
if (!tree.rootExists && tree.diagnostics.length === 0) {
|
|
1142
|
-
diagnostics.push({
|
|
1143
|
-
severity: "error",
|
|
1144
|
-
code: "archive_root_missing",
|
|
1145
|
-
path: ".rsp/archives",
|
|
1146
|
-
message: "archive root does not exist; run rsp init or rsp update before querying history"
|
|
1147
|
-
});
|
|
1148
|
-
}
|
|
1149
|
-
for (const sourcePath of tree.files) {
|
|
1150
|
-
const path = archiveOutputPath(sourcePath, archivesDir);
|
|
1151
|
-
let content;
|
|
1152
|
-
try {
|
|
1153
|
-
content = await readFile2(sourcePath, "utf-8");
|
|
1154
|
-
} catch (error) {
|
|
1155
|
-
const message = toErrorMessage(error);
|
|
1156
|
-
diagnostics.push({ severity: "error", code: "archive_read_failed", path, message: `unable to read archived work: ${message}` });
|
|
1157
|
-
continue;
|
|
1158
|
-
}
|
|
1159
|
-
const parsed = parseArchiveEntry(sourcePath, path, archivesDir, content);
|
|
1160
|
-
if (parsed.diagnostic)
|
|
1161
|
-
diagnostics.push(parsed.diagnostic);
|
|
1162
|
-
if (parsed.record)
|
|
1163
|
-
records.push(parsed.record);
|
|
1164
|
-
if (parsed.groupBrief)
|
|
1165
|
-
groupBriefs.push(parsed.groupBrief);
|
|
1166
|
-
}
|
|
1167
|
-
records.sort(compareHistoryRecords);
|
|
1168
|
-
groupBriefs.sort(compareArchivedGroupBriefs);
|
|
1169
|
-
diagnostics.sort(compareDiagnostics);
|
|
1170
|
-
const diagnosticTotal = diagnostics.length;
|
|
1171
|
-
const boundedDiagnostics = diagnostics.slice(0, HISTORY_MAX_DIAGNOSTICS);
|
|
1172
|
-
return {
|
|
1173
|
-
rootExists: tree.rootExists,
|
|
1174
|
-
records,
|
|
1175
|
-
groupBriefs,
|
|
1176
|
-
diagnostics: boundedDiagnostics,
|
|
1177
|
-
diagnosticSummary: {
|
|
1178
|
-
total: diagnosticTotal,
|
|
1179
|
-
returned: boundedDiagnostics.length,
|
|
1180
|
-
hasMore: diagnosticTotal > boundedDiagnostics.length
|
|
1181
|
-
},
|
|
1182
|
-
runtime
|
|
1183
|
-
};
|
|
1184
|
-
}
|
|
1185
|
-
function parseArchiveEntry(sourcePath, path, archivesDir, content) {
|
|
1186
|
-
const nameMatch = basename(sourcePath).match(ARCHIVE_NAME_RE);
|
|
1187
|
-
if (!nameMatch || !isCalendarDate(nameMatch[1])) {
|
|
1188
|
-
return { diagnostic: errorDiagnostic("archive_name_invalid", path, "archive filename must use a valid YYYY-MM-DD_name.md date") };
|
|
1189
|
-
}
|
|
1190
|
-
const date = nameMatch[1];
|
|
1191
|
-
const archiveStem = nameMatch[2];
|
|
1192
|
-
const relativeDirectory = normalizeLogicalPath(relative3(archivesDir, dirname2(sourcePath)));
|
|
1193
|
-
const archiveGroup = relativeDirectory === "." || relativeDirectory === "" ? null : relativeDirectory;
|
|
1194
|
-
if (archiveGroup && !isCanonicalWorkRefSegment(archiveGroup))
|
|
1195
|
-
return { diagnostic: errorDiagnostic("archive_identity_mismatch", path, "archive group path is not a valid one-segment Group identity") };
|
|
1196
|
-
const changeDocument = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
|
|
1197
|
-
const groupDocument = parseRspDocument(content, GROUP_BRIEF_DOCUMENT_SCHEMA);
|
|
1198
|
-
const changeHeadings = getDocumentTitles(changeDocument, "identity");
|
|
1199
|
-
const groupHeadings = getDocumentTitles(groupDocument, "identity");
|
|
1200
|
-
if (changeHeadings.length + groupHeadings.length !== 1) {
|
|
1201
|
-
return { diagnostic: errorDiagnostic("archive_heading_invalid", path, "archive must contain exactly one Change or Change Group identity heading") };
|
|
1202
|
-
}
|
|
1203
|
-
let frontmatter;
|
|
1204
|
-
try {
|
|
1205
|
-
frontmatter = parseFrontmatter(content);
|
|
1206
|
-
} catch (error) {
|
|
1207
|
-
return { diagnostic: errorDiagnostic("archive_frontmatter_invalid", path, `archive frontmatter could not be parsed: ${toErrorMessage(error)}`) };
|
|
1208
|
-
}
|
|
1209
|
-
if (typeof frontmatter?.kind !== "string" || frontmatter.kind.trim() === "")
|
|
1210
|
-
return { diagnostic: errorDiagnostic("archive_kind_invalid", path, "archive frontmatter must declare a non-empty kind") };
|
|
1211
|
-
if (groupHeadings.length === 1) {
|
|
1212
|
-
const group = groupHeadings[0];
|
|
1213
|
-
if (!archiveGroup || group !== archiveGroup || frontmatter.kind !== "group" || !matchesArchiveStem(archiveStem, "brief")) {
|
|
1214
|
-
return { diagnostic: errorDiagnostic("archive_identity_mismatch", path, `archived Change Group identity ${group} does not match its archive path`) };
|
|
1215
|
-
}
|
|
1216
|
-
return { groupBrief: { date, group, path, sourcePath } };
|
|
1217
|
-
}
|
|
1218
|
-
let issues;
|
|
1219
|
-
if ("issues" in frontmatter) {
|
|
1220
|
-
try {
|
|
1221
|
-
issues = parseIssueRelationships(frontmatter);
|
|
1222
|
-
} catch {
|
|
1223
|
-
issues = void 0;
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
const workRef = changeHeadings[0];
|
|
1227
|
-
const segments = workRef.split("/");
|
|
1228
|
-
if (segments.length === 2 && (segments[1] === "brief" || segments[1] === "00-brief")) {
|
|
1229
|
-
return { diagnostic: errorDiagnostic("archive_work_ref_invalid", path, `archived executable Change uses reserved Group Brief identity: ${workRef}`) };
|
|
1230
|
-
}
|
|
1231
|
-
const expectedGroup = segments.length === 2 ? segments[0] : null;
|
|
1232
|
-
const expectedBase = segments.at(-1);
|
|
1233
|
-
if (!isCanonicalExecutableWorkRef(workRef) || expectedGroup !== archiveGroup || !matchesArchiveStem(archiveStem, expectedBase)) {
|
|
1234
|
-
return { diagnostic: errorDiagnostic("archive_identity_mismatch", path, `archived Change identity ${workRef} does not match its archive path`) };
|
|
1235
|
-
}
|
|
1236
|
-
const rawSummary = extractChangeSummary(content, changeDocument, { preservePlaceholder: true });
|
|
1237
|
-
if (!rawSummary)
|
|
1238
|
-
return { diagnostic: errorDiagnostic("archive_summary_missing", path, "archived Change must contain frontmatter summary, Proposal Outcome, or Proposal Summary") };
|
|
1239
|
-
const summary = boundText(rawSummary);
|
|
1240
|
-
return {
|
|
1241
|
-
record: {
|
|
1242
|
-
date,
|
|
1243
|
-
workRef,
|
|
1244
|
-
group: expectedGroup,
|
|
1245
|
-
kind: frontmatter.kind.trim(),
|
|
1246
|
-
summary: summary.value,
|
|
1247
|
-
summaryTruncated: summary.truncated,
|
|
1248
|
-
path,
|
|
1249
|
-
...issues === void 0 ? {} : { issues },
|
|
1250
|
-
sourcePath,
|
|
1251
|
-
searchSummary: rawSummary
|
|
1252
|
-
}
|
|
1253
|
-
};
|
|
1254
|
-
}
|
|
1255
|
-
function boundText(value) {
|
|
1256
|
-
const codePoints = [...value];
|
|
1257
|
-
return codePoints.length > HISTORY_MAX_TEXT_CODE_POINTS ? { value: codePoints.slice(0, HISTORY_MAX_TEXT_CODE_POINTS).join(""), truncated: true } : { value, truncated: false };
|
|
1258
|
-
}
|
|
1259
|
-
function matchesArchiveStem(stem, identityBase) {
|
|
1260
|
-
return stem === identityBase || new RegExp(`^${escapeRegExp(identityBase)}-(?:[2-9]|[1-9]\\d+)$`).test(stem);
|
|
1261
|
-
}
|
|
1262
|
-
function isCalendarDate(value) {
|
|
1263
|
-
const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
|
|
1264
|
-
return Number.isFinite(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value;
|
|
1265
|
-
}
|
|
1266
|
-
function archiveOutputPath(sourcePath, archivesDir) {
|
|
1267
|
-
return `.rsp/archives/${normalizeLogicalPath(relative3(archivesDir, sourcePath))}`;
|
|
1268
|
-
}
|
|
1269
|
-
function normalizeOutputPath(path, archivesDir) {
|
|
1270
|
-
const relativePath = normalizeLogicalPath(relative3(archivesDir, path));
|
|
1271
|
-
return relativePath === "" ? ".rsp/archives" : `.rsp/archives/${relativePath}`;
|
|
1272
|
-
}
|
|
1273
|
-
function errorDiagnostic(code, path, message) {
|
|
1274
|
-
return { severity: "error", code, path, message };
|
|
1275
|
-
}
|
|
1276
|
-
function escapeRegExp(value) {
|
|
1277
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1278
|
-
}
|
|
1279
|
-
function compareHistoryRecords(left, right) {
|
|
1280
|
-
return right.date.localeCompare(left.date) || left.workRef.localeCompare(right.workRef) || left.path.localeCompare(right.path);
|
|
1281
|
-
}
|
|
1282
|
-
function compareArchivedGroupBriefs(left, right) {
|
|
1283
|
-
return right.date.localeCompare(left.date) || left.group.localeCompare(right.group) || left.path.localeCompare(right.path);
|
|
1284
|
-
}
|
|
1285
|
-
function compareDiagnostics(left, right) {
|
|
1286
|
-
return (left.path ?? "").localeCompare(right.path ?? "") || left.code.localeCompare(right.code) || left.message.localeCompare(right.message);
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
// src/history/query.ts
|
|
1290
|
-
function queryArchiveHistory(records, query = {}) {
|
|
1291
|
-
validateArchiveHistoryQuery(query);
|
|
1292
|
-
const group = query.group === void 0 ? void 0 : normalizeWorkRefSegment(query.group);
|
|
1293
|
-
const limit = query.limit ?? HISTORY_DEFAULT_LIMIT;
|
|
1294
|
-
const matched = records.filter((record) => (!query.since || record.date >= query.since) && (!query.until || record.date <= query.until) && (!query.kind || record.kind === query.kind) && (!group || record.group === group) && (!query.search || `${record.workRef}
|
|
1295
|
-
${record.searchSummary ?? record.summary}`.toLowerCase().includes(query.search.toLowerCase()))).sort(compareHistoryRecords2);
|
|
1296
|
-
const selected = matched.slice(0, limit);
|
|
1297
|
-
return {
|
|
1298
|
-
records: selected.map(toOutputRecord),
|
|
1299
|
-
summary: {
|
|
1300
|
-
matched: matched.length,
|
|
1301
|
-
returned: selected.length,
|
|
1302
|
-
hasMore: matched.length > selected.length
|
|
1303
|
-
}
|
|
1304
|
-
};
|
|
1305
|
-
}
|
|
1306
|
-
function validateArchiveHistoryQuery(query) {
|
|
1307
|
-
const limit = query.limit ?? HISTORY_DEFAULT_LIMIT;
|
|
1308
|
-
if (!Number.isInteger(limit) || limit < 1 || limit > HISTORY_MAX_LIMIT)
|
|
1309
|
-
throw new ArchiveHistoryError("invalid_history_limit", `history limit must be an integer from 1 through ${HISTORY_MAX_LIMIT}`);
|
|
1310
|
-
if (query.since && !isCalendarDate2(query.since))
|
|
1311
|
-
throw new ArchiveHistoryError("invalid_history_since", "history since must be a valid YYYY-MM-DD date");
|
|
1312
|
-
if (query.until && !isCalendarDate2(query.until))
|
|
1313
|
-
throw new ArchiveHistoryError("invalid_history_until", "history until must be a valid YYYY-MM-DD date");
|
|
1314
|
-
if (query.since && query.until && query.since > query.until)
|
|
1315
|
-
throw new ArchiveHistoryError("invalid_history_date_range", "history since must be on or before until");
|
|
1316
|
-
if (query.kind !== void 0 && query.kind.trim() === "")
|
|
1317
|
-
throw new ArchiveHistoryError("invalid_history_kind", "history kind must be non-empty");
|
|
1318
|
-
if (query.group !== void 0) {
|
|
1319
|
-
try {
|
|
1320
|
-
normalizeWorkRefSegment(query.group);
|
|
1321
|
-
} catch {
|
|
1322
|
-
throw new ArchiveHistoryError("invalid_history_group", "history group must be one safe Unicode segment");
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
if (query.search !== void 0 && query.search.trim() === "")
|
|
1326
|
-
throw new ArchiveHistoryError("invalid_history_search", "history search must be non-empty");
|
|
1327
|
-
}
|
|
1328
|
-
function selectArchiveHistoryRecord(records, selector) {
|
|
1329
|
-
const matches = "path" in selector ? records.filter((record) => record.path === selector.path) : records.filter((record) => record.workRef === selector.workRef);
|
|
1330
|
-
const identity = "path" in selector ? selector.path : selector.workRef;
|
|
1331
|
-
if (matches.length === 0)
|
|
1332
|
-
throw new ArchiveHistoryError("archive_not_found", `archived Change not found: ${identity}`);
|
|
1333
|
-
if (matches.length > 1) {
|
|
1334
|
-
const candidates = matches.map((record) => record.path).sort();
|
|
1335
|
-
throw new ArchiveHistoryError("archive_ambiguous", `multiple archives match WorkRef ${identity}`, candidates);
|
|
1336
|
-
}
|
|
1337
|
-
return matches[0];
|
|
1338
|
-
}
|
|
1339
|
-
function selectArchivedGroupBrief(records, selector) {
|
|
1340
|
-
const matches = "path" in selector ? records.filter((record) => record.path === selector.path) : records.filter((record) => record.group === selector.group);
|
|
1341
|
-
const identity = "path" in selector ? selector.path : selector.group;
|
|
1342
|
-
if (matches.length === 0)
|
|
1343
|
-
throw new ArchiveHistoryError("archive_not_found", `archived Change Group not found: ${identity}`);
|
|
1344
|
-
if (matches.length > 1) {
|
|
1345
|
-
const candidates = matches.map((record) => record.path).sort();
|
|
1346
|
-
throw new ArchiveHistoryError("archive_ambiguous", `multiple archives match Change Group ${identity}`, candidates);
|
|
1347
|
-
}
|
|
1348
|
-
return matches[0];
|
|
1349
|
-
}
|
|
1350
|
-
async function readArchiveHistoryDetail(record) {
|
|
1351
|
-
let content;
|
|
1352
|
-
try {
|
|
1353
|
-
content = await readFile3(record.sourcePath, "utf-8");
|
|
1354
|
-
} catch (error) {
|
|
1355
|
-
throw new ArchiveHistoryError("archive_read_failed", `unable to read archived Change ${record.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1356
|
-
}
|
|
1357
|
-
const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
|
|
1358
|
-
const tasks = getDocumentSectionBody(document, "tasks");
|
|
1359
|
-
const verify = getDocumentSectionBody(document, "verify");
|
|
1360
|
-
const blockers = getDocumentSectionBody(document, "blockers");
|
|
1361
|
-
return {
|
|
1362
|
-
...toOutputRecord(record),
|
|
1363
|
-
scenarioCount: parseScenarios(content).length,
|
|
1364
|
-
checkboxes: {
|
|
1365
|
-
tasks: countCheckboxes(tasks),
|
|
1366
|
-
verify: countCheckboxes(verify)
|
|
1367
|
-
},
|
|
1368
|
-
evidence: {
|
|
1369
|
-
tasks: boundEvidence(tasks),
|
|
1370
|
-
verify: boundEvidence(verify),
|
|
1371
|
-
blockers: boundEvidence(blockers, true)
|
|
1372
|
-
}
|
|
1373
|
-
};
|
|
1374
|
-
}
|
|
1375
|
-
function historyInspectionComplete(inspection) {
|
|
1376
|
-
return inspection.diagnostics.every((diagnostic) => diagnostic.severity !== "error");
|
|
1377
|
-
}
|
|
1378
|
-
function toOutputRecord(record) {
|
|
1379
|
-
const { searchSummary: _searchSummary, sourcePath: _sourcePath, ...output } = record;
|
|
1380
|
-
return output;
|
|
1381
|
-
}
|
|
1382
|
-
function boundEvidence(section, omitNone = false) {
|
|
1383
|
-
const rawItems = section.split("\n").map((line) => line.trim()).filter(Boolean).filter((line) => !omitNone || !/^[-*]\s*(?:none)?$/i.test(line) && !/^none$/i.test(line));
|
|
1384
|
-
const boundedItems = rawItems.slice(0, HISTORY_MAX_EVIDENCE_ITEMS).map((item) => boundText(item));
|
|
1385
|
-
return {
|
|
1386
|
-
items: boundedItems.map((item) => item.value),
|
|
1387
|
-
truncated: rawItems.length > HISTORY_MAX_EVIDENCE_ITEMS || boundedItems.some((item) => item.truncated)
|
|
1388
|
-
};
|
|
1389
|
-
}
|
|
1390
|
-
function isCalendarDate2(value) {
|
|
1391
|
-
if (!/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
1392
|
-
return false;
|
|
1393
|
-
const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
|
|
1394
|
-
return Number.isFinite(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value;
|
|
1395
|
-
}
|
|
1396
|
-
function compareHistoryRecords2(left, right) {
|
|
1397
|
-
return right.date.localeCompare(left.date) || left.workRef.localeCompare(right.workRef) || left.path.localeCompare(right.path);
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
// src/core/dependency-plan.ts
|
|
1401
|
-
var NONE_BLOCKER_RE = /^[-*]\s*(?:none)?$/i;
|
|
1402
|
-
var REQUIRES_RE = /^[-*]\s+requires\s+`([^`]+)`:[ \t]*(\S.*)$/i;
|
|
1403
|
-
async function inspectChangeDependencies(options = {}) {
|
|
1404
|
-
const workTree = options.workTree ?? await inspectWorkTree();
|
|
1405
|
-
const archiveTree = options.archiveTree ?? await inspectArchiveTree();
|
|
1406
|
-
const archiveHistory = archiveTree.rootExists ? options.archiveHistory ?? await inspectArchiveHistory({ archiveTree }) : null;
|
|
1407
|
-
const openNames = new Set(workTree.changes.map((ref) => ref.name));
|
|
1408
|
-
const archivedNames = new Set(
|
|
1409
|
-
archiveHistory && historyInspectionComplete(archiveHistory) ? archiveHistory.records.map((record) => record.workRef) : []
|
|
1410
|
-
);
|
|
1411
|
-
const parsedByName = /* @__PURE__ */ new Map();
|
|
1412
|
-
const blockedGroups = /* @__PURE__ */ new Set();
|
|
1413
|
-
const diagnostics = archiveTree.diagnostics.map((diagnostic) => ({
|
|
1414
|
-
severity: "error",
|
|
1415
|
-
code: diagnostic.code,
|
|
1416
|
-
path: diagnostic.path,
|
|
1417
|
-
message: diagnostic.message
|
|
1418
|
-
}));
|
|
1419
|
-
if (archiveHistory)
|
|
1420
|
-
diagnostics.push(...archiveHistory.diagnostics);
|
|
1421
|
-
for (const brief of workTree.briefs) {
|
|
1422
|
-
try {
|
|
1423
|
-
if (hasMeaningfulBlockers(await readFile4(brief.path, "utf-8")))
|
|
1424
|
-
blockedGroups.add(brief.group);
|
|
1425
|
-
} catch (error) {
|
|
1426
|
-
diagnostics.push({
|
|
1427
|
-
severity: "error",
|
|
1428
|
-
code: "group_brief_read_failed",
|
|
1429
|
-
change: brief.group,
|
|
1430
|
-
path: brief.path,
|
|
1431
|
-
message: error instanceof Error ? error.message : String(error)
|
|
1432
|
-
});
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
1435
|
-
for (const ref of workTree.changes) {
|
|
1436
|
-
try {
|
|
1437
|
-
const parsed = parseDependencyBlockers(await readFile4(ref.path, "utf-8"));
|
|
1438
|
-
if (ref.group && blockedGroups.has(ref.group))
|
|
1439
|
-
parsed.external = true;
|
|
1440
|
-
parsedByName.set(ref.name, parsed);
|
|
1441
|
-
for (const line of parsed.malformed) {
|
|
1442
|
-
diagnostics.push({
|
|
1443
|
-
severity: "error",
|
|
1444
|
-
code: "dependency_syntax_invalid",
|
|
1445
|
-
change: ref.name,
|
|
1446
|
-
path: ref.path,
|
|
1447
|
-
message: `invalid dependency blocker syntax: ${line}`,
|
|
1448
|
-
hint: "Use: - requires `<change-work-ref>`: <reason>"
|
|
1449
|
-
});
|
|
1450
|
-
}
|
|
1451
|
-
} catch (error) {
|
|
1452
|
-
diagnostics.push({
|
|
1453
|
-
severity: "error",
|
|
1454
|
-
code: "change_read_failed",
|
|
1455
|
-
change: ref.name,
|
|
1456
|
-
path: ref.path,
|
|
1457
|
-
message: error instanceof Error ? error.message : String(error)
|
|
1458
|
-
});
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
const edges = [];
|
|
1462
|
-
for (const [change, blockers] of parsedByName) {
|
|
1463
|
-
for (const requirement of blockers.requires) {
|
|
1464
|
-
const required = requirement.name;
|
|
1465
|
-
const edge = {
|
|
1466
|
-
change,
|
|
1467
|
-
requires: required,
|
|
1468
|
-
reason: requirement.reason,
|
|
1469
|
-
state: openNames.has(required) ? "open" : archivedNames.has(required) ? "archived" : "missing"
|
|
1470
|
-
};
|
|
1471
|
-
edges.push(edge);
|
|
1472
|
-
if (!isCanonicalExecutableWorkRef(required)) {
|
|
1473
|
-
diagnostics.push({
|
|
1474
|
-
severity: "error",
|
|
1475
|
-
code: "dependency_target_invalid",
|
|
1476
|
-
change,
|
|
1477
|
-
message: `dependency target must be an executable Change WorkRef: ${required}`
|
|
1478
|
-
});
|
|
1479
|
-
} else if (required === change) {
|
|
1480
|
-
diagnostics.push({
|
|
1481
|
-
severity: "error",
|
|
1482
|
-
code: "dependency_self_reference",
|
|
1483
|
-
change,
|
|
1484
|
-
message: "Change cannot require itself"
|
|
1485
|
-
});
|
|
1486
|
-
} else if (edge.state === "missing") {
|
|
1487
|
-
diagnostics.push({
|
|
1488
|
-
severity: "error",
|
|
1489
|
-
code: "dependency_target_missing",
|
|
1490
|
-
change,
|
|
1491
|
-
message: `dependency target is neither open nor archived: ${required}`
|
|
1492
|
-
});
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
edges.sort(compareEdges);
|
|
1497
|
-
diagnostics.push(...findCycleDiagnostics(edges));
|
|
1498
|
-
const activeBlockers = /* @__PURE__ */ new Map();
|
|
1499
|
-
const blocked = [...parsedByName].map(([change, blockers]) => {
|
|
1500
|
-
const requires = edges.filter((edge) => edge.change === change && edge.state !== "archived").map((edge) => edge.requires).sort();
|
|
1501
|
-
activeBlockers.set(change, blockers.external || requires.length > 0);
|
|
1502
|
-
return { change, requires, external: blockers.external };
|
|
1503
|
-
}).filter((blocker) => blocker.external || blocker.requires.length > 0).sort((left, right) => left.change.localeCompare(right.change));
|
|
1504
|
-
const waves = buildWaves([...openNames], edges, parsedByName, options.preferredOrder ?? []);
|
|
1505
|
-
const graphValid = diagnostics.every((diagnostic) => diagnostic.severity !== "error");
|
|
1506
|
-
if (!graphValid) {
|
|
1507
|
-
for (const name of openNames)
|
|
1508
|
-
activeBlockers.set(name, true);
|
|
1509
|
-
}
|
|
1510
|
-
const ready = graphValid ? waves[0] ?? [] : [];
|
|
1511
|
-
const executable = new Set(graphValid ? waves.flat() : []);
|
|
1512
|
-
const nodes = [.../* @__PURE__ */ new Set([...openNames, ...edges.map((edge) => edge.requires)])].sort().map((name) => {
|
|
1513
|
-
let state;
|
|
1514
|
-
if (!openNames.has(name) && archivedNames.has(name))
|
|
1515
|
-
state = "archived";
|
|
1516
|
-
else if (!openNames.has(name))
|
|
1517
|
-
state = "missing";
|
|
1518
|
-
else if (ready.includes(name))
|
|
1519
|
-
state = "ready";
|
|
1520
|
-
else if (executable.has(name))
|
|
1521
|
-
state = "waiting";
|
|
1522
|
-
else
|
|
1523
|
-
state = "blocked";
|
|
1524
|
-
return { name, selection: openNames.has(name) ? "selected" : "prerequisite", state };
|
|
1525
|
-
});
|
|
1526
|
-
return {
|
|
1527
|
-
plan: {
|
|
1528
|
-
nodes,
|
|
1529
|
-
ready,
|
|
1530
|
-
edges,
|
|
1531
|
-
blocked,
|
|
1532
|
-
waves: graphValid ? waves : []
|
|
1533
|
-
},
|
|
1534
|
-
activeBlockers,
|
|
1535
|
-
diagnostics
|
|
1536
|
-
};
|
|
1537
|
-
}
|
|
1538
|
-
function parseDependencyBlockers(content) {
|
|
1539
|
-
const requires = /* @__PURE__ */ new Map();
|
|
1540
|
-
let external = false;
|
|
1541
|
-
const malformed = [];
|
|
1542
|
-
for (const line of extractBlockerLines(content)) {
|
|
1543
|
-
if (!line || NONE_BLOCKER_RE.test(line) || /^none$/i.test(line))
|
|
1544
|
-
continue;
|
|
1545
|
-
const dependency = line.match(REQUIRES_RE);
|
|
1546
|
-
if (dependency) {
|
|
1547
|
-
if (!requires.has(dependency[1]))
|
|
1548
|
-
requires.set(dependency[1], dependency[2]);
|
|
1549
|
-
} else if (/^[-*]\s+requires\b/i.test(line)) {
|
|
1550
|
-
malformed.push(line);
|
|
1551
|
-
external = true;
|
|
1552
|
-
} else {
|
|
1553
|
-
external = true;
|
|
1554
|
-
}
|
|
1555
|
-
}
|
|
1556
|
-
return {
|
|
1557
|
-
requires: [...requires].map(([name, reason]) => ({ name, reason })).sort((left, right) => left.name.localeCompare(right.name)),
|
|
1558
|
-
external,
|
|
1559
|
-
malformed
|
|
1560
|
-
};
|
|
1561
|
-
}
|
|
1562
|
-
function buildWaves(names, edges, blockersByName, preferredOrder) {
|
|
1563
|
-
const orderIndex = new Map(preferredOrder.map((name, index) => [name, index]));
|
|
1564
|
-
const excluded = new Set(
|
|
1565
|
-
[...blockersByName].filter(([, blockers]) => blockers.external).map(([name]) => name)
|
|
1566
|
-
);
|
|
1567
|
-
let changed = true;
|
|
1568
|
-
while (changed) {
|
|
1569
|
-
changed = false;
|
|
1570
|
-
for (const edge of edges) {
|
|
1571
|
-
if ((edge.state === "missing" || excluded.has(edge.requires)) && !excluded.has(edge.change)) {
|
|
1572
|
-
excluded.add(edge.change);
|
|
1573
|
-
changed = true;
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
}
|
|
1577
|
-
const remaining = new Set(names.filter((name) => !excluded.has(name)));
|
|
1578
|
-
const waves = [];
|
|
1579
|
-
while (remaining.size > 0) {
|
|
1580
|
-
const wave = [...remaining].filter((name) => !edges.some((edge) => edge.change === name && edge.state === "open" && remaining.has(edge.requires))).sort((left, right) => comparePlanNames(left, right, orderIndex));
|
|
1581
|
-
if (wave.length === 0)
|
|
1582
|
-
break;
|
|
1583
|
-
waves.push(wave);
|
|
1584
|
-
for (const name of wave)
|
|
1585
|
-
remaining.delete(name);
|
|
1586
|
-
}
|
|
1587
|
-
return waves;
|
|
1588
|
-
}
|
|
1589
|
-
function comparePlanNames(left, right, orderIndex) {
|
|
1590
|
-
const leftGroup = left.includes("/") ? left.split("/")[0] : null;
|
|
1591
|
-
const rightGroup = right.includes("/") ? right.split("/")[0] : null;
|
|
1592
|
-
if (leftGroup && leftGroup === rightGroup) {
|
|
1593
|
-
const leftIndex = orderIndex.get(left);
|
|
1594
|
-
const rightIndex = orderIndex.get(right);
|
|
1595
|
-
if (leftIndex !== void 0 && rightIndex !== void 0)
|
|
1596
|
-
return leftIndex - rightIndex;
|
|
1597
|
-
}
|
|
1598
|
-
return left.localeCompare(right);
|
|
1599
|
-
}
|
|
1600
|
-
function compareEdges(left, right) {
|
|
1601
|
-
return left.change.localeCompare(right.change) || left.requires.localeCompare(right.requires);
|
|
1602
|
-
}
|
|
1603
|
-
function findCycleDiagnostics(edges) {
|
|
1604
|
-
const adjacency = /* @__PURE__ */ new Map();
|
|
1605
|
-
for (const edge of edges) {
|
|
1606
|
-
if (edge.state !== "open" || edge.change === edge.requires)
|
|
1607
|
-
continue;
|
|
1608
|
-
const targets = adjacency.get(edge.change) ?? [];
|
|
1609
|
-
targets.push(edge.requires);
|
|
1610
|
-
adjacency.set(edge.change, targets.sort());
|
|
1611
|
-
}
|
|
1612
|
-
const visited = /* @__PURE__ */ new Set();
|
|
1613
|
-
const active = /* @__PURE__ */ new Set();
|
|
1614
|
-
const stack = [];
|
|
1615
|
-
const reported = /* @__PURE__ */ new Set();
|
|
1616
|
-
const diagnostics = [];
|
|
1617
|
-
const visit = (name) => {
|
|
1618
|
-
if (active.has(name)) {
|
|
1619
|
-
const start = stack.indexOf(name);
|
|
1620
|
-
const cycle = [...stack.slice(start), name];
|
|
1621
|
-
const key = [...new Set(cycle)].sort().join("|");
|
|
1622
|
-
if (!reported.has(key)) {
|
|
1623
|
-
reported.add(key);
|
|
1624
|
-
const message = `dependency cycle detected: ${cycle.join(" -> ")}`;
|
|
1625
|
-
for (const participant of [...new Set(cycle)].sort()) {
|
|
1626
|
-
diagnostics.push({
|
|
1627
|
-
severity: "error",
|
|
1628
|
-
code: "dependency_cycle",
|
|
1629
|
-
change: participant,
|
|
1630
|
-
message
|
|
1631
|
-
});
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
return;
|
|
1635
|
-
}
|
|
1636
|
-
if (visited.has(name))
|
|
1637
|
-
return;
|
|
1638
|
-
visited.add(name);
|
|
1639
|
-
active.add(name);
|
|
1640
|
-
stack.push(name);
|
|
1641
|
-
for (const target of adjacency.get(name) ?? [])
|
|
1642
|
-
visit(target);
|
|
1643
|
-
stack.pop();
|
|
1644
|
-
active.delete(name);
|
|
1645
|
-
};
|
|
1646
|
-
for (const name of [...adjacency.keys()].sort())
|
|
1647
|
-
visit(name);
|
|
1648
|
-
return diagnostics;
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
// src/status/inspect.ts
|
|
1652
|
-
async function inspectProjectStatus(options = {}) {
|
|
1653
|
-
const runtime = [];
|
|
1654
|
-
const diagnostics = [];
|
|
1655
|
-
let manage = resolveManagePolicy({});
|
|
1656
|
-
let language = resolveLanguagePolicy({});
|
|
1657
|
-
try {
|
|
1658
|
-
const configInspection = await inspectRspConfig();
|
|
1659
|
-
const configValid = configInspection.issues.length === 0;
|
|
1660
|
-
manage = resolveManagePolicy(configInspection.config, { configValid });
|
|
1661
|
-
language = resolveLanguagePolicy(configInspection.config, { configValid });
|
|
1662
|
-
if (!configValid) {
|
|
1663
|
-
diagnostics.push({
|
|
1664
|
-
severity: "error",
|
|
1665
|
-
code: "invalid_config",
|
|
1666
|
-
path: CONFIG_PATH,
|
|
1667
|
-
message: configInspection.issues.join("; ")
|
|
1668
|
-
});
|
|
1669
|
-
}
|
|
1670
|
-
} catch (error) {
|
|
1671
|
-
manage = resolveManagePolicy({}, { configValid: false });
|
|
1672
|
-
language = resolveLanguagePolicy({}, { configValid: false });
|
|
1673
|
-
diagnostics.push({
|
|
1674
|
-
severity: "error",
|
|
1675
|
-
code: "invalid_config",
|
|
1676
|
-
path: CONFIG_PATH,
|
|
1677
|
-
message: toErrorMessage(error)
|
|
1678
|
-
});
|
|
1679
|
-
}
|
|
1680
|
-
const focusTree = await inspectFocusTree();
|
|
1681
|
-
for (const diagnostic of focusTree.diagnostics) {
|
|
1682
|
-
diagnostics.push({
|
|
1683
|
-
severity: "error",
|
|
1684
|
-
code: diagnostic.code,
|
|
1685
|
-
change: diagnostic.input,
|
|
1686
|
-
path: diagnostic.path,
|
|
1687
|
-
message: diagnostic.message
|
|
1688
|
-
});
|
|
1689
|
-
}
|
|
1690
|
-
const rawFocusedSet = new Set(focusTree.markers.map((marker) => marker.name));
|
|
1691
|
-
const focusedSet = /* @__PURE__ */ new Set();
|
|
1692
|
-
const focusedPaths = /* @__PURE__ */ new Map();
|
|
1693
|
-
for (const name of rawFocusedSet) {
|
|
1694
|
-
try {
|
|
1695
|
-
const ref = resolveWorkRef(name, { executable: true });
|
|
1696
|
-
focusedSet.add(ref.name);
|
|
1697
|
-
focusedPaths.set(ref.name, ref.path);
|
|
1698
|
-
} catch (error) {
|
|
1699
|
-
if (!(error instanceof WorkRefError))
|
|
1700
|
-
throw error;
|
|
1701
|
-
diagnostics.push({
|
|
1702
|
-
severity: "error",
|
|
1703
|
-
code: error.code,
|
|
1704
|
-
path: `.rsp/focus.d/${name}`,
|
|
1705
|
-
change: name,
|
|
1706
|
-
message: error.message
|
|
1707
|
-
});
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
const workTree = await inspectWorkTree();
|
|
1711
|
-
const archiveTree = await inspectArchiveTree();
|
|
1712
|
-
const historyInspection = await inspectArchiveHistory({ archiveTree });
|
|
1713
|
-
diagnostics.push(...workTree.diagnostics.map((diagnostic) => ({
|
|
1714
|
-
severity: "error",
|
|
1715
|
-
code: diagnostic.code,
|
|
1716
|
-
change: diagnostic.input,
|
|
1717
|
-
path: diagnostic.path,
|
|
1718
|
-
message: diagnostic.message
|
|
1719
|
-
})));
|
|
1720
|
-
const groupInspection = await inspectChangeGroups({ workTree, focusTree, archiveTree });
|
|
1721
|
-
diagnostics.push(...groupInspection.diagnostics);
|
|
1722
|
-
const dependencyInspection = await inspectChangeDependencies({
|
|
1723
|
-
workTree,
|
|
1724
|
-
archiveTree,
|
|
1725
|
-
archiveHistory: historyInspection,
|
|
1726
|
-
preferredOrder: groupInspection.groups.flatMap((group) => group.slices.map((slice) => slice.name))
|
|
1727
|
-
});
|
|
1728
|
-
diagnostics.push(...dependencyInspection.diagnostics);
|
|
1729
|
-
const changeMap = new Map(workTree.changes.map((ref) => [ref.name, ref.path]));
|
|
1730
|
-
const allNames = [.../* @__PURE__ */ new Set([...changeMap.keys(), ...focusedSet])].sort();
|
|
1731
|
-
const records = [];
|
|
1732
|
-
const nowMs = options.nowMs ?? Date.now();
|
|
1733
|
-
for (const name of allNames) {
|
|
1734
|
-
const path = changeMap.get(name);
|
|
1735
|
-
let kind = "\u2014";
|
|
1736
|
-
let summary = null;
|
|
1737
|
-
let title = name;
|
|
1738
|
-
let blockerEntries = [];
|
|
1739
|
-
let readiness = emptyReadiness(false);
|
|
1740
|
-
let ageDays = null;
|
|
1741
|
-
const isFocused = focusedSet.has(name);
|
|
1742
|
-
let isBlocked = false;
|
|
1743
|
-
let done = 0;
|
|
1744
|
-
let total = 0;
|
|
1745
|
-
let progressKnown = false;
|
|
1746
|
-
let issues = [];
|
|
1747
|
-
if (path) {
|
|
1748
|
-
try {
|
|
1749
|
-
const content = await readFile5(path, "utf-8");
|
|
1750
|
-
title = extractChangeTitle(content) ?? name;
|
|
1751
|
-
summary = extractChangeSummary(content);
|
|
1752
|
-
blockerEntries = extractBlockerEntries(content);
|
|
1753
|
-
try {
|
|
1754
|
-
const frontmatter = parseFrontmatter(content);
|
|
1755
|
-
kind = frontmatter?.kind ? String(frontmatter.kind) : "\u2014";
|
|
1756
|
-
issues = parseIssueRelationships(frontmatter);
|
|
1757
|
-
} catch (error) {
|
|
1758
|
-
kind = "(invalid)";
|
|
1759
|
-
const issueError = error instanceof IssueRelationshipError;
|
|
1760
|
-
if (issueError) {
|
|
1761
|
-
diagnostics.push({
|
|
1762
|
-
severity: "error",
|
|
1763
|
-
code: error.code,
|
|
1764
|
-
change: name,
|
|
1765
|
-
path,
|
|
1766
|
-
message: error.message
|
|
1767
|
-
});
|
|
1768
|
-
}
|
|
1769
|
-
runtime.push({
|
|
1770
|
-
code: issueError ? error.code : "frontmatter_parse_failed",
|
|
1771
|
-
operation: "parseFrontmatter",
|
|
1772
|
-
path,
|
|
1773
|
-
message: toErrorMessage(error)
|
|
1774
|
-
});
|
|
1775
|
-
}
|
|
1776
|
-
isBlocked = dependencyInspection.activeBlockers.get(name) ?? hasMeaningfulBlockers(content);
|
|
1777
|
-
const checkboxes = countCheckboxes(content);
|
|
1778
|
-
done = checkboxes.done;
|
|
1779
|
-
total = checkboxes.total;
|
|
1780
|
-
progressKnown = true;
|
|
1781
|
-
const details = collectArchiveReadiness(content, { activeBlockers: isBlocked });
|
|
1782
|
-
readiness = {
|
|
1783
|
-
incompleteTasks: details.taskTodos.length,
|
|
1784
|
-
incompleteVerify: details.verifyTodos.length,
|
|
1785
|
-
activeBlockers: details.activeBlockers,
|
|
1786
|
-
missingScenarios: details.missingScenarios,
|
|
1787
|
-
deterministic: details.deterministic,
|
|
1788
|
-
semantic: details.semantic,
|
|
1789
|
-
archiveReady: details.archiveReady
|
|
1790
|
-
};
|
|
1791
|
-
} catch (error) {
|
|
1792
|
-
diagnostics.push({
|
|
1793
|
-
severity: "error",
|
|
1794
|
-
code: "change_read_failed",
|
|
1795
|
-
change: name,
|
|
1796
|
-
path,
|
|
1797
|
-
message: "unable to read open Change",
|
|
1798
|
-
hint: toErrorMessage(error)
|
|
1799
|
-
});
|
|
1800
|
-
runtime.push({
|
|
1801
|
-
code: "change_read_failed",
|
|
1802
|
-
operation: "readFile",
|
|
1803
|
-
path,
|
|
1804
|
-
message: toErrorMessage(error)
|
|
1805
|
-
});
|
|
1806
|
-
}
|
|
1807
|
-
try {
|
|
1808
|
-
const metadata = await stat(path);
|
|
1809
|
-
ageDays = Math.floor((nowMs - metadata.mtime.getTime()) / (1e3 * 60 * 60 * 24));
|
|
1810
|
-
} catch (error) {
|
|
1811
|
-
diagnostics.push({
|
|
1812
|
-
severity: "error",
|
|
1813
|
-
code: "change_stat_failed",
|
|
1814
|
-
change: name,
|
|
1815
|
-
path,
|
|
1816
|
-
message: "unable to inspect open Change metadata",
|
|
1817
|
-
hint: toErrorMessage(error)
|
|
1818
|
-
});
|
|
1819
|
-
runtime.push({
|
|
1820
|
-
code: "change_stat_failed",
|
|
1821
|
-
operation: "stat",
|
|
1822
|
-
path,
|
|
1823
|
-
message: toErrorMessage(error)
|
|
1824
|
-
});
|
|
1825
|
-
}
|
|
1826
|
-
} else {
|
|
1827
|
-
kind = "(missing)";
|
|
1828
|
-
const path2 = focusedPaths.get(name);
|
|
1829
|
-
diagnostics.push({
|
|
1830
|
-
severity: "error",
|
|
1831
|
-
code: "focused_change_missing",
|
|
1832
|
-
change: name,
|
|
1833
|
-
path: path2,
|
|
1834
|
-
message: "focus marker points to a missing Change file"
|
|
1835
|
-
});
|
|
1836
|
-
runtime.push({
|
|
1837
|
-
code: "focused_change_missing",
|
|
1838
|
-
operation: "resolveChange",
|
|
1839
|
-
path: path2,
|
|
1840
|
-
message: "focus marker points to a missing change file"
|
|
1841
|
-
});
|
|
1842
|
-
}
|
|
1843
|
-
records.push({
|
|
1844
|
-
output: {
|
|
1845
|
-
name,
|
|
1846
|
-
summary,
|
|
1847
|
-
kind,
|
|
1848
|
-
progress: { done, total },
|
|
1849
|
-
ageDays,
|
|
1850
|
-
isFocused,
|
|
1851
|
-
isBlocked,
|
|
1852
|
-
path: path ? normalizeLogicalPath(path) : null,
|
|
1853
|
-
issues
|
|
1854
|
-
},
|
|
1855
|
-
progressKnown,
|
|
1856
|
-
title,
|
|
1857
|
-
blockerEntries,
|
|
1858
|
-
readiness
|
|
1859
|
-
});
|
|
1860
|
-
}
|
|
1861
|
-
return {
|
|
1862
|
-
manage,
|
|
1863
|
-
language,
|
|
1864
|
-
focused: [...focusedSet].sort(),
|
|
1865
|
-
records,
|
|
1866
|
-
groups: groupInspection.groups,
|
|
1867
|
-
plan: dependencyInspection.plan,
|
|
1868
|
-
archiveTrend: historyInspectionComplete(historyInspection) ? deriveArchiveTrend(historyInspection.records) : [],
|
|
1869
|
-
diagnostics: [...diagnostics, ...historyInspection.rootExists ? historyInspection.diagnostics : []],
|
|
1870
|
-
runtime: [...runtime, ...historyInspection.runtime]
|
|
1871
|
-
};
|
|
1872
|
-
}
|
|
1873
|
-
function emptyReadiness(activeBlockers) {
|
|
1874
|
-
return {
|
|
1875
|
-
incompleteTasks: 0,
|
|
1876
|
-
incompleteVerify: 0,
|
|
1877
|
-
activeBlockers,
|
|
1878
|
-
missingScenarios: false,
|
|
1879
|
-
deterministic: "pass",
|
|
1880
|
-
semantic: "needs-review",
|
|
1881
|
-
archiveReady: activeBlockers ? "no" : "judgment"
|
|
1882
|
-
};
|
|
1883
|
-
}
|
|
1884
|
-
function extractChangeTitle(content) {
|
|
1885
|
-
return getDocumentTitle(parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA), "spaced");
|
|
1886
|
-
}
|
|
1887
|
-
function extractBlockerEntries(content) {
|
|
1888
|
-
const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
|
|
1889
|
-
return getDocumentSectionBody(document, "blockers").split("\n").map((line) => line.trim()).filter((line) => /^[-*]\s+/.test(line)).map((line) => line.replace(/^[-*]\s+/, "").trim()).filter((line) => line.toLowerCase() !== "none");
|
|
1890
|
-
}
|
|
1891
|
-
function deriveArchiveTrend(records) {
|
|
1892
|
-
const counts = /* @__PURE__ */ new Map();
|
|
1893
|
-
for (const record of records) {
|
|
1894
|
-
const month = record.date.slice(0, 7);
|
|
1895
|
-
counts.set(month, (counts.get(month) ?? 0) + 1);
|
|
1896
|
-
}
|
|
1897
|
-
return [...counts].sort(([left], [right]) => left.localeCompare(right)).map(([month, count]) => ({ month, count }));
|
|
1898
|
-
}
|
|
1899
|
-
|
|
1900
|
-
// src/status/dependency-forest.ts
|
|
1901
|
-
function defaultRoots(plan) {
|
|
1902
|
-
const selected = new Set(plan.nodes.filter((node) => node.selection === "selected").map((node) => node.name));
|
|
1903
|
-
const nested = new Set(plan.edges.filter((edge) => selected.has(edge.change) && selected.has(edge.requires)).map((edge) => edge.requires));
|
|
1904
|
-
const roots = [...selected].filter((name) => !nested.has(name)).sort();
|
|
1905
|
-
return roots.length > 0 ? roots : [...selected].sort();
|
|
1906
|
-
}
|
|
1907
|
-
function projectDependencyForest(plan, requestedRoots = defaultRoots(plan)) {
|
|
1908
|
-
const nodes = new Map(plan.nodes.map((node) => [node.name, node]));
|
|
1909
|
-
const edges = /* @__PURE__ */ new Map();
|
|
1910
|
-
for (const edge of plan.edges) {
|
|
1911
|
-
const children = edges.get(edge.change) ?? [];
|
|
1912
|
-
children.push(edge);
|
|
1913
|
-
edges.set(edge.change, children);
|
|
1914
|
-
}
|
|
1915
|
-
for (const children of edges.values())
|
|
1916
|
-
children.sort((left, right) => left.requires.localeCompare(right.requires));
|
|
1917
|
-
const expanded = /* @__PURE__ */ new Set();
|
|
1918
|
-
const visit = (name, edge) => {
|
|
1919
|
-
const node = nodes.get(name);
|
|
1920
|
-
if (!node)
|
|
1921
|
-
return null;
|
|
1922
|
-
const shared = expanded.has(name);
|
|
1923
|
-
if (!shared)
|
|
1924
|
-
expanded.add(name);
|
|
1925
|
-
return {
|
|
1926
|
-
...node,
|
|
1927
|
-
...edge ? { reason: edge.reason, edgeState: edge.state } : {},
|
|
1928
|
-
shared,
|
|
1929
|
-
children: shared ? [] : (edges.get(name) ?? []).map((child) => visit(child.requires, child)).filter((child) => child !== null)
|
|
1930
|
-
};
|
|
1931
|
-
};
|
|
1932
|
-
return requestedRoots.map((name) => visit(name)).filter((node) => node !== null);
|
|
1933
|
-
}
|
|
1934
|
-
|
|
1935
|
-
export {
|
|
1936
|
-
GROUP_BRIEF_FILENAME,
|
|
1937
|
-
WorkRefError,
|
|
1938
|
-
normalizeWorkRefSegment,
|
|
1939
|
-
normalizeExecutableWorkRef,
|
|
1940
|
-
resolveWorkRef,
|
|
1941
|
-
resolveFocusMarkerPath,
|
|
1942
|
-
resolveArchiveDirectory,
|
|
1943
|
-
inspectFocusTree,
|
|
1944
|
-
inspectArchiveTree,
|
|
1945
|
-
inspectWorkTree,
|
|
1946
|
-
GROUP_REOPEN_COMPLETION_PREFIX,
|
|
1947
|
-
resolveExecutableChange,
|
|
1948
|
-
generateGroupBriefContent,
|
|
1949
|
-
inspectChangeGroups,
|
|
1950
|
-
getGroupReopenEvidence,
|
|
1951
|
-
hasArchivedGroupBrief,
|
|
1952
|
-
IssueRelationshipError,
|
|
1953
|
-
parseIssueRelationships,
|
|
1954
|
-
createIssueRelationship,
|
|
1955
|
-
HISTORY_DEFAULT_LIMIT,
|
|
1956
|
-
HISTORY_MAX_LIMIT,
|
|
1957
|
-
ArchiveHistoryError,
|
|
1958
|
-
inspectArchiveHistory,
|
|
1959
|
-
queryArchiveHistory,
|
|
1960
|
-
selectArchiveHistoryRecord,
|
|
1961
|
-
selectArchivedGroupBrief,
|
|
1962
|
-
readArchiveHistoryDetail,
|
|
1963
|
-
historyInspectionComplete,
|
|
1964
|
-
inspectChangeDependencies,
|
|
1965
|
-
inspectProjectStatus,
|
|
1966
|
-
projectDependencyForest
|
|
1967
|
-
};
|