@doxbrix/doxloop 0.1.5 → 0.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 (357) hide show
  1. package/CHANGELOG.md +1145 -0
  2. package/README.md +422 -227
  3. package/assets/doxbrix-preview.css +77 -15
  4. package/contracts/agent-events-v1.schema.json +1 -0
  5. package/contracts/coverage-v1.schema.json +59 -0
  6. package/contracts/drift-v1.schema.json +1 -0
  7. package/contracts/evaluation-v1.schema.json +62 -0
  8. package/contracts/quality-codes-v1.json +1 -0
  9. package/contracts/quality-report-v1.schema.json +1 -0
  10. package/contracts/validation-v1.schema.json +1 -0
  11. package/dist/agent-failure.d.ts +36 -0
  12. package/dist/agent-failure.js +98 -0
  13. package/dist/agent-isolation.d.ts +17 -0
  14. package/dist/agent-isolation.js +94 -0
  15. package/dist/agent-log.d.ts +197 -0
  16. package/dist/agent-log.js +1094 -0
  17. package/dist/agent-process.d.ts +63 -0
  18. package/dist/agent-process.js +128 -0
  19. package/dist/agent-reply.d.ts +47 -0
  20. package/dist/agent-reply.js +315 -0
  21. package/dist/agents.d.ts +24 -5
  22. package/dist/agents.js +60 -9
  23. package/dist/api-coverage.d.ts +11 -0
  24. package/dist/api-coverage.js +72 -0
  25. package/dist/application-probe.d.ts +28 -0
  26. package/dist/application-probe.js +93 -0
  27. package/dist/application-url.d.ts +14 -0
  28. package/dist/application-url.js +26 -0
  29. package/dist/args.js +15 -0
  30. package/dist/artifact-deploy.d.ts +9 -0
  31. package/dist/artifact-deploy.js +28 -8
  32. package/dist/assets.d.ts +72 -0
  33. package/dist/assets.js +382 -0
  34. package/dist/auth.d.ts +10 -0
  35. package/dist/auth.js +38 -9
  36. package/dist/author.d.ts +206 -4
  37. package/dist/author.js +1650 -52
  38. package/dist/authoring-batches.d.ts +190 -0
  39. package/dist/authoring-batches.js +489 -0
  40. package/dist/authoring-postpass.d.ts +79 -0
  41. package/dist/authoring-postpass.js +1289 -0
  42. package/dist/authoring-progress.d.ts +95 -0
  43. package/dist/authoring-progress.js +269 -0
  44. package/dist/autosync.d.ts +56 -0
  45. package/dist/autosync.js +442 -0
  46. package/dist/batch-artifacts.d.ts +81 -0
  47. package/dist/batch-artifacts.js +412 -0
  48. package/dist/batch-limits.d.ts +37 -0
  49. package/dist/batch-limits.js +80 -0
  50. package/dist/branding.d.ts +41 -0
  51. package/dist/branding.js +229 -0
  52. package/dist/bundle-upload.d.ts +26 -0
  53. package/dist/bundle-upload.js +99 -0
  54. package/dist/capture-auth.d.ts +95 -0
  55. package/dist/capture-auth.js +204 -0
  56. package/dist/capture.d.ts +48 -0
  57. package/dist/capture.js +20 -1
  58. package/dist/cli.js +839 -32
  59. package/dist/content-links.d.ts +10 -0
  60. package/dist/content-links.js +49 -0
  61. package/dist/contract-validation.d.ts +4 -0
  62. package/dist/contract-validation.js +25 -0
  63. package/dist/coverage-actions.d.ts +8 -0
  64. package/dist/coverage-actions.js +71 -0
  65. package/dist/coverage-resolutions.d.ts +16 -0
  66. package/dist/coverage-resolutions.js +58 -0
  67. package/dist/db.d.ts +37 -0
  68. package/dist/db.js +288 -0
  69. package/dist/demo.d.ts +12 -0
  70. package/dist/demo.js +122 -0
  71. package/dist/deploy-credentials.d.ts +7 -0
  72. package/dist/deploy-credentials.js +76 -0
  73. package/dist/deploy-targets/github-pages.d.ts +3 -0
  74. package/dist/deploy-targets/github-pages.js +100 -0
  75. package/dist/deploy-targets/index.d.ts +8 -0
  76. package/dist/deploy-targets/index.js +78 -0
  77. package/dist/deploy-targets/netlify.d.ts +3 -0
  78. package/dist/deploy-targets/netlify.js +49 -0
  79. package/dist/deploy-targets/types.d.ts +32 -0
  80. package/dist/deploy-targets/types.js +2 -0
  81. package/dist/deploy-targets/vercel.d.ts +3 -0
  82. package/dist/deploy-targets/vercel.js +95 -0
  83. package/dist/deploy.d.ts +9 -0
  84. package/dist/deploy.js +87 -13
  85. package/dist/deterministic-capture.d.ts +90 -0
  86. package/dist/deterministic-capture.js +435 -0
  87. package/dist/direct-edit.d.ts +40 -0
  88. package/dist/direct-edit.js +159 -0
  89. package/dist/docs-crawl.d.ts +86 -0
  90. package/dist/docs-crawl.js +536 -0
  91. package/dist/docs-site.d.ts +49 -0
  92. package/dist/docs-site.js +233 -0
  93. package/dist/doctor.js +8 -0
  94. package/dist/documentation-collections.d.ts +23 -0
  95. package/dist/documentation-collections.js +191 -0
  96. package/dist/documentation-plan.d.ts +253 -0
  97. package/dist/documentation-plan.js +2563 -0
  98. package/dist/doxbrix-build.d.ts +19 -0
  99. package/dist/doxbrix-build.js +167 -0
  100. package/dist/doxbrix-markdown.d.ts +9 -0
  101. package/dist/doxbrix-markdown.js +115 -14
  102. package/dist/drift.d.ts +10 -0
  103. package/dist/drift.js +164 -0
  104. package/dist/evaluation.d.ts +46 -0
  105. package/dist/evaluation.js +113 -0
  106. package/dist/evidence-pack.d.ts +47 -0
  107. package/dist/evidence-pack.js +358 -0
  108. package/dist/evidence.d.ts +25 -0
  109. package/dist/evidence.js +175 -0
  110. package/dist/fs.d.ts +8 -2
  111. package/dist/fs.js +41 -11
  112. package/dist/generator-api.d.ts +89 -0
  113. package/dist/generator-preflight.d.ts +27 -0
  114. package/dist/generator-preflight.js +105 -0
  115. package/dist/generator-runtime.d.ts +7 -0
  116. package/dist/generator-runtime.js +17 -1
  117. package/dist/generators.d.ts +24 -2
  118. package/dist/generators.js +54 -1
  119. package/dist/git-delivery.d.ts +17 -0
  120. package/dist/git-delivery.js +123 -0
  121. package/dist/globs.d.ts +16 -0
  122. package/dist/globs.js +65 -0
  123. package/dist/glossary.d.ts +26 -0
  124. package/dist/glossary.js +179 -0
  125. package/dist/history.d.ts +106 -0
  126. package/dist/history.js +600 -0
  127. package/dist/html-markdown.d.ts +46 -0
  128. package/dist/html-markdown.js +423 -0
  129. package/dist/interactive.js +3 -0
  130. package/dist/job-events.d.ts +74 -0
  131. package/dist/job-events.js +377 -0
  132. package/dist/keep-awake.d.ts +50 -0
  133. package/dist/keep-awake.js +123 -0
  134. package/dist/local-source-snapshot.d.ts +20 -0
  135. package/dist/local-source-snapshot.js +61 -0
  136. package/dist/mintlify-detect.d.ts +3 -0
  137. package/dist/mintlify-detect.js +18 -0
  138. package/dist/mintlify-import.d.ts +75 -0
  139. package/dist/mintlify-import.js +190 -0
  140. package/dist/navigation.d.ts +98 -0
  141. package/dist/navigation.js +310 -0
  142. package/dist/openapi.d.ts +60 -0
  143. package/dist/openapi.js +439 -0
  144. package/dist/page-editor-bridge.d.ts +3 -0
  145. package/dist/page-editor-bridge.js +109 -0
  146. package/dist/page-editor-preview.d.ts +10 -0
  147. package/dist/page-editor-preview.js +55 -0
  148. package/dist/page-extension.d.ts +9 -0
  149. package/dist/page-extension.js +15 -0
  150. package/dist/page-metadata.d.ts +28 -0
  151. package/dist/page-metadata.js +166 -0
  152. package/dist/page-operations.d.ts +34 -0
  153. package/dist/page-operations.js +215 -0
  154. package/dist/page-routes.d.ts +4 -0
  155. package/dist/page-routes.js +61 -0
  156. package/dist/pages.d.ts +20 -0
  157. package/dist/pages.js +184 -0
  158. package/dist/plan-generator.d.ts +3 -0
  159. package/dist/plan-generator.js +21 -0
  160. package/dist/plan-navigation.d.ts +11 -0
  161. package/dist/plan-navigation.js +30 -0
  162. package/dist/planning-captures.d.ts +20 -0
  163. package/dist/planning-captures.js +143 -0
  164. package/dist/planning-research.d.ts +135 -0
  165. package/dist/planning-research.js +472 -0
  166. package/dist/planning-triage.d.ts +23 -0
  167. package/dist/planning-triage.js +131 -0
  168. package/dist/preview.d.ts +24 -0
  169. package/dist/preview.js +280 -29
  170. package/dist/project-detect.d.ts +36 -0
  171. package/dist/project-detect.js +251 -0
  172. package/dist/project-import.d.ts +54 -0
  173. package/dist/project-import.js +157 -0
  174. package/dist/project-lock.d.ts +6 -0
  175. package/dist/project-lock.js +96 -0
  176. package/dist/project-registry.d.ts +25 -0
  177. package/dist/project-registry.js +79 -0
  178. package/dist/project.d.ts +35 -3
  179. package/dist/project.js +388 -38
  180. package/dist/prompts.d.ts +9 -0
  181. package/dist/prompts.js +25 -0
  182. package/dist/proposal-replay.d.ts +32 -0
  183. package/dist/proposal-replay.js +99 -0
  184. package/dist/quality-claims.d.ts +8 -0
  185. package/dist/quality-claims.js +168 -0
  186. package/dist/quality-config.d.ts +5 -0
  187. package/dist/quality-config.js +84 -0
  188. package/dist/quality-contract.d.ts +37 -0
  189. package/dist/quality-contract.js +46 -0
  190. package/dist/quality-examples.d.ts +4 -0
  191. package/dist/quality-examples.js +233 -0
  192. package/dist/quality-gates.d.ts +16 -0
  193. package/dist/quality-gates.js +192 -0
  194. package/dist/quality-links.d.ts +7 -0
  195. package/dist/quality-links.js +149 -0
  196. package/dist/quality-lint.d.ts +6 -0
  197. package/dist/quality-lint.js +124 -0
  198. package/dist/quality-rendered.d.ts +11 -0
  199. package/dist/quality-rendered.js +222 -0
  200. package/dist/quality-schema.d.ts +3 -0
  201. package/dist/quality-schema.js +71 -0
  202. package/dist/release-notes.d.ts +44 -0
  203. package/dist/release-notes.js +183 -0
  204. package/dist/remote-monitor.d.ts +16 -0
  205. package/dist/remote-monitor.js +74 -0
  206. package/dist/remote-source.d.ts +34 -0
  207. package/dist/remote-source.js +426 -0
  208. package/dist/review-diff.d.ts +82 -0
  209. package/dist/review-diff.js +400 -0
  210. package/dist/review-learning.d.ts +11 -0
  211. package/dist/review-learning.js +60 -0
  212. package/dist/review-render.d.ts +38 -0
  213. package/dist/review-render.js +224 -0
  214. package/dist/review-report.d.ts +9 -0
  215. package/dist/review-report.js +89 -0
  216. package/dist/review-ui.d.ts +14 -0
  217. package/dist/review-ui.js +1248 -0
  218. package/dist/schedule.d.ts +78 -0
  219. package/dist/schedule.js +480 -0
  220. package/dist/screen-capture-provider.d.ts +52 -0
  221. package/dist/screen-capture-provider.js +218 -0
  222. package/dist/screenshot-workflow.d.ts +167 -0
  223. package/dist/screenshot-workflow.js +1237 -0
  224. package/dist/settings.d.ts +1 -1
  225. package/dist/settings.js +95 -7
  226. package/dist/site-export.d.ts +18 -0
  227. package/dist/site-export.js +87 -0
  228. package/dist/source-connectors.d.ts +33 -0
  229. package/dist/source-connectors.js +268 -0
  230. package/dist/source-discovery.d.ts +132 -0
  231. package/dist/source-discovery.js +823 -0
  232. package/dist/source-intelligence.d.ts +9 -0
  233. package/dist/source-intelligence.js +306 -0
  234. package/dist/sync-review.d.ts +28 -0
  235. package/dist/sync-review.js +264 -0
  236. package/dist/sync-runs.d.ts +192 -0
  237. package/dist/sync-runs.js +2244 -0
  238. package/dist/sync.d.ts +35 -0
  239. package/dist/sync.js +298 -32
  240. package/dist/text-diff.d.ts +9 -0
  241. package/dist/text-diff.js +59 -0
  242. package/dist/types.d.ts +946 -1
  243. package/dist/ui/assets/doxloop-logo-light-De7Nx7j7.png +0 -0
  244. package/dist/ui/assets/index-BHBYU2aG.css +1 -0
  245. package/dist/ui/assets/index-Cq3RPQiC.js +33 -0
  246. package/dist/ui/index.html +18 -0
  247. package/dist/ui-server.d.ts +83 -0
  248. package/dist/ui-server.js +3532 -0
  249. package/dist/usage-budget.d.ts +28 -0
  250. package/dist/usage-budget.js +90 -0
  251. package/dist/validation.d.ts +25 -1
  252. package/dist/validation.js +312 -26
  253. package/dist/workspace-tools.d.ts +54 -0
  254. package/dist/workspace-tools.js +123 -0
  255. package/docs/agent-compatibility.md +49 -32
  256. package/docs/ci-and-automation.md +105 -66
  257. package/docs/doxbrix-http-api.md +8 -1
  258. package/docs/existing-documentation.md +80 -0
  259. package/docs/generation-performance.md +108 -0
  260. package/docs/generator-authoring.md +68 -5
  261. package/docs/generator-selection.md +50 -13
  262. package/docs/mintlify-import.md +71 -0
  263. package/docs/openapi-security.md +25 -0
  264. package/docs/project-format.md +331 -36
  265. package/docs/release-quality.md +158 -0
  266. package/docs/releasing.md +72 -0
  267. package/docs/review-workflows.md +51 -0
  268. package/docs/security-model.md +102 -39
  269. package/docs/troubleshooting.md +226 -93
  270. package/package.json +46 -15
  271. package/scripts/test-auto-screenshot.mjs +172 -0
  272. package/skills/doxloop-authoring/SKILL.md +242 -361
  273. package/skills/doxloop-authoring/references/existing-documentation.md +94 -0
  274. package/skills/doxloop-authoring/references/navigation-architecture.md +18 -7
  275. package/skills/doxloop-authoring/references/page-depth.md +169 -0
  276. package/skills/doxloop-authoring/references/project-format.md +106 -7
  277. package/skills/doxloop-authoring/references/quality.md +10 -0
  278. package/skills/doxloop-authoring/references/screenshot-manifest.md +113 -0
  279. package/skills/doxloop-authoring/references/screenshots.md +149 -235
  280. package/skills/doxloop-authoring/references/workflows.md +76 -0
  281. package/skills/doxloop-doxbrix/SKILL.md +38 -22
  282. package/skills/doxloop-doxbrix/references/api-endpoints.md +15 -14
  283. package/skills/doxloop-doxbrix/references/components.md +30 -3
  284. package/skills/doxloop-doxbrix/references/manifest.md +3 -2
  285. package/vendor/doxbrix-import/LICENSE +202 -0
  286. package/vendor/doxbrix-import/README.md +17 -0
  287. package/vendor/doxbrix-import/UPSTREAM.json +47 -0
  288. package/vendor/doxbrix-import/dist/docs/frontmatter.d.ts +13 -0
  289. package/vendor/doxbrix-import/dist/docs/frontmatter.js +83 -0
  290. package/vendor/doxbrix-import/dist/docs/import.d.ts +21 -0
  291. package/vendor/doxbrix-import/dist/docs/import.js +147 -0
  292. package/vendor/doxbrix-import/dist/docs/manifest.d.ts +163 -0
  293. package/vendor/doxbrix-import/dist/docs/manifest.js +64 -0
  294. package/vendor/doxbrix-import/dist/docs/project.d.ts +25 -0
  295. package/vendor/doxbrix-import/dist/docs/project.js +77 -0
  296. package/vendor/doxbrix-import/dist/docs/starter.d.ts +4 -0
  297. package/vendor/doxbrix-import/dist/docs/starter.js +11 -0
  298. package/vendor/doxbrix-import/dist/importer.d.ts +227 -0
  299. package/vendor/doxbrix-import/dist/importer.js +1567 -0
  300. package/vendor/doxbrix-import/dist/mintlify-openapi.d.ts +37 -0
  301. package/vendor/doxbrix-import/dist/mintlify-openapi.js +305 -0
  302. package/vendor/doxbrix-import/dist/safe-path.d.ts +9 -0
  303. package/vendor/doxbrix-import/dist/safe-path.js +47 -0
  304. package/dist/agents.d.ts.map +0 -1
  305. package/dist/agents.js.map +0 -1
  306. package/dist/args.d.ts.map +0 -1
  307. package/dist/args.js.map +0 -1
  308. package/dist/artifact-deploy.d.ts.map +0 -1
  309. package/dist/artifact-deploy.js.map +0 -1
  310. package/dist/auth.d.ts.map +0 -1
  311. package/dist/auth.js.map +0 -1
  312. package/dist/author.d.ts.map +0 -1
  313. package/dist/author.js.map +0 -1
  314. package/dist/capture.d.ts.map +0 -1
  315. package/dist/capture.js.map +0 -1
  316. package/dist/cli.d.ts.map +0 -1
  317. package/dist/cli.js.map +0 -1
  318. package/dist/deploy.d.ts.map +0 -1
  319. package/dist/deploy.js.map +0 -1
  320. package/dist/deployment-visibility.d.ts.map +0 -1
  321. package/dist/deployment-visibility.js.map +0 -1
  322. package/dist/doctor.d.ts.map +0 -1
  323. package/dist/doctor.js.map +0 -1
  324. package/dist/doxbrix-markdown.d.ts.map +0 -1
  325. package/dist/doxbrix-markdown.js.map +0 -1
  326. package/dist/errors.d.ts.map +0 -1
  327. package/dist/errors.js.map +0 -1
  328. package/dist/fs.d.ts.map +0 -1
  329. package/dist/fs.js.map +0 -1
  330. package/dist/generator-api.d.ts.map +0 -1
  331. package/dist/generator-api.js.map +0 -1
  332. package/dist/generator-manager.d.ts.map +0 -1
  333. package/dist/generator-manager.js.map +0 -1
  334. package/dist/generator-runtime.d.ts.map +0 -1
  335. package/dist/generator-runtime.js.map +0 -1
  336. package/dist/generators.d.ts.map +0 -1
  337. package/dist/generators.js.map +0 -1
  338. package/dist/interactive.d.ts.map +0 -1
  339. package/dist/interactive.js.map +0 -1
  340. package/dist/preview.d.ts.map +0 -1
  341. package/dist/preview.js.map +0 -1
  342. package/dist/progress.d.ts.map +0 -1
  343. package/dist/progress.js.map +0 -1
  344. package/dist/project.d.ts.map +0 -1
  345. package/dist/project.js.map +0 -1
  346. package/dist/prompts.d.ts.map +0 -1
  347. package/dist/prompts.js.map +0 -1
  348. package/dist/settings.d.ts.map +0 -1
  349. package/dist/settings.js.map +0 -1
  350. package/dist/sync.d.ts.map +0 -1
  351. package/dist/sync.js.map +0 -1
  352. package/dist/types.d.ts.map +0 -1
  353. package/dist/types.js.map +0 -1
  354. package/dist/validation.d.ts.map +0 -1
  355. package/dist/validation.js.map +0 -1
  356. package/dist/version.d.ts.map +0 -1
  357. package/dist/version.js.map +0 -1
