@doxbrix/doxloop 0.1.4 → 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 +1153 -0
  2. package/README.md +422 -226
  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 +40 -4
  22. package/dist/agents.js +115 -11
  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 +16 -15
  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 +33 -4
  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 +50 -28
  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,9 @@
1
+ import { type DiscoveryEvidence } from './source-discovery.js';
2
+ import type { SourceIntelligenceReport } from './types.js';
3
+ /** Deterministic source health, coverage, and evidence precision report. */
4
+ export declare function buildSourceIntelligence(root: string): Promise<SourceIntelligenceReport>;
5
+ export declare function formatSourceIntelligence(report: SourceIntelligenceReport): string;
6
+ /** Whether a page's text documents one row of an API contract. */
7
+ export declare function contractPartDocumented(signal: Pick<DiscoveryEvidence, 'kind' | 'label'>, content: string): boolean;
8
+ export declare function evidenceMatches(signal: DiscoveryEvidence, identifier: string): boolean;
9
+ //# sourceMappingURL=source-intelligence.d.ts.map
@@ -0,0 +1,306 @@
1
+ import { lstat, readFile } from 'node:fs/promises';
2
+ import { documentedOperations } from './api-coverage.js';
3
+ import { resolve } from 'node:path';
4
+ import { assertPublicContract } from './contract-validation.js';
5
+ import { coverageJourneyId, coverageSignalId, coveragePageId, readCoverageResolutions } from './coverage-resolutions.js';
6
+ import { listDocumentationPlans } from './documentation-plan.js';
7
+ import { readEvidenceMap } from './evidence.js';
8
+ import { matchesGlob } from './globs.js';
9
+ import { computeDrift } from './drift.js';
10
+ import { loadPages, relativePath, loadProject } from './project.js';
11
+ import { sourceHealth } from './source-connectors.js';
12
+ import { KEYWORD_SIGNAL_KINDS, discoverDocumentationSources, signalModule } from './source-discovery.js';
13
+ const SURFACES = [
14
+ { id: 'commands', label: 'Commands', kinds: ['command'], denominator: 'Public commands discovered in configured sources, less explicit plan exclusions.' },
15
+ { id: 'exports', label: 'Exports & schemas', kinds: ['export'], denominator: 'Public exports and OpenAPI schemas discovered in configured sources, less explicit plan exclusions.' },
16
+ { id: 'http-operations', label: 'HTTP operations', kinds: ['operation', 'route'], denominator: 'HTTP routes and OpenAPI operations discovered in configured sources, less explicit plan exclusions.' },
17
+ { id: 'configuration', label: 'Configuration', kinds: ['configuration'], denominator: 'Configuration keys discovered in configured sources, less explicit plan exclusions.' },
18
+ { id: 'security', label: 'Authentication & permissions', kinds: ['authentication', 'authorization'], denominator: 'Authentication, authorization, role, permission, and security surfaces discovered in configured sources, less explicit plan exclusions.' },
19
+ { id: 'errors', label: 'Errors & recovery', kinds: ['error'], denominator: 'Public errors and failure contracts discovered in configured sources, less explicit plan exclusions.' },
20
+ { id: 'events-integrations', label: 'Events & integrations', kinds: ['event', 'integration'], denominator: 'Events, webhooks, connectors, providers, and integrations discovered in configured sources, less explicit plan exclusions.' },
21
+ { id: 'reader-journeys', label: 'Reader journeys', denominator: 'Priority reader outcomes configured in the documentation brief.' },
22
+ { id: 'verified-pages', label: 'Verified pages', denominator: 'All existing documentation pages; verified requires current source evidence and no detected drift.' },
23
+ ];
24
+ /** Deterministic source health, coverage, and evidence precision report. */
25
+ export async function buildSourceIntelligence(root) {
26
+ const project = await loadProject(root);
27
+ const [{ inventory }, rawMap, plans, health, resolutions, files, drift] = await Promise.all([
28
+ discoverDocumentationSources(root),
29
+ readEvidenceMap(root),
30
+ listDocumentationPlans(root),
31
+ sourceHealth(root, project.sources),
32
+ readCoverageResolutions(root),
33
+ loadPages(root, project),
34
+ computeDrift(root, project),
35
+ ]);
36
+ const live = new Set(files.map((file) => relativePath(root, file)));
37
+ const contents = await pageContents(root, files);
38
+ const map = rawMap ? { ...rawMap, pages: Object.fromEntries(Object.entries(rawMap.pages).filter(([page]) => live.has(page))) } : undefined;
39
+ const stale = new Set(drift.pages.map((page) => page.page));
40
+ const currentVerification = (page, evidence) => evidence.confidence === 'verified' && evidence.sources.length > 0 && drift.status !== 'unknown' && !stale.has(page) && evidence.sources.every((entry) => {
41
+ const source = health.find((item) => item.name === entry.source);
42
+ const date = evidence.verifiedOn?.[entry.source];
43
+ const revision = evidence.verifiedAt?.[entry.source];
44
+ // A page is verified against the commit the project is synchronized to.
45
+ // Health reports the provider's live head, which moves on every upstream
46
+ // push; that is drift for the sync report to raise, not a lost verification.
47
+ const baseline = drift.sources.find((item) => item.name === entry.source)?.baseline ?? source?.revision;
48
+ return source?.status !== 'error' && Boolean(source) && (Boolean(revision && baseline === revision) || Boolean(!baseline && date && Number.isFinite(Date.parse(date)) && (!project.sync.maxVerificationAgeDays || Date.now() - Date.parse(date) <= project.sync.maxVerificationAgeDays * 86_400_000)));
49
+ });
50
+ // A partially inventoried source cannot support a coverage percentage the
51
+ // reader should trust, so the cut is reported next to the source itself.
52
+ for (const source of inventory.sources) {
53
+ const item = health.find((entry) => entry.name === source.name);
54
+ if (!item || !source.truncated)
55
+ continue;
56
+ item.status = item.status === 'error' ? 'error' : 'warning';
57
+ item.summary = `${item.summary}; ${source.filesScanned} of ${source.filesAvailable} files inventoried`;
58
+ item.details.push(...source.warnings);
59
+ }
60
+ const plan = plans.find((item) => ['generated', 'generating', 'approved'].includes(item.status));
61
+ const signals = inventory.sources.flatMap((source) => source.evidence);
62
+ const exclusions = new Set(plan?.capabilities.filter((item) => item.disposition === 'excluded').flatMap((item) => [item.id, item.title, ...item.evidence.map((entry) => entry.label ?? entry.path)]) ?? []);
63
+ const signalMetrics = SURFACES.slice(0, 7).map((surface) => coverageForSignals(surface, signals, map, plan?.capabilities ?? [], exclusions, resolutions, contents));
64
+ const journeys = project.documentation.priorityOutcomes ?? [];
65
+ const plannedOutcomes = new Set(plan?.outcomes.map(normalize) ?? []);
66
+ const mappedPages = map ? Object.values(map.pages) : [];
67
+ const pagePaths = Object.keys(map?.pages ?? {});
68
+ const journeyItems = journeys.map((journey) => {
69
+ const id = coverageJourneyId(journey);
70
+ const resolution = resolutions.items[id];
71
+ const linkedPage = resolution?.page && pagePaths.includes(resolution.page) ? resolution.page : undefined;
72
+ const suggestion = linkedPage ? undefined : suggestedPage(journey, pagePaths, plan?.pages ?? []);
73
+ // A journey the generated plan set out to serve, with a page on disk that
74
+ // answers to it, is delivered; asking the reviewer to hand-link every
75
+ // priority outcome left this row at zero on every finished project.
76
+ const delivered = plan?.status === 'generated' && plannedOutcomes.has(normalize(journey)) && Boolean(suggestion);
77
+ const documented = (resolution?.disposition === 'documented' && linkedPage) || (resolution === undefined && delivered);
78
+ return {
79
+ id,
80
+ surface: 'reader-journeys',
81
+ label: journey,
82
+ state: documented ? 'documented' : resolution?.disposition === 'needs-human' ? 'needs-human' : plannedOutcomes.has(normalize(journey)) ? 'planned' : 'uncovered',
83
+ ...(linkedPage ? { page: linkedPage } : documented && suggestion ? { page: suggestion } : {}),
84
+ ...(suggestion && !documented ? { suggestedPage: suggestion } : {}),
85
+ ...(resolution?.reason ? { reason: resolution.reason } : {}),
86
+ };
87
+ });
88
+ const journeyDocumented = journeyItems.filter((item) => item.state === 'documented').length;
89
+ const verifiedItems = [...live].map((page) => ({
90
+ id: coveragePageId(page),
91
+ surface: 'verified-pages',
92
+ label: page,
93
+ state: resolutions.items[coveragePageId(page)]?.disposition === 'needs-human' ? 'needs-human' : map?.pages[page] && currentVerification(page, map.pages[page]) ? 'documented' : stale.has(page) ? 'stale' : 'uncovered',
94
+ page,
95
+ }));
96
+ const verifiedPages = verifiedItems.filter((item) => item.state === 'documented').length;
97
+ const metrics = [
98
+ ...signalMetrics,
99
+ metric(SURFACES[7], journeyDocumented, journeys.length, 0, journeyItems),
100
+ metric(SURFACES[8], verifiedPages, live.size, 0, verifiedItems),
101
+ ];
102
+ const groups = inventory.sources.map((source) => {
103
+ const relevant = source.evidence.filter((item) => ['command', 'export', 'operation', 'route', 'configuration', 'authentication', 'authorization', 'error', 'event', 'integration'].includes(item.kind));
104
+ const included = relevant.filter((item) => !isSignalExcluded(item, exclusions, resolutions));
105
+ const documented = included.filter((item) => isSignalDocumented(item, map, contents)).length;
106
+ return { source: source.name, ...(source.scope ? { scope: source.scope } : {}), documented, total: included.length, percent: percentage(documented, included.length), status: included.length === 0 ? 'unknown' : 'measured' };
107
+ });
108
+ const report = {
109
+ generatedAt: new Date().toISOString(),
110
+ health,
111
+ coverage: {
112
+ metrics,
113
+ groups,
114
+ pages: pagePaths,
115
+ disclaimer: 'Coverage counts existing pages linked to source evidence. Planned pages are shown separately and do not count as documented; verified pages require current evidence. It does not prove that prose, examples, or behavior are correct.',
116
+ },
117
+ evidenceDiagnostics: await evidenceDiagnostics(root, project, rawMap, inventory.sources.flatMap((source) => source.evidence)),
118
+ };
119
+ await assertPublicContract('coverage-v1', report);
120
+ return report;
121
+ }
122
+ export function formatSourceIntelligence(report) {
123
+ const lines = ['Documentation source intelligence', ''];
124
+ for (const item of report.health)
125
+ lines.push(` ${item.status === 'healthy' ? '✓' : item.status === 'warning' ? '!' : '×'} ${item.name}: ${item.summary}`);
126
+ lines.push('', 'Coverage');
127
+ for (const item of report.coverage.metrics)
128
+ lines.push(item.status === 'unknown'
129
+ ? ` ${item.label.padEnd(20)} none detected (discovery completed with no items in this category)`
130
+ : ` ${item.label.padEnd(20)} ${String(item.percent).padStart(3)}% ${item.documented}/${item.total}${item.excluded ? ` (${item.excluded} excluded)` : ''}`);
131
+ lines.push('', ` ${report.coverage.disclaimer}`);
132
+ if (report.evidenceDiagnostics.length) {
133
+ lines.push('', `Evidence precision: ${report.evidenceDiagnostics.length} issue${report.evidenceDiagnostics.length === 1 ? '' : 's'}`);
134
+ for (const issue of report.evidenceDiagnostics)
135
+ lines.push(` ${issue.severity === 'error' ? '×' : '!'} ${issue.page}: ${issue.message}`, ` Suggestion: ${issue.suggestion}`);
136
+ }
137
+ else
138
+ lines.push('', 'Evidence precision: no issues found.');
139
+ return lines.join('\n');
140
+ }
141
+ function coverageForSignals(surface, signals, map, capabilities, exclusions, resolutions, contents = new Map()) {
142
+ const candidates = signals.filter((item) => surface.kinds?.includes(item.kind));
143
+ const excluded = candidates.filter((item) => isSignalExcluded(item, exclusions, resolutions));
144
+ const included = candidates.filter((item) => !excluded.includes(item));
145
+ const documented = included.filter((item) => isSignalDocumented(item, map, contents)).length;
146
+ const items = candidates.map((item) => {
147
+ const id = coverageSignalId(item.source, item.kind, item.path, item.label);
148
+ const resolution = resolutions.items[id];
149
+ const isExcluded = excluded.includes(item);
150
+ const isDocumented = !isExcluded && isSignalDocumented(item, map, contents);
151
+ return {
152
+ id,
153
+ surface: surface.id,
154
+ label: item.label,
155
+ source: item.source,
156
+ path: item.path,
157
+ kind: item.kind,
158
+ state: isExcluded ? 'excluded' : isDocumented ? 'documented' : resolution?.disposition === 'needs-human' ? 'needs-human' : isSignalPlanned(item, capabilities) ? 'planned' : 'uncovered',
159
+ ...(resolution?.page ? { page: resolution.page } : {}),
160
+ ...(resolution?.reason ? { reason: resolution.reason } : {}),
161
+ };
162
+ });
163
+ return metric(surface, documented, included.length, excluded.length, items);
164
+ }
165
+ function isSignalExcluded(signal, exclusions, resolutions) {
166
+ return exclusions.has(signal.label) || exclusions.has(signal.path) || resolutions.items[coverageSignalId(signal.source, signal.kind, signal.path, signal.label)]?.disposition === 'excluded';
167
+ }
168
+ function isSignalPlanned(signal, capabilities) {
169
+ return capabilities.some((capability) => capability.disposition === 'planned' && capability.pageIds.length > 0 && capability.evidence.some((item) => item.source === signal.source && (item.label === signal.label || item.path === signal.path)));
170
+ }
171
+ function isSignalDocumented(signal, map, contents = new Map()) {
172
+ return Object.entries(map?.pages ?? {}).some(([page, evidence]) => evidence.sources.some((entry) => {
173
+ if (entry.source !== signal.source)
174
+ return false;
175
+ const identifiers = [...(entry.paths ?? []), ...(entry.operations ?? [])];
176
+ if (identifiers.some((identifier) => evidenceMatches(signal, identifier)))
177
+ return true;
178
+ // Writers cite the contract file, not each operation. A page that cites
179
+ // the spec documents the operations, schemas, schemes, and error statuses
180
+ // its own text covers, and no others.
181
+ if (!signal.contract || !identifiers.some((identifier) => identifier === signal.path || matchesGlob(signal.path, identifier)))
182
+ return false;
183
+ const content = contents.get(page);
184
+ return content !== undefined && contractPartDocumented(signal, content);
185
+ }));
186
+ }
187
+ /** Whether a page's text documents one row of an API contract. */
188
+ export function contractPartDocumented(signal, content) {
189
+ const operation = (label) => {
190
+ const [method = '', ...rest] = label.split(' ');
191
+ const path = rest.join(' ');
192
+ return documentedOperations(content).includes(`${method} ${path}`) || content.includes(`${method} ${path}`);
193
+ };
194
+ if (signal.kind === 'operation' || signal.kind === 'integration')
195
+ return /^[A-Z]+ \//.test(signal.label) && operation(signal.label);
196
+ if (signal.kind === 'error') {
197
+ const match = /^(\S+ \S+) (\d{3}|\dXX)$/i.exec(signal.label);
198
+ return Boolean(match && operation(match[1]) && new RegExp(`\\b${match[2]}\\b`).test(content));
199
+ }
200
+ const name = signal.label.replace(/^(?:Schema |Webhook )/, '');
201
+ return new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(content);
202
+ }
203
+ async function pageContents(root, files) {
204
+ const contents = new Map();
205
+ await Promise.all(files.map(async (file) => {
206
+ try {
207
+ contents.set(relativePath(root, file), await readFile(file, 'utf8'));
208
+ }
209
+ catch { /* A page removed mid-scan. */ }
210
+ }));
211
+ return contents;
212
+ }
213
+ export function evidenceMatches(signal, identifier) {
214
+ // Keyword signals stand for a folder: a page citing any file in it covers it.
215
+ if (KEYWORD_SIGNAL_KINDS.has(signal.kind)) {
216
+ if (signalModule(identifier) === signal.path || identifier === signal.path || identifier.startsWith(`${signal.path}/`))
217
+ return true;
218
+ }
219
+ const schema = signal.kind === 'export' && (identifier === `schema:${signal.label.replace(/^Schema /, '')}` || identifier === signal.label.replace(/^Schema /, ''));
220
+ // Every row of an API contract shares the spec's path; a page citing the
221
+ // spec file documents only the operations and schemas it names.
222
+ if (signal.contract)
223
+ return identifier === signal.label || schema || (signal.kind === 'error' && signal.label.startsWith(`${identifier} `));
224
+ return identifier === signal.label || identifier === signal.path || matchesGlob(signal.path, identifier) || schema;
225
+ }
226
+ function metric(surface, documented, total, excluded, items) {
227
+ return { id: surface.id, label: surface.label, documented, total, excluded, percent: percentage(documented, total), status: total === 0 ? 'unknown' : 'measured', denominator: surface.denominator, items };
228
+ }
229
+ function percentage(documented, total) { return total === 0 ? 0 : Math.round((documented / total) * 100); }
230
+ async function evidenceDiagnostics(root, project, map, signals) {
231
+ if (!map)
232
+ return [];
233
+ const output = [];
234
+ const sources = new Map(project.sources.map((source) => [source.name, source]));
235
+ for (const [page, evidence] of Object.entries(map.pages)) {
236
+ for (const entry of evidence.sources) {
237
+ const source = sources.get(entry.source);
238
+ if (!source) {
239
+ output.push(diagnostic('error', 'unknown-source', page, `Source "${entry.source}" is not configured.`, 'Remove the binding or reconnect the source.', entry.source));
240
+ continue;
241
+ }
242
+ const identifiers = [...(entry.paths ?? []), ...(entry.operations ?? [])];
243
+ if (!identifiers.length)
244
+ output.push(diagnostic('warning', 'source-only', page, `Evidence names source "${entry.source}" without a precise path or operation.`, 'Record the exact files, exported symbols, or HTTP operations used by this page.', entry.source));
245
+ for (const identifier of identifiers) {
246
+ if (isBroad(identifier))
247
+ output.push(diagnostic('warning', 'broad-pattern', page, `Evidence pattern "${identifier}" is too broad for localized drift.`, 'Replace it with the smallest relevant file, directory, operation, or schema.', entry.source, identifier));
248
+ if (!(await identifierExists(root, source.path, source.kind === 'openapi', entry.source, identifier, signals)))
249
+ output.push(diagnostic('warning', 'deleted-identifier', page, `Evidence identifier "${identifier}" was not found in the current source inventory.`, 'Choose a current identifier or remove claims that depended on the deleted surface.', entry.source, identifier));
250
+ }
251
+ // One relationship check per page and source: a screen component named
252
+ // differently from its guide is normal, so only a page whose evidence
253
+ // shares no vocabulary with it at all is worth a reviewer's attention.
254
+ if (identifiers.length > 0 && identifiers.every((identifier) => weakRelation(page, identifier))) {
255
+ const closest = closestSignal(entry.source, page, signals);
256
+ output.push(diagnostic('warning', 'weak-relation', page, `None of the ${identifiers.length} evidence identifiers from "${entry.source}" share vocabulary with this page.`, `Confirm the relationship${closest ? ` or prefer a closer identifier, such as "${closest}"` : ''}.`, entry.source, identifiers[0]));
257
+ }
258
+ }
259
+ }
260
+ return uniqueDiagnostics(output);
261
+ }
262
+ function diagnostic(severity, code, page, message, suggestion, source, identifier) {
263
+ return { severity, code, page, ...(source ? { source } : {}), ...(identifier ? { identifier } : {}), message, suggestion };
264
+ }
265
+ function isBroad(identifier) { return identifier === '*' || identifier === '**' || identifier === '**/*' || identifier.endsWith('/**') && identifier.split('/').length <= 2; }
266
+ async function identifierExists(root, path, openapi, source, identifier, signals) {
267
+ if (signals.some((item) => item.source === source && evidenceMatches(item, identifier)))
268
+ return true;
269
+ // A literal file wins over pattern matching: Next.js route folders such as
270
+ // `app/(main)/websites/[websiteId]/page.tsx` contain glob characters and
271
+ // were reported as deleted while sitting on disk.
272
+ if (!openapi) {
273
+ try {
274
+ const stat = await lstat(resolve(root, path, identifier));
275
+ if (stat.isFile() || stat.isDirectory())
276
+ return true;
277
+ }
278
+ catch { /* not a literal path; fall through to pattern matching */ }
279
+ }
280
+ if (openapi || /[*?]/.test(identifier))
281
+ return signals.some((item) => item.source === source && (matchesGlob(item.path, identifier) || matchesGlob(item.label, identifier)));
282
+ return false;
283
+ }
284
+ function weakRelation(page, identifier) {
285
+ const pageTokens = tokens(page.replace(/\.[^.]+$/, ''));
286
+ const evidenceTokens = tokens(identifier);
287
+ return pageTokens.length > 0 && evidenceTokens.length > 0 && !pageTokens.some((token) => evidenceTokens.some((candidate) => candidate === token || candidate.startsWith(token) || token.startsWith(candidate)));
288
+ }
289
+ function closestSignal(source, page, signals) {
290
+ const wanted = tokens(page);
291
+ const best = signals.filter((item) => item.source === source).map((item) => ({ label: item.label, score: tokens(item.label).filter((token) => wanted.includes(token)).length })).sort((left, right) => right.score - left.score)[0];
292
+ return best && best.score > 0 ? best.label : undefined;
293
+ }
294
+ /** Lower-case words of a path, title, or symbol, with camelCase split. */
295
+ function tokens(value) { return normalize(value.replace(/([a-z0-9])([A-Z])/g, '$1 $2')).split(/[^a-z0-9]+/).filter((item) => item.length >= 3 && !['docs', 'documentation', 'page', 'index', 'src', 'app', 'main', 'components', 'component', 'lib', 'server', 'client', 'mdx', 'tsx', 'vue'].includes(item)); }
296
+ function normalize(value) { return value.trim().toLowerCase(); }
297
+ function suggestedPage(outcome, pages, plannedPages) {
298
+ const wanted = tokens(outcome);
299
+ const planned = plannedPages.map((page) => ({ page, score: tokens(`${page.title} ${page.purpose}`).filter((token) => wanted.includes(token)).length })).sort((left, right) => right.score - left.score)[0];
300
+ const plannedPath = planned && planned.score > 0 ? pages.find((page) => page.replace(/\.[^.]+$/, '').endsWith(planned.page.path.replace(/\.[^.]+$/, ''))) : undefined;
301
+ if (plannedPath)
302
+ return plannedPath;
303
+ return pages.map((page) => ({ page, score: tokens(page).filter((token) => wanted.includes(token)).length })).sort((left, right) => right.score - left.score)[0]?.page;
304
+ }
305
+ function uniqueDiagnostics(items) { return [...new Map(items.map((item) => [`${item.code}:${item.page}:${item.source ?? ''}:${item.identifier ?? ''}`, item])).values()]; }
306
+ //# sourceMappingURL=source-intelligence.js.map
@@ -0,0 +1,28 @@
1
+ import { type AcceptOptions, type AcceptSelection } from './sync-runs.js';
2
+ import type { SyncFileChange, SyncRun } from './types.js';
3
+ interface ReviewOptions {
4
+ root: string;
5
+ host: string;
6
+ port: number;
7
+ open: boolean;
8
+ }
9
+ export declare function startSyncReview(options: ReviewOptions): Promise<void>;
10
+ export declare function requireSyncReviewChange(run: SyncRun, id: string): SyncFileChange;
11
+ /** Whether the reviewer confirmed replacing files that changed in the project while the agent ran. */
12
+ export declare function syncReviewAcceptOptions(body: unknown): AcceptOptions;
13
+ export declare function syncReviewSelectionsFromBody(run: SyncRun, body: unknown): AcceptSelection[];
14
+ /** Unified diff of the proposal against the file as it stood when it was generated. */
15
+ export declare function syncReviewSourceDiff(root: string, run: SyncRun, change: SyncFileChange): Promise<{
16
+ binary: boolean;
17
+ rows: unknown[];
18
+ added: number;
19
+ removed: number;
20
+ }>;
21
+ /** Rendered before/after comparison for the preview frame. */
22
+ export declare function syncReviewComparisonDocument(root: string, run: SyncRun, change: SyncFileChange, view: {
23
+ layout: 'split' | 'unified';
24
+ onlyChanges: boolean;
25
+ theme?: 'light' | 'dark';
26
+ }): Promise<string>;
27
+ export {};
28
+ //# sourceMappingURL=sync-review.d.ts.map
@@ -0,0 +1,264 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { spawn } from 'node:child_process';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { createServer } from 'node:http';
5
+ import { join, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { DoxloopError } from './errors.js';
8
+ import { pathExists } from './fs.js';
9
+ import { readPage } from './project.js';
10
+ import { unifiedRows } from './review-diff.js';
11
+ import { renderedDiff, renderedDiffDocument } from './review-render.js';
12
+ import { reviewDocument } from './review-ui.js';
13
+ import { acceptSyncChanges, listSyncRuns, readSyncRun, rejectSyncRun, runBeforeRoot, runWorkspace, } from './sync-runs.js';
14
+ const PACKAGE_ROOT = resolve(fileURLToPath(new URL('..', import.meta.url)));
15
+ const DOXBRIX_CSS = resolve(PACKAGE_ROOT, 'assets', 'doxbrix-preview.css');
16
+ const BRAND = {
17
+ '/brand/logo.png': resolve(PACKAGE_ROOT, 'assets', 'brand', 'doxloop-logo-light.png'),
18
+ '/brand/favicon.png': resolve(PACKAGE_ROOT, 'assets', 'brand', 'doxloop-favicon.png'),
19
+ };
20
+ /** Screens the single-page application owns; each one serves the shell. */
21
+ const APP_ROUTES = /^\/(?:runs\/[a-z0-9-]+(?:\/pages\/\d+|\/files\/change-\d+|\/done)?\/?)?$/;
22
+ export async function startSyncReview(options) {
23
+ const token = randomBytes(24).toString('hex');
24
+ const readerCss = await readFile(DOXBRIX_CSS, 'utf8');
25
+ const server = createServer((request, response) => {
26
+ void handle(request, response, options, token, readerCss);
27
+ });
28
+ await new Promise((resolveListen, reject) => {
29
+ server.once('error', reject);
30
+ server.listen(options.port, options.host, resolveListen);
31
+ });
32
+ const url = `http://${shownHost(options.host)}:${options.port}`;
33
+ process.stdout.write(`Doxloop review center: ${url}\nThe actual documentation changes only after you accept a proposal.\nPress Ctrl+C to stop.\n`);
34
+ if (options.open)
35
+ openBrowser(url);
36
+ let stopping = false;
37
+ const stop = async () => {
38
+ if (stopping)
39
+ return;
40
+ stopping = true;
41
+ await new Promise((resolveClose) => server.close(() => resolveClose()));
42
+ };
43
+ process.once('SIGINT', () => void stop());
44
+ process.once('SIGTERM', () => void stop());
45
+ }
46
+ async function handle(request, response, options, token, readerCss) {
47
+ try {
48
+ const url = new URL(request.url ?? '/', `http://${request.headers.host ?? 'localhost'}`);
49
+ if (request.method === 'GET' && APP_ROUTES.test(url.pathname)) {
50
+ send(response, 200, 'text/html; charset=utf-8', reviewDocument(token));
51
+ return;
52
+ }
53
+ if (request.method === 'GET' && url.pathname === '/reader.css') {
54
+ send(response, 200, 'text/css; charset=utf-8', readerCss);
55
+ return;
56
+ }
57
+ const brand = BRAND[url.pathname];
58
+ if (request.method === 'GET' && brand) {
59
+ sendBinary(response, 200, 'image/png', await readFile(brand));
60
+ return;
61
+ }
62
+ if (request.method === 'GET' && url.pathname === '/api/runs') {
63
+ sendJson(response, 200, await listSyncRuns(options.root));
64
+ return;
65
+ }
66
+ const diff = /^\/api\/runs\/([a-z0-9-]+)\/changes\/(change-\d+)\/diff$/.exec(url.pathname);
67
+ if (request.method === 'GET' && diff) {
68
+ const run = await readSyncRun(options.root, diff[1]);
69
+ sendJson(response, 200, await syncReviewSourceDiff(options.root, run, requireSyncReviewChange(run, diff[2])));
70
+ return;
71
+ }
72
+ const apiRun = /^\/api\/runs\/([a-z0-9-]+)$/.exec(url.pathname);
73
+ if (request.method === 'GET' && apiRun) {
74
+ sendJson(response, 200, await readSyncRun(options.root, apiRun[1]));
75
+ return;
76
+ }
77
+ const preview = /^\/preview\/([a-z0-9-]+)\/(change-\d+)$/.exec(url.pathname);
78
+ if (request.method === 'GET' && preview) {
79
+ const run = await readSyncRun(options.root, preview[1]);
80
+ send(response, 200, 'text/html; charset=utf-8', await syncReviewComparisonDocument(options.root, run, requireSyncReviewChange(run, preview[2]), {
81
+ layout: url.searchParams.get('layout') === 'unified' ? 'unified' : 'split',
82
+ onlyChanges: url.searchParams.get('only') === '1',
83
+ theme: url.searchParams.get('theme') === 'dark' ? 'dark' : 'light',
84
+ }));
85
+ return;
86
+ }
87
+ const action = /^\/api\/runs\/([a-z0-9-]+)\/(accept|reject)$/.exec(url.pathname);
88
+ if (request.method === 'POST' && action) {
89
+ requireToken(request, token);
90
+ if (action[2] === 'reject') {
91
+ sendJson(response, 200, await rejectSyncRun(options.root, action[1]));
92
+ return;
93
+ }
94
+ const body = await readBody(request);
95
+ const run = await readSyncRun(options.root, action[1]);
96
+ const selections = syncReviewSelectionsFromBody(run, body);
97
+ sendJson(response, 200, await acceptSyncChanges(options.root, run.id, selections, syncReviewAcceptOptions(body)));
98
+ return;
99
+ }
100
+ sendJson(response, 404, { error: 'Not found' });
101
+ }
102
+ catch (error) {
103
+ sendJson(response, error instanceof DoxloopError ? 409 : 500, {
104
+ error: error instanceof Error ? error.message : String(error),
105
+ });
106
+ }
107
+ }
108
+ export function requireSyncReviewChange(run, id) {
109
+ const change = run.changes.find((candidate) => candidate.id === id);
110
+ if (!change)
111
+ throw new DoxloopError('The requested documentation change does not exist.');
112
+ return change;
113
+ }
114
+ /** Whether the reviewer confirmed replacing files that changed in the project while the agent ran. */
115
+ export function syncReviewAcceptOptions(body) {
116
+ const value = body && typeof body === 'object' ? body : {};
117
+ return value.confirmChangedDuringRun === true ? { confirmChangedDuringRun: true } : {};
118
+ }
119
+ export function syncReviewSelectionsFromBody(run, body) {
120
+ if (!body || typeof body !== 'object')
121
+ throw new DoxloopError('Invalid review action.');
122
+ const value = body;
123
+ if (value.scope === 'folder' && typeof value.folder === 'string') {
124
+ const folder = value.folder.replace(/\\/g, '/').replace(/\/$/, '');
125
+ if (!folder || folder.startsWith('/') || folder.split('/').some((part) => part === '..' || part === '.'))
126
+ throw new DoxloopError('Select a valid proposal folder.');
127
+ const selections = run.changes.filter((change) => change.path.startsWith(`${folder}/`) && change.hunks.some((hunk) => !hunk.acceptedAt && !hunk.rejectedAt)).map((change) => ({ changeId: change.id, hunkIds: change.hunks.filter((hunk) => !hunk.acceptedAt && !hunk.rejectedAt).map((hunk) => hunk.id) }));
128
+ if (!selections.length)
129
+ throw new DoxloopError('No pending changes in this folder.');
130
+ return selections;
131
+ }
132
+ if (value.scope === 'all') {
133
+ return run.changes.map((change) => ({ changeId: change.id }));
134
+ }
135
+ if (typeof value.changeId !== 'string') {
136
+ throw new DoxloopError('Select a documentation page or change.');
137
+ }
138
+ if (value.scope === 'page')
139
+ return [{ changeId: value.changeId }];
140
+ if (value.scope === 'hunk' && typeof value.hunkId === 'string') {
141
+ return [{ changeId: value.changeId, hunkIds: [value.hunkId] }];
142
+ }
143
+ throw new DoxloopError('Invalid review selection.');
144
+ }
145
+ /** Unified diff of the proposal against the file as it stood when it was generated. */
146
+ export async function syncReviewSourceDiff(root, run, change) {
147
+ if (change.binary)
148
+ return { binary: true, rows: [], added: 0, removed: 0 };
149
+ const beforePath = join(runBeforeRoot(root, run.id), change.path);
150
+ const before = (await pathExists(beforePath)) ? await readFile(beforePath, 'utf8') : '';
151
+ const diff = unifiedRows(before, change.hunks);
152
+ return { binary: false, rows: diff.rows, added: diff.added, removed: diff.removed };
153
+ }
154
+ /** Rendered before/after comparison for the preview frame. */
155
+ export async function syncReviewComparisonDocument(root, run, change, view) {
156
+ const beforePath = join(runBeforeRoot(root, run.id), change.path);
157
+ const afterPath = join(runWorkspace(root, run.id), change.path);
158
+ const before = await readSide(beforePath, change);
159
+ const after = await readSide(afterPath, change);
160
+ const notice = change.category === 'page' && !change.binary
161
+ ? undefined
162
+ : 'This file has no rendered form. The source diff shows it line by line.';
163
+ return renderedDiffDocument({
164
+ diff: renderedDiff({ before, after }),
165
+ layout: view.layout,
166
+ onlyChanges: view.onlyChanges,
167
+ theme: view.theme ?? 'light',
168
+ ...(notice ? { notice } : {}),
169
+ });
170
+ }
171
+ async function readSide(path, change) {
172
+ if (!(await pathExists(path))) {
173
+ return { title: change.title, description: '', body: '', exists: false };
174
+ }
175
+ if (change.binary) {
176
+ return {
177
+ title: change.title,
178
+ description: '',
179
+ body: `_Binary file \`${change.path}\` — accept the file to apply it._`,
180
+ exists: true,
181
+ };
182
+ }
183
+ if (change.category !== 'page') {
184
+ // Configuration and evidence files have no rendered form; an indented code
185
+ // block shows them verbatim without colliding with fences in the content.
186
+ const text = await readFile(path, 'utf8');
187
+ return {
188
+ title: change.title,
189
+ description: change.path,
190
+ body: text.split('\n').map((line) => ` ${line}`).join('\n'),
191
+ exists: true,
192
+ };
193
+ }
194
+ try {
195
+ const page = await readPage(path);
196
+ return {
197
+ title: page.title || change.title,
198
+ description: page.description ?? '',
199
+ body: page.body,
200
+ exists: true,
201
+ };
202
+ }
203
+ catch {
204
+ return {
205
+ title: change.title,
206
+ description: '',
207
+ body: await readFile(path, 'utf8').catch(() => ''),
208
+ exists: true,
209
+ };
210
+ }
211
+ }
212
+ async function readBody(request) {
213
+ const chunks = [];
214
+ let size = 0;
215
+ for await (const chunk of request) {
216
+ const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
217
+ size += value.length;
218
+ if (size > 1_000_000)
219
+ throw new DoxloopError('Review request is too large.');
220
+ chunks.push(value);
221
+ }
222
+ try {
223
+ return JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
224
+ }
225
+ catch {
226
+ throw new DoxloopError('Review request is not valid JSON.');
227
+ }
228
+ }
229
+ function requireToken(request, token) {
230
+ if (request.headers['x-doxloop-review-token'] !== token) {
231
+ throw new DoxloopError('Invalid local review token.');
232
+ }
233
+ }
234
+ function sendJson(response, status, value) {
235
+ send(response, status, 'application/json; charset=utf-8', JSON.stringify(value));
236
+ }
237
+ function send(response, status, type, body) {
238
+ response.writeHead(status, {
239
+ 'Content-Type': type,
240
+ 'Cache-Control': 'no-store',
241
+ 'X-Content-Type-Options': 'nosniff',
242
+ 'Referrer-Policy': 'no-referrer',
243
+ });
244
+ response.end(body);
245
+ }
246
+ function sendBinary(response, status, type, body) {
247
+ response.writeHead(status, {
248
+ 'Content-Type': type,
249
+ 'Cache-Control': 'no-store',
250
+ 'X-Content-Type-Options': 'nosniff',
251
+ 'Referrer-Policy': 'no-referrer',
252
+ });
253
+ response.end(body);
254
+ }
255
+ function shownHost(host) {
256
+ return host === '0.0.0.0' ? 'localhost' : host;
257
+ }
258
+ function openBrowser(url) {
259
+ const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
260
+ const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
261
+ const child = spawn(command, args, { detached: true, stdio: 'ignore' });
262
+ child.unref();
263
+ }
264
+ //# sourceMappingURL=sync-review.js.map