@oevery/rsp 3.3.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -17
- package/README.zh-CN.md +25 -18
- package/dist/chunk-JO6NDJM4.mjs +360 -0
- package/dist/chunk-LGDCIQYL.mjs +725 -0
- package/dist/{chunk-7HXQNBIG.mjs → chunk-P3KO3GZW.mjs} +420 -268
- package/dist/chunk-U56XMIOO.mjs +66 -0
- package/dist/{chunk-TSUDEX5Y.mjs → chunk-VDRXFKBP.mjs} +142 -867
- package/dist/cli.mjs +1111 -2347
- package/dist/client-EHRWH5Q7.mjs +108 -0
- package/dist/{entry-GIDNR7CJ.mjs → entry-NDASTAI5.mjs} +128 -38
- package/dist/viewer/NOTICE.txt +299 -0
- package/dist/viewer/app.css +2 -0
- package/dist/viewer/app.js +670 -0
- package/dist/viewer-build.json +1 -0
- package/dist/viewer-service.mjs +551 -0
- package/package.json +51 -25
- package/skills/rsp/SKILL.md +44 -38
- package/skills/rsp/references/architecture.md +39 -0
- package/skills/rsp/references/closeout.md +38 -0
- package/skills/rsp/references/context-migration.md +22 -0
- package/skills/rsp/references/control-outcome.md +3 -29
- package/skills/rsp/references/coordination-recovery.md +25 -0
- package/skills/rsp/references/coordination-review.md +15 -0
- package/skills/rsp/references/coordination.md +59 -0
- package/skills/rsp/references/durable-review.md +60 -12
- package/skills/rsp/references/focus-continuation.md +12 -4
- package/skills/rsp/references/groups-dependencies.md +6 -4
- package/skills/rsp/references/response-language.md +2 -0
- package/skills/rsp/references/setup-repair.md +42 -7
- package/skills/rsp/references/work-model.md +37 -0
- package/skills/rsp-commit/SKILL.md +76 -31
- package/skills/rsp-commit/references/message.md +34 -0
- package/skills/rsp-doc/SKILL.md +68 -0
- package/skills/rsp-doc/agents/openai.yaml +3 -0
- package/skills/rsp-doc/references/release-communication.md +39 -0
- package/skills/rsp-doc/references/skill-authoring.md +36 -0
- package/skills/rsp-implement/SKILL.md +60 -25
- package/skills/rsp-implement/references/diagnosis.md +10 -0
- package/skills/rsp-implement/references/finding-resolution.md +21 -0
- package/skills/rsp-implement/references/test-first.md +7 -0
- package/skills/rsp-review/SKILL.md +46 -45
- package/skills/rsp-review/references/code-review.md +30 -9
- package/skills/rsp-review/references/document-review.md +24 -9
- package/skills/rsp-review/references/release-communication.md +21 -0
- package/skills/rsp-shape/SKILL.md +26 -45
- package/skills/rsp-shape/references/complex-shaping.md +3 -1
- package/skills/rsp-shape/references/deep-clarification.md +4 -4
- package/skills/rsp-shape/references/domain-modeling.md +13 -0
- package/skills/rsp-shape/references/module-seams.md +13 -0
- package/skills/rsp-shape/references/planning.md +42 -0
- package/skills/{rsp-design → rsp-shape}/references/reversible-exploration.md +2 -2
- package/skills/rsp-verify/SKILL.md +39 -12
- package/dist/chunk-JOMFFQRG.mjs +0 -99
- package/dist/chunk-QM2UVPQK.mjs +0 -375
- package/dist/entry-YAW3PYFA.mjs +0 -174
- package/rules/rsp-rules.md +0 -48
- package/skills/rsp/references/implementation-evidence.md +0 -12
- package/skills/rsp/references/managed-routing.md +0 -45
- package/skills/rsp-design/SKILL.md +0 -53
- package/skills/rsp-design/references/domain-modeling.md +0 -11
- package/skills/rsp-design/references/module-seams.md +0 -11
- package/skills/rsp-diagnose/SKILL.md +0 -48
- package/skills/rsp-manage/SKILL.md +0 -101
- package/skills/rsp-manage/references/closeout.md +0 -13
- package/skills/rsp-manage/references/delegation.md +0 -47
- package/skills/rsp-manage/references/interruption-recovery.md +0 -19
- package/skills/rsp-manage/references/review-convergence.md +0 -9
- package/skills/rsp-release-docs/SKILL.md +0 -77
- package/skills/rsp-release-docs/references/convention-discovery.md +0 -65
- package/skills/rsp-release-docs/references/evidence-and-surfaces.md +0 -21
- package/skills/rsp-release-docs/references/output-contracts.md +0 -97
- package/skills/rsp-release-docs/references/publication-lifecycle.md +0 -28
- package/skills/rsp-resolve-findings/SKILL.md +0 -61
- package/skills/rsp-structural-audit/SKILL.md +0 -71
- package/skills/rsp-structural-audit/references/structural-lenses.md +0 -41
- package/skills/rsp-tdd/SKILL.md +0 -56
|
@@ -1,163 +1,62 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
SpecsQueryError,
|
|
4
|
+
normalizeLogicalPath,
|
|
5
|
+
readSpecsDetail,
|
|
6
|
+
searchSpecs,
|
|
7
|
+
specsInspectionComplete,
|
|
8
|
+
toErrorMessage
|
|
9
|
+
} from "./chunk-LGDCIQYL.mjs";
|
|
2
10
|
import {
|
|
3
11
|
ARCHIVES_DIR,
|
|
4
12
|
CHANGES_DIR,
|
|
5
13
|
CHANGE_DOCUMENT_SCHEMA,
|
|
6
14
|
CONFIG_PATH,
|
|
7
|
-
DEFAULT_DECISION_RECORDS_PATH,
|
|
8
15
|
FOCUS_DIR,
|
|
9
16
|
GROUP_BRIEF_DOCUMENT_SCHEMA,
|
|
10
17
|
ManagedPathError,
|
|
11
|
-
|
|
12
|
-
RSP_DIR,
|
|
13
|
-
RSP_RULES_PATH,
|
|
14
|
-
classifyVerifyCheckboxes,
|
|
15
|
-
collectVerifyCheckboxLines,
|
|
18
|
+
collectVerifyCheckboxes,
|
|
16
19
|
countCheckboxes,
|
|
17
|
-
extractBlockerLines,
|
|
18
20
|
getCanonicalSectionHeadings,
|
|
19
|
-
|
|
21
|
+
getDocumentSectionMarkdown,
|
|
20
22
|
getDocumentTitle,
|
|
21
23
|
getDocumentTitles,
|
|
22
24
|
getOpenCheckboxes,
|
|
25
|
+
getVisibleBodyLines,
|
|
23
26
|
hasMeaningfulBlockerBody,
|
|
24
27
|
hasMeaningfulBlockers,
|
|
25
28
|
inspectManagedDirectory,
|
|
26
|
-
inspectManagedFile,
|
|
27
|
-
inspectManagedFileTree,
|
|
28
29
|
inspectRspConfig,
|
|
29
|
-
|
|
30
|
+
markdownListItems,
|
|
31
|
+
markdownText,
|
|
32
|
+
openCheckboxLines,
|
|
30
33
|
parseFrontmatter,
|
|
34
|
+
parseMarkdown,
|
|
31
35
|
parseRspDocument,
|
|
32
36
|
parseScenarios,
|
|
33
|
-
|
|
37
|
+
readBlockerEntries,
|
|
34
38
|
renderDocumentSectionHeading,
|
|
35
39
|
renderDocumentTitle,
|
|
36
40
|
requireManagedDirectory,
|
|
37
41
|
requireManagedFile,
|
|
38
|
-
resolveDecisionRecordsPath,
|
|
39
42
|
resolveLanguagePolicy,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
} from "./chunk-
|
|
43
|
+
resolveWorkflowPolicy,
|
|
44
|
+
summarizeVerifyCheckboxes,
|
|
45
|
+
visibleMarkdownSource
|
|
46
|
+
} from "./chunk-P3KO3GZW.mjs";
|
|
44
47
|
|
|
45
48
|
// src/status/inspect.ts
|
|
46
|
-
import { readFile as
|
|
49
|
+
import { readFile as readFile3, stat } from "fs/promises";
|
|
47
50
|
|
|
48
51
|
// src/core/change-group.ts
|
|
49
|
-
import { readFile
|
|
50
|
-
import { dirname
|
|
51
|
-
|
|
52
|
-
// src/core/filesystem.ts
|
|
53
|
-
import { existsSync } from "fs";
|
|
54
|
-
import { readdir, readFile, rmdir } from "fs/promises";
|
|
55
|
-
import { basename, dirname, join, relative, sep } from "path";
|
|
56
|
-
async function walkMarkdownFiles(dir, options = {}) {
|
|
57
|
-
const all = await walkFiles(dir, options);
|
|
58
|
-
return all.filter((f) => f.endsWith(".md"));
|
|
59
|
-
}
|
|
60
|
-
async function inspectUnsupportedRules() {
|
|
61
|
-
const rulesDir = join(RSP_DIR, "rules");
|
|
62
|
-
const diagnostics = [];
|
|
63
|
-
const rawEntries = await walkAllEntries(rulesDir, diagnostics);
|
|
64
|
-
const entries = rawEntries.filter((entry) => entry.path !== OBSOLETE_RSP_RULES_PATH).map((entry) => ({ path: normalizeLogicalPath(relative(rulesDir, entry.path)), kind: entry.kind }));
|
|
65
|
-
const obsoleteFallbackPresent = existsSync(OBSOLETE_RSP_RULES_PATH);
|
|
66
|
-
return {
|
|
67
|
-
entries,
|
|
68
|
-
diagnostics,
|
|
69
|
-
directoryRemaining: existsSync(rulesDir) && (!obsoleteFallbackPresent || entries.length > 0)
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
async function walkAllEntries(dir, diagnostics) {
|
|
73
|
-
const entries = [];
|
|
74
|
-
try {
|
|
75
|
-
const items = await readdir(dir, { withFileTypes: true });
|
|
76
|
-
for (const item of items) {
|
|
77
|
-
const path = join(dir, item.name);
|
|
78
|
-
const kind = item.isDirectory() ? "directory" : "file";
|
|
79
|
-
entries.push({ path, kind });
|
|
80
|
-
if (kind === "directory")
|
|
81
|
-
entries.push(...await walkAllEntries(path, diagnostics));
|
|
82
|
-
}
|
|
83
|
-
} catch (error) {
|
|
84
|
-
if (error.code !== "ENOENT") {
|
|
85
|
-
diagnostics.push({ code: "walk_failed", operation: "readdir", path: dir, message: toErrorMessage(error) });
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return entries;
|
|
89
|
-
}
|
|
90
|
-
async function walkFiles(dir, options = {}) {
|
|
91
|
-
const files = [];
|
|
92
|
-
try {
|
|
93
|
-
const items = await readdir(dir, { withFileTypes: true });
|
|
94
|
-
for (const item of items) {
|
|
95
|
-
if (item.name.startsWith("."))
|
|
96
|
-
continue;
|
|
97
|
-
const filePath = join(dir, item.name);
|
|
98
|
-
if (item.isDirectory())
|
|
99
|
-
files.push(...await walkFiles(filePath, options));
|
|
100
|
-
else
|
|
101
|
-
files.push(filePath);
|
|
102
|
-
}
|
|
103
|
-
} catch (error) {
|
|
104
|
-
if (error.code !== "ENOENT") {
|
|
105
|
-
options.onError?.({ code: "walk_failed", operation: "readdir", path: dir, message: toErrorMessage(error) });
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return files;
|
|
109
|
-
}
|
|
110
|
-
function normalizeLogicalPath(pathValue) {
|
|
111
|
-
return pathValue.split(sep).join("/").replace(/\\/g, "/");
|
|
112
|
-
}
|
|
113
|
-
var SPEC_NAME_RE = /^[a-z0-9-]+(?:\/[a-z0-9-]+)*$/;
|
|
114
|
-
function isValidSpecName(name) {
|
|
115
|
-
return SPEC_NAME_RE.test(name);
|
|
116
|
-
}
|
|
117
|
-
function guardRspInitialized() {
|
|
118
|
-
const designPath = join(RSP_DIR, "specs", "design.md");
|
|
119
|
-
const rules = inspectManagedFile(RSP_RULES_PATH, "fallback protocol", { allowMissing: true });
|
|
120
|
-
const design = inspectManagedFile(designPath, "design Spec", { allowMissing: true });
|
|
121
|
-
if (rules.issue || design.issue || !rules.exists || !design.exists) {
|
|
122
|
-
const initialized = existsSync(RSP_DIR);
|
|
123
|
-
console.error(` ${pc.red("Error:")} ${initialized ? "RSP project requires an update" : "RSP is not initialized in this project"}`);
|
|
124
|
-
console.error(` ${pc.dim(initialized ? "Run: rsp update" : "Run: rsp init")}`);
|
|
125
|
-
process.exit(1);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
async function cleanupEmptyParentDirs(path, stopDir) {
|
|
129
|
-
let parent = dirname(path);
|
|
130
|
-
while (parent !== stopDir) {
|
|
131
|
-
try {
|
|
132
|
-
const remaining = await readdir(parent);
|
|
133
|
-
if (remaining.length === 0)
|
|
134
|
-
await rmdir(parent);
|
|
135
|
-
else
|
|
136
|
-
break;
|
|
137
|
-
} catch {
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
parent = dirname(parent);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
async function detectProjectName() {
|
|
144
|
-
try {
|
|
145
|
-
if (existsSync("package.json")) {
|
|
146
|
-
const raw = await readFile("package.json", "utf-8");
|
|
147
|
-
const pkg = JSON.parse(raw);
|
|
148
|
-
if (pkg.name && pkg.name !== "")
|
|
149
|
-
return pkg.name;
|
|
150
|
-
}
|
|
151
|
-
} catch {
|
|
152
|
-
}
|
|
153
|
-
return basename(process.cwd());
|
|
154
|
-
}
|
|
52
|
+
import { readFile } from "fs/promises";
|
|
53
|
+
import { dirname, join as join2, relative as relative2 } from "path";
|
|
155
54
|
|
|
156
55
|
// src/core/work-ref.ts
|
|
157
56
|
import { Buffer } from "buffer";
|
|
158
57
|
import { lstatSync, readdirSync } from "fs";
|
|
159
|
-
import { readdir
|
|
160
|
-
import { isAbsolute, join
|
|
58
|
+
import { readdir } from "fs/promises";
|
|
59
|
+
import { isAbsolute, join, relative } from "path";
|
|
161
60
|
var WORK_SEGMENT_RE = /^[\p{Ll}\p{Lo}\p{Lm}\p{M}\p{Nd}]+(?:-[\p{Ll}\p{Lo}\p{Lm}\p{M}\p{Nd}]+)*$/u;
|
|
162
61
|
var WORK_SEGMENT_MAX_CODE_POINTS = 100;
|
|
163
62
|
var WORK_SEGMENT_MAX_UTF8_BYTES = 200;
|
|
@@ -215,7 +114,7 @@ function resolveWorkRef(name, options = {}) {
|
|
|
215
114
|
assertValidWorkRoot(changesDir, normalizedName);
|
|
216
115
|
assertNoNormalizationCollision(changesDir, segments, normalizedName, isGroupBrief);
|
|
217
116
|
assertNoIdentityCollision(changesDir, segments, normalizedName, isGroupBrief ? GROUP_BRIEF_FILENAME : void 0);
|
|
218
|
-
const path =
|
|
117
|
+
const path = join(changesDir, ...segments.slice(0, -1), isGroupBrief ? GROUP_BRIEF_FILENAME : `${segments.at(-1)}.md`);
|
|
219
118
|
const ref = segments.length === 1 ? { kind: "change", name: normalizedName, path, group: null } : isGroupBrief ? { kind: "group-brief", name: normalizedName, path, group: segments[0] } : { kind: "group-change", name: normalizedName, path, group: segments[0] };
|
|
220
119
|
if (ref.kind === "group-change" && options.executable !== false)
|
|
221
120
|
assertGroupBrief(changesDir, ref);
|
|
@@ -230,7 +129,7 @@ function resolveWorkRef(name, options = {}) {
|
|
|
230
129
|
return ref;
|
|
231
130
|
}
|
|
232
131
|
function assertGroupBrief(changesDir, ref) {
|
|
233
|
-
const briefPath =
|
|
132
|
+
const briefPath = join(changesDir, ref.group, GROUP_BRIEF_FILENAME);
|
|
234
133
|
const kind = getPathKind(briefPath, ref.name);
|
|
235
134
|
if (kind === "missing") {
|
|
236
135
|
throw new WorkRefError(
|
|
@@ -274,7 +173,7 @@ function assertRegularWorkFile(ref, mustExist) {
|
|
|
274
173
|
}
|
|
275
174
|
function resolveWorkRefPath(path, options = {}) {
|
|
276
175
|
const changesDir = options.changesDir ?? CHANGES_DIR;
|
|
277
|
-
const logicalPath = normalizeLogicalPath2(
|
|
176
|
+
const logicalPath = normalizeLogicalPath2(relative(changesDir, path));
|
|
278
177
|
if (isAbsolute(logicalPath) || logicalPath === ".." || logicalPath.startsWith("../") || !logicalPath.endsWith(".md")) {
|
|
279
178
|
throw new WorkRefError(
|
|
280
179
|
"invalid_work_ref_path",
|
|
@@ -302,13 +201,13 @@ function resolveFocusMarkerPath(ref, options = {}) {
|
|
|
302
201
|
assertNoManagedNormalizationCollision(focusDir, ref.name.split("/"), ref.name, "invalid_focus_path");
|
|
303
202
|
if (ref.group) {
|
|
304
203
|
assertOptionalManagedDirectory(
|
|
305
|
-
|
|
204
|
+
join(focusDir, ref.group),
|
|
306
205
|
ref.name,
|
|
307
206
|
"invalid_focus_path",
|
|
308
207
|
"focus path"
|
|
309
208
|
);
|
|
310
209
|
}
|
|
311
|
-
const markerPath =
|
|
210
|
+
const markerPath = join(focusDir, ...ref.name.split("/"));
|
|
312
211
|
try {
|
|
313
212
|
requireManagedFile(markerPath, "focus marker", { allowMissing: true });
|
|
314
213
|
} catch (error) {
|
|
@@ -324,7 +223,7 @@ function resolveArchiveDirectory(ref, options = {}) {
|
|
|
324
223
|
if (!ref.group)
|
|
325
224
|
return archivesDir;
|
|
326
225
|
assertNoManagedNormalizationCollision(archivesDir, [ref.group], ref.name, "invalid_archive_path");
|
|
327
|
-
const groupPath =
|
|
226
|
+
const groupPath = join(archivesDir, ref.group);
|
|
328
227
|
assertOptionalManagedDirectory(groupPath, ref.name, "invalid_archive_path", "archive path");
|
|
329
228
|
return groupPath;
|
|
330
229
|
}
|
|
@@ -343,7 +242,7 @@ async function inspectFocusTree(options = {}) {
|
|
|
343
242
|
if (!rootEntries)
|
|
344
243
|
return result;
|
|
345
244
|
for (const entry of rootEntries) {
|
|
346
|
-
const path =
|
|
245
|
+
const path = join(focusDir, entry.name);
|
|
347
246
|
if (entry.name === ".gitkeep" && entry.isFile())
|
|
348
247
|
continue;
|
|
349
248
|
if (entry.isDirectory()) {
|
|
@@ -386,7 +285,7 @@ async function inspectArchiveTree(options = {}) {
|
|
|
386
285
|
if (!rootEntries)
|
|
387
286
|
return result;
|
|
388
287
|
for (const entry of rootEntries) {
|
|
389
|
-
const path =
|
|
288
|
+
const path = join(archivesDir, entry.name);
|
|
390
289
|
if (entry.name === "INDEX.md" && entry.isFile())
|
|
391
290
|
continue;
|
|
392
291
|
if (entry.isDirectory()) {
|
|
@@ -424,7 +323,7 @@ async function inspectWorkTree(options = {}) {
|
|
|
424
323
|
const rootMarkdownNames = new Set(rootEntries.filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name.slice(0, -3)));
|
|
425
324
|
const collidingGroups = new Set([...rootDirectories].filter((name) => rootMarkdownNames.has(name)));
|
|
426
325
|
for (const entry of rootEntries) {
|
|
427
|
-
const path =
|
|
326
|
+
const path = join(changesDir, entry.name);
|
|
428
327
|
if (entry.name === ".gitkeep" && entry.isFile())
|
|
429
328
|
continue;
|
|
430
329
|
if (entry.isDirectory()) {
|
|
@@ -471,17 +370,17 @@ async function inspectGroupDirectory(group, groupPath, changesDir, result, colli
|
|
|
471
370
|
"group_brief_missing",
|
|
472
371
|
`Change Group "${group}" requires a Group Brief; run: rsp group create ${group}`,
|
|
473
372
|
group
|
|
474
|
-
),
|
|
373
|
+
), join(groupPath, GROUP_BRIEF_FILENAME));
|
|
475
374
|
} else if (!briefEntry.isFile()) {
|
|
476
375
|
addDiagnostic(result, new WorkRefError(
|
|
477
376
|
"work_ref_not_file",
|
|
478
|
-
`Group Brief must be a regular file: ${
|
|
377
|
+
`Group Brief must be a regular file: ${join(groupPath, GROUP_BRIEF_FILENAME)}`,
|
|
479
378
|
group
|
|
480
|
-
),
|
|
379
|
+
), join(groupPath, GROUP_BRIEF_FILENAME));
|
|
481
380
|
}
|
|
482
381
|
for (const entry of entries) {
|
|
483
382
|
const input = `${group}/${entry.name}`;
|
|
484
|
-
const path =
|
|
383
|
+
const path = join(groupPath, entry.name);
|
|
485
384
|
if (entry.name === GROUP_BRIEF_FILENAME && !entry.isFile())
|
|
486
385
|
continue;
|
|
487
386
|
if (entry.name === "brief.md") {
|
|
@@ -510,7 +409,7 @@ async function inspectFocusGroup(group, groupPath, changesDir, result) {
|
|
|
510
409
|
return;
|
|
511
410
|
for (const entry of entries) {
|
|
512
411
|
const input = `${group}/${entry.name}`;
|
|
513
|
-
const path =
|
|
412
|
+
const path = join(groupPath, entry.name);
|
|
514
413
|
if (entry.isDirectory()) {
|
|
515
414
|
addDiagnostic(result, new WorkRefError(
|
|
516
415
|
"unsupported_work_depth",
|
|
@@ -536,7 +435,7 @@ async function inspectArchiveGroup(group, groupPath, result) {
|
|
|
536
435
|
return;
|
|
537
436
|
for (const entry of entries) {
|
|
538
437
|
const input = `${group}/${entry.name}`;
|
|
539
|
-
const path =
|
|
438
|
+
const path = join(groupPath, entry.name);
|
|
540
439
|
if (!entry.isFile() || !entry.name.endsWith(".md")) {
|
|
541
440
|
addArchivePathDiagnostic(result, input, path);
|
|
542
441
|
continue;
|
|
@@ -569,7 +468,7 @@ function collectResolvedPath(path, changesDir, result) {
|
|
|
569
468
|
}
|
|
570
469
|
async function readDirectory(path, input, result) {
|
|
571
470
|
try {
|
|
572
|
-
return (await
|
|
471
|
+
return (await readdir(path, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
573
472
|
} catch (error) {
|
|
574
473
|
addDiagnostic(result, asReadFailure(path, input, error), path);
|
|
575
474
|
return null;
|
|
@@ -630,7 +529,7 @@ function normalizeWorkRefName(name, allowGroupBrief) {
|
|
|
630
529
|
}
|
|
631
530
|
function assertNoNormalizationCollision(changesDir, segments, input, isGroupBrief) {
|
|
632
531
|
for (let depth = 0; depth < segments.length; depth++) {
|
|
633
|
-
const parent =
|
|
532
|
+
const parent = join(changesDir, ...segments.slice(0, depth));
|
|
634
533
|
const parentKind = getPathKind(parent, input);
|
|
635
534
|
if (parentKind === "missing")
|
|
636
535
|
return;
|
|
@@ -663,7 +562,7 @@ function assertNoNormalizationCollision(changesDir, segments, input, isGroupBrie
|
|
|
663
562
|
}
|
|
664
563
|
function assertNoManagedNormalizationCollision(root, segments, input, code) {
|
|
665
564
|
for (let depth = 0; depth < segments.length; depth++) {
|
|
666
|
-
const parent =
|
|
565
|
+
const parent = join(root, ...segments.slice(0, depth));
|
|
667
566
|
const parentKind = getPathKind(parent, input);
|
|
668
567
|
if (parentKind === "missing")
|
|
669
568
|
return;
|
|
@@ -688,8 +587,8 @@ function assertNoManagedNormalizationCollision(root, segments, input, code) {
|
|
|
688
587
|
}
|
|
689
588
|
function assertNoIdentityCollision(changesDir, segments, input, finalFilename) {
|
|
690
589
|
for (let depth = 1; depth <= segments.length; depth++) {
|
|
691
|
-
const identityPath =
|
|
692
|
-
const filePath = finalFilename && depth === segments.length ?
|
|
590
|
+
const identityPath = join(changesDir, ...segments.slice(0, depth));
|
|
591
|
+
const filePath = finalFilename && depth === segments.length ? join(changesDir, ...segments.slice(0, -1), finalFilename) : `${identityPath}.md`;
|
|
693
592
|
const fileKind = getPathKind(filePath, input);
|
|
694
593
|
const identityKind = getPathKind(identityPath, input);
|
|
695
594
|
const isPrefix = depth < segments.length;
|
|
@@ -783,13 +682,13 @@ function normalizeLogicalPath2(path) {
|
|
|
783
682
|
var PLACEHOLDER_SUMMARY = "<\u2026>";
|
|
784
683
|
function extractChangeSummary(content, document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA), options = {}) {
|
|
785
684
|
try {
|
|
786
|
-
const frontmatter = parseFrontmatter(
|
|
685
|
+
const frontmatter = parseFrontmatter(document.markdown);
|
|
787
686
|
const frontmatterSummary = meaningfulSummary(frontmatter?.summary, options);
|
|
788
687
|
if (frontmatterSummary)
|
|
789
688
|
return frontmatterSummary;
|
|
790
689
|
} catch {
|
|
791
690
|
}
|
|
792
|
-
const proposalLines =
|
|
691
|
+
const proposalLines = visibleSummaryItemLines(getDocumentSectionMarkdown(document, "proposal"));
|
|
793
692
|
for (const field of ["Outcome", "Summary"]) {
|
|
794
693
|
const prefix = `- ${field}:`;
|
|
795
694
|
for (const line of proposalLines) {
|
|
@@ -801,7 +700,7 @@ function extractChangeSummary(content, document = parseRspDocument(content, CHAN
|
|
|
801
700
|
return null;
|
|
802
701
|
}
|
|
803
702
|
function extractGroupSummary(content, document = parseRspDocument(content, GROUP_BRIEF_DOCUMENT_SCHEMA)) {
|
|
804
|
-
for (const line of
|
|
703
|
+
for (const line of visibleSummaryItemLines(getDocumentSectionMarkdown(document, "goal"))) {
|
|
805
704
|
const trimmed = line.trim();
|
|
806
705
|
const summary = trimmed.startsWith("- ") || trimmed.startsWith("* ") ? meaningfulSummary(trimmed.slice(2)) : null;
|
|
807
706
|
if (summary)
|
|
@@ -809,6 +708,10 @@ function extractGroupSummary(content, document = parseRspDocument(content, GROUP
|
|
|
809
708
|
}
|
|
810
709
|
return null;
|
|
811
710
|
}
|
|
711
|
+
function visibleSummaryItemLines(document) {
|
|
712
|
+
const source = visibleMarkdownSource(document);
|
|
713
|
+
return markdownListItems(document).map(({ node }) => source.slice(node.position.start.offset - document.start, node.position.end.offset - document.start).split(/\r?\n/, 1)[0].trim());
|
|
714
|
+
}
|
|
812
715
|
function meaningfulSummary(value, options = {}) {
|
|
813
716
|
if (typeof value !== "string")
|
|
814
717
|
return null;
|
|
@@ -843,12 +746,15 @@ ${groupSectionHeading("sharedConstraints")}
|
|
|
843
746
|
- ${placeholder}
|
|
844
747
|
|
|
845
748
|
${groupSectionHeading("slices")}
|
|
749
|
+
<!-- Declare at least two independently executable direct child WorkRefs before creating them; keep their Tasks and evidence in each Change. -->
|
|
846
750
|
- \`${name}/<change>\`: ${placeholder}
|
|
847
751
|
|
|
848
752
|
${groupSectionHeading("completionConditions")}
|
|
753
|
+
<!-- State shared completion conditions. For Group-level verification, name an Integration: condition and retain its acceptance and evidence here; child passes alone do not prove it. -->
|
|
849
754
|
- [ ] ${placeholder}
|
|
850
755
|
|
|
851
756
|
${groupSectionHeading("durableOutcomes")}
|
|
757
|
+
<!-- Identify current-fact and lasting-rationale owners only when an update is needed; do not duplicate child results, current Specs or execution history. -->
|
|
852
758
|
- Current facts: ${placeholder}
|
|
853
759
|
- Lasting rationale: ${placeholder}
|
|
854
760
|
|
|
@@ -868,12 +774,12 @@ async function inspectChangeGroups(options = {}) {
|
|
|
868
774
|
const archivedGroupBriefPaths = /* @__PURE__ */ new Map();
|
|
869
775
|
const openGroupNames = new Set(workTree.briefs.map((brief) => brief.group));
|
|
870
776
|
for (const path of archiveTree.files) {
|
|
871
|
-
const archivePath = normalizeLogicalPath(
|
|
777
|
+
const archivePath = normalizeLogicalPath(relative2(ARCHIVES_DIR, path));
|
|
872
778
|
const archiveGroup = archivePath.includes("/") ? archivePath.split("/")[0] : null;
|
|
873
779
|
if (!archiveGroup || !openGroupNames.has(archiveGroup))
|
|
874
780
|
continue;
|
|
875
781
|
try {
|
|
876
|
-
const content = await
|
|
782
|
+
const content = await readFile(path, "utf-8");
|
|
877
783
|
const archivedChange = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
|
|
878
784
|
const identity = getDocumentTitle(archivedChange, "identity");
|
|
879
785
|
if (identity?.includes("/")) {
|
|
@@ -891,10 +797,11 @@ async function inspectChangeGroups(options = {}) {
|
|
|
891
797
|
archivedSlicesByGroup.set(archiveGroup, groupSlices);
|
|
892
798
|
}
|
|
893
799
|
}
|
|
894
|
-
|
|
800
|
+
const archivedBrief = parseRspDocument(archivedChange.markdown, GROUP_BRIEF_DOCUMENT_SCHEMA);
|
|
801
|
+
if (getDocumentTitle(archivedBrief, "identity") === archiveGroup) {
|
|
895
802
|
archivedGroups.add(archiveGroup);
|
|
896
803
|
const evidence = archivedGroupReopenEvidence.get(archiveGroup) ?? /* @__PURE__ */ new Set();
|
|
897
|
-
for (const item of getGroupReopenEvidence(
|
|
804
|
+
for (const item of getGroupReopenEvidence(archivedBrief))
|
|
898
805
|
evidence.add(item.key);
|
|
899
806
|
archivedGroupReopenEvidence.set(archiveGroup, evidence);
|
|
900
807
|
const retainedPaths = archivedGroupBriefPaths.get(archiveGroup) ?? /* @__PURE__ */ new Set();
|
|
@@ -914,7 +821,7 @@ async function inspectChangeGroups(options = {}) {
|
|
|
914
821
|
for (const brief of workTree.briefs) {
|
|
915
822
|
let content;
|
|
916
823
|
try {
|
|
917
|
-
content = await
|
|
824
|
+
content = await readFile(brief.path, "utf-8");
|
|
918
825
|
} catch (error) {
|
|
919
826
|
diagnostics.push({
|
|
920
827
|
severity: "error",
|
|
@@ -925,10 +832,11 @@ async function inspectChangeGroups(options = {}) {
|
|
|
925
832
|
});
|
|
926
833
|
continue;
|
|
927
834
|
}
|
|
928
|
-
const
|
|
835
|
+
const document = parseRspDocument(content, GROUP_BRIEF_DOCUMENT_SCHEMA);
|
|
836
|
+
const parsed = parseGroupBrief(brief.group, document);
|
|
929
837
|
diagnostics.push(...parsed.diagnostics.map((diagnostic) => ({ ...diagnostic, path: brief.path })));
|
|
930
838
|
if (archivedGroups.has(brief.group) && !hasExplicitGroupReopenEvidence(
|
|
931
|
-
|
|
839
|
+
document,
|
|
932
840
|
archivedGroupReopenEvidence.get(brief.group) ?? /* @__PURE__ */ new Set(),
|
|
933
841
|
archivedGroupBriefPaths.get(brief.group) ?? /* @__PURE__ */ new Set()
|
|
934
842
|
)) {
|
|
@@ -943,7 +851,7 @@ async function inspectChangeGroups(options = {}) {
|
|
|
943
851
|
for (const diagnostic of archiveTree.diagnostics.filter((item) => {
|
|
944
852
|
if (item.code === "invalid_archive_root")
|
|
945
853
|
return true;
|
|
946
|
-
const archivePath = normalizeLogicalPath(
|
|
854
|
+
const archivePath = normalizeLogicalPath(relative2(ARCHIVES_DIR, item.path));
|
|
947
855
|
return archivePath === brief.group || archivePath.startsWith(`${brief.group}/`);
|
|
948
856
|
})) {
|
|
949
857
|
diagnostics.push({
|
|
@@ -1011,7 +919,7 @@ async function inspectChangeGroups(options = {}) {
|
|
|
1011
919
|
const readyToClose = diagnostics.every((diagnostic) => diagnostic.change !== brief.group || diagnostic.severity !== "error") && slices.length >= 2 && slices.every((slice) => slice.state === "archived") && parsed.completion.total > 0 && parsed.completion.done === parsed.completion.total && !parsed.blockers;
|
|
1012
920
|
groups.push({
|
|
1013
921
|
name: brief.group,
|
|
1014
|
-
summary: extractGroupSummary(content),
|
|
922
|
+
summary: extractGroupSummary(content, document),
|
|
1015
923
|
path: normalizeLogicalPath(brief.path),
|
|
1016
924
|
slices,
|
|
1017
925
|
completion: parsed.completion,
|
|
@@ -1024,25 +932,27 @@ async function inspectChangeGroups(options = {}) {
|
|
|
1024
932
|
return { groups, diagnostics };
|
|
1025
933
|
}
|
|
1026
934
|
function getGroupReopenEvidence(content) {
|
|
1027
|
-
const
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
935
|
+
const document = typeof content === "string" ? parseRspDocument(content, GROUP_BRIEF_DOCUMENT_SCHEMA) : content;
|
|
936
|
+
const completion = getDocumentSectionMarkdown(document, "completionConditions");
|
|
937
|
+
return markdownListItems(completion).flatMap(({ line }) => {
|
|
938
|
+
const match = line.trim().match(/^-\s+\[([ /xX-])\]\s+(Resolve reopened concern from `(\.rsp\/archives\/[^`]+)`: (\S.*))$/);
|
|
939
|
+
const reason = match?.[4];
|
|
940
|
+
return match && reason && markdownText(parseMarkdown(reason).root).trim() ? [{ active: match[1] !== "-", key: match[2], sourcePath: match[3] }] : [];
|
|
1031
941
|
});
|
|
1032
942
|
}
|
|
1033
943
|
function hasExplicitGroupReopenEvidence(content, retainedEvidence, retainedGroupBriefPaths) {
|
|
1034
944
|
return getGroupReopenEvidence(content).some((item) => item.active && retainedGroupBriefPaths.has(item.sourcePath) && !retainedEvidence.has(item.key));
|
|
1035
945
|
}
|
|
1036
946
|
async function requireDeclaredGroupSlice(ref) {
|
|
1037
|
-
const briefPath =
|
|
947
|
+
const briefPath = join2(dirname(ref.path), GROUP_BRIEF_FILENAME);
|
|
1038
948
|
let content;
|
|
1039
949
|
try {
|
|
1040
|
-
content = await
|
|
950
|
+
content = await readFile(briefPath, "utf-8");
|
|
1041
951
|
} catch (error) {
|
|
1042
952
|
const message = error instanceof Error ? error.message : String(error);
|
|
1043
953
|
throw new WorkRefError("work_tree_read_failed", `unable to read Group Brief ${briefPath}: ${message}`, ref.name);
|
|
1044
954
|
}
|
|
1045
|
-
const parsed = parseGroupBrief(ref.group, content);
|
|
955
|
+
const parsed = parseGroupBrief(ref.group, parseRspDocument(content, GROUP_BRIEF_DOCUMENT_SCHEMA));
|
|
1046
956
|
const errors = parsed.diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
1047
957
|
if (errors.length > 0) {
|
|
1048
958
|
throw new WorkRefError(
|
|
@@ -1067,11 +977,11 @@ async function hasArchivedGroupBrief(group) {
|
|
|
1067
977
|
}
|
|
1068
978
|
const groupPrefix = `${group}/`;
|
|
1069
979
|
for (const path of archiveTree.files) {
|
|
1070
|
-
if (!normalizeLogicalPath(
|
|
980
|
+
if (!normalizeLogicalPath(relative2(ARCHIVES_DIR, path)).startsWith(groupPrefix))
|
|
1071
981
|
continue;
|
|
1072
982
|
let content;
|
|
1073
983
|
try {
|
|
1074
|
-
content = await
|
|
984
|
+
content = await readFile(path, "utf-8");
|
|
1075
985
|
} catch (error) {
|
|
1076
986
|
const message = error instanceof Error ? error.message : String(error);
|
|
1077
987
|
throw new WorkRefError("work_tree_read_failed", `unable to inspect archived Change Group identity at ${path}: ${message}`, group);
|
|
@@ -1081,12 +991,11 @@ async function hasArchivedGroupBrief(group) {
|
|
|
1081
991
|
}
|
|
1082
992
|
return false;
|
|
1083
993
|
}
|
|
1084
|
-
function parseGroupBrief(group,
|
|
994
|
+
function parseGroupBrief(group, document) {
|
|
1085
995
|
const diagnostics = [];
|
|
1086
|
-
const document = parseRspDocument(content, GROUP_BRIEF_DOCUMENT_SCHEMA);
|
|
1087
996
|
let frontmatter;
|
|
1088
997
|
try {
|
|
1089
|
-
frontmatter = parseFrontmatter(
|
|
998
|
+
frontmatter = parseFrontmatter(document.markdown);
|
|
1090
999
|
} catch (error) {
|
|
1091
1000
|
diagnostics.push({ severity: "error", code: "group_frontmatter_invalid", change: group, message: error instanceof Error ? error.message : String(error) });
|
|
1092
1001
|
}
|
|
@@ -1095,12 +1004,12 @@ function parseGroupBrief(group, content) {
|
|
|
1095
1004
|
if (getDocumentTitle(document, "identity") !== group)
|
|
1096
1005
|
diagnostics.push({ severity: "error", code: "group_heading_mismatch", change: group, message: `Group Brief heading must be: # Change Group: ${group}` });
|
|
1097
1006
|
for (const definition of GROUP_BRIEF_DOCUMENT_SCHEMA.sections) {
|
|
1098
|
-
if (
|
|
1007
|
+
if (getVisibleBodyLines(getDocumentSectionMarkdown(document, definition.id)).length === 0)
|
|
1099
1008
|
diagnostics.push({ severity: "error", code: "group_section_missing", change: group, message: `Group Brief missing required section: ${definition.heading}` });
|
|
1100
1009
|
}
|
|
1101
1010
|
const slices = [];
|
|
1102
1011
|
const seen = /* @__PURE__ */ new Set();
|
|
1103
|
-
for (const line of
|
|
1012
|
+
for (const { line } of markdownListItems(getDocumentSectionMarkdown(document, "slices"))) {
|
|
1104
1013
|
const trimmed = line.trim();
|
|
1105
1014
|
const identity = trimmed.match(/^-\s+`(\S+)`/);
|
|
1106
1015
|
if (!identity || identity[1].includes("<"))
|
|
@@ -1129,13 +1038,13 @@ function parseGroupBrief(group, content) {
|
|
|
1129
1038
|
}
|
|
1130
1039
|
if (slices.length < 2)
|
|
1131
1040
|
diagnostics.push({ severity: "warning", code: "group_slices_incomplete", change: group, message: "Change Group must declare at least two direct child slices" });
|
|
1132
|
-
const completion = countCheckboxes(
|
|
1041
|
+
const completion = countCheckboxes(getDocumentSectionMarkdown(document, "completionConditions"));
|
|
1133
1042
|
if (completion.total === 0)
|
|
1134
1043
|
diagnostics.push({ severity: "warning", code: "group_completion_missing", change: group, message: "Change Group must declare at least one completion condition" });
|
|
1135
1044
|
return {
|
|
1136
1045
|
slices,
|
|
1137
1046
|
completion: { done: completion.done + completion.dropped, total: completion.total },
|
|
1138
|
-
blockers: hasMeaningfulBlockerBody(
|
|
1047
|
+
blockers: hasMeaningfulBlockerBody(getDocumentSectionMarkdown(document, "blockers")),
|
|
1139
1048
|
diagnostics
|
|
1140
1049
|
};
|
|
1141
1050
|
}
|
|
@@ -1144,13 +1053,13 @@ function groupSectionHeading(sectionId) {
|
|
|
1144
1053
|
}
|
|
1145
1054
|
|
|
1146
1055
|
// src/core/dependency-plan.ts
|
|
1147
|
-
import { readFile as
|
|
1056
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1148
1057
|
|
|
1149
1058
|
// src/history/current-file.ts
|
|
1150
1059
|
import { Buffer as Buffer2 } from "buffer";
|
|
1151
1060
|
import { constants } from "fs";
|
|
1152
1061
|
import { lstat, open, realpath } from "fs/promises";
|
|
1153
|
-
import { isAbsolute as isAbsolute2, relative as
|
|
1062
|
+
import { isAbsolute as isAbsolute2, relative as relative3, resolve, sep } from "path";
|
|
1154
1063
|
|
|
1155
1064
|
// src/history/model.ts
|
|
1156
1065
|
var HISTORY_DEFAULT_LIMIT = 20;
|
|
@@ -1181,8 +1090,8 @@ var READ_NO_FOLLOW_FLAGS = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0);
|
|
|
1181
1090
|
async function readCurrentArchiveFile(options) {
|
|
1182
1091
|
const sourcePath = resolve(options.sourcePath);
|
|
1183
1092
|
const archivesDir = resolve(options.archivesDir);
|
|
1184
|
-
const relativePath =
|
|
1185
|
-
if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${
|
|
1093
|
+
const relativePath = relative3(archivesDir, sourcePath);
|
|
1094
|
+
if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute2(relativePath)) {
|
|
1186
1095
|
throw changed(options.projectPath);
|
|
1187
1096
|
}
|
|
1188
1097
|
let inspectedRoot;
|
|
@@ -1297,8 +1206,8 @@ function sameFileSnapshot(left, right) {
|
|
|
1297
1206
|
return left.device === right.dev && left.inode === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
1298
1207
|
}
|
|
1299
1208
|
function isContained(root, path) {
|
|
1300
|
-
const candidate =
|
|
1301
|
-
return candidate !== "" && candidate !== ".." && !candidate.startsWith(`..${
|
|
1209
|
+
const candidate = relative3(root, path);
|
|
1210
|
+
return candidate !== "" && candidate !== ".." && !candidate.startsWith(`..${sep}`) && !isAbsolute2(candidate);
|
|
1302
1211
|
}
|
|
1303
1212
|
function changed(projectPath) {
|
|
1304
1213
|
return new ArchiveHistoryError(
|
|
@@ -1324,7 +1233,7 @@ function isPermissionError(error) {
|
|
|
1324
1233
|
}
|
|
1325
1234
|
|
|
1326
1235
|
// src/history/inspect.ts
|
|
1327
|
-
import { basename
|
|
1236
|
+
import { basename, dirname as dirname2, relative as relative4, resolve as resolve2 } from "path";
|
|
1328
1237
|
|
|
1329
1238
|
// src/core/issue-relationship.ts
|
|
1330
1239
|
var ISSUE_RELATIONS = /* @__PURE__ */ new Set(["relates", "closes"]);
|
|
@@ -1456,18 +1365,18 @@ async function inspectArchiveHistory(options = {}) {
|
|
|
1456
1365
|
}
|
|
1457
1366
|
function parseArchiveEntry(current, path, maxFileBytes) {
|
|
1458
1367
|
const { sourcePath, archivesDir, content } = current;
|
|
1459
|
-
const nameMatch =
|
|
1368
|
+
const nameMatch = basename(sourcePath).match(ARCHIVE_NAME_RE);
|
|
1460
1369
|
if (!nameMatch || !isCalendarDate(nameMatch[1])) {
|
|
1461
1370
|
return { diagnostic: errorDiagnostic("archive_name_invalid", path, "archive filename must use a valid YYYY-MM-DD_name.md date") };
|
|
1462
1371
|
}
|
|
1463
1372
|
const date = nameMatch[1];
|
|
1464
1373
|
const archiveStem = nameMatch[2];
|
|
1465
|
-
const relativeDirectory = normalizeLogicalPath(
|
|
1374
|
+
const relativeDirectory = normalizeLogicalPath(relative4(archivesDir, dirname2(sourcePath)));
|
|
1466
1375
|
const archiveGroup = relativeDirectory === "." || relativeDirectory === "" ? null : relativeDirectory;
|
|
1467
1376
|
if (archiveGroup && !isCanonicalWorkRefSegment(archiveGroup))
|
|
1468
1377
|
return { diagnostic: errorDiagnostic("archive_identity_mismatch", path, "archive group path is not a valid one-segment Group identity") };
|
|
1469
1378
|
const changeDocument = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
|
|
1470
|
-
const groupDocument = parseRspDocument(
|
|
1379
|
+
const groupDocument = parseRspDocument(changeDocument.markdown, GROUP_BRIEF_DOCUMENT_SCHEMA);
|
|
1471
1380
|
const changeHeadings = getDocumentTitles(changeDocument, "identity");
|
|
1472
1381
|
const groupHeadings = getDocumentTitles(groupDocument, "identity");
|
|
1473
1382
|
if (changeHeadings.length + groupHeadings.length !== 1) {
|
|
@@ -1475,7 +1384,7 @@ function parseArchiveEntry(current, path, maxFileBytes) {
|
|
|
1475
1384
|
}
|
|
1476
1385
|
let frontmatter;
|
|
1477
1386
|
try {
|
|
1478
|
-
frontmatter = parseFrontmatter(
|
|
1387
|
+
frontmatter = parseFrontmatter(changeDocument.markdown);
|
|
1479
1388
|
} catch (error) {
|
|
1480
1389
|
return { diagnostic: errorDiagnostic("archive_frontmatter_invalid", path, `archive frontmatter could not be parsed: ${toErrorMessage(error)}`) };
|
|
1481
1390
|
}
|
|
@@ -1540,10 +1449,10 @@ function isCalendarDate(value) {
|
|
|
1540
1449
|
return Number.isFinite(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value;
|
|
1541
1450
|
}
|
|
1542
1451
|
function archiveOutputPath(sourcePath, archivesDir) {
|
|
1543
|
-
return `.rsp/archives/${normalizeLogicalPath(
|
|
1452
|
+
return `.rsp/archives/${normalizeLogicalPath(relative4(resolve2(archivesDir), resolve2(sourcePath)))}`;
|
|
1544
1453
|
}
|
|
1545
1454
|
function normalizeOutputPath(path, archivesDir) {
|
|
1546
|
-
const relativePath = normalizeLogicalPath(
|
|
1455
|
+
const relativePath = normalizeLogicalPath(relative4(resolve2(archivesDir), resolve2(path)));
|
|
1547
1456
|
return relativePath === "" ? ".rsp/archives" : `.rsp/archives/${relativePath}`;
|
|
1548
1457
|
}
|
|
1549
1458
|
function errorDiagnostic(code, path, message) {
|
|
@@ -1642,20 +1551,20 @@ async function readArchiveHistoryDocument(record) {
|
|
|
1642
1551
|
throw new ArchiveHistoryError("archive_read_failed", `unable to read archived Change ${record.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1643
1552
|
}
|
|
1644
1553
|
const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
|
|
1645
|
-
const tasks =
|
|
1646
|
-
const verify =
|
|
1647
|
-
const blockers =
|
|
1554
|
+
const tasks = getDocumentSectionMarkdown(document, "tasks");
|
|
1555
|
+
const verify = getDocumentSectionMarkdown(document, "verify");
|
|
1556
|
+
const blockers = getDocumentSectionMarkdown(document, "blockers");
|
|
1648
1557
|
const detail = {
|
|
1649
1558
|
...toOutputRecord(record),
|
|
1650
|
-
scenarioCount: parseScenarios(
|
|
1559
|
+
scenarioCount: parseScenarios(document.markdown).length,
|
|
1651
1560
|
checkboxes: {
|
|
1652
1561
|
tasks: countCheckboxes(tasks),
|
|
1653
1562
|
verify: countCheckboxes(verify)
|
|
1654
1563
|
},
|
|
1655
1564
|
evidence: {
|
|
1656
|
-
tasks: boundEvidence(tasks),
|
|
1657
|
-
verify: boundEvidence(verify),
|
|
1658
|
-
blockers: boundEvidence(blockers
|
|
1565
|
+
tasks: boundEvidence(getVisibleBodyLines(tasks)),
|
|
1566
|
+
verify: boundEvidence(getVisibleBodyLines(verify)),
|
|
1567
|
+
blockers: boundEvidence(readBlockerEntries(blockers).map((entry) => entry.line))
|
|
1659
1568
|
}
|
|
1660
1569
|
};
|
|
1661
1570
|
return { content, detail };
|
|
@@ -1674,8 +1583,7 @@ function toOutputRecord(record) {
|
|
|
1674
1583
|
} = record;
|
|
1675
1584
|
return output;
|
|
1676
1585
|
}
|
|
1677
|
-
function boundEvidence(
|
|
1678
|
-
const rawItems = section.split("\n").map((line) => line.trim()).filter(Boolean).filter((line) => !omitNone || !/^[-*]\s*(?:none)?$/i.test(line) && !/^none$/i.test(line));
|
|
1586
|
+
function boundEvidence(rawItems) {
|
|
1679
1587
|
const boundedItems = rawItems.slice(0, HISTORY_MAX_EVIDENCE_ITEMS).map((item) => boundText(item));
|
|
1680
1588
|
return {
|
|
1681
1589
|
items: boundedItems.map((item) => item.value),
|
|
@@ -1693,7 +1601,6 @@ function compareHistoryRecords2(left, right) {
|
|
|
1693
1601
|
}
|
|
1694
1602
|
|
|
1695
1603
|
// src/core/dependency-plan.ts
|
|
1696
|
-
var REQUIRES_RE = /^[-*]\s+requires\s+`([^`]+)`:[ \t]*(\S.*)$/i;
|
|
1697
1604
|
async function inspectChangeDependencies(options = {}) {
|
|
1698
1605
|
const workTree = options.workTree ?? await inspectWorkTree();
|
|
1699
1606
|
const archiveTree = options.archiveTree ?? await inspectArchiveTree();
|
|
@@ -1714,7 +1621,7 @@ async function inspectChangeDependencies(options = {}) {
|
|
|
1714
1621
|
diagnostics.push(...archiveHistory.diagnostics);
|
|
1715
1622
|
for (const brief of workTree.briefs) {
|
|
1716
1623
|
try {
|
|
1717
|
-
if (hasMeaningfulBlockers(await
|
|
1624
|
+
if (hasMeaningfulBlockers(await readFile2(brief.path, "utf-8")))
|
|
1718
1625
|
blockedGroups.add(brief.group);
|
|
1719
1626
|
} catch (error) {
|
|
1720
1627
|
diagnostics.push({
|
|
@@ -1728,7 +1635,7 @@ async function inspectChangeDependencies(options = {}) {
|
|
|
1728
1635
|
}
|
|
1729
1636
|
for (const ref of workTree.changes) {
|
|
1730
1637
|
try {
|
|
1731
|
-
const parsed = parseDependencyBlockers(await
|
|
1638
|
+
const parsed = parseDependencyBlockers(await readFile2(ref.path, "utf-8"));
|
|
1732
1639
|
if (ref.group && blockedGroups.has(ref.group))
|
|
1733
1640
|
parsed.external = true;
|
|
1734
1641
|
parsedByName.set(ref.name, parsed);
|
|
@@ -1833,18 +1740,16 @@ function parseDependencyBlockers(content) {
|
|
|
1833
1740
|
const requires = /* @__PURE__ */ new Map();
|
|
1834
1741
|
let external = false;
|
|
1835
1742
|
const malformed = [];
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
requires.set(dependency[1], dependency[2]);
|
|
1843
|
-
} else if (/^[-*]\s+requires\b/i.test(line)) {
|
|
1844
|
-
malformed.push(line);
|
|
1845
|
-
external = true;
|
|
1743
|
+
const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
|
|
1744
|
+
const blockers = getDocumentSectionMarkdown(document, "blockers");
|
|
1745
|
+
for (const entry of readBlockerEntries(blockers)) {
|
|
1746
|
+
if (entry.kind === "dependency") {
|
|
1747
|
+
if (!requires.has(entry.name))
|
|
1748
|
+
requires.set(entry.name, entry.reason);
|
|
1846
1749
|
} else {
|
|
1847
1750
|
external = true;
|
|
1751
|
+
if (entry.kind === "malformed-dependency")
|
|
1752
|
+
malformed.push(entry.line);
|
|
1848
1753
|
}
|
|
1849
1754
|
}
|
|
1850
1755
|
return {
|
|
@@ -1947,29 +1852,32 @@ function collectArchiveReadiness(content, options = {}) {
|
|
|
1947
1852
|
const warnings = [];
|
|
1948
1853
|
const documentDiagnostics = options.documentDiagnostics ?? [];
|
|
1949
1854
|
warnings.push(...documentDiagnostics.map((diagnostic) => diagnostic.message));
|
|
1950
|
-
const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
|
|
1951
|
-
const
|
|
1952
|
-
|
|
1855
|
+
const document = typeof content === "string" ? parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA) : content;
|
|
1856
|
+
const missingSummary = !extractChangeSummary(document.source, document, { preservePlaceholder: true });
|
|
1857
|
+
if (missingSummary)
|
|
1858
|
+
warnings.push("archived Change must contain frontmatter summary, Proposal Outcome, or Proposal Summary");
|
|
1859
|
+
const tasksSection = getDocumentSectionMarkdown(document, "tasks");
|
|
1860
|
+
const verifySection = getDocumentSectionMarkdown(document, "verify");
|
|
1953
1861
|
const taskTodos = getOpenCheckboxes(tasksSection);
|
|
1954
1862
|
if (taskTodos.length > 0)
|
|
1955
1863
|
warnings.push(`${taskTodos.length} task item(s) still incomplete`);
|
|
1956
|
-
const
|
|
1957
|
-
const
|
|
1958
|
-
const requiredVerifyTodos =
|
|
1959
|
-
const optionalVerifyTodos =
|
|
1864
|
+
const verifyItems = collectVerifyCheckboxes(verifySection);
|
|
1865
|
+
const verifyCriticality = summarizeVerifyCheckboxes(verifyItems);
|
|
1866
|
+
const requiredVerifyTodos = openCheckboxLines([...verifyItems.required, ...verifyItems.unclassified]);
|
|
1867
|
+
const optionalVerifyTodos = openCheckboxLines(verifyItems.optional);
|
|
1960
1868
|
const verifyTodos = [...requiredVerifyTodos, ...optionalVerifyTodos];
|
|
1961
1869
|
if (requiredVerifyTodos.length > 0)
|
|
1962
1870
|
warnings.push(`${requiredVerifyTodos.length} required Verify item(s) are still incomplete`);
|
|
1963
1871
|
if (optionalVerifyTodos.length > 0)
|
|
1964
1872
|
warnings.push(`${optionalVerifyTodos.length} optional Verify item(s) are still incomplete`);
|
|
1965
|
-
const activeBlockers = options.activeBlockers ?? hasMeaningfulBlockers(
|
|
1873
|
+
const activeBlockers = options.activeBlockers ?? hasMeaningfulBlockers(document);
|
|
1966
1874
|
if (activeBlockers)
|
|
1967
1875
|
warnings.push("active blockers are present in the change file");
|
|
1968
|
-
const scenarios = parseScenarios(
|
|
1876
|
+
const scenarios = parseScenarios(document.markdown);
|
|
1969
1877
|
const missingScenarios = scenarios.length === 0;
|
|
1970
1878
|
if (missingScenarios)
|
|
1971
1879
|
warnings.push("no Scenario blocks found (some changes do not need them)");
|
|
1972
|
-
const archiveReady = documentDiagnostics.some((diagnostic) => diagnostic.severity === "error") || activeBlockers || taskTodos.length > 0 || requiredVerifyTodos.length > 0 ? "no" : "yes";
|
|
1880
|
+
const archiveReady = documentDiagnostics.some((diagnostic) => diagnostic.severity === "error") || missingSummary || activeBlockers || taskTodos.length > 0 || requiredVerifyTodos.length > 0 ? "no" : "yes";
|
|
1973
1881
|
return {
|
|
1974
1882
|
taskTodos,
|
|
1975
1883
|
verifyTodos,
|
|
@@ -2022,12 +1930,12 @@ function getDurableReviewCandidateTargets() {
|
|
|
2022
1930
|
async function inspectProjectStatus(options = {}) {
|
|
2023
1931
|
const runtime = [];
|
|
2024
1932
|
const diagnostics = [];
|
|
2025
|
-
let
|
|
1933
|
+
let workflow = resolveWorkflowPolicy({});
|
|
2026
1934
|
let language = resolveLanguagePolicy({});
|
|
2027
1935
|
try {
|
|
2028
1936
|
const configInspection = await inspectRspConfig();
|
|
2029
1937
|
const configValid = configInspection.issues.length === 0;
|
|
2030
|
-
|
|
1938
|
+
workflow = resolveWorkflowPolicy(configInspection.config, { configValid });
|
|
2031
1939
|
language = resolveLanguagePolicy(configInspection.config, { configValid });
|
|
2032
1940
|
if (!configValid) {
|
|
2033
1941
|
diagnostics.push({
|
|
@@ -2038,7 +1946,7 @@ async function inspectProjectStatus(options = {}) {
|
|
|
2038
1946
|
});
|
|
2039
1947
|
}
|
|
2040
1948
|
} catch (error) {
|
|
2041
|
-
|
|
1949
|
+
workflow = resolveWorkflowPolicy({}, { configValid: false });
|
|
2042
1950
|
language = resolveLanguagePolicy({}, { configValid: false });
|
|
2043
1951
|
diagnostics.push({
|
|
2044
1952
|
severity: "error",
|
|
@@ -2105,7 +2013,7 @@ async function inspectProjectStatus(options = {}) {
|
|
|
2105
2013
|
let kind = "\u2014";
|
|
2106
2014
|
let summary = null;
|
|
2107
2015
|
let title = name;
|
|
2108
|
-
let
|
|
2016
|
+
let externalBlockers = [];
|
|
2109
2017
|
let readiness = emptyReadiness(false);
|
|
2110
2018
|
let ageDays = null;
|
|
2111
2019
|
const isFocused = focusedSet.has(name);
|
|
@@ -2116,12 +2024,13 @@ async function inspectProjectStatus(options = {}) {
|
|
|
2116
2024
|
let issues = [];
|
|
2117
2025
|
if (path) {
|
|
2118
2026
|
try {
|
|
2119
|
-
const content = await
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2027
|
+
const content = await readFile3(path, "utf-8");
|
|
2028
|
+
const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
|
|
2029
|
+
title = getDocumentTitle(document, "spaced") ?? name;
|
|
2030
|
+
summary = extractChangeSummary(content, document);
|
|
2031
|
+
externalBlockers = readBlockerEntries(getDocumentSectionMarkdown(document, "blockers")).filter((entry) => entry.kind !== "dependency").map((entry) => entry.text);
|
|
2123
2032
|
try {
|
|
2124
|
-
const frontmatter = parseFrontmatter(
|
|
2033
|
+
const frontmatter = parseFrontmatter(document.markdown);
|
|
2125
2034
|
kind = frontmatter?.kind ? String(frontmatter.kind) : "\u2014";
|
|
2126
2035
|
issues = parseIssueRelationships(frontmatter);
|
|
2127
2036
|
} catch (error) {
|
|
@@ -2143,10 +2052,9 @@ async function inspectProjectStatus(options = {}) {
|
|
|
2143
2052
|
message: toErrorMessage(error)
|
|
2144
2053
|
});
|
|
2145
2054
|
}
|
|
2146
|
-
isBlocked = dependencyInspection.activeBlockers.get(name) ?? hasMeaningfulBlockers(
|
|
2147
|
-
const details = collectArchiveReadiness(
|
|
2148
|
-
const
|
|
2149
|
-
const taskCheckboxes = countCheckboxes(getDocumentSectionBody(document, "tasks"));
|
|
2055
|
+
isBlocked = dependencyInspection.activeBlockers.get(name) ?? hasMeaningfulBlockers(document);
|
|
2056
|
+
const details = collectArchiveReadiness(document, { activeBlockers: isBlocked });
|
|
2057
|
+
const taskCheckboxes = countCheckboxes(getDocumentSectionMarkdown(document, "tasks"));
|
|
2150
2058
|
done = taskCheckboxes.done + details.verifyCriticality.required.done;
|
|
2151
2059
|
total = taskCheckboxes.total + details.verifyCriticality.required.total;
|
|
2152
2060
|
progressKnown = true;
|
|
@@ -2217,12 +2125,12 @@ async function inspectProjectStatus(options = {}) {
|
|
|
2217
2125
|
},
|
|
2218
2126
|
progressKnown,
|
|
2219
2127
|
title,
|
|
2220
|
-
|
|
2128
|
+
externalBlockers,
|
|
2221
2129
|
readiness
|
|
2222
2130
|
});
|
|
2223
2131
|
}
|
|
2224
2132
|
return {
|
|
2225
|
-
|
|
2133
|
+
workflow,
|
|
2226
2134
|
language,
|
|
2227
2135
|
focused: [...focusedSet].sort(),
|
|
2228
2136
|
records,
|
|
@@ -2251,13 +2159,6 @@ function emptyReadiness(activeBlockers) {
|
|
|
2251
2159
|
archiveReady: activeBlockers ? "no" : "judgment"
|
|
2252
2160
|
};
|
|
2253
2161
|
}
|
|
2254
|
-
function extractChangeTitle(content) {
|
|
2255
|
-
return getDocumentTitle(parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA), "spaced");
|
|
2256
|
-
}
|
|
2257
|
-
function extractBlockerEntries(content) {
|
|
2258
|
-
const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
|
|
2259
|
-
return getDocumentSectionBody(document, "blockers").split("\n").map((line) => line.trim()).filter((line) => /^[-*]\s+/.test(line)).map((line) => line.replace(/^[-*]\s+/, "").trim()).filter((line) => line.toLowerCase() !== "none");
|
|
2260
|
-
}
|
|
2261
2162
|
function deriveArchiveTrend(records) {
|
|
2262
2163
|
const counts = /* @__PURE__ */ new Map();
|
|
2263
2164
|
for (const record of records) {
|
|
@@ -2267,616 +2168,6 @@ function deriveArchiveTrend(records) {
|
|
|
2267
2168
|
return [...counts].sort(([left], [right]) => left.localeCompare(right)).map(([month, count]) => ({ month, count }));
|
|
2268
2169
|
}
|
|
2269
2170
|
|
|
2270
|
-
// src/specs/model.ts
|
|
2271
|
-
var SPECS_DEFAULT_RESULT_LIMIT = 20;
|
|
2272
|
-
var SPECS_MAX_RESULT_LIMIT = 100;
|
|
2273
|
-
var SPECS_MAX_CANDIDATES = 1e3;
|
|
2274
|
-
var SPECS_MAX_FILE_BYTES = 512 * 1024;
|
|
2275
|
-
var SPECS_SEARCH_EXCERPT_CODE_POINTS = 240;
|
|
2276
|
-
var SPECS_DETAIL_CONTENT_CODE_POINTS = 12e3;
|
|
2277
|
-
var SPECS_MAX_DIAGNOSTICS = 20;
|
|
2278
|
-
|
|
2279
|
-
// src/specs/query.ts
|
|
2280
|
-
import { Buffer as Buffer3 } from "buffer";
|
|
2281
|
-
import { spawnSync } from "child_process";
|
|
2282
|
-
import { constants as constants2 } from "fs";
|
|
2283
|
-
import { lstat as lstat2, open as open2, realpath as realpath2 } from "fs/promises";
|
|
2284
|
-
import { basename as basename3, dirname as dirname4, isAbsolute as isAbsolute3, join as join4, relative as relative6, resolve as resolve3, sep as sep3 } from "path";
|
|
2285
|
-
var SPECS_ROOT = join4(RSP_DIR, "specs");
|
|
2286
|
-
var GENERATED_INDEX_FILENAME = "00-index.md";
|
|
2287
|
-
var LEGACY_GENERATED_INDEX_FILENAME = "INDEX.md";
|
|
2288
|
-
var READ_NO_FOLLOW_FLAGS2 = constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0);
|
|
2289
|
-
var SpecsQueryError = class extends Error {
|
|
2290
|
-
constructor(code, message) {
|
|
2291
|
-
super(message);
|
|
2292
|
-
this.code = code;
|
|
2293
|
-
this.name = "SpecsQueryError";
|
|
2294
|
-
}
|
|
2295
|
-
code;
|
|
2296
|
-
};
|
|
2297
|
-
async function inspectSpecs(options = {}) {
|
|
2298
|
-
const projectRoot = await realpath2(resolve3(options.cwd ?? process.cwd()));
|
|
2299
|
-
const specsRoot = resolve3(projectRoot, SPECS_ROOT);
|
|
2300
|
-
const maxCandidates = options.maxCandidates ?? SPECS_MAX_CANDIDATES;
|
|
2301
|
-
const maxFileBytes = options.maxFileBytes ?? SPECS_MAX_FILE_BYTES;
|
|
2302
|
-
validatePositiveBound(maxCandidates, "maxCandidates");
|
|
2303
|
-
validatePositiveBound(maxFileBytes, "maxFileBytes");
|
|
2304
|
-
const configInspection = await inspectRspConfig(projectRoot);
|
|
2305
|
-
if (configInspection.issues.length > 0)
|
|
2306
|
-
throw new SpecsQueryError("invalid_config", configInspection.issues.join("; "));
|
|
2307
|
-
const decisionRecordsPath = resolveDecisionRecordsPath(configInspection.config);
|
|
2308
|
-
const decisionPathIssue = await validateDecisionRecordsFilesystemPath(decisionRecordsPath, projectRoot);
|
|
2309
|
-
if (decisionPathIssue)
|
|
2310
|
-
throw new SpecsQueryError("invalid_decision_records_path", decisionPathIssue);
|
|
2311
|
-
const decisionRecordsRoot = resolve3(projectRoot, decisionRecordsPath);
|
|
2312
|
-
const accumulator = {
|
|
2313
|
-
documents: [],
|
|
2314
|
-
generatedIndexes: [],
|
|
2315
|
-
diagnostics: [],
|
|
2316
|
-
candidateCount: 0,
|
|
2317
|
-
candidateLimitReported: false
|
|
2318
|
-
};
|
|
2319
|
-
const excludedSpecDirectories = [DEFAULT_DECISION_RECORDS_PATH, decisionRecordsPath].map((path) => resolve3(projectRoot, path)).filter((path) => isInside(specsRoot, path));
|
|
2320
|
-
await inspectDocumentRoot({
|
|
2321
|
-
projectRoot,
|
|
2322
|
-
root: specsRoot,
|
|
2323
|
-
label: "Specs",
|
|
2324
|
-
kind: "spec",
|
|
2325
|
-
excludedDirectories: excludedSpecDirectories,
|
|
2326
|
-
accumulator,
|
|
2327
|
-
maxCandidates,
|
|
2328
|
-
maxFileBytes,
|
|
2329
|
-
allowMissing: false
|
|
2330
|
-
});
|
|
2331
|
-
await inspectDocumentRoot({
|
|
2332
|
-
projectRoot,
|
|
2333
|
-
root: decisionRecordsRoot,
|
|
2334
|
-
label: "Decision Records",
|
|
2335
|
-
kind: "decision-record",
|
|
2336
|
-
excludedDirectories: [],
|
|
2337
|
-
accumulator,
|
|
2338
|
-
maxCandidates,
|
|
2339
|
-
maxFileBytes,
|
|
2340
|
-
allowMissing: options.allowMissingDecisionRecords === true
|
|
2341
|
-
});
|
|
2342
|
-
detectPathCollisions(accumulator);
|
|
2343
|
-
accumulator.documents.sort(compareDocument);
|
|
2344
|
-
accumulator.generatedIndexes.sort((left, right) => left.path.localeCompare(right.path));
|
|
2345
|
-
const boundedDiagnostics = accumulator.diagnostics.slice(0, SPECS_MAX_DIAGNOSTICS);
|
|
2346
|
-
return {
|
|
2347
|
-
source: await inspectSourceIdentity(projectRoot),
|
|
2348
|
-
roots: {
|
|
2349
|
-
specs: toProjectPath(projectRoot, specsRoot),
|
|
2350
|
-
decisions: toProjectPath(projectRoot, decisionRecordsRoot)
|
|
2351
|
-
},
|
|
2352
|
-
tree: buildDirectoryTree(projectRoot, specsRoot, "spec", accumulator.documents),
|
|
2353
|
-
decisionRecords: buildDirectoryTree(projectRoot, decisionRecordsRoot, "decision-record", accumulator.documents),
|
|
2354
|
-
documents: accumulator.documents.map(stripSource),
|
|
2355
|
-
generatedIndexes: accumulator.generatedIndexes,
|
|
2356
|
-
diagnostics: boundedDiagnostics,
|
|
2357
|
-
diagnosticSummary: {
|
|
2358
|
-
total: accumulator.diagnostics.length,
|
|
2359
|
-
returned: boundedDiagnostics.length,
|
|
2360
|
-
hasMore: accumulator.diagnostics.length > boundedDiagnostics.length
|
|
2361
|
-
},
|
|
2362
|
-
runtime: [],
|
|
2363
|
-
limits: {
|
|
2364
|
-
candidates: maxCandidates,
|
|
2365
|
-
fileBytes: maxFileBytes,
|
|
2366
|
-
results: SPECS_MAX_RESULT_LIMIT,
|
|
2367
|
-
searchExcerptCodePoints: SPECS_SEARCH_EXCERPT_CODE_POINTS,
|
|
2368
|
-
detailContentCodePoints: SPECS_DETAIL_CONTENT_CODE_POINTS
|
|
2369
|
-
}
|
|
2370
|
-
};
|
|
2371
|
-
}
|
|
2372
|
-
function specsInspectionComplete(inspection) {
|
|
2373
|
-
return inspection.diagnostics.every((diagnostic) => diagnostic.severity !== "error");
|
|
2374
|
-
}
|
|
2375
|
-
async function readSpecsDetail(inspection, projectPath) {
|
|
2376
|
-
const current = await readCurrentSpecsDocument(inspection, projectPath);
|
|
2377
|
-
const bounded = boundCodePoints(current.content, SPECS_DETAIL_CONTENT_CODE_POINTS);
|
|
2378
|
-
return {
|
|
2379
|
-
...current,
|
|
2380
|
-
content: bounded.value,
|
|
2381
|
-
contentTruncated: bounded.truncated
|
|
2382
|
-
};
|
|
2383
|
-
}
|
|
2384
|
-
async function readCurrentSpecsDocument(inspection, projectPath) {
|
|
2385
|
-
const normalized = normalizeRequestedPath(projectPath);
|
|
2386
|
-
const document = inspection.documents.find((candidate) => candidate.path === normalized);
|
|
2387
|
-
if (!document)
|
|
2388
|
-
throw new SpecsQueryError("specs_document_not_found", `Specs document not found: ${projectPath}`);
|
|
2389
|
-
const sourcePath = resolve3(inspection.source.root, normalized);
|
|
2390
|
-
const current = await readCurrentDocument(
|
|
2391
|
-
sourcePath,
|
|
2392
|
-
inspection.source.root,
|
|
2393
|
-
document.path,
|
|
2394
|
-
inspection.limits.fileBytes
|
|
2395
|
-
);
|
|
2396
|
-
const currentDocument = parseDocumentRecord(current.content, current.bytes, document.path, document.kind);
|
|
2397
|
-
return {
|
|
2398
|
-
...currentDocument,
|
|
2399
|
-
content: current.content
|
|
2400
|
-
};
|
|
2401
|
-
}
|
|
2402
|
-
async function searchSpecs(inspection, literal, options = {}) {
|
|
2403
|
-
return (await searchSpecsWithSources(inspection, literal, options)).result;
|
|
2404
|
-
}
|
|
2405
|
-
async function searchSpecsWithSources(inspection, literal, options = {}) {
|
|
2406
|
-
const query = literal.trim();
|
|
2407
|
-
if (query === "")
|
|
2408
|
-
throw new SpecsQueryError("invalid_specs_search", "Specs search literal must be non-empty");
|
|
2409
|
-
const limit = options.limit ?? 20;
|
|
2410
|
-
if (!Number.isInteger(limit) || limit < 1 || limit > SPECS_MAX_RESULT_LIMIT)
|
|
2411
|
-
throw new SpecsQueryError("invalid_specs_limit", `Specs search limit must be an integer from 1 through ${SPECS_MAX_RESULT_LIMIT}`);
|
|
2412
|
-
const excerptCodePoints = options.excerptCodePoints ?? SPECS_SEARCH_EXCERPT_CODE_POINTS;
|
|
2413
|
-
if (!Number.isInteger(excerptCodePoints) || excerptCodePoints < 40 || excerptCodePoints > 1e3)
|
|
2414
|
-
throw new SpecsQueryError("invalid_specs_excerpt", "Specs search excerpt must be an integer from 40 through 1000 code points");
|
|
2415
|
-
const lowered = query.toLowerCase();
|
|
2416
|
-
const matches = [];
|
|
2417
|
-
const documents = /* @__PURE__ */ new Map();
|
|
2418
|
-
let matched = 0;
|
|
2419
|
-
for (const document of inspection.documents) {
|
|
2420
|
-
const current = await readCurrentDocument(
|
|
2421
|
-
resolve3(inspection.source.root, document.path),
|
|
2422
|
-
inspection.source.root,
|
|
2423
|
-
document.path,
|
|
2424
|
-
inspection.limits.fileBytes
|
|
2425
|
-
);
|
|
2426
|
-
const currentDocument = parseDocumentRecord(current.content, current.bytes, document.path, document.kind);
|
|
2427
|
-
const lines = current.content.split(/\r?\n/);
|
|
2428
|
-
let heading = null;
|
|
2429
|
-
for (let index = 0; index < lines.length; index++) {
|
|
2430
|
-
const parsedHeading = parseMarkdownHeading(lines[index]);
|
|
2431
|
-
if (parsedHeading)
|
|
2432
|
-
heading = parsedHeading.title;
|
|
2433
|
-
const position = lines[index].toLowerCase().indexOf(lowered);
|
|
2434
|
-
if (position < 0)
|
|
2435
|
-
continue;
|
|
2436
|
-
matched += 1;
|
|
2437
|
-
if (matches.length < limit) {
|
|
2438
|
-
if (!documents.has(document.path)) {
|
|
2439
|
-
documents.set(document.path, {
|
|
2440
|
-
...currentDocument,
|
|
2441
|
-
content: current.content
|
|
2442
|
-
});
|
|
2443
|
-
}
|
|
2444
|
-
matches.push({
|
|
2445
|
-
path: document.path,
|
|
2446
|
-
kind: document.kind,
|
|
2447
|
-
title: currentDocument.title,
|
|
2448
|
-
heading,
|
|
2449
|
-
line: index + 1,
|
|
2450
|
-
excerpt: excerptAround(
|
|
2451
|
-
lines[index],
|
|
2452
|
-
[...lines[index].slice(0, position)].length,
|
|
2453
|
-
[...query].length,
|
|
2454
|
-
excerptCodePoints
|
|
2455
|
-
)
|
|
2456
|
-
});
|
|
2457
|
-
}
|
|
2458
|
-
}
|
|
2459
|
-
}
|
|
2460
|
-
return {
|
|
2461
|
-
result: {
|
|
2462
|
-
matches,
|
|
2463
|
-
summary: {
|
|
2464
|
-
candidates: inspection.documents.length,
|
|
2465
|
-
searched: inspection.documents.length,
|
|
2466
|
-
matched,
|
|
2467
|
-
returned: matches.length,
|
|
2468
|
-
hasMore: matched > matches.length
|
|
2469
|
-
}
|
|
2470
|
-
},
|
|
2471
|
-
documents
|
|
2472
|
-
};
|
|
2473
|
-
}
|
|
2474
|
-
async function inspectDocumentRoot(options) {
|
|
2475
|
-
const inspection = await inspectManagedFileTree(options.root, options.label, { allowMissing: options.allowMissing });
|
|
2476
|
-
for (const issue of inspection.issues) {
|
|
2477
|
-
options.accumulator.diagnostics.push({
|
|
2478
|
-
severity: "error",
|
|
2479
|
-
code: "specs_tree_invalid",
|
|
2480
|
-
path: toProjectPath(options.projectRoot, issue.path),
|
|
2481
|
-
message: issue.message
|
|
2482
|
-
});
|
|
2483
|
-
}
|
|
2484
|
-
if (!inspection.rootExists)
|
|
2485
|
-
return;
|
|
2486
|
-
for (const path of inspection.files) {
|
|
2487
|
-
if (options.excludedDirectories.some((directory) => isInside(directory, path)))
|
|
2488
|
-
continue;
|
|
2489
|
-
const projectPath = toProjectPath(options.projectRoot, path);
|
|
2490
|
-
const reserved = reservedIndexKind(options.root, path);
|
|
2491
|
-
if (reserved) {
|
|
2492
|
-
await classifyGeneratedIndex(path, options.projectRoot, projectPath, options.maxFileBytes, options.accumulator);
|
|
2493
|
-
continue;
|
|
2494
|
-
}
|
|
2495
|
-
if (!path.endsWith(".md"))
|
|
2496
|
-
continue;
|
|
2497
|
-
options.accumulator.candidateCount += 1;
|
|
2498
|
-
if (options.accumulator.candidateCount > options.maxCandidates) {
|
|
2499
|
-
if (!options.accumulator.candidateLimitReported) {
|
|
2500
|
-
options.accumulator.diagnostics.push({
|
|
2501
|
-
severity: "error",
|
|
2502
|
-
code: "specs_candidate_limit_exceeded",
|
|
2503
|
-
path: projectPath,
|
|
2504
|
-
message: `Specs candidate limit exceeded (${options.maxCandidates})`
|
|
2505
|
-
});
|
|
2506
|
-
options.accumulator.candidateLimitReported = true;
|
|
2507
|
-
}
|
|
2508
|
-
continue;
|
|
2509
|
-
}
|
|
2510
|
-
await inspectDocument(path, options.projectRoot, projectPath, options.kind, options.maxFileBytes, options.accumulator);
|
|
2511
|
-
}
|
|
2512
|
-
}
|
|
2513
|
-
async function classifyGeneratedIndex(path, projectRoot, projectPath, maxFileBytes, accumulator) {
|
|
2514
|
-
try {
|
|
2515
|
-
const current = await readCurrentDocument(path, projectRoot, projectPath, maxFileBytes);
|
|
2516
|
-
const recognized = isRecognizedGeneratedSpecsIndex(current.content, projectPath);
|
|
2517
|
-
accumulator.generatedIndexes.push({
|
|
2518
|
-
path: projectPath,
|
|
2519
|
-
classification: recognized ? "safe-removal" : "owner-controlled",
|
|
2520
|
-
reason: recognized ? "metadata recognizes the reserved path as a generated Specs index" : "reserved path content is not recognized generated Specs-index output"
|
|
2521
|
-
});
|
|
2522
|
-
if (!recognized) {
|
|
2523
|
-
accumulator.diagnostics.push({
|
|
2524
|
-
severity: "error",
|
|
2525
|
-
code: "unrecognized_reserved_specs_index",
|
|
2526
|
-
path: projectPath,
|
|
2527
|
-
message: `reserved Specs index is owner-controlled and cannot be treated as generated content: ${projectPath}`
|
|
2528
|
-
});
|
|
2529
|
-
}
|
|
2530
|
-
} catch (error) {
|
|
2531
|
-
accumulator.generatedIndexes.push({
|
|
2532
|
-
path: projectPath,
|
|
2533
|
-
classification: "owner-controlled",
|
|
2534
|
-
reason: "reserved path could not be boundedly inspected as generated Specs-index output"
|
|
2535
|
-
});
|
|
2536
|
-
accumulator.diagnostics.push({
|
|
2537
|
-
severity: "error",
|
|
2538
|
-
code: error instanceof SpecsQueryError ? error.code : "specs_index_read_failed",
|
|
2539
|
-
path: projectPath,
|
|
2540
|
-
message: `unable to inspect reserved Specs index ${projectPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
2541
|
-
});
|
|
2542
|
-
}
|
|
2543
|
-
}
|
|
2544
|
-
function isRecognizedGeneratedSpecsIndex(content, projectPath) {
|
|
2545
|
-
const metadata = parseFrontmatter(content);
|
|
2546
|
-
const expectedSourceDirectory = normalizeLogicalPath(dirname4(projectPath));
|
|
2547
|
-
return metadata?.kind === "generated-index" && metadata.index_type === "specs" && metadata.source_dir === expectedSourceDirectory;
|
|
2548
|
-
}
|
|
2549
|
-
async function inspectDocument(path, projectRoot, projectPath, kind, maxFileBytes, accumulator) {
|
|
2550
|
-
try {
|
|
2551
|
-
const current = await readCurrentDocument(path, projectRoot, projectPath, maxFileBytes);
|
|
2552
|
-
const metadata = parseFrontmatter(current.content);
|
|
2553
|
-
if (metadata?.kind === "generated-index")
|
|
2554
|
-
return;
|
|
2555
|
-
accumulator.documents.push({
|
|
2556
|
-
sourcePath: path,
|
|
2557
|
-
...parseDocumentRecord(current.content, current.bytes, projectPath, kind)
|
|
2558
|
-
});
|
|
2559
|
-
} catch (error) {
|
|
2560
|
-
accumulator.diagnostics.push({
|
|
2561
|
-
severity: "error",
|
|
2562
|
-
code: error instanceof SpecsQueryError ? error.code : "specs_file_read_failed",
|
|
2563
|
-
path: projectPath,
|
|
2564
|
-
message: `unable to read Specs document ${projectPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
2565
|
-
});
|
|
2566
|
-
}
|
|
2567
|
-
}
|
|
2568
|
-
function detectPathCollisions(accumulator) {
|
|
2569
|
-
const seen = /* @__PURE__ */ new Map();
|
|
2570
|
-
for (const document of accumulator.documents) {
|
|
2571
|
-
const canonical = document.path.normalize("NFC");
|
|
2572
|
-
const existing = seen.get(canonical);
|
|
2573
|
-
if (existing && existing !== document.path) {
|
|
2574
|
-
accumulator.diagnostics.push({
|
|
2575
|
-
severity: "error",
|
|
2576
|
-
code: "specs_path_collision",
|
|
2577
|
-
path: document.path,
|
|
2578
|
-
message: `Specs path normalization collision: ${existing} and ${document.path}`
|
|
2579
|
-
});
|
|
2580
|
-
} else {
|
|
2581
|
-
seen.set(canonical, document.path);
|
|
2582
|
-
}
|
|
2583
|
-
if (canonical !== document.path) {
|
|
2584
|
-
accumulator.diagnostics.push({
|
|
2585
|
-
severity: "error",
|
|
2586
|
-
code: "specs_path_not_canonical",
|
|
2587
|
-
path: document.path,
|
|
2588
|
-
message: `Specs path must use canonical NFC form: ${document.path}`
|
|
2589
|
-
});
|
|
2590
|
-
}
|
|
2591
|
-
}
|
|
2592
|
-
}
|
|
2593
|
-
function buildDirectoryTree(projectRoot, root, kind, documents) {
|
|
2594
|
-
const rootPath = toProjectPath(projectRoot, root);
|
|
2595
|
-
const tree = {
|
|
2596
|
-
name: basename3(root),
|
|
2597
|
-
path: rootPath,
|
|
2598
|
-
directories: [],
|
|
2599
|
-
documents: []
|
|
2600
|
-
};
|
|
2601
|
-
for (const document of documents.filter((candidate) => candidate.kind === kind)) {
|
|
2602
|
-
const rel = relative6(root, document.sourcePath);
|
|
2603
|
-
if (rel === ".." || rel.startsWith(`..${sep3}`) || isAbsolute3(rel))
|
|
2604
|
-
continue;
|
|
2605
|
-
const segments = normalizeLogicalPath(rel).split("/");
|
|
2606
|
-
let current = tree;
|
|
2607
|
-
for (const segment of segments.slice(0, -1)) {
|
|
2608
|
-
let child = current.directories.find((directory) => directory.name === segment);
|
|
2609
|
-
if (!child) {
|
|
2610
|
-
child = {
|
|
2611
|
-
name: segment,
|
|
2612
|
-
path: `${current.path}/${segment}`,
|
|
2613
|
-
directories: [],
|
|
2614
|
-
documents: []
|
|
2615
|
-
};
|
|
2616
|
-
current.directories.push(child);
|
|
2617
|
-
current.directories.sort((left, right) => left.name.localeCompare(right.name));
|
|
2618
|
-
}
|
|
2619
|
-
current = child;
|
|
2620
|
-
}
|
|
2621
|
-
current.documents.push(stripSource(document));
|
|
2622
|
-
current.documents.sort(compareDocument);
|
|
2623
|
-
}
|
|
2624
|
-
return tree;
|
|
2625
|
-
}
|
|
2626
|
-
function extractHeadings(content) {
|
|
2627
|
-
const headings = [];
|
|
2628
|
-
const lines = content.split(/\r?\n/);
|
|
2629
|
-
for (let index = 0; index < lines.length; index++) {
|
|
2630
|
-
const heading = parseMarkdownHeading(lines[index]);
|
|
2631
|
-
if (!heading)
|
|
2632
|
-
continue;
|
|
2633
|
-
headings.push({
|
|
2634
|
-
depth: heading.depth,
|
|
2635
|
-
title: heading.title,
|
|
2636
|
-
line: index + 1
|
|
2637
|
-
});
|
|
2638
|
-
}
|
|
2639
|
-
return headings;
|
|
2640
|
-
}
|
|
2641
|
-
function parseMarkdownHeading(line) {
|
|
2642
|
-
let depth = 0;
|
|
2643
|
-
while (depth < 6 && line[depth] === "#")
|
|
2644
|
-
depth += 1;
|
|
2645
|
-
if (depth === 0 || line[depth] === "#" || !isWhitespace(line[depth]))
|
|
2646
|
-
return null;
|
|
2647
|
-
let contentStart = depth;
|
|
2648
|
-
while (contentStart < line.length && isWhitespace(line[contentStart]))
|
|
2649
|
-
contentStart += 1;
|
|
2650
|
-
if (contentStart === line.length)
|
|
2651
|
-
return null;
|
|
2652
|
-
const rawTitle = line.slice(contentStart);
|
|
2653
|
-
let titleEnd = rawTitle.length;
|
|
2654
|
-
while (titleEnd > 0 && isSpaceOrTab(rawTitle[titleEnd - 1]))
|
|
2655
|
-
titleEnd -= 1;
|
|
2656
|
-
let closingStart = titleEnd;
|
|
2657
|
-
while (closingStart > 0 && rawTitle[closingStart - 1] === "#")
|
|
2658
|
-
closingStart -= 1;
|
|
2659
|
-
const closingPredecessor = closingStart === 0 ? line[contentStart - 1] : rawTitle[closingStart - 1];
|
|
2660
|
-
if (closingStart < titleEnd && isSpaceOrTab(closingPredecessor)) {
|
|
2661
|
-
titleEnd = closingStart;
|
|
2662
|
-
while (titleEnd > 0 && isSpaceOrTab(rawTitle[titleEnd - 1]))
|
|
2663
|
-
titleEnd -= 1;
|
|
2664
|
-
}
|
|
2665
|
-
const title = rawTitle.slice(0, titleEnd).trim();
|
|
2666
|
-
return { depth, title };
|
|
2667
|
-
}
|
|
2668
|
-
function isWhitespace(character) {
|
|
2669
|
-
return character !== void 0 && character.trim() === "";
|
|
2670
|
-
}
|
|
2671
|
-
function isSpaceOrTab(character) {
|
|
2672
|
-
return character === " " || character === " ";
|
|
2673
|
-
}
|
|
2674
|
-
function extractSummary(content, frontmatterSummary) {
|
|
2675
|
-
if (typeof frontmatterSummary === "string" && frontmatterSummary.trim() !== "")
|
|
2676
|
-
return frontmatterSummary.trim();
|
|
2677
|
-
const frontmatter = content.match(/^---\r?\n[\s\S]*?\r?\n(?:---|\.\.\.)\r?\n/);
|
|
2678
|
-
const body = frontmatter ? content.slice(frontmatter[0].length) : content;
|
|
2679
|
-
for (const rawLine of body.split(/\r?\n/)) {
|
|
2680
|
-
const line = rawLine.trim();
|
|
2681
|
-
if (line === "" || line.startsWith("#") || line.startsWith("<!--"))
|
|
2682
|
-
continue;
|
|
2683
|
-
return line.replace(/^[-*]\s*/, "").trim() || null;
|
|
2684
|
-
}
|
|
2685
|
-
return null;
|
|
2686
|
-
}
|
|
2687
|
-
async function inspectSourceIdentity(projectRoot) {
|
|
2688
|
-
const root = normalizeLogicalPath(projectRoot);
|
|
2689
|
-
const head = runGit(["rev-parse", "HEAD"], projectRoot);
|
|
2690
|
-
const branch = runGit(["symbolic-ref", "--quiet", "--short", "HEAD"], projectRoot);
|
|
2691
|
-
const status = spawnSync("git", ["status", "--porcelain=v1", "--untracked-files=normal"], {
|
|
2692
|
-
cwd: root,
|
|
2693
|
-
encoding: "utf-8",
|
|
2694
|
-
maxBuffer: 256 * 1024
|
|
2695
|
-
});
|
|
2696
|
-
return {
|
|
2697
|
-
root,
|
|
2698
|
-
gitHead: head,
|
|
2699
|
-
gitBranch: branch,
|
|
2700
|
-
dirty: status.status === 0 ? status.stdout.trim() !== "" : status.error && status.error.code === "ENOBUFS" ? true : null
|
|
2701
|
-
};
|
|
2702
|
-
}
|
|
2703
|
-
function runGit(args, cwd) {
|
|
2704
|
-
const result = spawnSync("git", args, { cwd, encoding: "utf-8", maxBuffer: 64 * 1024 });
|
|
2705
|
-
return result.status === 0 ? result.stdout.trim() || null : null;
|
|
2706
|
-
}
|
|
2707
|
-
function normalizeRequestedPath(pathValue) {
|
|
2708
|
-
const trimmed = pathValue.trim();
|
|
2709
|
-
if (trimmed === "")
|
|
2710
|
-
throw new SpecsQueryError("invalid_specs_path", "Specs detail path must be non-empty");
|
|
2711
|
-
const normalized = normalizeLogicalPath(trimmed.replace(/^\.\//, ""));
|
|
2712
|
-
if (isAbsolute3(trimmed) || normalized === ".." || normalized.startsWith("../"))
|
|
2713
|
-
throw new SpecsQueryError("invalid_specs_path", "Specs detail path must be one project-relative path returned by rsp specs");
|
|
2714
|
-
return normalized;
|
|
2715
|
-
}
|
|
2716
|
-
function reservedIndexKind(root, path) {
|
|
2717
|
-
if (basename3(path) === GENERATED_INDEX_FILENAME)
|
|
2718
|
-
return "current";
|
|
2719
|
-
if (path === join4(root, LEGACY_GENERATED_INDEX_FILENAME))
|
|
2720
|
-
return "legacy";
|
|
2721
|
-
return null;
|
|
2722
|
-
}
|
|
2723
|
-
function excerptAround(line, matchStart, matchLength, limit) {
|
|
2724
|
-
const codePoints = [...line];
|
|
2725
|
-
if (codePoints.length <= limit)
|
|
2726
|
-
return codePoints.join("").trim();
|
|
2727
|
-
const before = Math.max(0, matchStart - Math.floor((limit - matchLength) / 2));
|
|
2728
|
-
const value = codePoints.slice(before, before + limit).join("").trim();
|
|
2729
|
-
return `${before > 0 ? "\u2026" : ""}${value}${before + limit < codePoints.length ? "\u2026" : ""}`;
|
|
2730
|
-
}
|
|
2731
|
-
async function readCurrentDocument(path, root, projectPath, maxFileBytes) {
|
|
2732
|
-
const changed2 = () => new SpecsQueryError("specs_file_changed", `Specs document is no longer a readable regular file: ${projectPath}`);
|
|
2733
|
-
const sourcePath = resolve3(path);
|
|
2734
|
-
const sourceRoot = resolve3(root);
|
|
2735
|
-
const relativePath = relative6(sourceRoot, sourcePath);
|
|
2736
|
-
if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${sep3}`) || isAbsolute3(relativePath)) {
|
|
2737
|
-
throw changed2();
|
|
2738
|
-
}
|
|
2739
|
-
const inspection = inspectManagedFile(sourcePath, "Specs document");
|
|
2740
|
-
if (inspection.issue)
|
|
2741
|
-
throw changed2();
|
|
2742
|
-
let inspectedRoot;
|
|
2743
|
-
let inspected;
|
|
2744
|
-
let realRoot;
|
|
2745
|
-
try {
|
|
2746
|
-
inspectedRoot = await lstat2(sourceRoot, { bigint: true });
|
|
2747
|
-
inspected = await lstat2(sourcePath, { bigint: true });
|
|
2748
|
-
realRoot = await realpath2(sourceRoot);
|
|
2749
|
-
const resolvedPath = await realpath2(sourcePath);
|
|
2750
|
-
if (!inspectedRoot.isDirectory() || !inspected.isFile() || resolvedPath !== resolve3(realRoot, relativePath) || !isContained2(realRoot, resolvedPath)) {
|
|
2751
|
-
throw changed2();
|
|
2752
|
-
}
|
|
2753
|
-
} catch (error) {
|
|
2754
|
-
if (error instanceof SpecsQueryError)
|
|
2755
|
-
throw error;
|
|
2756
|
-
throw changed2();
|
|
2757
|
-
}
|
|
2758
|
-
let handle;
|
|
2759
|
-
try {
|
|
2760
|
-
handle = await open2(sourcePath, READ_NO_FOLLOW_FLAGS2);
|
|
2761
|
-
} catch {
|
|
2762
|
-
throw changed2();
|
|
2763
|
-
}
|
|
2764
|
-
try {
|
|
2765
|
-
const opened = await handle.stat({ bigint: true });
|
|
2766
|
-
const currentRoot = await realpath2(sourceRoot);
|
|
2767
|
-
const currentPath = await realpath2(sourcePath);
|
|
2768
|
-
if (!opened.isFile() || currentRoot !== realRoot || currentPath !== resolve3(currentRoot, relativePath) || !isContained2(currentRoot, currentPath) || !sameIdentity2(inspected, opened) || !sameSnapshot2(inspected, opened)) {
|
|
2769
|
-
throw changed2();
|
|
2770
|
-
}
|
|
2771
|
-
if (opened.size > BigInt(maxFileBytes))
|
|
2772
|
-
throw new SpecsQueryError("specs_file_too_large", `Specs document exceeds the ${maxFileBytes}-byte inspection limit: ${projectPath}`);
|
|
2773
|
-
const current = await readBoundedDocument(handle, opened, projectPath, maxFileBytes);
|
|
2774
|
-
await validateCurrentSpecsPath({
|
|
2775
|
-
sourcePath,
|
|
2776
|
-
sourceRoot,
|
|
2777
|
-
relativePath,
|
|
2778
|
-
realRoot,
|
|
2779
|
-
inspectedRoot,
|
|
2780
|
-
finalHandle: current.final,
|
|
2781
|
-
projectPath
|
|
2782
|
-
});
|
|
2783
|
-
return {
|
|
2784
|
-
content: current.content,
|
|
2785
|
-
bytes: current.bytes
|
|
2786
|
-
};
|
|
2787
|
-
} catch (error) {
|
|
2788
|
-
if (error instanceof SpecsQueryError)
|
|
2789
|
-
throw error;
|
|
2790
|
-
throw changed2();
|
|
2791
|
-
} finally {
|
|
2792
|
-
await handle.close();
|
|
2793
|
-
}
|
|
2794
|
-
}
|
|
2795
|
-
async function readBoundedDocument(handle, opened, projectPath, maxFileBytes) {
|
|
2796
|
-
const buffer = Buffer3.allocUnsafe(maxFileBytes + 1);
|
|
2797
|
-
let bytes = 0;
|
|
2798
|
-
while (bytes < buffer.length) {
|
|
2799
|
-
const result = await handle.read(buffer, bytes, buffer.length - bytes, bytes);
|
|
2800
|
-
if (result.bytesRead === 0)
|
|
2801
|
-
break;
|
|
2802
|
-
bytes += result.bytesRead;
|
|
2803
|
-
}
|
|
2804
|
-
const final = await handle.stat({ bigint: true });
|
|
2805
|
-
if (bytes > maxFileBytes || final.size > BigInt(maxFileBytes))
|
|
2806
|
-
throw new SpecsQueryError("specs_file_too_large", `Specs document exceeds the ${maxFileBytes}-byte inspection limit: ${projectPath}`);
|
|
2807
|
-
if (!sameIdentity2(opened, final) || !sameSnapshot2(opened, final) || final.size !== BigInt(bytes)) {
|
|
2808
|
-
throw new SpecsQueryError("specs_file_changed", `Specs document is no longer a readable regular file: ${projectPath}`);
|
|
2809
|
-
}
|
|
2810
|
-
return {
|
|
2811
|
-
content: buffer.subarray(0, bytes).toString("utf-8"),
|
|
2812
|
-
bytes,
|
|
2813
|
-
final
|
|
2814
|
-
};
|
|
2815
|
-
}
|
|
2816
|
-
async function validateCurrentSpecsPath(options) {
|
|
2817
|
-
const changed2 = () => new SpecsQueryError("specs_file_changed", `Specs document is no longer a readable regular file: ${options.projectPath}`);
|
|
2818
|
-
try {
|
|
2819
|
-
const currentRoot = await lstat2(options.sourceRoot, { bigint: true });
|
|
2820
|
-
const currentPath = await lstat2(options.sourcePath, { bigint: true });
|
|
2821
|
-
const realCurrentRoot = await realpath2(options.sourceRoot);
|
|
2822
|
-
const realCurrentPath = await realpath2(options.sourcePath);
|
|
2823
|
-
if (!currentRoot.isDirectory() || !currentPath.isFile() || !sameIdentity2(options.inspectedRoot, currentRoot) || realCurrentRoot !== options.realRoot || realCurrentPath !== resolve3(realCurrentRoot, options.relativePath) || !isContained2(realCurrentRoot, realCurrentPath) || !sameIdentity2(currentPath, options.finalHandle) || !sameSnapshot2(currentPath, options.finalHandle)) {
|
|
2824
|
-
throw changed2();
|
|
2825
|
-
}
|
|
2826
|
-
} catch (error) {
|
|
2827
|
-
if (error instanceof SpecsQueryError)
|
|
2828
|
-
throw error;
|
|
2829
|
-
throw changed2();
|
|
2830
|
-
}
|
|
2831
|
-
}
|
|
2832
|
-
function sameIdentity2(left, right) {
|
|
2833
|
-
return left.dev === right.dev && left.ino === right.ino;
|
|
2834
|
-
}
|
|
2835
|
-
function sameSnapshot2(left, right) {
|
|
2836
|
-
return left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
2837
|
-
}
|
|
2838
|
-
function isContained2(root, path) {
|
|
2839
|
-
const candidate = relative6(root, path);
|
|
2840
|
-
return candidate !== "" && candidate !== ".." && !candidate.startsWith(`..${sep3}`) && !isAbsolute3(candidate);
|
|
2841
|
-
}
|
|
2842
|
-
function boundCodePoints(value, limit) {
|
|
2843
|
-
const codePoints = [...value];
|
|
2844
|
-
return {
|
|
2845
|
-
value: codePoints.slice(0, limit).join(""),
|
|
2846
|
-
truncated: codePoints.length > limit
|
|
2847
|
-
};
|
|
2848
|
-
}
|
|
2849
|
-
function stripSource(document) {
|
|
2850
|
-
const { sourcePath: _sourcePath, ...record } = document;
|
|
2851
|
-
return record;
|
|
2852
|
-
}
|
|
2853
|
-
function parseDocumentRecord(content, bytes, path, kind) {
|
|
2854
|
-
const metadata = parseFrontmatter(content);
|
|
2855
|
-
const headings = extractHeadings(content);
|
|
2856
|
-
return {
|
|
2857
|
-
path,
|
|
2858
|
-
kind,
|
|
2859
|
-
title: typeof metadata?.title === "string" && metadata.title.trim() !== "" ? metadata.title.trim() : headings.find((heading) => heading.depth === 1)?.title ?? basename3(path, ".md"),
|
|
2860
|
-
summary: extractSummary(content, metadata?.summary),
|
|
2861
|
-
bytes,
|
|
2862
|
-
headings
|
|
2863
|
-
};
|
|
2864
|
-
}
|
|
2865
|
-
function compareDocument(left, right) {
|
|
2866
|
-
return left.path.localeCompare(right.path);
|
|
2867
|
-
}
|
|
2868
|
-
function validatePositiveBound(value, label) {
|
|
2869
|
-
if (!Number.isInteger(value) || value < 1)
|
|
2870
|
-
throw new SpecsQueryError("invalid_specs_bound", `${label} must be a positive integer`);
|
|
2871
|
-
}
|
|
2872
|
-
function isInside(directory, path) {
|
|
2873
|
-
const rel = relative6(directory, path);
|
|
2874
|
-
return rel === "" || !isAbsolute3(rel) && rel !== ".." && !rel.startsWith(`..${sep3}`);
|
|
2875
|
-
}
|
|
2876
|
-
function toProjectPath(projectRoot, path) {
|
|
2877
|
-
return normalizeLogicalPath(relative6(projectRoot, path)) || ".";
|
|
2878
|
-
}
|
|
2879
|
-
|
|
2880
2171
|
// src/specs/projection.ts
|
|
2881
2172
|
function projectSpecsTree(inspection) {
|
|
2882
2173
|
assertCompleteInspection(inspection);
|
|
@@ -2970,13 +2261,6 @@ export {
|
|
|
2970
2261
|
IssueRelationshipError,
|
|
2971
2262
|
parseIssueRelationships,
|
|
2972
2263
|
createIssueRelationship,
|
|
2973
|
-
walkMarkdownFiles,
|
|
2974
|
-
inspectUnsupportedRules,
|
|
2975
|
-
normalizeLogicalPath,
|
|
2976
|
-
isValidSpecName,
|
|
2977
|
-
guardRspInitialized,
|
|
2978
|
-
cleanupEmptyParentDirs,
|
|
2979
|
-
detectProjectName,
|
|
2980
2264
|
GROUP_BRIEF_FILENAME,
|
|
2981
2265
|
WorkRefError,
|
|
2982
2266
|
normalizeWorkRefSegment,
|
|
@@ -3005,15 +2289,6 @@ export {
|
|
|
3005
2289
|
readArchiveHistoryDocument,
|
|
3006
2290
|
historyInspectionComplete,
|
|
3007
2291
|
inspectChangeDependencies,
|
|
3008
|
-
SPECS_DEFAULT_RESULT_LIMIT,
|
|
3009
|
-
SPECS_MAX_RESULT_LIMIT,
|
|
3010
|
-
SPECS_MAX_FILE_BYTES,
|
|
3011
|
-
SPECS_SEARCH_EXCERPT_CODE_POINTS,
|
|
3012
|
-
SPECS_DETAIL_CONTENT_CODE_POINTS,
|
|
3013
|
-
SpecsQueryError,
|
|
3014
|
-
inspectSpecs,
|
|
3015
|
-
specsInspectionComplete,
|
|
3016
|
-
isRecognizedGeneratedSpecsIndex,
|
|
3017
2292
|
collectArchiveReadiness,
|
|
3018
2293
|
toArchiveReadinessOutput,
|
|
3019
2294
|
buildDurableReviewGuidance,
|