@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.
Files changed (76) hide show
  1. package/README.md +24 -17
  2. package/README.zh-CN.md +25 -18
  3. package/dist/chunk-JO6NDJM4.mjs +360 -0
  4. package/dist/chunk-LGDCIQYL.mjs +725 -0
  5. package/dist/{chunk-7HXQNBIG.mjs → chunk-P3KO3GZW.mjs} +420 -268
  6. package/dist/chunk-U56XMIOO.mjs +66 -0
  7. package/dist/{chunk-TSUDEX5Y.mjs → chunk-VDRXFKBP.mjs} +142 -867
  8. package/dist/cli.mjs +1111 -2347
  9. package/dist/client-EHRWH5Q7.mjs +108 -0
  10. package/dist/{entry-GIDNR7CJ.mjs → entry-NDASTAI5.mjs} +128 -38
  11. package/dist/viewer/NOTICE.txt +299 -0
  12. package/dist/viewer/app.css +2 -0
  13. package/dist/viewer/app.js +670 -0
  14. package/dist/viewer-build.json +1 -0
  15. package/dist/viewer-service.mjs +551 -0
  16. package/package.json +51 -25
  17. package/skills/rsp/SKILL.md +44 -38
  18. package/skills/rsp/references/architecture.md +39 -0
  19. package/skills/rsp/references/closeout.md +38 -0
  20. package/skills/rsp/references/context-migration.md +22 -0
  21. package/skills/rsp/references/control-outcome.md +3 -29
  22. package/skills/rsp/references/coordination-recovery.md +25 -0
  23. package/skills/rsp/references/coordination-review.md +15 -0
  24. package/skills/rsp/references/coordination.md +59 -0
  25. package/skills/rsp/references/durable-review.md +60 -12
  26. package/skills/rsp/references/focus-continuation.md +12 -4
  27. package/skills/rsp/references/groups-dependencies.md +6 -4
  28. package/skills/rsp/references/response-language.md +2 -0
  29. package/skills/rsp/references/setup-repair.md +42 -7
  30. package/skills/rsp/references/work-model.md +37 -0
  31. package/skills/rsp-commit/SKILL.md +76 -31
  32. package/skills/rsp-commit/references/message.md +34 -0
  33. package/skills/rsp-doc/SKILL.md +68 -0
  34. package/skills/rsp-doc/agents/openai.yaml +3 -0
  35. package/skills/rsp-doc/references/release-communication.md +39 -0
  36. package/skills/rsp-doc/references/skill-authoring.md +36 -0
  37. package/skills/rsp-implement/SKILL.md +60 -25
  38. package/skills/rsp-implement/references/diagnosis.md +10 -0
  39. package/skills/rsp-implement/references/finding-resolution.md +21 -0
  40. package/skills/rsp-implement/references/test-first.md +7 -0
  41. package/skills/rsp-review/SKILL.md +46 -45
  42. package/skills/rsp-review/references/code-review.md +30 -9
  43. package/skills/rsp-review/references/document-review.md +24 -9
  44. package/skills/rsp-review/references/release-communication.md +21 -0
  45. package/skills/rsp-shape/SKILL.md +26 -45
  46. package/skills/rsp-shape/references/complex-shaping.md +3 -1
  47. package/skills/rsp-shape/references/deep-clarification.md +4 -4
  48. package/skills/rsp-shape/references/domain-modeling.md +13 -0
  49. package/skills/rsp-shape/references/module-seams.md +13 -0
  50. package/skills/rsp-shape/references/planning.md +42 -0
  51. package/skills/{rsp-design → rsp-shape}/references/reversible-exploration.md +2 -2
  52. package/skills/rsp-verify/SKILL.md +39 -12
  53. package/dist/chunk-JOMFFQRG.mjs +0 -99
  54. package/dist/chunk-QM2UVPQK.mjs +0 -375
  55. package/dist/entry-YAW3PYFA.mjs +0 -174
  56. package/rules/rsp-rules.md +0 -48
  57. package/skills/rsp/references/implementation-evidence.md +0 -12
  58. package/skills/rsp/references/managed-routing.md +0 -45
  59. package/skills/rsp-design/SKILL.md +0 -53
  60. package/skills/rsp-design/references/domain-modeling.md +0 -11
  61. package/skills/rsp-design/references/module-seams.md +0 -11
  62. package/skills/rsp-diagnose/SKILL.md +0 -48
  63. package/skills/rsp-manage/SKILL.md +0 -101
  64. package/skills/rsp-manage/references/closeout.md +0 -13
  65. package/skills/rsp-manage/references/delegation.md +0 -47
  66. package/skills/rsp-manage/references/interruption-recovery.md +0 -19
  67. package/skills/rsp-manage/references/review-convergence.md +0 -9
  68. package/skills/rsp-release-docs/SKILL.md +0 -77
  69. package/skills/rsp-release-docs/references/convention-discovery.md +0 -65
  70. package/skills/rsp-release-docs/references/evidence-and-surfaces.md +0 -21
  71. package/skills/rsp-release-docs/references/output-contracts.md +0 -97
  72. package/skills/rsp-release-docs/references/publication-lifecycle.md +0 -28
  73. package/skills/rsp-resolve-findings/SKILL.md +0 -61
  74. package/skills/rsp-structural-audit/SKILL.md +0 -71
  75. package/skills/rsp-structural-audit/references/structural-lenses.md +0 -41
  76. 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
