@oevery/rsp 4.0.0 → 4.2.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 (141) hide show
  1. package/dist/chunk-AUZYUJ46.mjs +418 -0
  2. package/dist/chunk-DNRGBQS7.mjs +108 -0
  3. package/dist/chunk-LGDCIQYL.mjs +725 -0
  4. package/dist/chunk-P3KO3GZW.mjs +1023 -0
  5. package/dist/{chunk-HFSTLCCK.mjs → chunk-VDRXFKBP.mjs} +133 -1800
  6. package/dist/cli.mjs +286 -294
  7. package/dist/client-U2PN3VS4.mjs +108 -0
  8. package/dist/{entry-RIJQ2X46.mjs → entry-NDASTAI5.mjs} +13 -9
  9. package/dist/viewer/NOTICE.txt +299 -0
  10. package/dist/viewer/app.css +2 -0
  11. package/dist/viewer/app.js +670 -0
  12. package/dist/viewer/chunks/abnfDiagram-S7VZAUFO-K5EOZOEF.js +1 -0
  13. package/dist/viewer/chunks/architecture-WOLXFQ4H-UAT5D5BF.js +1 -0
  14. package/dist/viewer/chunks/architectureDiagram-YGSQEFNW-UNPSYM2L.js +36 -0
  15. package/dist/viewer/chunks/blockDiagram-APKKGZFW-2F7TTPAV.js +140 -0
  16. package/dist/viewer/chunks/bpmnIcons-APHXDYXF-ZWPFFN3O.js +1 -0
  17. package/dist/viewer/chunks/c4Diagram-KJ2TOQU5-KNLN6CME.js +38 -0
  18. package/dist/viewer/chunks/chunk-274PZN6B.js +1 -0
  19. package/dist/viewer/chunks/chunk-2VGDQLQD.js +1 -0
  20. package/dist/viewer/chunks/chunk-33XESOCS.js +1 -0
  21. package/dist/viewer/chunks/chunk-3ZSL2TGQ.js +1 -0
  22. package/dist/viewer/chunks/chunk-432I4U3B.js +1 -0
  23. package/dist/viewer/chunks/chunk-4PJNGS3V.js +1 -0
  24. package/dist/viewer/chunks/chunk-54NLSLTF.js +9 -0
  25. package/dist/viewer/chunks/chunk-6QRHYJ5Q.js +1 -0
  26. package/dist/viewer/chunks/chunk-6RCFKBVR.js +1 -0
  27. package/dist/viewer/chunks/chunk-6Y2HP7DC.js +213 -0
  28. package/dist/viewer/chunks/chunk-7MAT7NHP.js +1 -0
  29. package/dist/viewer/chunks/chunk-AKHBD6LT.js +2 -0
  30. package/dist/viewer/chunks/chunk-CZIM56BV.js +1 -0
  31. package/dist/viewer/chunks/chunk-DN4Y7LXJ.js +1 -0
  32. package/dist/viewer/chunks/chunk-E2UQN267.js +1 -0
  33. package/dist/viewer/chunks/chunk-GBARLDDT.js +1 -0
  34. package/dist/viewer/chunks/chunk-H4YEV7VR.js +1 -0
  35. package/dist/viewer/chunks/chunk-HUVVDQHZ.js +1 -0
  36. package/dist/viewer/chunks/chunk-IAKC32WV.js +72 -0
  37. package/dist/viewer/chunks/chunk-JTW4SY4L.js +1 -0
  38. package/dist/viewer/chunks/chunk-JZV675LF.js +1 -0
  39. package/dist/viewer/chunks/chunk-KDUDYTWO.js +1 -0
  40. package/dist/viewer/chunks/chunk-KHX3A5RZ.js +331 -0
  41. package/dist/viewer/chunks/chunk-KPXJJSUR.js +15 -0
  42. package/dist/viewer/chunks/chunk-LEGNM6PH.js +1 -0
  43. package/dist/viewer/chunks/chunk-LYY4ZY72.js +1 -0
  44. package/dist/viewer/chunks/chunk-MKDEK2LU.js +1 -0
  45. package/dist/viewer/chunks/chunk-MYJRFART.js +129 -0
  46. package/dist/viewer/chunks/chunk-NUXZBURC.js +1 -0
  47. package/dist/viewer/chunks/chunk-NXHT5I2L.js +1 -0
  48. package/dist/viewer/chunks/chunk-OF62XRVM.js +62 -0
  49. package/dist/viewer/chunks/chunk-ONCOEVBS.js +1 -0
  50. package/dist/viewer/chunks/chunk-P44SH3OO.js +1 -0
  51. package/dist/viewer/chunks/chunk-PDXKDBIX.js +1 -0
  52. package/dist/viewer/chunks/chunk-PHKIAKHS.js +1 -0
  53. package/dist/viewer/chunks/chunk-PJDMAGPE.js +2 -0
  54. package/dist/viewer/chunks/chunk-RC2NPHSX.js +88 -0
  55. package/dist/viewer/chunks/chunk-T6DM5RSK.js +10 -0
  56. package/dist/viewer/chunks/chunk-U5STX66M.js +2 -0
  57. package/dist/viewer/chunks/chunk-UIAJQPOF.js +1 -0
  58. package/dist/viewer/chunks/chunk-UX2S3LP3.js +1 -0
  59. package/dist/viewer/chunks/chunk-VEMTQQ7Y.js +2 -0
  60. package/dist/viewer/chunks/chunk-WKCPBR3C.js +1 -0
  61. package/dist/viewer/chunks/chunk-WMQ4Y2MS.js +32 -0
  62. package/dist/viewer/chunks/chunk-YPWIQAHX.js +3 -0
  63. package/dist/viewer/chunks/chunk-Z4ZDIMO2.js +136 -0
  64. package/dist/viewer/chunks/chunk-ZATQCNXH.js +1 -0
  65. package/dist/viewer/chunks/chunk-ZE5RDJX6.js +1 -0
  66. package/dist/viewer/chunks/chunk-ZO62R4L2.js +1 -0
  67. package/dist/viewer/chunks/classDiagram-v2-L25IHNXF-5F2M2CZ4.js +217 -0
  68. package/dist/viewer/chunks/cose-bilkent-JH36ORCC-S56HJ5WN.js +1 -0
  69. package/dist/viewer/chunks/cynefin-EF2NZ3EQ-6WY2NCLE.js +1 -0
  70. package/dist/viewer/chunks/cynefinDiagram-L36ESFBY-DE6PZNQA.js +62 -0
  71. package/dist/viewer/chunks/dagre-SPEB5RXH-QEOTKFV2.js +4 -0
  72. package/dist/viewer/chunks/diagram-3B7GL73J-RZPIU7FY.js +200 -0
  73. package/dist/viewer/chunks/diagram-DNEMPZML-YD26TJKC.js +24 -0
  74. package/dist/viewer/chunks/diagram-L3MUGCDN-TJUEX4QI.js +41 -0
  75. package/dist/viewer/chunks/diagram-LA4NIPWU-3PUQS6UW.js +24 -0
  76. package/dist/viewer/chunks/diagram-MHCOC4E2-YBTBD4PG.js +3 -0
  77. package/dist/viewer/chunks/diagram-XPFHEONP-VF2TQBNR.js +30 -0
  78. package/dist/viewer/chunks/ebnfDiagram-UZSYDL2S-QE7DIEME.js +1 -0
  79. package/dist/viewer/chunks/elk-XV6XMXQE-X5OZRIL3.js +27 -0
  80. package/dist/viewer/chunks/erDiagram-EYBAPDEH-5B2S7VB4.js +99 -0
  81. package/dist/viewer/chunks/eventmodeling-K75KTNOO-CHCQYZBI.js +1 -0
  82. package/dist/viewer/chunks/flowDiagram-MALGAFMO-XJFC5NTN.js +1 -0
  83. package/dist/viewer/chunks/ganttDiagram-23REXITY-TUUZ3QYO.js +292 -0
  84. package/dist/viewer/chunks/gitGraph-VSP46ZUC-VFZPJQ6A.js +1 -0
  85. package/dist/viewer/chunks/gitGraphDiagram-STJD7YCW-4CVV5LSH.js +106 -0
  86. package/dist/viewer/chunks/info-OHQRW6UA-P4744SJ6.js +1 -0
  87. package/dist/viewer/chunks/infoDiagram-KBXO4MBG-Q2WYYNNN.js +2 -0
  88. package/dist/viewer/chunks/ishikawaDiagram-M35NCXHK-PLQPTX2U.js +70 -0
  89. package/dist/viewer/chunks/journeyDiagram-OPLOXDXO-FB73RE6F.js +139 -0
  90. package/dist/viewer/chunks/kanban-definition-BZKZHSQY-6FU4TXYJ.js +89 -0
  91. package/dist/viewer/chunks/katex-IN4S3RHS.js +257 -0
  92. package/dist/viewer/chunks/mindmap-definition-I2QAS2VK-ZQ36ABVW.js +98 -0
  93. package/dist/viewer/chunks/packet-JDAUHWVQ-NYXIQQVA.js +1 -0
  94. package/dist/viewer/chunks/pegDiagram-FD4XPPHP-5XXVT6XN.js +1 -0
  95. package/dist/viewer/chunks/pie-XZMESJXO-32CUE6KZ.js +1 -0
  96. package/dist/viewer/chunks/pieDiagram-J4AKIIQI-WRTBDFM5.js +39 -0
  97. package/dist/viewer/chunks/quadrantDiagram-XTYBLID2-YPBDQIAK.js +7 -0
  98. package/dist/viewer/chunks/radar-ABXABTNO-272FILRO.js +1 -0
  99. package/dist/viewer/chunks/railroad-I3PHUGI6-6EGAH4XA.js +1 -0
  100. package/dist/viewer/chunks/railroad-abnf-LNEFI6M7-G6E7ATER.js +1 -0
  101. package/dist/viewer/chunks/railroad-ebnf-DWYD2UWJ-MR4P6BDR.js +1 -0
  102. package/dist/viewer/chunks/railroad-peg-JR7BVNR7-WXLQUGUO.js +1 -0
  103. package/dist/viewer/chunks/railroadDiagram-MRQ6NCDL-XHZQ2BLY.js +1 -0
  104. package/dist/viewer/chunks/requirementDiagram-Y7LWUY3S-W5FHRRML.js +84 -0
  105. package/dist/viewer/chunks/sankeyDiagram-WGJFYUXA-OJJIRVTI.js +40 -0
  106. package/dist/viewer/chunks/sequenceDiagram-5NDRBG5M-D6T4DRNG.js +169 -0
  107. package/dist/viewer/chunks/sizeCapture-INFHLROL-WL3FTUJX.js +1 -0
  108. package/dist/viewer/chunks/stateDiagram-v2-FJ4FJNEA-LAXL7KGG.js +291 -0
  109. package/dist/viewer/chunks/swimlanes-UMQ2AFYF-XIJBD726.js +1 -0
  110. package/dist/viewer/chunks/swimlanesDiagram-OMPLPG4O-7O7MHFW7.js +8 -0
  111. package/dist/viewer/chunks/timeline-definition-SEUHKTQE-BAZQA47I.js +120 -0
  112. package/dist/viewer/chunks/treeView-D4JQ5SDB-6YMDAAPA.js +1 -0
  113. package/dist/viewer/chunks/treemap-SAJKECNS-2X573PPS.js +1 -0
  114. package/dist/viewer/chunks/usecaseDiagram-AFA6YXN4-4YCEMWJB.js +370 -0
  115. package/dist/viewer/chunks/vennDiagram-NNMJR3RB-GEMROLAE.js +34 -0
  116. package/dist/viewer/chunks/wardley-7MLQ67FV-DGTGHYYC.js +1 -0
  117. package/dist/viewer/chunks/wardleyDiagram-S7YFUSEH-K4HVD3SS.js +78 -0
  118. package/dist/viewer/chunks/xychartDiagram-GGNKY7IX-HVLWCUEO.js +7 -0
  119. package/dist/viewer/mermaid.js +12 -0
  120. package/dist/viewer-build.json +1 -0
  121. package/dist/viewer-service.mjs +607 -0
  122. package/package.json +27 -8
  123. package/skills/rsp/SKILL.md +13 -10
  124. package/skills/rsp/references/architecture.md +39 -0
  125. package/skills/rsp/references/closeout.md +4 -4
  126. package/skills/rsp/references/durable-review.md +6 -9
  127. package/skills/rsp/references/focus-continuation.md +6 -2
  128. package/skills/rsp/references/setup-repair.md +11 -6
  129. package/skills/rsp/references/work-model.md +14 -4
  130. package/skills/rsp-commit/SKILL.md +2 -2
  131. package/skills/rsp-doc/SKILL.md +13 -10
  132. package/skills/rsp-doc/references/skill-authoring.md +2 -2
  133. package/skills/rsp-implement/SKILL.md +16 -12
  134. package/skills/rsp-review/SKILL.md +12 -12
  135. package/skills/rsp-review/references/code-review.md +11 -5
  136. package/skills/rsp-review/references/document-review.md +8 -10
  137. package/skills/rsp-shape/SKILL.md +3 -1
  138. package/skills/rsp-shape/references/planning.md +4 -4
  139. package/skills/rsp-verify/SKILL.md +9 -14
  140. package/skills/rsp-doc/references/artifact-lenses.md +0 -43
  141. package/skills/rsp-review/references/structural-review.md +0 -41