@@ -0,0 +1,823 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { lstat, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
3
+ import { extname, join, relative, resolve } from 'node:path';
4
+ import { matchesAnyGlob } from './globs.js';
5
+ import { readDocsSiteManifest } from './docs-site.js';
6
+ import { loadOpenApiSource, parseOpenApi } from './openapi.js';
7
+ import { loadPages, loadProject, sourceKind } from './project.js';
8
+ import { sourceSnapshotFingerprints } from './sync.js';
9
+ const DISCOVERY_SCHEMA_VERSION = 1;
10
+ /** Bump when inventory rules change so cached inventories are rebuilt. */
11
+ const DISCOVERY_RULES_VERSION = 6;
12
+ const DISCOVERY_CACHE_DIRECTORY = join('.doxloop', 'cache', 'discovery');
13
+ /**
14
+ * Inspectable text files per source. The cap applies after product code is
15
+ * placed first, so a repository whose `public/` folder holds hundreds of
16
+ * icons still has its `src/` routes, screens, and configuration inventoried.
17
+ */
18
+ const MAX_FILES_PER_SOURCE = 2500;
19
+ /**
20
+ * Inventory rows sent to the planner. Public-surface kinds are kept ahead of
21
+ * file-level kinds so a large repository's documentation and test files do not
22
+ * crowd out its commands, routes, and configuration.
23
+ */
24
+ const MAX_SIGNALS_PER_SOURCE = 800;
25
+ const KIND_PRIORITY = ['package', 'command', 'route', 'operation', 'export', 'configuration', 'authentication', 'authorization', 'error', 'event', 'integration', 'example', 'documentation', 'test', 'asset'];
26
+ const MAX_FILE_BYTES = 128 * 1024;
27
+ const MAX_TEXT_BYTES_PER_SOURCE = 12 * 1024 * 1024;
28
+ /**
29
+ * Inspection order. Product code carries the public surface; prose, examples,
30
+ * and tests are supporting evidence; assets never produce a signal worth a
31
+ * reader's attention, so they are inventoried last and never inspected.
32
+ */
33
+ const ROLE_PRIORITY = { code: 0, other: 1, infrastructure: 2, documentation: 3, example: 4, test: 5, fixture: 6, asset: 7 };
34
+ /** English UI message catalogs that hold the strings a reader actually sees. */
35
+ const LABEL_CATALOG_PATH = /(^|\/)(?:lang|langs|locales?|i18n|intl\/messages|intl|translations?|messages|l10n)\/(?:en|en[-_][A-Za-z]{2})\.(?:json|ya?ml)$/i;
36
+ const TEXT_EXTENSIONS = new Set([
37
+ '.c', '.cc', '.cpp', '.cs', '.go', '.graphql', '.gql', '.h', '.hpp', '.html',
38
+ '.java', '.js', '.json', '.jsx', '.kt', '.md', '.mdx', '.php', '.proto',
39
+ '.py', '.rb', '.rs', '.rst', '.sh', '.sql', '.swift', '.toml', '.ts', '.tsx',
40
+ '.vue', '.xml', '.yaml', '.yml',
41
+ ]);
42
+ const IGNORED_DIRECTORIES = new Set([
43
+ '.git', '.hg', '.svn', '.turbo', '.next', '.nuxt', '.output', '.cache',
44
+ 'build', 'coverage', 'dist', 'node_modules', 'target', 'vendor',
45
+ ]);
46
+ /**
47
+ * Build a bounded, deterministic public-surface inventory. The cache key is
48
+ * derived from configured sources, safe source fingerprints, and discovery
49
+ * rules so unchanged sources do not consume another planning pass.
50
+ */
51
+ export async function discoverDocumentationSources(root) {
52
+ const project = await loadProject(root);
53
+ const syncFingerprints = await sourceSnapshotFingerprints(root, project.sources);
54
+ const fingerprints = {};
55
+ for (const source of project.sources) {
56
+ fingerprints[source.name] = sourceKind(source) === 'directory'
57
+ ? await safeDirectoryFingerprint(resolve(root, source.path), project.sync.ignore)
58
+ : syncFingerprints[source.name] ?? null;
59
+ }
60
+ const cacheKey = createHash('sha256').update(JSON.stringify({
61
+ schemaVersion: DISCOVERY_SCHEMA_VERSION,
62
+ rulesVersion: DISCOVERY_RULES_VERSION,
63
+ sources: project.sources,
64
+ fingerprints,
65
+ ignore: project.sync.ignore,
66
+ })).digest('hex');
67
+ const cachePath = join(root, DISCOVERY_CACHE_DIRECTORY, `${cacheKey}.json`);
68
+ try {
69
+ const cached = JSON.parse(await readFile(cachePath, 'utf8'));
70
+ if (isDiscoveryInventory(cached, cacheKey))
71
+ return { inventory: cached, cacheHit: true };
72
+ }
73
+ catch {
74
+ // A missing or malformed cache is replaced by deterministic discovery.
75
+ }
76
+ const sources = [];
77
+ for (const source of project.sources) {
78
+ sources.push(await discoverSource(root, project, source, fingerprints[source.name] ?? null));
79
+ }
80
+ const pages = await safeExistingPages(root, project);
81
+ const existingPages = pages.map((page) => portable(relative(root, page)));
82
+ const navigationFiles = await existingNavigationFiles(root, project);
83
+ // Pages of an existing documentation site describe the product second-hand.
84
+ // They are listed so the planner can account for every one of them, but they
85
+ // are not public product surface and must not inflate the suggested size.
86
+ const publicSignals = sources.filter((source) => source.kind !== 'docs-site').reduce((total, source) => total + source.evidence.length, 0);
87
+ const inventory = {
88
+ schemaVersion: DISCOVERY_SCHEMA_VERSION,
89
+ cacheKey,
90
+ generatedAt: new Date().toISOString(),
91
+ sources,
92
+ existingPages,
93
+ navigationFiles,
94
+ totals: {
95
+ sources: sources.length,
96
+ filesScanned: sources.reduce((total, source) => total + source.filesScanned, 0),
97
+ publicSignals,
98
+ existingPages: existingPages.length,
99
+ },
100
+ suggestedPages: suggestedPageCounts(publicSignals, existingPages.length),
101
+ };
102
+ await mkdir(join(root, DISCOVERY_CACHE_DIRECTORY), { recursive: true });
103
+ await writeFile(cachePath, `${JSON.stringify(inventory, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
104
+ return { inventory, cacheHit: false };
105
+ }
106
+ /** Compact JSON for prompt injection; the on-disk cache stays pretty-printed. */
107
+ /**
108
+ * Evidence kinds whose rows are regex hits on code lines (`const oauth =
109
+ * require('./oauth.js')`, `update:modelValue`). Hundreds of them tell the
110
+ * planner nothing a file list does not, so they are summarised per file.
111
+ */
112
+ const SUMMARISED_EVIDENCE_KINDS = new Set(['authentication', 'authorization', 'event', 'integration']);
113
+ const MAXIMUM_LISTED_ROWS_PER_KIND = 160;
114
+ const MAXIMUM_SUMMARISED_FILES_PER_KIND = 24;
115
+ /**
116
+ * The inventory as the planner reads it: grouped text instead of one JSON
117
+ * line. The same JSON for a mid-sized product ran to 120k characters and was
118
+ * re-sent on every planning turn; grouping rows by kind and file keeps every
119
+ * citation (source, path, kind, label, line) at a fraction of the size.
120
+ */
121
+ export function formatDiscoveryInventory(inventory) {
122
+ const lines = [];
123
+ const { totals, suggestedPages } = inventory;
124
+ lines.push(`Inventory ${inventory.cacheKey.slice(0, 12)} generated ${inventory.generatedAt}: ${totals.sources} source${totals.sources === 1 ? '' : 's'}, ${totals.filesScanned} files scanned, ${totals.publicSignals} public signals, ${totals.existingPages} existing documentation page${totals.existingPages === 1 ? '' : 's'}. Suggested page counts: starter ${suggestedPages.starter}, standard ${suggestedPages.standard}, comprehensive ${suggestedPages.comprehensive}.`);
125
+ if (inventory.existingPages.length > 0)
126
+ lines.push(`Existing documentation pages: ${inventory.existingPages.join(', ')}`);
127
+ if (inventory.navigationFiles.length > 0)
128
+ lines.push(`Navigation files: ${inventory.navigationFiles.join(', ')}`);
129
+ for (const source of inventory.sources) {
130
+ lines.push('');
131
+ const coverage = source.truncated
132
+ ? `${source.filesScanned} of ${source.filesAvailable} files scanned (partial)`
133
+ : `${source.filesScanned} file${source.filesScanned === 1 ? '' : 's'} scanned`;
134
+ const facts = [
135
+ source.languages.length > 0 ? source.languages.join(', ') : undefined,
136
+ source.packageNames.length > 0 ? `packages ${source.packageNames.join(', ')}` : undefined,
137
+ coverage,
138
+ source.revision ? `revision ${source.revision.slice(0, 12)}` : undefined,
139
+ ].filter((item) => Boolean(item));
140
+ lines.push(`Source "${source.name}" (${source.kind}, ${source.location}): ${facts.join('; ')}. Cite rows as source "${source.name}" with the path, kind, label, and line shown.`);
141
+ if (source.uiLabelCatalogs.length > 0)
142
+ lines.push(` UI label catalogs: ${source.uiLabelCatalogs.join(', ')}`);
143
+ for (const warning of source.warnings)
144
+ lines.push(` Warning: ${warning}`);
145
+ const byKind = new Map();
146
+ for (const row of source.evidence) {
147
+ const rows = byKind.get(row.kind) ?? [];
148
+ rows.push(row);
149
+ byKind.set(row.kind, rows);
150
+ }
151
+ for (const [kind, rows] of byKind) {
152
+ lines.push(SUMMARISED_EVIDENCE_KINDS.has(kind) ? summarisedKindLine(kind, rows) : listedKindLines(kind, rows));
153
+ }
154
+ }
155
+ return lines.join('\n');
156
+ }
157
+ function summarisedKindLine(kind, rows) {
158
+ const counts = new Map();
159
+ for (const row of rows)
160
+ counts.set(row.path, (counts.get(row.path) ?? 0) + 1);
161
+ const files = [...counts.entries()].sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]));
162
+ const shown = files.slice(0, MAXIMUM_SUMMARISED_FILES_PER_KIND).map(([path, count]) => (count === 1 ? path : `${path} (${count})`));
163
+ const rest = files.length - shown.length;
164
+ return ` ${kind} (${rows.length} matches in ${files.length} file${files.length === 1 ? '' : 's'}; read the files for the behavior): ${shown.join(', ')}${rest > 0 ? `, +${rest} more files` : ''}`;
165
+ }
166
+ function listedKindLines(kind, rows) {
167
+ const byPath = new Map();
168
+ let listed = 0;
169
+ for (const row of rows) {
170
+ if (listed >= MAXIMUM_LISTED_ROWS_PER_KIND)
171
+ break;
172
+ const group = byPath.get(row.path) ?? [];
173
+ group.push(row);
174
+ byPath.set(row.path, group);
175
+ listed += 1;
176
+ }
177
+ const groups = [...byPath.entries()].map(([path, group]) => `${path}: ${group.map((row) => (row.line === undefined ? row.label : `${row.label} @${row.line}`)).join('; ')}`);
178
+ const rest = rows.length - listed;
179
+ const header = ` ${kind} (${rows.length})${rest > 0 ? `, first ${listed} shown, +${rest} more` : ''}:`;
180
+ return `${header}\n${groups.map((group) => ` ${group}`).join('\n')}`;
181
+ }
182
+ async function discoverSource(root, project, source, revision) {
183
+ if (sourceKind(source) === 'openapi')
184
+ return discoverOpenApiSource(root, source, revision);
185
+ if (sourceKind(source) === 'docs-site')
186
+ return discoverDocsSiteSource(root, source, revision);
187
+ const sourceRoot = resolve(root, source.path);
188
+ const files = await safeSourceFiles(sourceRoot, project.sync.ignore);
189
+ const evidence = [];
190
+ const languages = new Set();
191
+ const packageNames = new Set();
192
+ const warnings = [];
193
+ const uiLabelCatalogs = [];
194
+ let textBytes = 0;
195
+ let filesScanned = 0;
196
+ // Only text files can yield a signal, and product code must be read before
197
+ // anything else: an alphabetical cap once spent the whole budget on a
198
+ // `public/` folder of icons and never opened `src/`.
199
+ const inspectable = files
200
+ .map((path) => ({ path, sourcePath: portable(relative(sourceRoot, path)) }))
201
+ .filter(({ path, sourcePath }) => TEXT_EXTENSIONS.has(extname(path).toLowerCase()) || sourcePath.toLowerCase().endsWith('.env.example'))
202
+ .map((entry) => ({ ...entry, role: classifySourceFile(entry.sourcePath) }))
203
+ .filter((entry) => entry.role !== 'asset' || LABEL_CATALOG_PATH.test(entry.sourcePath))
204
+ .sort((left, right) => ROLE_PRIORITY[left.role] - ROLE_PRIORITY[right.role] || left.sourcePath.localeCompare(right.sourcePath));
205
+ const candidates = inspectable.slice(0, MAX_FILES_PER_SOURCE);
206
+ // Package entry points decide which exports count as public surface, and a
207
+ // package manifest can sort after the code it describes, so read them first.
208
+ const packageFolders = [];
209
+ const entryPoints = await packageEntryPoints(sourceRoot, candidates.map((entry) => entry.path), packageFolders);
210
+ const noEntryPoints = new Set();
211
+ let budgetExhausted = false;
212
+ for (const { path, sourcePath } of candidates) {
213
+ const extension = extname(path).toLowerCase();
214
+ if (LABEL_CATALOG_PATH.test(sourcePath))
215
+ uiLabelCatalogs.push(sourcePath);
216
+ const size = (await lstat(path)).size;
217
+ if (size > MAX_FILE_BYTES)
218
+ continue;
219
+ if (textBytes + size > MAX_TEXT_BYTES_PER_SOURCE) {
220
+ budgetExhausted = true;
221
+ break;
222
+ }
223
+ let content;
224
+ try {
225
+ content = await readFile(path, 'utf8');
226
+ }
227
+ catch {
228
+ continue;
229
+ }
230
+ textBytes += size;
231
+ filesScanned += 1;
232
+ languages.add(languageForExtension(extension));
233
+ inspectTextFile(source.name, sourcePath, content, evidence, packageNames, exportsArePublic(sourcePath, packageFolders) ? entryPoints : noEntryPoints);
234
+ }
235
+ evidence.push(...await repositoryOpenApiEvidence(source.name, inspectable, warnings));
236
+ const truncated = inspectable.length > MAX_FILES_PER_SOURCE || budgetExhausted;
237
+ if (inspectable.length > MAX_FILES_PER_SOURCE)
238
+ warnings.push(`Inventory inspected ${filesScanned} of ${inspectable.length} inspectable files; product code was read first and supporting files were cut off.`);
239
+ if (budgetExhausted)
240
+ warnings.push(`Inventory text budget reached after ${filesScanned} of ${inspectable.length} inspectable files; product code was read first.`);
241
+ return {
242
+ name: source.name,
243
+ kind: 'directory',
244
+ location: source.path,
245
+ revision,
246
+ filesScanned,
247
+ filesAvailable: files.length,
248
+ truncated,
249
+ languages: [...languages].filter(Boolean).sort(),
250
+ packageNames: [...packageNames].sort(),
251
+ evidence: uniqueEvidence(evidence),
252
+ uiLabelCatalogs: uiLabelCatalogs.sort(),
253
+ warnings,
254
+ ...(source.scope ? { scope: source.scope } : {}),
255
+ };
256
+ }
257
+ async function discoverOpenApiSource(root, source, revision) {
258
+ const loaded = await loadOpenApiSource(root, source);
259
+ const evidence = openApiEvidence(source.name, portable(source.path), loaded);
260
+ return {
261
+ name: source.name,
262
+ kind: 'openapi',
263
+ location: source.path,
264
+ revision,
265
+ filesScanned: 1,
266
+ filesAvailable: 1,
267
+ truncated: false,
268
+ languages: ['OpenAPI'],
269
+ packageNames: [],
270
+ evidence: uniqueEvidence(evidence),
271
+ uiLabelCatalogs: [],
272
+ warnings: [],
273
+ ...(source.scope ? { scope: source.scope } : {}),
274
+ };
275
+ }
276
+ /**
277
+ * Signals for one API contract: the product, every operation, schema,
278
+ * security scheme, documented 4xx/5xx response, and webhook. Shared by
279
+ * OpenAPI sources and by specs found inside a repository.
280
+ */
281
+ export function openApiEvidence(source, path, loaded) {
282
+ const contract = (kind, label) => ({ source, path, kind, label, contract: true });
283
+ const paths = record(loaded.document.paths);
284
+ const errors = [];
285
+ for (const operation of Object.keys(loaded.snapshot.operations)) {
286
+ const [method = '', ...rest] = operation.split(' ');
287
+ const responses = record(record(record(paths[rest.join(' ')])[method.toLowerCase()]).responses);
288
+ for (const status of Object.keys(responses))
289
+ if (/^[45](?:\d\d|XX)$/i.test(status))
290
+ errors.push(contract('error', `${operation} ${status}`));
291
+ }
292
+ return [
293
+ { source, path, kind: 'package', label: loaded.summary.title },
294
+ ...Object.keys(loaded.snapshot.operations).map((label) => contract('operation', label)),
295
+ ...Object.keys(loaded.snapshot.schemas).map((schema) => contract('export', `Schema ${schema}`)),
296
+ ...loaded.summary.securitySchemes.map((scheme) => contract('authentication', scheme)),
297
+ ...errors,
298
+ ...Object.keys(loaded.snapshot.operations).filter((operation) => /webhook|event|callback/i.test(operation)).map((operation) => contract('integration', operation)),
299
+ ...Object.keys(record(loaded.document.webhooks)).flatMap((webhook) => [contract('event', webhook), contract('integration', `Webhook ${webhook}`)]),
300
+ ];
301
+ }
302
+ /**
303
+ * OpenAPI and Swagger files kept inside a repository. RealWorld keeps its
304
+ * contract at `specs/api/openapi.yml`; classified by folder it is a test
305
+ * file and yielded nothing, so an API-only product added as a repository
306
+ * showed no operations at all. Test fixtures and vendored specs are not the
307
+ * product's contract.
308
+ */
309
+ const OPENAPI_FILE_NAME = /(?:^|\/)(?:[^/]*(?:openapi|swagger)[^/]*|api)\.(?:ya?ml|json)$/i;
310
+ const OPENAPI_NOISE_PATH = /(^|\/)(?:__tests__|tests?|testdata|fixtures?|mocks?|__mocks__|node_modules|vendor|third[_-]?party|examples?|samples?)(\/|$)/i;
311
+ const OPENAPI_HEADER = /^\s*["']?(?:openapi|swagger)["']?\s*:\s*["']?\d/m;
312
+ const MAX_OPENAPI_FILES_PER_SOURCE = 5;
313
+ async function repositoryOpenApiEvidence(source, files, warnings) {
314
+ const evidence = [];
315
+ let found = 0;
316
+ for (const { path, sourcePath } of files) {
317
+ if (found >= MAX_OPENAPI_FILES_PER_SOURCE)
318
+ break;
319
+ if (!OPENAPI_FILE_NAME.test(sourcePath) || OPENAPI_NOISE_PATH.test(sourcePath))
320
+ continue;
321
+ let content;
322
+ try {
323
+ if ((await lstat(path)).size > 5 * 1024 * 1024)
324
+ continue;
325
+ content = await readFile(path, 'utf8');
326
+ }
327
+ catch {
328
+ continue;
329
+ }
330
+ if (!OPENAPI_HEADER.test(content.slice(0, 4096)))
331
+ continue;
332
+ try {
333
+ evidence.push(...openApiEvidence(source, sourcePath, parseOpenApi(content, sourcePath)));
334
+ found += 1;
335
+ }
336
+ catch (error) {
337
+ warnings.push(`API contract ${sourcePath} could not be read: ${error instanceof Error ? error.message : String(error)}`);
338
+ }
339
+ }
340
+ return evidence;
341
+ }
342
+ /**
343
+ * One `documentation` row per crawled page, labeled with the page title, so
344
+ * the planner sees the whole existing site map without opening every file.
345
+ * The snapshot's `index.md` carries the same table with original URLs.
346
+ */
347
+ async function discoverDocsSiteSource(root, source, revision) {
348
+ const manifest = await readDocsSiteManifest(root, source);
349
+ const evidence = [
350
+ { source: source.name, path: 'index.md', kind: 'documentation', label: `Existing documentation site ${manifest.url} (${manifest.totals.pages} pages)` },
351
+ ...manifest.pages.slice(0, MAX_SIGNALS_PER_SOURCE - 1).map((page) => ({ source: source.name, path: page.file, kind: 'documentation', label: page.title })),
352
+ ];
353
+ const warnings = [...manifest.warnings];
354
+ if (manifest.brokenLinks.length > 0)
355
+ warnings.push(`${manifest.brokenLinks.length} internal links on the existing site point at pages that failed to load; see index.md.`);
356
+ return {
357
+ name: source.name,
358
+ kind: 'docs-site',
359
+ location: manifest.url,
360
+ revision: revision ?? manifest.hash,
361
+ filesScanned: manifest.pages.length,
362
+ filesAvailable: manifest.pages.length,
363
+ truncated: Boolean(manifest.truncated),
364
+ languages: ['Markdown'],
365
+ packageNames: [],
366
+ evidence,
367
+ uiLabelCatalogs: [],
368
+ warnings,
369
+ ...(source.scope ? { scope: source.scope } : {}),
370
+ };
371
+ }
372
+ async function safeSourceFiles(root, ignoredGlobs) {
373
+ const files = [];
374
+ await walk(root, root, ignoredGlobs, files);
375
+ return files.sort();
376
+ }
377
+ async function safeDirectoryFingerprint(root, ignoredGlobs) {
378
+ const files = await safeSourceFiles(root, ignoredGlobs);
379
+ const hash = createHash('sha256');
380
+ for (const path of files) {
381
+ const sourcePath = portable(relative(root, path));
382
+ hash.update(sourcePath);
383
+ hash.update('\0');
384
+ try {
385
+ hash.update(await readFile(path));
386
+ }
387
+ catch {
388
+ hash.update('unreadable');
389
+ }
390
+ hash.update('\0');
391
+ }
392
+ return files.length ? hash.digest('hex') : null;
393
+ }
394
+ async function walk(root, directory, ignoredGlobs, files) {
395
+ let entries;
396
+ try {
397
+ entries = await readdir(directory, { withFileTypes: true });
398
+ }
399
+ catch {
400
+ return;
401
+ }
402
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
403
+ const path = join(directory, entry.name);
404
+ const relativePath = portable(relative(root, path));
405
+ if (entry.isSymbolicLink() || isSensitiveSourcePath(relativePath) || matchesAnyGlob(relativePath, ignoredGlobs))
406
+ continue;
407
+ if (entry.isDirectory()) {
408
+ if (!IGNORED_DIRECTORIES.has(entry.name) && !entry.name.startsWith('.'))
409
+ await walk(root, path, ignoredGlobs, files);
410
+ }
411
+ else if (entry.isFile())
412
+ files.push(path);
413
+ }
414
+ }
415
+ const FIXTURE_PATH = /(^|\/)(?:evals?|evaluations?|fixtures?|__fixtures__|__mocks__|mocks?|testdata|test-data|snapshots?|__snapshots__|e2e|benchmarks?|playground|sandbox|scripts?|tools?|\.?storybook|stories)(\/|$)/i;
416
+ /**
417
+ * Deployment, database, and maintenance code. Its environment variables are
418
+ * real operator configuration, but a migration that creates an `api_key`
419
+ * table or a helper that exits with status 1 is not a reader-facing
420
+ * authentication surface or command.
421
+ */
422
+ const INFRASTRUCTURE_PATH = /(^|\/)(?:db|database|migrations?|migrate|seeds?|prisma|drizzle|docker|podman|k8s|kubernetes|helm|charts?|deploy|deployment|infra|infrastructure|terraform|ansible|extra|extras|ci|\.github|\.gitlab|\.circleci)(\/|$)/i;
423
+ // Test files by each ecosystem's own convention: `*.test.ts`, Go's
424
+ // `*_test.go`, Python's `test_*.py` / `*_test.py`, Ruby's `*_spec.rb`,
425
+ // Java/C#'s `*Test.java` / `*Tests.cs`.
426
+ const TEST_PATH = /(^|\/)(?:__tests__|tests?|specs?)(\/|$)|\.(?:test|spec|stories)\.[^.]+$|_test\.(?:go|py|rb|exs?)$|(^|\/)test_[^/]+\.py$|_spec\.rb$|Tests?\.(?:java|kt|cs)$/;
427
+ const EXAMPLE_PATH = /(^|\/)(?:examples?|demos?|samples?|recipes?)(\/|$)/i;
428
+ const DOCUMENTATION_PATH = /(^|\/)(?:docs?|documentation|skills?|prompts?|references?|guides?|wiki|adr|rfcs?|proposals?)(\/|$)|\.(?:md|mdx|rst|txt)$/i;
429
+ const ASSET_PATH = /(^|\/)(?:assets?|public|static|images?|fonts?|media)(\/|$)/i;
430
+ const CODE_EXTENSIONS = new Set(['.c', '.cc', '.cpp', '.cs', '.go', '.graphql', '.gql', '.h', '.hpp', '.java', '.js', '.jsx', '.kt', '.php', '.proto', '.py', '.rb', '.rs', '.swift', '.ts', '.tsx', '.vue']);
431
+ export function classifySourceFile(path) {
432
+ if (FIXTURE_PATH.test(path))
433
+ return 'fixture';
434
+ if (TEST_PATH.test(path))
435
+ return 'test';
436
+ if (EXAMPLE_PATH.test(path))
437
+ return 'example';
438
+ if (DOCUMENTATION_PATH.test(path))
439
+ return 'documentation';
440
+ if (ASSET_PATH.test(path))
441
+ return 'asset';
442
+ if (INFRASTRUCTURE_PATH.test(path))
443
+ return 'infrastructure';
444
+ return CODE_EXTENSIONS.has(extname(path).toLowerCase()) ? 'code' : 'other';
445
+ }
446
+ /**
447
+ * Front-end source of a web app or admin dashboard. Its components mention
448
+ * sign-in, providers, and events on every screen without being an API or
449
+ * integration surface of their own; what the reader sees there is covered by
450
+ * UI journeys and screenshots, so keyword families do not apply to it.
451
+ */
452
+ const FRONTEND_PATH = /(^|\/)(?:ui|web|webapp|frontend|client|dashboard|admin|app-ui|desktop|components|pages|views|layouts|composables|stores|[\w-]*-(?:web|desktop|ui|frontend))\/.*\.(?:jsx?|tsx?|mjs|vue|svelte)$|\.(?:vue|svelte|jsx|tsx)$/i;
453
+ /** Keyword-derived signal kinds, counted once per folder rather than per file. */
454
+ export const KEYWORD_SIGNAL_KINDS = new Set(['authentication', 'authorization', 'integration', 'event']);
455
+ /** Build and shell variables every toolchain reads; not product configuration. */
456
+ export const TOOLCHAIN_ENV = /^(?:NODE_ENV|CI|HOME|PATH|PWD|SHELL|TERM|USER|LANG|TZ|TMPDIR|npm_\w+|TAURI_\w+|VITEST\w*|JEST_\w+|GITHUB_\w+|RUNNER_\w+)$/;
457
+ const MONOREPO_ROOTS = new Set(['packages', 'apps', 'libs', 'services', 'crates', 'modules', 'plugins', 'extensions']);
458
+ /**
459
+ * The module a file belongs to, for counting keyword signals once per module:
460
+ * a folder at most three levels deep, or four inside a monorepo's
461
+ * `packages/<name>/src/<module>`. A page citing any file in the module covers
462
+ * it. Shared by discovery and coverage so both group the same way.
463
+ */
464
+ export function signalModule(path) {
465
+ const segments = path.split('/').slice(0, -1);
466
+ if (segments.length === 0)
467
+ return '.';
468
+ const depth = MONOREPO_ROOTS.has(segments[0]) ? 4 : 3;
469
+ return segments.slice(0, depth).join('/');
470
+ }
471
+ /**
472
+ * Framework-owned HTTP routes that no `app.get(...)` line declares. A Next.js
473
+ * App Router handler lives at `app/api/users/[id]/route.ts` and exports one
474
+ * function per method, so the route is derived from the file path.
475
+ */
476
+ export function frameworkRoutes(path, content) {
477
+ const match = /(?:^|\/)app\/(.*?)\/?route\.(?:ts|tsx|js|jsx|mjs)$/.exec(path);
478
+ if (!match)
479
+ return [];
480
+ const route = `/${match[1]
481
+ .split('/')
482
+ .filter((segment) => segment && !/^\(.*\)$/.test(segment) && !/^@/.test(segment))
483
+ .map((segment) => segment.replace(/^\[\[?\.\.\.([^\]]+)\]?\]$/, ':$1*').replace(/^\[([^\]]+)\]$/, ':$1'))
484
+ .join('/')}`;
485
+ const methods = [...content.matchAll(/\bexport\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\b/g)].map((item) => item[1]);
486
+ const aliased = [...content.matchAll(/\bexport\s*\{[^}]*\b(?:as\s+)?(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\b[^}]*\}/g)].map((item) => item[1]);
487
+ return [...new Set([...methods, ...aliased])].map((method) => `${method} ${route}`);
488
+ }
489
+ /**
490
+ * Lines that mention authentication or providers without being behavior:
491
+ * comments, markup, translated UI strings, and SQL. A Vue template that
492
+ * renders `$t("Add API Key")` is a label, not an authentication surface.
493
+ */
494
+ function isProseLine(line, inTemplate) {
495
+ const trimmed = line.trim();
496
+ return inTemplate
497
+ || /^(?:\/\/|#|\*|\/\*|<!--|--|\{\/\*)/.test(trimmed)
498
+ || /^<[A-Za-z!/]/.test(trimmed)
499
+ || /\$t\(|\bt\(\s*['"`]|i18n|\btranslate\(/.test(trimmed)
500
+ || /^\s*(?:CREATE|ALTER|DROP|INSERT|UPDATE|SELECT)\s/i.test(trimmed);
501
+ }
502
+ /**
503
+ * Exports are public surface only in a package people install. A monorepo's
504
+ * apps and internal libraries are `"private": true`, and their hundreds of
505
+ * index exports read as undocumented API to a coverage count.
506
+ */
507
+ function exportsArePublic(sourcePath, packages) {
508
+ let nearest;
509
+ for (const entry of packages) {
510
+ if (entry.dir === '' || sourcePath.startsWith(`${entry.dir}/`)) {
511
+ if (!nearest || entry.dir.length > nearest.dir.length)
512
+ nearest = entry;
513
+ }
514
+ }
515
+ return !nearest?.private;
516
+ }
517
+ async function packageEntryPoints(sourceRoot, files, packages = []) {
518
+ const stems = new Set(['index', 'main', 'lib', 'mod', 'public-api', '__init__']);
519
+ for (const path of files) {
520
+ if (path.split(/[\\/]/).at(-1) !== 'package.json' || classifySourceFile(portable(relative(sourceRoot, path))) === 'fixture')
521
+ continue;
522
+ let json;
523
+ try {
524
+ json = safeJson(await readFile(path, 'utf8'));
525
+ }
526
+ catch {
527
+ continue;
528
+ }
529
+ if (!json)
530
+ continue;
531
+ const manifest = portable(relative(sourceRoot, path));
532
+ packages.push({ dir: manifest.includes('/') ? manifest.slice(0, manifest.lastIndexOf('/')) : '', private: json.private === true });
533
+ const values = [json.main, json.module, json.types, json.typings, json.browser];
534
+ values.push(...Object.values(record(json.bin)));
535
+ const walkExports = (value) => {
536
+ if (typeof value === 'string')
537
+ values.push(value);
538
+ else if (value && typeof value === 'object')
539
+ Object.values(value).forEach(walkExports);
540
+ };
541
+ walkExports(json.exports);
542
+ for (const value of values) {
543
+ if (typeof value !== 'string')
544
+ continue;
545
+ const stem = value.replace(/\\/g, '/').split('/').at(-1)?.replace(/\.[^.]+$/, '').replace(/\.d$/, '');
546
+ if (stem && stem !== '.' && stem !== '*')
547
+ stems.add(stem);
548
+ }
549
+ }
550
+ return stems;
551
+ }
552
+ function isPublicEntryPoint(path, entryPoints) {
553
+ const stem = path.split('/').at(-1)?.replace(/\.[^.]+$/, '').replace(/\.d$/, '') ?? '';
554
+ return entryPoints.has(stem);
555
+ }
556
+ function inspectTextFile(source, path, content, evidence, packages, entryPoints = new Set(['index', 'main', 'lib', 'mod'])) {
557
+ const base = path.split('/').at(-1)?.toLowerCase() ?? '';
558
+ const role = classifySourceFile(path);
559
+ if (base === 'package.json' && role !== 'fixture') {
560
+ const json = safeJson(content);
561
+ const name = text(json?.name);
562
+ if (name)
563
+ packages.add(name);
564
+ if (name)
565
+ evidence.push({ source, path, kind: 'package', label: name });
566
+ for (const command of Object.keys(record(json?.bin)))
567
+ evidence.push({ source, path, kind: 'command', label: command });
568
+ for (const exported of Object.keys(record(json?.exports)))
569
+ evidence.push({ source, path, kind: 'export', label: exported });
570
+ // Operator-facing scripts only. A repository's forty `test-*`, `lint`,
571
+ // and `build-docker-nightly` scripts are contributor tooling, and each
572
+ // one counted as an undocumented public command.
573
+ // Only the repository's own package: `ui/package.json` scripts build the
574
+ // bundled dashboard and are contributor tooling, not reader commands.
575
+ for (const script of (path === 'package.json' ? Object.keys(record(json?.scripts)) : []).filter((item) => /^(?:start|dev|serve|build|migrate|deploy|preview|setup)(?::[a-z0-9-]+)?$/.test(item))) {
576
+ evidence.push({ source, path, kind: 'command', label: `npm run ${script}` });
577
+ }
578
+ }
579
+ const fileKind = role === 'example' ? 'example'
580
+ : role === 'test' || role === 'fixture' ? 'test'
581
+ : role === 'documentation' ? 'documentation'
582
+ : undefined;
583
+ if (fileKind)
584
+ evidence.push({ source, path, kind: fileKind, label: path });
585
+ // Key extraction applies to data configuration files (YAML, TOML, JSON,
586
+ // .env.example). A `vitest.config.ts` is build tooling, and its keys are
587
+ // not product configuration.
588
+ const configurationFile = role !== 'code' && /(?:^|\/)(?:config|configuration|settings)(?:[./_-]|$)|\.env\.example$/i.test(path);
589
+ // Prose, fixtures, tests, and examples are evidence about behavior, never
590
+ // behavior themselves. Only product code and configuration files below.
591
+ if (role !== 'code' && role !== 'infrastructure' && !(role === 'other' && configurationFile))
592
+ return;
593
+ // Deployment and database code contributes operator configuration only.
594
+ if (role === 'infrastructure') {
595
+ content.split(/\r?\n/).forEach((line, index) => {
596
+ for (const match of line.matchAll(/\b(?:process\.env\.|env\[['"`]|ENV\[['"`])([A-Z][A-Z0-9_]{2,})/g)) {
597
+ if (!TOOLCHAIN_ENV.test(match[1]))
598
+ evidence.push({ source, path, kind: 'configuration', label: match[1], line: index + 1 });
599
+ }
600
+ });
601
+ return;
602
+ }
603
+ for (const route of frameworkRoutes(path, content))
604
+ evidence.push({ source, path, kind: 'route', label: route });
605
+ const publicExports = isPublicEntryPoint(path, entryPoints);
606
+ // One signal per keyword family per file: "this module authenticates" is a
607
+ // public-surface fact; the forty lines that mention a token are not.
608
+ const seenKeywords = new Set();
609
+ const frontend = FRONTEND_PATH.test(path);
610
+ const lines = content.split(/\r?\n/);
611
+ let inTemplate = false;
612
+ lines.forEach((line, index) => {
613
+ if (path.endsWith('.vue')) {
614
+ if (/^\s*<template\b/.test(line))
615
+ inTemplate = true;
616
+ else if (/^\s*<\/template>/.test(line))
617
+ inTemplate = false;
618
+ }
619
+ const prose = isProseLine(line, inTemplate);
620
+ const exported = /\bexport\s+(?:default\s+)?(?:async\s+)?(?:class|function|const|let|var|interface|type|enum)\s+([A-Za-z_$][\w$]*)/.exec(line)?.[1]
621
+ ?? /\bpub\s+(?:async\s+)?(?:fn|struct|enum|trait|type)\s+([A-Za-z_][\w]*)/.exec(line)?.[1];
622
+ if (exported && publicExports)
623
+ evidence.push({ source, path, kind: 'export', label: exported, line: index + 1 });
624
+ const route = /\b(?:app|router|server)\.(get|post|put|patch|delete|options|head)\s*\(\s*['"`]([^'"`]+)/i.exec(line)
625
+ ?? /\bmap(get|post|put|patch|delete|options|head)\s*\(\s*['"`]([^'"`]+)/i.exec(line)
626
+ ?? /@(get|post|put|patch|delete|request)mapping\s*\(\s*(?:value\s*=\s*)?['"`]([^'"`]+)/i.exec(line)
627
+ // Go routers (net/http 1.22 patterns aside): `rg.GET("/records", h)`,
628
+ // gin/echo/chi/PocketBase style, with an upper-case method name.
629
+ ?? /\.(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\(\s*["'`](\/[^"'`]*)/.exec(line);
630
+ if (route)
631
+ evidence.push({ source, path, kind: 'route', label: `${route[1].toUpperCase()} ${route[2]}`, line: index + 1 });
632
+ const command = /\.(?:command|option)\(\s*['"`]([^'"`]+)/.exec(line)?.[1];
633
+ if (command)
634
+ evidence.push({ source, path, kind: 'command', label: command, line: index + 1 });
635
+ const configuration = /\b(?:interface|type|class)\s+([A-Za-z_$][\w$]*(?:Config|Options|Settings))\b/.exec(line)?.[1];
636
+ if (configuration && publicExports)
637
+ evidence.push({ source, path, kind: 'configuration', label: configuration, line: index + 1 });
638
+ for (const match of line.matchAll(/\b(?:process\.env\.|env\[['"`]|ENV\[['"`])([A-Z][A-Z0-9_]{2,})/g)) {
639
+ if (!TOOLCHAIN_ENV.test(match[1]))
640
+ evidence.push({ source, path, kind: 'configuration', label: match[1], line: index + 1 });
641
+ }
642
+ const option = /(?:add_argument|addOption|option)\(\s*['"`](-{1,2}[a-z0-9][\w-]*)/i.exec(line)?.[1]
643
+ ?? /\b(?:flag\.(?:String|Bool|Int|Duration)|StringVar|BoolVar|IntVar)\s*\([^,]*,?\s*['"`]([a-z0-9][\w-]*)/i.exec(line)?.[1];
644
+ if (option)
645
+ evidence.push({ source, path, kind: 'command', label: option, line: index + 1 });
646
+ // Comments, markup, and translated strings mention these words without
647
+ // implementing anything; keyword families below apply to code lines only.
648
+ if (prose || frontend)
649
+ return;
650
+ const authentication = /\b(oauth2?|oidc|sso|bearer|api[_ -]?key|access[_ -]?token|refresh[_ -]?token|authenticate|authentication|sign[ -]?in|login)(?:\b|(?=[A-Z_]))/i.exec(line)?.[1];
651
+ if (authentication && keywordOnce(seenKeywords, 'authentication', authentication))
652
+ evidence.push({ source, path, kind: 'authentication', label: conciseLabel(line, authentication), line: index + 1 });
653
+ // Bare "scope", "policy", and "role" are ordinary vocabulary in most code;
654
+ // count them only beside an access-control word.
655
+ const authorization = /\b(authori[sz]e|authorization|permission|permissions|rbac)\b/i.exec(line)?.[1]
656
+ ?? (/\b(?:auth|token|grant|access|permission|admin|user)/i.test(line) ? /\b(role|roles|scope|scopes|policy)\b/i.exec(line)?.[1] : undefined);
657
+ if (authorization && keywordOnce(seenKeywords, 'authorization', authorization))
658
+ evidence.push({ source, path, kind: 'authorization', label: conciseLabel(line, authorization), line: index + 1 });
659
+ const error = /\b(?:throw\s+new\s+([A-Za-z_$][\w$]*Error)|raise\s+([A-Za-z_$][\w$]*(?:Error|Exception))|status\s*[:=]\s*(4\d\d|5\d\d)|HTTPException\s*\(\s*status_code\s*=\s*(\d{3}))/i.exec(line);
660
+ const errorLabel = error ? error.slice(1).find(Boolean) ?? 'error contract' : undefined;
661
+ if (errorLabel && keywordOnce(seenKeywords, 'error', errorLabel))
662
+ evidence.push({ source, path, kind: 'error', label: errorLabel, line: index + 1 });
663
+ const event = /\b(?:emit|publish|dispatch|subscribe)\(\s*['"`]([^'"`]+)['"`]/i.exec(line)?.[1]
664
+ ?? /\b(event|webhook)\s*[:=]\s*['"`]([^'"`]+)['"`]/i.exec(line)?.[2];
665
+ if (event)
666
+ evidence.push({ source, path, kind: 'event', label: event, line: index + 1 });
667
+ const integration = /(webhook|integration|connector|plugin|provider|adapter)/i.exec(line)?.[1];
668
+ if (integration && keywordOnce(seenKeywords, 'integration', integration))
669
+ evidence.push({ source, path, kind: 'integration', label: conciseLabel(line, integration), line: index + 1 });
670
+ if (configurationFile) {
671
+ const key = /^\s{0,4}([A-Za-z][A-Za-z0-9_.-]{2,})\s*[:=]/.exec(line)?.[1];
672
+ if (key && !['const', 'export', 'function', 'import', 'return'].includes(key.toLowerCase()))
673
+ evidence.push({ source, path, kind: 'configuration', label: key, line: index + 1 });
674
+ }
675
+ });
676
+ }
677
+ function keywordOnce(seen, family, keyword) {
678
+ const key = `${family}:${keyword.toLowerCase()}`;
679
+ if (seen.has(key))
680
+ return false;
681
+ seen.add(key);
682
+ return true;
683
+ }
684
+ function conciseLabel(line, fallback) {
685
+ const normalized = line.trim().replace(/\s+/g, ' ');
686
+ return normalized.length > 100 ? fallback.toLowerCase() : normalized;
687
+ }
688
+ async function safeExistingPages(root, project) {
689
+ try {
690
+ return await loadPages(root, project);
691
+ }
692
+ catch {
693
+ return [];
694
+ }
695
+ }
696
+ async function existingNavigationFiles(root, project) {
697
+ const candidates = ['docs.json', 'sidebars.js', 'sidebars.ts', 'mkdocs.yml', 'mkdocs.yaml', 'astro.config.mjs', 'docusaurus.config.js', 'docusaurus.config.ts', 'nav.yml', 'nav.yaml'];
698
+ const roots = [root, resolve(root, project.contentDir)];
699
+ const found = [];
700
+ for (const directory of roots) {
701
+ for (const candidate of candidates) {
702
+ const path = join(directory, candidate);
703
+ try {
704
+ if ((await lstat(path)).isFile())
705
+ found.push(portable(relative(root, path)));
706
+ }
707
+ catch { /* absent candidates are expected */ }
708
+ }
709
+ }
710
+ return [...new Set(found)].sort();
711
+ }
712
+ /**
713
+ * Evidence-derived page estimates. Comprehensive scales with the public
714
+ * surface instead of stopping at a fixed ceiling: a product with sixty
715
+ * commands, routes, and configuration groups needs more than thirty pages, and
716
+ * the reviewer can raise or lower the target page count on the plan anyway.
717
+ */
718
+ export const MAXIMUM_SUGGESTED_PAGES = 120;
719
+ export function suggestedPageCounts(signals, existingPages) {
720
+ const complexity = Math.max(1, Math.ceil(signals / 6), Math.ceil(existingPages / 2));
721
+ return {
722
+ starter: Math.min(5, Math.max(3, 2 + Math.ceil(complexity / 4))),
723
+ standard: Math.min(MAXIMUM_SUGGESTED_PAGES, Math.max(7, 6 + Math.ceil(complexity / 2))),
724
+ comprehensive: Math.min(MAXIMUM_SUGGESTED_PAGES, Math.max(12, 11 + complexity)),
725
+ };
726
+ }
727
+ /**
728
+ * Keyword signals ("this file mentions OAuth") count once per folder and
729
+ * family: a module that authenticates is one surface to document, not one
730
+ * per file. The signal's path is the folder, and its label names the
731
+ * keywords found there, so the gap reads as "Authentication in apis".
732
+ */
733
+ export function groupKeywordSignals(items) {
734
+ const groups = new Map();
735
+ const out = [];
736
+ for (const item of items) {
737
+ // A contract's security schemes and webhooks are exact names, not keyword hits.
738
+ if (!KEYWORD_SIGNAL_KINDS.has(item.kind) || item.contract) {
739
+ out.push(item);
740
+ continue;
741
+ }
742
+ const folder = signalModule(item.path);
743
+ const key = `${item.source}\0${item.kind}\0${folder}`;
744
+ const keyword = item.kind === 'event' ? item.label.slice(0, 40) : keywordOf(item.label);
745
+ const group = groups.get(key);
746
+ if (group) {
747
+ if (keyword)
748
+ group.keywords.add(keyword);
749
+ continue;
750
+ }
751
+ const created = { item: { source: item.source, kind: item.kind, path: folder, label: '' }, keywords: new Set(keyword ? [keyword] : []) };
752
+ groups.set(key, created);
753
+ out.push(created.item);
754
+ }
755
+ for (const { item, keywords } of groups.values()) {
756
+ const family = item.kind === 'authentication' ? 'Authentication' : item.kind === 'authorization' ? 'Permissions' : item.kind === 'event' ? 'Events' : 'Integrations';
757
+ const found = [...keywords].slice(0, 4).join(', ');
758
+ item.label = `${family} in ${item.path === '.' ? 'the project root' : item.path}${found ? ` (${found})` : ''}`;
759
+ }
760
+ return out;
761
+ }
762
+ function keywordOf(label) {
763
+ return /\b(oauth2?|oidc|sso|bearer|api[_ -]?key|access[_ -]?token|refresh[_ -]?token|authenticate|authentication|sign[ -]?in|login|authori[sz]e|authorization|permissions?|rbac|roles?|scopes?|policy|webhook|integration|connector|plugin|provider|adapter)/i.exec(label)?.[1]?.toLowerCase();
764
+ }
765
+ function uniqueEvidence(items) {
766
+ const seen = new Set();
767
+ return groupKeywordSignals(items).filter((item) => {
768
+ const key = `${item.source}\0${item.path}\0${item.kind}\0${item.label}`;
769
+ if (seen.has(key))
770
+ return false;
771
+ seen.add(key);
772
+ return true;
773
+ }).sort((left, right) => evidencePriority(left.kind) - evidencePriority(right.kind)).slice(0, MAX_SIGNALS_PER_SOURCE);
774
+ }
775
+ function isSensitiveSourcePath(path) {
776
+ const segments = path.toLowerCase().split(/[\\/]/);
777
+ const name = segments.at(-1) ?? '';
778
+ return segments.includes('.ssh') || name === '.env' || (name.startsWith('.env.') && name !== '.env.example') ||
779
+ name === 'credentials' || name === 'credentials.json' || name === 'id_rsa' || name === 'id_ed25519' ||
780
+ name.endsWith('.key') || name.endsWith('.pem') || name.endsWith('.p12') || name.endsWith('.pfx');
781
+ }
782
+ function languageForExtension(extension) {
783
+ return { '.js': 'JavaScript', '.jsx': 'JavaScript', '.ts': 'TypeScript', '.tsx': 'TypeScript', '.py': 'Python', '.go': 'Go', '.rs': 'Rust', '.java': 'Java', '.kt': 'Kotlin', '.rb': 'Ruby', '.php': 'PHP', '.cs': 'C#', '.swift': 'Swift', '.proto': 'Protocol Buffers', '.graphql': 'GraphQL', '.gql': 'GraphQL', '.yaml': 'YAML', '.yml': 'YAML', '.json': 'JSON', '.md': 'Markdown', '.mdx': 'MDX', '.rst': 'reStructuredText' }[extension] ?? extension.replace(/^\./, '').toUpperCase();
784
+ }
785
+ function safeJson(content) {
786
+ try {
787
+ return record(JSON.parse(content));
788
+ }
789
+ catch {
790
+ return undefined;
791
+ }
792
+ }
793
+ function record(value) {
794
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
795
+ }
796
+ function text(value) {
797
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
798
+ }
799
+ function portable(path) { return path.split('\\').join('/'); }
800
+ function isDiscoveryInventory(value, cacheKey) {
801
+ return value?.schemaVersion === DISCOVERY_SCHEMA_VERSION && value.cacheKey === cacheKey && Array.isArray(value.sources);
802
+ }
803
+ function evidencePriority(kind) {
804
+ const index = KIND_PRIORITY.indexOf(kind);
805
+ return index === -1 ? KIND_PRIORITY.length : index;
806
+ }
807
+ /**
808
+ * What the planner must know about the inventory beyond its rows: which
809
+ * files hold the strings readers see, and whether the inventory is partial.
810
+ */
811
+ export function discoveryGuidance(discovery) {
812
+ const lines = [];
813
+ const catalogs = discovery.sources.flatMap((source) => (source.uiLabelCatalogs ?? []).map((path) => `${source.name}: ${path}`));
814
+ if (catalogs.length > 0) {
815
+ lines.push(`UI label catalogs (the exact English strings the product displays; read them before naming any button, tab, field, or menu, and quote the displayed value rather than its key):\n${catalogs.map((item) => `- ${item}`).join('\n')}`);
816
+ }
817
+ const partial = discovery.sources.filter((source) => source.truncated);
818
+ if (partial.length > 0) {
819
+ lines.push(`Partial inventory: ${partial.map((source) => `${source.name} (${source.filesScanned} of ${source.filesAvailable} files)`).join(', ')}. Product code was inventoried first; read the source directly for any surface the inventory may have cut off before deciding it does not exist.`);
820
+ }
821
+ return lines.length ? `${lines.join('\n\n')}\n` : '';
822
+ }
823
+ //# sourceMappingURL=source-discovery.js.map