- OBSOLETE_RSP_RULES_PATH,
12
- RSP_DIR,
13
- RSP_RULES_PATH,
14
- classifyVerifyCheckboxes,
15
- collectVerifyCheckboxLines,
18
+ collectVerifyCheckboxes,
16
19
  countCheckboxes,
17
- extractBlockerLines,
18
20
  getCanonicalSectionHeadings,
19
- getDocumentSectionBody,
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
- isEmptyBlockerLine,
30
+ markdownListItems,
31
+ markdownText,
32
+ openCheckboxLines,
30
33
  parseFrontmatter,
34
+ parseMarkdown,
31
35
  parseRspDocument,
32
36
  parseScenarios,
33
- pc,
37
+ readBlockerEntries,
34
38
  renderDocumentSectionHeading,
35
39
  renderDocumentTitle,
36
40
  requireManagedDirectory,
37
41
  requireManagedFile,
38
- resolveDecisionRecordsPath,
39
42
  resolveLanguagePolicy,
40
- resolveManagePolicy,
41
- toErrorMessage,
42
- validateDecisionRecordsFilesystemPath
43
- } from "./chunk-7HXQNBIG.mjs";
43
+ resolveWorkflowPolicy,
44
+ summarizeVerifyCheckboxes,
45
+ visibleMarkdownSource
46
+ } from "./chunk-P3KO3GZW.mjs";
44
47
 
45
48
  // src/status/inspect.ts
46
- import { readFile as readFile4, stat } from "fs/promises";
49
+ import { readFile as readFile3, stat } from "fs/promises";
47
50
 
48
51
  // src/core/change-group.ts
49
- import { readFile as readFile2 } from "fs/promises";
50
- import { dirname as dirname2, join as join3, relative as relative3 } from "path";
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 as readdir2 } from "fs/promises";
160
- import { isAbsolute, join as join2, relative as relative2 } from "path";
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 = join2(changesDir, ...segments.slice(0, -1), isGroupBrief ? GROUP_BRIEF_FILENAME : `${segments.at(-1)}.md`);
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 = join2(changesDir, ref.group, GROUP_BRIEF_FILENAME);
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(relative2(changesDir, path));
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
- join2(focusDir, ref.group),
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 = join2(focusDir, ...ref.name.split("/"));
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 = join2(archivesDir, ref.group);
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 = join2(focusDir, entry.name);
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 = join2(archivesDir, entry.name);
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 = join2(changesDir, entry.name);
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
- ), join2(groupPath, GROUP_BRIEF_FILENAME));
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: ${join2(groupPath, GROUP_BRIEF_FILENAME)}`,
377
+ `Group Brief must be a regular file: ${join(groupPath, GROUP_BRIEF_FILENAME)}`,
479
378
  group
480
- ), join2(groupPath, GROUP_BRIEF_FILENAME));
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 = join2(groupPath, entry.name);
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 = join2(groupPath, entry.name);
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 = join2(groupPath, entry.name);
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 readdir2(path, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
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 = join2(changesDir, ...segments.slice(0, depth));
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 = join2(root, ...segments.slice(0, depth));
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 = join2(changesDir, ...segments.slice(0, depth));
692
- const filePath = finalFilename && depth === segments.length ? join2(changesDir, ...segments.slice(0, -1), finalFilename) : `${identityPath}.md`;
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(content);
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 = getDocumentSectionBody(document, "proposal").split("\n").map((line) => line.trim());
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 getDocumentSectionBody(document, "goal").split("\n")) {
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(relative3(ARCHIVES_DIR, path));
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 readFile2(path, "utf-8");
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
- if (getDocumentTitle(parseRspDocument(content, GROUP_BRIEF_DOCUMENT_SCHEMA), "identity") === archiveGroup) {
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(content))
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 readFile2(brief.path, "utf-8");
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 parsed = parseGroupBrief(brief.group, content);
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
- content,
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(relative3(ARCHIVES_DIR, item.path));
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 completion = getDocumentSectionBody(parseRspDocument(content, GROUP_BRIEF_DOCUMENT_SCHEMA), "completionConditions");
1028
- return completion.split("\n").flatMap((line) => {
1029
- const match = line.trim().match(/^-\s+\[([ /xX-])\]\s+(Resolve reopened concern from `(\.rsp\/archives\/[^`]+)`: \S.*)$/);
1030
- return match ? [{ active: match[1] !== "-", key: match[2], sourcePath: match[3] }] : [];
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 = join3(dirname2(ref.path), GROUP_BRIEF_FILENAME);
947
+ const briefPath = join2(dirname(ref.path), GROUP_BRIEF_FILENAME);
1038
948
  let content;