@@ -0,0 +1,725 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ DEFAULT_DECISION_RECORDS_PATH,
4
+ OBSOLETE_RSP_RULES_PATH,
5
+ RSP_DIR,
6
+ inspectManagedFile,
7
+ inspectManagedFileTree,
8
+ inspectRspConfig,
9
+ markdownText,
10
+ parseFrontmatter,
11
+ parseMarkdown,
12
+ pc,
13
+ resolveDecisionRecordsPath,
14
+ validateDecisionRecordsFilesystemPath
15
+ } from "./chunk-P3KO3GZW.mjs";
16
+
17
+ // src/core/output.ts
18
+ import pc2 from "picocolors";
19
+ function toErrorMessage(error) {
20
+ return error instanceof Error ? error.message : String(error);
21
+ }
22
+ function emitJson(data, options = {}) {
23
+ const indentation = options.compact ? void 0 : 2;
24
+ process.stdout.write(`${JSON.stringify(data, null, indentation)}
25
+ `);
26
+ }
27
+
28
+ // src/core/filesystem.ts
29
+ import { existsSync } from "fs";
30
+ import { readdir, readFile, rmdir } from "fs/promises";
31
+ import { basename, dirname, join, relative, sep } from "path";
32
+ async function walkMarkdownFiles(dir, options = {}) {
33
+ const all = await walkFiles(dir, options);
34
+ return all.filter((f) => f.endsWith(".md"));
35
+ }
36
+ async function inspectUnsupportedRules() {
37
+ const rulesDir = join(RSP_DIR, "rules");
38
+ const diagnostics = [];
39
+ const rawEntries = await walkAllEntries(rulesDir, diagnostics);
40
+ const entries = rawEntries.filter((entry) => entry.path !== OBSOLETE_RSP_RULES_PATH).map((entry) => ({ path: normalizeLogicalPath(relative(rulesDir, entry.path)), kind: entry.kind }));
41
+ return {
42
+ entries,
43
+ diagnostics,
44
+ directoryRemaining: entries.length > 0
45
+ };
46
+ }
47
+ async function walkAllEntries(dir, diagnostics) {
48
+ const entries = [];
49
+ try {
50
+ const items = await readdir(dir, { withFileTypes: true });
51
+ for (const item of items) {
52
+ const path = join(dir, item.name);
53
+ const kind = item.isDirectory() ? "directory" : "file";
54
+ entries.push({ path, kind });
55
+ if (kind === "directory")
56
+ entries.push(...await walkAllEntries(path, diagnostics));
57
+ }
58
+ } catch (error) {
59
+ if (error.code !== "ENOENT") {
60
+ diagnostics.push({ code: "walk_failed", operation: "readdir", path: dir, message: toErrorMessage(error) });
61
+ }
62
+ }
63
+ return entries;
64
+ }
65
+ async function walkFiles(dir, options = {}) {
66
+ const files = [];
67
+ try {
68
+ const items = await readdir(dir, { withFileTypes: true });
69
+ for (const item of items) {
70
+ if (item.name.startsWith("."))
71
+ continue;
72
+ const filePath = join(dir, item.name);
73
+ if (item.isDirectory())
74
+ files.push(...await walkFiles(filePath, options));
75
+ else
76
+ files.push(filePath);
77
+ }
78
+ } catch (error) {
79
+ if (error.code !== "ENOENT") {
80
+ options.onError?.({ code: "walk_failed", operation: "readdir", path: dir, message: toErrorMessage(error) });
81
+ }
82
+ }
83
+ return files;
84
+ }
85
+ function normalizeLogicalPath(pathValue) {
86
+ return pathValue.split(sep).join("/").replace(/\\/g, "/");
87
+ }
88
+ var SPEC_NAME_RE = /^[a-z0-9-]+(?:\/[a-z0-9-]+)*$/;
89
+ function isValidSpecName(name) {
90
+ return SPEC_NAME_RE.test(name);
91
+ }
92
+ function inspectRspInitialization() {
93
+ const designPath = join(RSP_DIR, "specs", "design.md");
94
+ const design = inspectManagedFile(designPath, "design Spec", { allowMissing: true });
95
+ if (design.issue)
96
+ return `${design.issue.message}
97
+ Check path types and permissions; init/update do not repair existing user content.`;
98
+ if (design.exists)
99
+ return void 0;
100
+ return existsSync(RSP_DIR) ? `${designPath} is missing.
101
+ Restore your design file, or run rsp init to create missing scaffolding.` : "RSP is not initialized in this project\n Run: rsp init";
102
+ }
103
+ function guardRspInitialized() {
104
+ const issue = inspectRspInitialization();
105
+ if (issue) {
106
+ console.error(` ${pc.red("Error:")} ${issue}`);
107
+ process.exit(1);
108
+ }
109
+ }
110
+ async function cleanupEmptyParentDirs(path, stopDir) {
111
+ let parent = dirname(path);
112
+ while (parent !== stopDir) {
113
+ try {
114
+ const remaining = await readdir(parent);
115
+ if (remaining.length === 0)
116
+ await rmdir(parent);
117
+ else
118
+ break;
119
+ } catch {
120
+ break;
121
+ }
122
+ parent = dirname(parent);
123
+ }
124
+ }
125
+ async function detectProjectName() {
126
+ try {
127
+ if (existsSync("package.json")) {
128
+ const raw = await readFile("package.json", "utf-8");
129
+ const pkg = JSON.parse(raw);
130
+ if (pkg.name && pkg.name !== "")
131
+ return pkg.name;
132
+ }
133
+ } catch {
134
+ }
135
+ return basename(process.cwd());
136
+ }
137
+
138
+ // src/specs/model.ts
139
+ var SPECS_DEFAULT_RESULT_LIMIT = 20;
140
+ var SPECS_MAX_RESULT_LIMIT = 100;
141
+ var SPECS_MAX_CANDIDATES = 1e3;
142
+ var SPECS_MAX_FILE_BYTES = 512 * 1024;
143
+ var SPECS_SEARCH_EXCERPT_CODE_POINTS = 240;
144
+ var SPECS_DETAIL_CONTENT_CODE_POINTS = 12e3;
145
+ var SPECS_MAX_DIAGNOSTICS = 20;
146
+
147
+ // src/specs/query.ts
148
+ import { Buffer } from "buffer";
149
+ import { spawnSync } from "child_process";
150
+ import { constants } from "fs";
151
+ import { lstat, open, realpath } from "fs/promises";
152
+ import { basename as basename2, dirname as dirname2, isAbsolute, join as join2, relative as relative2, resolve, sep as sep2 } from "path";
153
+ var SPECS_ROOT = join2(RSP_DIR, "specs");
154
+ var GENERATED_INDEX_FILENAME = "00-index.md";
155
+ var LEGACY_GENERATED_INDEX_FILENAME = "INDEX.md";
156
+ var READ_NO_FOLLOW_FLAGS = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0);
157
+ var SpecsQueryError = class extends Error {
158
+ constructor(code, message) {
159
+ super(message);
160
+ this.code = code;
161
+ this.name = "SpecsQueryError";
162
+ }
163
+ code;
164
+ };
165
+ async function inspectSpecs(options = {}) {
166
+ const projectRoot = await realpath(resolve(options.cwd ?? process.cwd()));
167
+ const specsRoot = resolve(projectRoot, SPECS_ROOT);
168
+ const maxCandidates = options.maxCandidates ?? SPECS_MAX_CANDIDATES;
169
+ const maxFileBytes = options.maxFileBytes ?? SPECS_MAX_FILE_BYTES;
170
+ validatePositiveBound(maxCandidates, "maxCandidates");
171
+ validatePositiveBound(maxFileBytes, "maxFileBytes");
172
+ const configInspection = await inspectRspConfig(projectRoot);
173
+ if (configInspection.issues.length > 0)
174
+ throw new SpecsQueryError("invalid_config", configInspection.issues.join("; "));
175
+ const decisionRecordsPath = resolveDecisionRecordsPath(configInspection.config);
176
+ const decisionPathIssue = await validateDecisionRecordsFilesystemPath(decisionRecordsPath, projectRoot);
177
+ if (decisionPathIssue)
178
+ throw new SpecsQueryError("invalid_decision_records_path", decisionPathIssue);
179
+ const decisionRecordsRoot = resolve(projectRoot, decisionRecordsPath);
180
+ const accumulator = {
181
+ documents: [],
182
+ generatedIndexes: [],
183
+ diagnostics: [],
184
+ candidateCount: 0,
185
+ candidateLimitReported: false
186
+ };
187
+ const excludedSpecDirectories = [DEFAULT_DECISION_RECORDS_PATH, decisionRecordsPath].map((path) => resolve(projectRoot, path)).filter((path) => isInside(specsRoot, path));
188
+ await inspectDocumentRoot({
189
+ projectRoot,
190
+ root: specsRoot,
191
+ label: "Specs",
192
+ kind: "spec",
193
+ excludedDirectories: excludedSpecDirectories,
194
+ accumulator,
195
+ maxCandidates,
196
+ maxFileBytes,
197
+ allowMissing: false
198
+ });
199
+ await inspectDocumentRoot({
200
+ projectRoot,
201
+ root: decisionRecordsRoot,
202
+ label: "Decision Records",
203
+ kind: "decision-record",
204
+ excludedDirectories: [],
205
+ accumulator,
206
+ maxCandidates,
207
+ maxFileBytes,
208
+ allowMissing: options.allowMissingDecisionRecords === true
209
+ });
210
+ detectPathCollisions(accumulator);
211
+ accumulator.documents.sort(compareDocument);
212
+ accumulator.generatedIndexes.sort((left, right) => left.path.localeCompare(right.path));
213
+ const boundedDiagnostics = accumulator.diagnostics.slice(0, SPECS_MAX_DIAGNOSTICS);
214
+ return {
215
+ source: await inspectSourceIdentity(projectRoot),
216
+ roots: {
217
+ specs: toProjectPath(projectRoot, specsRoot),
218
+ decisions: toProjectPath(projectRoot, decisionRecordsRoot)
219
+ },
220
+ tree: buildDirectoryTree(projectRoot, specsRoot, "spec", accumulator.documents),
221
+ decisionRecords: buildDirectoryTree(projectRoot, decisionRecordsRoot, "decision-record", accumulator.documents),
222
+ documents: accumulator.documents.map(stripSource),
223
+ generatedIndexes: accumulator.generatedIndexes,
224
+ diagnostics: boundedDiagnostics,
225
+ diagnosticSummary: {
226
+ total: accumulator.diagnostics.length,
227
+ returned: boundedDiagnostics.length,
228
+ hasMore: accumulator.diagnostics.length > boundedDiagnostics.length
229
+ },
230
+ runtime: [],
231
+ limits: {
232
+ candidates: maxCandidates,
233
+ fileBytes: maxFileBytes,
234
+ results: SPECS_MAX_RESULT_LIMIT,
235
+ searchExcerptCodePoints: SPECS_SEARCH_EXCERPT_CODE_POINTS,
236
+ detailContentCodePoints: SPECS_DETAIL_CONTENT_CODE_POINTS
237
+ }
238
+ };
239
+ }
240
+ function specsInspectionComplete(inspection) {
241
+ return inspection.diagnostics.every((diagnostic) => diagnostic.severity !== "error");
242
+ }
243
+ async function readSpecsDetail(inspection, projectPath) {
244
+ const current = await readCurrentSpecsDocument(inspection, projectPath);
245
+ const bounded = boundCodePoints(current.content, SPECS_DETAIL_CONTENT_CODE_POINTS);
246
+ return {
247
+ ...current,
248
+ content: bounded.value,
249
+ contentTruncated: bounded.truncated
250
+ };
251
+ }
252
+ async function readCurrentSpecsDocument(inspection, projectPath) {
253
+ const normalized = normalizeRequestedPath(projectPath);
254
+ const document = inspection.documents.find((candidate) => candidate.path === normalized);
255
+ if (!document)
256
+ throw new SpecsQueryError("specs_document_not_found", `Specs document not found: ${projectPath}`);
257
+ const sourcePath = resolve(inspection.source.root, normalized);
258
+ const current = await readCurrentDocument(
259
+ sourcePath,
260
+ inspection.source.root,
261
+ document.path,
262
+ inspection.limits.fileBytes
263
+ );
264
+ const currentDocument = parseDocumentRecord(parseMarkdown(current.content), current.bytes, document.path, document.kind);
265
+ return {
266
+ ...currentDocument,
267
+ content: current.content
268
+ };
269
+ }
270
+ async function searchSpecs(inspection, literal, options = {}) {
271
+ return (await searchSpecsWithSources(inspection, literal, options)).result;
272
+ }
273
+ async function searchSpecsWithSources(inspection, literal, options = {}) {
274
+ const query = literal.trim();
275
+ if (query === "")
276
+ throw new SpecsQueryError("invalid_specs_search", "Specs search literal must be non-empty");
277
+ const limit = options.limit ?? 20;
278
+ if (!Number.isInteger(limit) || limit < 1 || limit > SPECS_MAX_RESULT_LIMIT)
279
+ throw new SpecsQueryError("invalid_specs_limit", `Specs search limit must be an integer from 1 through ${SPECS_MAX_RESULT_LIMIT}`);
280
+ const excerptCodePoints = options.excerptCodePoints ?? SPECS_SEARCH_EXCERPT_CODE_POINTS;
281
+ if (!Number.isInteger(excerptCodePoints) || excerptCodePoints < 40 || excerptCodePoints > 1e3)
282
+ throw new SpecsQueryError("invalid_specs_excerpt", "Specs search excerpt must be an integer from 40 through 1000 code points");
283
+ const lowered = query.toLowerCase();
284
+ const matches = [];
285
+ const documents = /* @__PURE__ */ new Map();
286
+ let matched = 0;
287
+ for (const document of inspection.documents) {
288
+ const current = await readCurrentDocument(
289
+ resolve(inspection.source.root, document.path),
290
+ inspection.source.root,
291
+ document.path,
292
+ inspection.limits.fileBytes
293
+ );
294
+ const currentDocument = parseDocumentRecord(parseMarkdown(current.content), current.bytes, document.path, document.kind);
295
+ const lines = current.content.split(/\r?\n/);
296
+ const headingsByLine = new Map(currentDocument.headings.map((item) => [item.line, item.title]));
297
+ let heading = null;
298
+ for (let index = 0; index < lines.length; index++) {
299
+ heading = headingsByLine.get(index + 1) ?? heading;
300
+ const position = lines[index].toLowerCase().indexOf(lowered);
301
+ if (position < 0)
302
+ continue;
303
+ matched += 1;
304
+ if (matches.length < limit) {
305
+ if (!documents.has(document.path)) {
306
+ documents.set(document.path, {
307
+ ...currentDocument,
308
+ content: current.content
309
+ });
310
+ }
311
+ matches.push({
312
+ path: document.path,
313
+ kind: document.kind,
314
+ title: currentDocument.title,
315
+ heading,
316
+ line: index + 1,
317
+ excerpt: excerptAround(
318
+ lines[index],
319
+ [...lines[index].slice(0, position)].length,
320
+ [...query].length,
321
+ excerptCodePoints
322
+ )
323
+ });
324
+ }
325
+ }
326
+ }
327
+ return {
328
+ result: {
329
+ matches,
330
+ summary: {
331
+ candidates: inspection.documents.length,
332
+ searched: inspection.documents.length,
333
+ matched,
334
+ returned: matches.length,
335
+ hasMore: matched > matches.length
336
+ }
337
+ },
338
+ documents
339
+ };
340
+ }
341
+ async function inspectDocumentRoot(options) {
342
+ const inspection = await inspectManagedFileTree(options.root, options.label, { allowMissing: options.allowMissing });
343
+ for (const issue of inspection.issues) {
344
+ options.accumulator.diagnostics.push({
345
+ severity: "error",
346
+ code: "specs_tree_invalid",
347
+ path: toProjectPath(options.projectRoot, issue.path),
348
+ message: issue.message
349
+ });
350
+ }
351
+ if (!inspection.rootExists)
352
+ return;
353
+ for (const path of inspection.files) {
354
+ if (options.excludedDirectories.some((directory) => isInside(directory, path)))
355
+ continue;
356
+ const projectPath = toProjectPath(options.projectRoot, path);
357
+ const reserved = reservedIndexKind(options.root, path);
358
+ if (reserved) {
359
+ await classifyGeneratedIndex(path, options.projectRoot, projectPath, options.maxFileBytes, options.accumulator);
360
+ continue;
361
+ }
362
+ if (!path.endsWith(".md"))
363
+ continue;
364
+ options.accumulator.candidateCount += 1;
365
+ if (options.accumulator.candidateCount > options.maxCandidates) {
366
+ if (!options.accumulator.candidateLimitReported) {
367
+ options.accumulator.diagnostics.push({
368
+ severity: "error",
369
+ code: "specs_candidate_limit_exceeded",
370
+ path: projectPath,
371
+ message: `Specs candidate limit exceeded (${options.maxCandidates})`
372
+ });
373
+ options.accumulator.candidateLimitReported = true;
374
+ }
375
+ continue;
376
+ }
377
+ await inspectDocument(path, options.projectRoot, projectPath, options.kind, options.maxFileBytes, options.accumulator);
378
+ }
379
+ }
380
+ async function classifyGeneratedIndex(path, projectRoot, projectPath, maxFileBytes, accumulator) {
381
+ try {
382
+ const current = await readCurrentDocument(path, projectRoot, projectPath, maxFileBytes);
383
+ const recognized = isRecognizedGeneratedSpecsIndex(current.content, projectPath);
384
+ accumulator.generatedIndexes.push({
385
+ path: projectPath,
386
+ classification: recognized ? "safe-removal" : "owner-controlled",
387
+ reason: recognized ? "metadata recognizes the reserved path as a generated Specs index" : "reserved path content is not recognized generated Specs-index output"
388
+ });
389
+ if (!recognized) {
390
+ accumulator.diagnostics.push({
391
+ severity: "error",
392
+ code: "unrecognized_reserved_specs_index",
393
+ path: projectPath,
394
+ message: `reserved Specs index is owner-controlled and cannot be treated as generated content: ${projectPath}`
395
+ });
396
+ }
397
+ } catch (error) {
398
+ accumulator.generatedIndexes.push({
399
+ path: projectPath,
400
+ classification: "owner-controlled",
401
+ reason: "reserved path could not be boundedly inspected as generated Specs-index output"
402
+ });
403
+ accumulator.diagnostics.push({
404
+ severity: "error",
405
+ code: error instanceof SpecsQueryError ? error.code : "specs_index_read_failed",
406
+ path: projectPath,
407
+ message: `unable to inspect reserved Specs index ${projectPath}: ${error instanceof Error ? error.message : String(error)}`
408
+ });
409
+ }
410
+ }
411
+ function isRecognizedGeneratedSpecsIndex(content, projectPath) {
412
+ const metadata = parseFrontmatter(content);
413
+ const expectedSourceDirectory = normalizeLogicalPath(dirname2(projectPath));
414
+ return metadata?.kind === "generated-index" && metadata.index_type === "specs" && metadata.source_dir === expectedSourceDirectory;
415
+ }
416
+ async function inspectDocument(path, projectRoot, projectPath, kind, maxFileBytes, accumulator) {
417
+ try {
418
+ const current = await readCurrentDocument(path, projectRoot, projectPath, maxFileBytes);
419
+ const document = parseMarkdown(current.content);
420
+ const metadata = parseFrontmatter(document);
421
+ if (metadata?.kind === "generated-index")
422
+ return;
423
+ accumulator.documents.push({
424
+ sourcePath: path,
425
+ ...parseDocumentRecord(document, current.bytes, projectPath, kind, metadata)
426
+ });
427
+ } catch (error) {
428
+ accumulator.diagnostics.push({
429
+ severity: "error",
430
+ code: error instanceof SpecsQueryError ? error.code : "specs_file_read_failed",
431
+ path: projectPath,
432
+ message: `unable to read Specs document ${projectPath}: ${error instanceof Error ? error.message : String(error)}`
433
+ });
434
+ }
435
+ }
436
+ function detectPathCollisions(accumulator) {
437
+ const seen = /* @__PURE__ */ new Map();
438
+ for (const document of accumulator.documents) {
439
+ const canonical = document.path.normalize("NFC");
440
+ const existing = seen.get(canonical);
441
+ if (existing && existing !== document.path) {
442
+ accumulator.diagnostics.push({
443
+ severity: "error",
444
+ code: "specs_path_collision",
445
+ path: document.path,
446
+ message: `Specs path normalization collision: ${existing} and ${document.path}`
447
+ });
448
+ } else {
449
+ seen.set(canonical, document.path);
450
+ }
451
+ if (canonical !== document.path) {
452
+ accumulator.diagnostics.push({
453
+ severity: "error",
454
+ code: "specs_path_not_canonical",
455
+ path: document.path,
456
+ message: `Specs path must use canonical NFC form: ${document.path}`
457
+ });
458
+ }
459
+ }
460
+ }
461
+ function buildDirectoryTree(projectRoot, root, kind, documents) {
462
+ const rootPath = toProjectPath(projectRoot, root);
463
+ const tree = {
464
+ name: basename2(root),
465
+ path: rootPath,
466
+ directories: [],
467
+ documents: []
468
+ };
469
+ for (const document of documents.filter((candidate) => candidate.kind === kind)) {
470
+ const rel = relative2(root, document.sourcePath);
471
+ if (rel === ".." || rel.startsWith(`..${sep2}`) || isAbsolute(rel))
472
+ continue;
473
+ const segments = normalizeLogicalPath(rel).split("/");
474
+ let current = tree;
475
+ for (const segment of segments.slice(0, -1)) {
476
+ let child = current.directories.find((directory) => directory.name === segment);
477
+ if (!child) {
478
+ child = {
479
+ name: segment,
480
+ path: `${current.path}/${segment}`,
481
+ directories: [],
482
+ documents: []
483
+ };
484
+ current.directories.push(child);
485
+ current.directories.sort((left, right) => left.name.localeCompare(right.name));
486
+ }
487
+ current = child;
488
+ }
489
+ current.documents.push(stripSource(document));
490
+ current.documents.sort(compareDocument);
491
+ }
492
+ return tree;
493
+ }
494
+ function extractHeadings(document) {
495
+ return document.root.children.flatMap((node) => node.type === "heading" ? [{ depth: node.depth, title: markdownText(node), line: node.position.start.line }] : []);
496
+ }
497
+ function extractSummary(document, frontmatterSummary) {
498
+ if (typeof frontmatterSummary === "string" && frontmatterSummary.trim() !== "")
499
+ return frontmatterSummary.trim();
500
+ for (const node of document.root.children) {
501
+ if (node.type !== "paragraph" && node.type !== "list")
502
+ continue;
503
+ const text = markdownText(node).split("\n", 1)[0].trim();
504
+ if (text)
505
+ return text;
506
+ }
507
+ return null;
508
+ }
509
+ async function inspectSourceIdentity(projectRoot) {
510
+ const root = normalizeLogicalPath(projectRoot);
511
+ const head = runGit(["rev-parse", "HEAD"], projectRoot);
512
+ const branch = runGit(["symbolic-ref", "--quiet", "--short", "HEAD"], projectRoot);
513
+ const status = spawnSync("git", ["status", "--porcelain=v1", "--untracked-files=normal"], {
514
+ cwd: root,
515
+ encoding: "utf-8",
516
+ maxBuffer: 256 * 1024
517
+ });
518
+ return {
519
+ root,
520
+ gitHead: head,
521
+ gitBranch: branch,
522
+ dirty: status.status === 0 ? status.stdout.trim() !== "" : status.error && status.error.code === "ENOBUFS" ? true : null
523
+ };
524
+ }
525
+ function runGit(args, cwd) {
526
+ const result = spawnSync("git", args, { cwd, encoding: "utf-8", maxBuffer: 64 * 1024 });
527
+ return result.status === 0 ? result.stdout.trim() || null : null;
528
+ }
529
+ function normalizeRequestedPath(pathValue) {
530
+ const trimmed = pathValue.trim();
531
+ if (trimmed === "")
532
+ throw new SpecsQueryError("invalid_specs_path", "Specs detail path must be non-empty");
533
+ const normalized = normalizeLogicalPath(trimmed.replace(/^\.\//, ""));
534
+ if (isAbsolute(trimmed) || normalized === ".." || normalized.startsWith("../"))
535
+ throw new SpecsQueryError("invalid_specs_path", "Specs detail path must be one project-relative path returned by rsp specs");
536
+ return normalized;
537
+ }
538
+ function reservedIndexKind(root, path) {
539
+ if (basename2(path) === GENERATED_INDEX_FILENAME)
540
+ return "current";
541
+ if (path === join2(root, LEGACY_GENERATED_INDEX_FILENAME))
542
+ return "legacy";
543
+ return null;
544
+ }
545
+ function excerptAround(line, matchStart, matchLength, limit) {
546
+ const codePoints = [...line];
547
+ if (codePoints.length <= limit)
548
+ return codePoints.join("").trim();
549
+ const before = Math.max(0, matchStart - Math.floor((limit - matchLength) / 2));
550
+ const value = codePoints.slice(before, before + limit).join("").trim();
551
+ return `${before > 0 ? "\u2026" : ""}${value}${before + limit < codePoints.length ? "\u2026" : ""}`;
552
+ }
553
+ async function readCurrentDocument(path, root, projectPath, maxFileBytes, binary = false) {
554
+ const changed = () => new SpecsQueryError("specs_file_changed", `Specs document is no longer a readable regular file: ${projectPath}`);
555
+ const sourcePath = resolve(path);
556
+ const sourceRoot = resolve(root);
557
+ const relativePath = relative2(sourceRoot, sourcePath);
558
+ if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${sep2}`) || isAbsolute(relativePath)) {
559
+ throw changed();
560
+ }
561
+ const inspection = inspectManagedFile(sourcePath, "Specs document");
562
+ if (inspection.issue)
563
+ throw changed();
564
+ let inspectedRoot;
565
+ let inspected;
566
+ let realRoot;
567
+ try {
568
+ inspectedRoot = await lstat(sourceRoot, { bigint: true });
569
+ inspected = await lstat(sourcePath, { bigint: true });
570
+ realRoot = await realpath(sourceRoot);
571
+ const resolvedPath = await realpath(sourcePath);
572
+ if (!inspectedRoot.isDirectory() || !inspected.isFile() || resolvedPath !== resolve(realRoot, relativePath) || !isContained(realRoot, resolvedPath)) {
573
+ throw changed();
574
+ }
575
+ } catch (error) {
576
+ if (error instanceof SpecsQueryError)
577
+ throw error;
578
+ throw changed();
579
+ }
580
+ let handle;
581
+ try {
582
+ handle = await open(sourcePath, READ_NO_FOLLOW_FLAGS);
583
+ } catch {
584
+ throw changed();
585
+ }
586
+ try {
587
+ const opened = await handle.stat({ bigint: true });
588
+ const currentRoot = await realpath(sourceRoot);
589
+ const currentPath = await realpath(sourcePath);
590
+ if (!opened.isFile() || currentRoot !== realRoot || currentPath !== resolve(currentRoot, relativePath) || !isContained(currentRoot, currentPath) || !sameIdentity(inspected, opened) || !sameSnapshot(inspected, opened)) {
591
+ throw changed();
592
+ }
593
+ if (opened.size > BigInt(maxFileBytes))
594
+ throw new SpecsQueryError("specs_file_too_large", `Specs document exceeds the ${maxFileBytes}-byte inspection limit: ${projectPath}`);
595
+ const current = await readBoundedDocument(handle, opened, projectPath, maxFileBytes, binary);
596
+ await validateCurrentSpecsPath({
597
+ sourcePath,
598
+ sourceRoot,
599
+ relativePath,
600
+ realRoot,
601
+ inspectedRoot,
602
+ finalHandle: current.final,
603
+ projectPath
604
+ });
605
+ return {
606
+ content: current.content,
607
+ bytes: current.bytes,
608
+ ...current.data ? { data: current.data } : {}
609
+ };
610
+ } catch (error) {
611
+ if (error instanceof SpecsQueryError)
612
+ throw error;
613
+ throw changed();
614
+ } finally {
615
+ await handle.close();
616
+ }
617
+ }
618
+ async function readBoundedDocument(handle, opened, projectPath, maxFileBytes, binary) {
619
+ const buffer = Buffer.allocUnsafe(maxFileBytes + 1);
620
+ let bytes = 0;
621
+ while (bytes < buffer.length) {
622
+ const result = await handle.read(buffer, bytes, buffer.length - bytes, bytes);
623
+ if (result.bytesRead === 0)
624
+ break;
625
+ bytes += result.bytesRead;
626
+ }
627
+ const final = await handle.stat({ bigint: true });
628
+ if (bytes > maxFileBytes || final.size > BigInt(maxFileBytes))
629
+ throw new SpecsQueryError("specs_file_too_large", `Specs document exceeds the ${maxFileBytes}-byte inspection limit: ${projectPath}`);
630
+ if (!sameIdentity(opened, final) || !sameSnapshot(opened, final) || final.size !== BigInt(bytes)) {
631
+ throw new SpecsQueryError("specs_file_changed", `Specs document is no longer a readable regular file: ${projectPath}`);
632
+ }
633
+ return {
634
+ content: binary ? "" : buffer.subarray(0, bytes).toString("utf-8"),
635
+ ...binary ? { data: Buffer.from(buffer.subarray(0, bytes)) } : {},
636
+ bytes,
637
+ final
638
+ };
639
+ }
640
+ async function validateCurrentSpecsPath(options) {
641
+ const changed = () => new SpecsQueryError("specs_file_changed", `Specs document is no longer a readable regular file: ${options.projectPath}`);
642
+ try {
643
+ const currentRoot = await lstat(options.sourceRoot, { bigint: true });
644
+ const currentPath = await lstat(options.sourcePath, { bigint: true });
645
+ const realCurrentRoot = await realpath(options.sourceRoot);
646
+ const realCurrentPath = await realpath(options.sourcePath);
647
+ if (!currentRoot.isDirectory() || !currentPath.isFile() || !sameIdentity(options.inspectedRoot, currentRoot) || realCurrentRoot !== options.realRoot || realCurrentPath !== resolve(realCurrentRoot, options.relativePath) || !isContained(realCurrentRoot, realCurrentPath) || !sameIdentity(currentPath, options.finalHandle) || !sameSnapshot(currentPath, options.finalHandle)) {
648
+ throw changed();
649
+ }
650
+ } catch (error) {
651
+ if (error instanceof SpecsQueryError)
652
+ throw error;
653
+ throw changed();
654
+ }
655
+ }
656
+ function sameIdentity(left, right) {
657
+ return left.dev === right.dev && left.ino === right.ino;
658
+ }
659
+ function sameSnapshot(left, right) {
660
+ return left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
661
+ }
662
+ function isContained(root, path) {
663
+ const candidate = relative2(root, path);
664
+ return candidate !== "" && candidate !== ".." && !candidate.startsWith(`..${sep2}`) && !isAbsolute(candidate);
665
+ }
666
+ function boundCodePoints(value, limit) {
667
+ const codePoints = [...value];
668
+ return {
669
+ value: codePoints.slice(0, limit).join(""),
670
+ truncated: codePoints.length > limit
671
+ };
672
+ }
673
+ function stripSource(document) {
674
+ const { sourcePath: _sourcePath, ...record } = document;
675
+ return record;
676
+ }
677
+ function parseDocumentRecord(document, bytes, path, kind, metadata = parseFrontmatter(document)) {
678
+ const headings = extractHeadings(document);
679
+ return {
680
+ path,
681
+ kind,
682
+ title: typeof metadata?.title === "string" && metadata.title.trim() !== "" ? metadata.title.trim() : headings.find((heading) => heading.depth === 1)?.title ?? basename2(path, ".md"),
683
+ summary: extractSummary(document, metadata?.summary),
684
+ bytes,
685
+ headings
686
+ };
687
+ }
688
+ function compareDocument(left, right) {
689
+ return left.path.localeCompare(right.path);
690
+ }
691
+ function validatePositiveBound(value, label) {
692
+ if (!Number.isInteger(value) || value < 1)
693
+ throw new SpecsQueryError("invalid_specs_bound", `${label} must be a positive integer`);
694
+ }
695
+ function isInside(directory, path) {
696
+ const rel = relative2(directory, path);
697
+ return rel === "" || !isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep2}`);
698
+ }
699
+ function toProjectPath(projectRoot, path) {
700
+ return normalizeLogicalPath(relative2(projectRoot, path)) || ".";
701
+ }
702
+
703
+ export {
704
+ toErrorMessage,
705
+ emitJson,
706
+ walkMarkdownFiles,
707
+ inspectUnsupportedRules,
708
+ normalizeLogicalPath,
709
+ isValidSpecName,
710
+ inspectRspInitialization,
711
+ guardRspInitialized,
712
+ cleanupEmptyParentDirs,
713
+ detectProjectName,
714
+ SPECS_DEFAULT_RESULT_LIMIT,
715
+ SPECS_MAX_RESULT_LIMIT,
716
+ SPECS_SEARCH_EXCERPT_CODE_POINTS,
717
+ SPECS_DETAIL_CONTENT_CODE_POINTS,
718
+ SpecsQueryError,
719
+ inspectSpecs,
720
+ specsInspectionComplete,
721
+ readSpecsDetail,
722
+ searchSpecs,
723
+ isRecognizedGeneratedSpecsIndex,
724
+ readCurrentDocument
725
+ };