1039
949
  try {
1040
- content = await readFile2(briefPath, "utf-8");
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(relative3(ARCHIVES_DIR, path)).startsWith(groupPrefix))
980
+ if (!normalizeLogicalPath(relative2(ARCHIVES_DIR, path)).startsWith(groupPrefix))
1071
981
  continue;
1072
982
  let content;
1073
983
  try {
1074
- content = await readFile2(path, "utf-8");
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, content) {
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(content);
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 (!getDocumentSectionBody(document, definition.id))
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 getDocumentSectionBody(document, "slices").split("\n")) {
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(getDocumentSectionBody(document, "completionConditions"));
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(getDocumentSectionBody(document, "blockers")),
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 readFile3 } from "fs/promises";
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 relative4, resolve, sep as sep2 } from "path";
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 = relative4(archivesDir, sourcePath);
1185
- if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${sep2}`) || isAbsolute2(relativePath)) {
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 = relative4(root, path);
1301
- return candidate !== "" && candidate !== ".." && !candidate.startsWith(`..${sep2}`) && !isAbsolute2(candidate);
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 as basename2, dirname as dirname3, relative as relative5, resolve as resolve2 } from "path";
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 = basename2(sourcePath).match(ARCHIVE_NAME_RE);
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(relative5(archivesDir, dirname3(sourcePath)));
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(content, GROUP_BRIEF_DOCUMENT_SCHEMA);
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(content);
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(relative5(resolve2(archivesDir), resolve2(sourcePath)))}`;
1452
+ return `.rsp/archives/${normalizeLogicalPath(relative4(resolve2(archivesDir), resolve2(sourcePath)))}`;
1544
1453
  }
1545
1454
  function normalizeOutputPath(path, archivesDir) {
1546
- const relativePath = normalizeLogicalPath(relative5(resolve2(archivesDir), resolve2(path)));
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 = getDocumentSectionBody(document, "tasks");
1646
- const verify = getDocumentSectionBody(document, "verify");
1647
- const blockers = getDocumentSectionBody(document, "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(content).length,
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, true)
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(section, omitNone = false) {
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 readFile3(brief.path, "utf-8")))
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 readFile3(ref.path, "utf-8"));
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
- for (const line of extractBlockerLines(content)) {
1837
- if (!line || isEmptyBlockerLine(line))
1838
- continue;
1839
- const dependency = line.match(REQUIRES_RE);
1840
- if (dependency) {
1841
- if (!requires.has(dependency[1]))
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 tasksSection = getDocumentSectionBody(document, "tasks");
1952
- const verifySection = getDocumentSectionBody(document, "verify");
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 verifyCriticality = classifyVerifyCheckboxes(verifySection);
1957
- const verifyLines = collectVerifyCheckboxLines(verifySection);
1958
- const requiredVerifyTodos = getOpenCheckboxes([...verifyLines.required, ...verifyLines.unclassified].join("\n"));
1959
- const optionalVerifyTodos = getOpenCheckboxes(verifyLines.optional.join("\n"));
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(content);
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(content);
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 manage = resolveManagePolicy({});
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
- manage = resolveManagePolicy(configInspection.config, { configValid });
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
- manage = resolveManagePolicy({}, { configValid: false });
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 blockerEntries = [];
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 readFile4(path, "utf-8");
2120
- title = extractChangeTitle(content) ?? name;
2121
- summary = extractChangeSummary(content);
2122
- blockerEntries = extractBlockerEntries(content);
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(content);
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(content);
2147
- const details = collectArchiveReadiness(content, { activeBlockers: isBlocked });
2148
- const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
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
- blockerEntries,
2128
+ externalBlockers,
2221
2129
  readiness
2222
2130
  });
2223
2131
  }
2224
2132
  return {
2225
- manage,
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,