@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,19 @@
1
+ export interface DoxbrixBuildOptions {
2
+ root: string;
3
+ outDir?: string;
4
+ /** Origin-relative mount point, for example `/repository`. */
5
+ basePath?: string;
6
+ /** Public origin used for canonical URLs and sitemap entries. */
7
+ siteUrl?: string;
8
+ clean?: boolean;
9
+ }
10
+ export interface StaticBuildResult {
11
+ outputDir: string;
12
+ pages: number;
13
+ assets: number;
14
+ basePath: string;
15
+ siteUrl?: string;
16
+ }
17
+ /** Build the built-in Doxbrix reader into portable static files. */
18
+ export declare function buildDoxbrixStaticSite(options: DoxbrixBuildOptions): Promise<StaticBuildResult>;
19
+ //# sourceMappingURL=doxbrix-build.d.ts.map
@@ -0,0 +1,167 @@
1
+ import { readRedirects } from './page-operations.js';
2
+ import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import { dirname, join, relative, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import matter from 'gray-matter';
6
+ import { renderMarkdown } from './doxbrix-markdown.js';
7
+ import { DoxloopError } from './errors.js';
8
+ import { listFiles, resolveContainedDirectory } from './fs.js';
9
+ import { loadQualityConfig } from './quality-config.js';
10
+ import { reverifyClaims } from './quality-claims.js';
11
+ import { loadPages, loadProject, loadSiteConfig, pageId, ROOT_CONTENT_IGNORED_DIRECTORIES, } from './project.js';
12
+ import { doxbrixDocument, firstSitePage, redirectTarget } from './preview.js';
13
+ const PACKAGE_ROOT = resolve(fileURLToPath(new URL('..', import.meta.url)));
14
+ const DOXBRIX_CSS = resolve(PACKAGE_ROOT, 'assets', 'doxbrix-preview.css');
15
+ const ASSET_EXTENSIONS = new Set([
16
+ '.avif', '.bmp', '.eot', '.gif', '.ico', '.jpeg', '.jpg', '.mov', '.mp3',
17
+ '.mp4', '.ogg', '.otf', '.pdf', '.png', '.svg', '.ttf', '.wav', '.webm',
18
+ '.webp', '.woff', '.woff2',
19
+ ]);
20
+ /** Build the built-in Doxbrix reader into portable static files. */
21
+ export async function buildDoxbrixStaticSite(options) {
22
+ const root = resolve(options.root);
23
+ const project = await loadProject(root);
24
+ if (project.generator !== 'doxbrix') {
25
+ throw new DoxloopError('The built-in static builder only supports Doxbrix projects.');
26
+ }
27
+ const outputDir = resolve(options.outDir ?? join(root, 'build'));
28
+ if (outputDir === root) {
29
+ throw new DoxloopError('Doxbrix build output cannot replace the project directory.');
30
+ }
31
+ const contentRoot = await resolveContainedDirectory(root, project.contentDir, 'Build content directory', { allowRoot: true });
32
+ if (options.clean !== false)
33
+ await rm(outputDir, { recursive: true, force: true });
34
+ await mkdir(join(outputDir, '__doxloop'), { recursive: true });
35
+ const [paths, site, css, qualityConfig, verification] = await Promise.all([
36
+ loadPages(root, project),
37
+ loadSiteConfig(root, project),
38
+ readFile(DOXBRIX_CSS, 'utf8'),
39
+ loadQualityConfig(root),
40
+ loadQualityConfig(root).then((config) => config.readerVerification?.enabled ? reverifyClaims(root, project, false).then((result) => result.metadata) : undefined),
41
+ ]);
42
+ if (paths.length === 0)
43
+ throw new DoxloopError('Doxbrix static build found no pages.');
44
+ const basePath = normalizeBasePath(options.basePath);
45
+ const siteUrl = publicSiteUrl(options.siteUrl, site.site);
46
+ const search = [];
47
+ const sitemap = [];
48
+ const ids = new Set(paths.map((path) => pageId(contentRoot, path)));
49
+ const navigationHome = firstSitePage(site);
50
+ const firstId = navigationHome && ids.has(navigationHome) ? navigationHome : pageId(contentRoot, paths[0]);
51
+ for (const path of paths) {
52
+ const id = pageId(contentRoot, path);
53
+ const parsed = matter(await readFile(path, 'utf8'));
54
+ const title = text(parsed.data.title) || labelFromId(id);
55
+ const description = text(parsed.data.description);
56
+ const href = `${basePath}/${id}`;
57
+ const canonical = text(parsed.data.canonical) || (siteUrl ? new URL(`${id}/`, trailingSlash(siteUrl)).toString() : undefined);
58
+ const socialImage = resolvePublicReference(text(parsed.data.socialImage), siteUrl);
59
+ const verificationEntry = qualityConfig.readerVerification?.enabled
60
+ ? verification?.pages[relative(root, path).replace(/\\/g, '/')]
61
+ : undefined;
62
+ const html = doxbrixDocument({
63
+ site,
64
+ title,
65
+ ...(description ? { description } : {}),
66
+ ...(canonical ? { canonical } : {}),
67
+ ...(socialImage ? { socialImage } : {}),
68
+ current: id,
69
+ rendered: renderMarkdown(parsed.content),
70
+ ...(verificationEntry ? { verification: verificationEntry } : {}),
71
+ basePath,
72
+ liveReload: false,
73
+ });
74
+ const pageOutput = join(outputDir, ...id.split('/'), 'index.html');
75
+ await mkdir(dirname(pageOutput), { recursive: true });
76
+ await writeFile(pageOutput, html, 'utf8');
77
+ if (id.endsWith('/index'))
78
+ await writeFile(join(outputDir, ...id.split('/').slice(0, -1), 'index.html'), html, 'utf8');
79
+ search.push({ title, description: description ?? '', href, text: markdownSearchText(parsed.content) });
80
+ if (siteUrl)
81
+ sitemap.push(new URL(`${id}/`, trailingSlash(siteUrl)).toString());
82
+ }
83
+ // Write last so an unlisted index page cannot replace the navigation homepage.
84
+ await cp(join(outputDir, ...firstId.split('/'), 'index.html'), join(outputDir, 'index.html'));
85
+ const builtPages = new Map([...ids].map((id) => [id, id]));
86
+ for (const [from, to] of Object.entries(await readRedirects(root))) {
87
+ // A redirect stub is only worth writing when the page it points at exists in this build.
88
+ if (to !== '/' && !redirectTarget(to, site, builtPages))
89
+ continue;
90
+ const target = `${basePath}${to}`;
91
+ const output = join(outputDir, from.replace(/^\//, ''), 'index.html');
92
+ await mkdir(dirname(output), { recursive: true });
93
+ await writeFile(output, `<!doctype html><meta charset="utf-8"><meta http-equiv="refresh" content="0; url=${target}"><link rel="canonical" href="${target}"><a href="${target}">This page has moved</a>`, 'utf8');
94
+ }
95
+ await writeFile(join(outputDir, '__doxloop', 'doxbrix.css'), css, 'utf8');
96
+ await writeFile(join(outputDir, '__doxloop', 'search-index'), JSON.stringify(search), 'utf8');
97
+ await writeFile(join(outputDir, '__doxloop', 'search-index.json'), `${JSON.stringify(search, null, 2)}\n`, 'utf8');
98
+ await writeFile(join(outputDir, 'sitemap.xml'), sitemapXml(sitemap), 'utf8');
99
+ await writeFile(join(outputDir, 'robots.txt'), robotsTxt(siteUrl), 'utf8');
100
+ let assets = 0;
101
+ const ignoredDirectories = new Set(ROOT_CONTENT_IGNORED_DIRECTORIES);
102
+ ignoredDirectories.add(relative(contentRoot, outputDir).split(/[\\/]/)[0] || 'build');
103
+ for (const asset of await listFiles(contentRoot, ASSET_EXTENSIONS, { ignoredDirectories })) {
104
+ const destination = join(outputDir, relative(contentRoot, asset));
105
+ await mkdir(dirname(destination), { recursive: true });
106
+ await cp(asset, destination);
107
+ assets++;
108
+ }
109
+ return {
110
+ outputDir,
111
+ pages: paths.length,
112
+ assets,
113
+ basePath,
114
+ ...(siteUrl ? { siteUrl } : {}),
115
+ };
116
+ }
117
+ function publicSiteUrl(explicit, site) {
118
+ const configured = explicit ?? process.env.DOXLOOP_SITE_URL ?? text(site?.url);
119
+ if (!configured)
120
+ return undefined;
121
+ let url;
122
+ try {
123
+ url = new URL(configured);
124
+ }
125
+ catch {
126
+ throw new DoxloopError(`Invalid static site URL: ${configured}`);
127
+ }
128
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
129
+ throw new DoxloopError('Static site URL must be an HTTP(S) URL without credentials, query, or fragment.');
130
+ }
131
+ return url.toString();
132
+ }
133
+ function resolvePublicReference(value, siteUrl) {
134
+ if (!value)
135
+ return undefined;
136
+ if (/^https?:\/\//i.test(value) || !siteUrl)
137
+ return value;
138
+ return new URL(value.replace(/^\//, ''), trailingSlash(siteUrl)).toString();
139
+ }
140
+ function normalizeBasePath(value = '') {
141
+ const trimmed = value.trim();
142
+ if (!trimmed || trimmed === '/')
143
+ return '';
144
+ if (/^[a-z][a-z\d+.-]*:/i.test(trimmed) || trimmed.startsWith('//')) {
145
+ throw new DoxloopError('Static build base path must be an origin-relative URL path.');
146
+ }
147
+ const normalized = `/${trimmed.replace(/^\/+|\/+$/g, '')}`;
148
+ if (normalized.split('/').some((part) => part === '..')) {
149
+ throw new DoxloopError('Static build base path cannot leave its URL root.');
150
+ }
151
+ return normalized;
152
+ }
153
+ function sitemapXml(urls) {
154
+ const body = urls.map((url) => ` <url><loc>${xml(url)}</loc></url>`).join('\n');
155
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${body}${body ? '\n' : ''}</urlset>\n`;
156
+ }
157
+ function robotsTxt(siteUrl) {
158
+ return `User-agent: *\nAllow: /\n${siteUrl ? `Sitemap: ${new URL('sitemap.xml', trailingSlash(siteUrl)).toString()}\n` : ''}`;
159
+ }
160
+ function trailingSlash(value) { return value.endsWith('/') ? value : `${value}/`; }
161
+ function text(value) { return typeof value === 'string' && value.trim() ? value.trim() : undefined; }
162
+ function xml(value) { return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&apos;'); }
163
+ function labelFromId(id) { return id.split('/').at(-1).replace(/[-_]+/g, ' ').replace(/\b\w/g, (letter) => letter.toUpperCase()); }
164
+ function markdownSearchText(markdown) {
165
+ return markdown.replace(/```[\s\S]*?```/g, ' ').replace(/`([^`]+)`/g, '$1').replace(/!\[[^\]]*\]\([^)]+\)/g, ' ').replace(/\[([^\]]+)\]\([^)]+\)/g, '$1').replace(/<[^>]+>/g, ' ').replace(/[#>*_~|=-]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 12_000);
166
+ }
167
+ //# sourceMappingURL=doxbrix-build.js.map
@@ -16,5 +16,14 @@ export interface RenderResult {
16
16
  html: string;
17
17
  toc: TocEntry[];
18
18
  }
19
+ export interface EditableBlock {
20
+ start: number;
21
+ end: number;
22
+ text: string;
23
+ }
24
+ /** Map editable rendered regions to exact source spans, including repeated blocks and CRLF. */
25
+ export declare function renderEditableMarkdown(md: string): RenderResult & {
26
+ blocks: EditableBlock[];
27
+ };
19
28
  export declare function renderMarkdown(md: string): RenderResult;
20
29
  //# sourceMappingURL=doxbrix-markdown.d.ts.map
@@ -18,6 +18,53 @@ const uid = () => `t${(uidCounter++).toString(36)}`;
18
18
  let tocEntries = [];
19
19
  const slugCounts = new Map();
20
20
  let renderDepth = 0;
21
+ let editableSource;
22
+ let editableBlocks = [];
23
+ let originalEditableSource = '';
24
+ let editableOffsets = [];
25
+ let editableCursor = 0;
26
+ let editScope;
27
+ /** Map editable rendered regions to exact source spans, including repeated blocks and CRLF. */
28
+ export function renderEditableMarkdown(md) {
29
+ originalEditableSource = md;
30
+ editableSource = md.replace(/\r\n/g, '\n');
31
+ editableOffsets = [];
32
+ for (let i = 0; i < md.length; i++) {
33
+ editableOffsets.push(i);
34
+ if (md[i] === '\r' && md[i + 1] === '\n')
35
+ i++;
36
+ }
37
+ editableOffsets.push(md.length);
38
+ editableCursor = 0;
39
+ editScope = undefined;
40
+ editableBlocks = [];
41
+ try {
42
+ return { ...renderMarkdown(md), blocks: [...editableBlocks] };
43
+ }
44
+ finally {
45
+ editableSource = undefined;
46
+ editableBlocks = [];
47
+ editableOffsets = [];
48
+ editScope = undefined;
49
+ }
50
+ }
51
+ function editAttributes(text, code = false) {
52
+ if (!editableSource || !text.trim())
53
+ return '';
54
+ const from = editScope?.cursor ?? 0;
55
+ const start = editableSource.indexOf(text, from);
56
+ if (start < 0 || (editScope ? start + text.length > editScope.end : editableSource.indexOf(text, start + 1) !== -1))
57
+ return '';
58
+ if (editScope)
59
+ editScope.cursor = start + text.length;
60
+ const sourceStart = editableOffsets[start];
61
+ const sourceEnd = editableOffsets[start + text.length];
62
+ if (editableBlocks.some((block) => sourceStart < block.end && sourceEnd > block.start))
63
+ return '';
64
+ const original = originalEditableSource.slice(sourceStart, sourceEnd);
65
+ editableBlocks.push({ start: sourceStart, end: sourceEnd, text: original });
66
+ return ` data-edit-start="${sourceStart}" data-edit-end="${sourceEnd}" data-edit-format="${code ? 'plain' : 'markdown'}" data-edit-source="${escAttr(original)}"`;
67
+ }
21
68
  export function renderMarkdown(md) {
22
69
  uidCounter = 0;
23
70
  tocEntries = [];
@@ -417,7 +464,7 @@ function codeBlock(code, lang) {
417
464
  catch {
418
465
  highlighted = esc(code);
419
466
  }
420
- return `<div class="dp-code-block dp-code-theme-auto dp-code-theme-light"><div class="dp-code-header"><span class="dp-code-lang">${esc(lang || 'plaintext')}</span><button class="dp-code-copy-btn" type="button" aria-label="Copy code">${copyIcon()}<span>Copy</span></button></div><pre class="dp-code-pre dp-hl-auto dp-hl-light"><code>${highlighted}</code></pre></div>`;
467
+ return `<div class="dp-code-block dp-code-theme-auto dp-code-theme-light"><div class="dp-code-header"><span class="dp-code-lang">${esc(lang || 'plaintext')}</span><button class="dp-code-copy-btn" type="button" aria-label="Copy code">${copyIcon()}<span>Copy</span></button></div><pre class="dp-code-pre dp-hl-auto dp-hl-light"><code${editAttributes(code, true)}>${highlighted}</code></pre></div>`;
421
468
  }
422
469
  function copyIcon() {
423
470
  return '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
@@ -675,7 +722,26 @@ function responseExample(_props, inner) {
675
722
  // Markdown chunk renderer.
676
723
  // ---------------------------------------------------------------------------
677
724
  function renderMarkdownChunk(md, top) {
725
+ const previous = editScope;
726
+ const start = editableSource?.indexOf(md, editableCursor) ?? -1;
727
+ editScope = start >= 0 ? { start, end: start + md.length, cursor: start } : undefined;
728
+ if (start >= 0)
729
+ editableCursor = start + md.length;
730
+ try {
731
+ return renderMarkdownLines(md, top);
732
+ }
733
+ finally {
734
+ editScope = previous;
735
+ }
736
+ }
737
+ function renderMarkdownLines(md, top) {
678
738
  const lines = md.split('\n');
739
+ const lineOffsets = [];
740
+ let offset = 0;
741
+ for (const line of lines) {
742
+ lineOffsets.push(offset);
743
+ offset += line.length + 1;
744
+ }
679
745
  const out = [];
680
746
  let i = 0;
681
747
  let guard = 0;
@@ -685,10 +751,14 @@ function renderMarkdownChunk(md, top) {
685
751
  break;
686
752
  }
687
753
  const line = lines[i];
688
- const fence = /^\s*(```|~~~)(\w*)\s*$/.exec(line);
754
+ if (editScope)
755
+ editScope.cursor = editScope.start + lineOffsets[i];
756
+ const fence = /^\s*(```+|~~~+)\s*([^\s`~]*)(?:\s+.*)?$/.exec(line);
689
757
  if (fence) {
690
758
  const lang = fence[2] ?? '';
691
759
  const body = [];
760
+ if (editScope)
761
+ editScope.cursor = editScope.start + lineOffsets[i] + line.length + 1;
692
762
  i++;
693
763
  while (i < lines.length && !/^\s*(```|~~~)\s*$/.test(lines[i]))
694
764
  body.push(lines[i++]);
@@ -712,8 +782,10 @@ function renderMarkdownChunk(md, top) {
712
782
  i++;
713
783
  continue;
714
784
  }
715
- const h = /^(#{1,6})\s+(.*)$/.exec(line);
785
+ const h = /^\s*(#{1,6})\s+(.*)$/.exec(line);
716
786
  if (h) {
787
+ if (editScope)
788
+ editScope.cursor += line.indexOf(h[2], h[1].length);
717
789
  out.push(heading(h[1].length, h[2], top));
718
790
  i++;
719
791
  continue;
@@ -742,10 +814,14 @@ function renderMarkdownChunk(md, top) {
742
814
  if (/^\s*([-*+]|\d+\.)\s+/.test(line)) {
743
815
  const ordered = /^\s*\d+\.\s+/.test(line);
744
816
  const items = [];
817
+ const itemStarts = [];
745
818
  while (i < lines.length && /^\s*([-*+]|\d+\.)\s+/.test(lines[i])) {
746
- items.push(lines[i++].replace(/^\s*([-*+]|\d+\.)\s+/, ''));
819
+ const item = lines[i].replace(/^\s*([-*+]|\d+\.)\s+/, '');
820
+ itemStarts.push((editScope?.start ?? 0) + lineOffsets[i] + lines[i].length - item.length);
821
+ items.push(item);
822
+ i++;
747
823
  }
748
- out.push(renderList(items, ordered));
824
+ out.push(renderList(items, ordered, itemStarts));
749
825
  continue;
750
826
  }
751
827
  // Lone-image paragraph -> block image (matches the reader's image block).
@@ -761,8 +837,11 @@ function renderMarkdownChunk(md, top) {
761
837
  !/^\s*(#{1,6}\s|>\s?|```|~~~|<!--|<\/[A-Za-z]|([-*+]|\d+\.)\s+)/.test(lines[i])) {
762
838
  para.push(lines[i++]);
763
839
  }
840
+ // A malformed block opener must still advance, preserving text below it.
841
+ if (!para.length)
842
+ para.push(lines[i++]);
764
843
  if (para.length)
765
- out.push(`<p class="dp-p">${inline(para.join(' '))}</p>`);
844
+ out.push(`<p class="dp-p"${editAttributes(para.join('\n'))}>${inline(para.join(' '))}</p>`);
766
845
  }
767
846
  return out.join('\n');
768
847
  }
@@ -772,31 +851,39 @@ function heading(level, text, top) {
772
851
  if (top && (lvl === 2 || lvl === 3 || lvl === 4)) {
773
852
  tocEntries.push({ id, level: lvl, title: stripInline(text) });
774
853
  }
775
- return `<h${lvl} id="${escAttr(id)}" class="dp-h${lvl}">${inline(text)}</h${lvl}>`;
854
+ return `<h${lvl} id="${escAttr(id)}" class="dp-h${lvl}"${editAttributes(text)}>${inline(text)}</h${lvl}>`;
776
855
  }
777
- function renderList(items, ordered) {
856
+ function renderList(items, ordered, starts) {
778
857
  const isTask = items.some((it) => /^\[[ xX]\]\s+/.test(it));
779
858
  if (isTask) {
780
859
  const lis = items
781
- .map((it) => {
860
+ .map((it, index) => {
861
+ if (editScope)
862
+ editScope.cursor = starts[index];
782
863
  const m = /^\[([ xX])\]\s+(.*)$/.exec(it);
783
864
  const checked = m && m[1] !== ' ';
784
865
  const icon = `<span class="dp-task-check-icon${checked ? ' checked' : ''}"></span>`;
785
866
  const txt = m ? m[2] : it;
867
+ if (editScope)
868
+ editScope.cursor += it.length - txt.length;
786
869
  const style = checked ? ' style="text-decoration:line-through;color:#9ca3af"' : '';
787
- return `<li class="dp-task-item">${icon}<span${style}>${inline(txt)}</span></li>`;
870
+ return `<li class="dp-task-item">${icon}<span${style}${editAttributes(txt)}>${inline(txt)}</span></li>`;
788
871
  })
789
872
  .join('');
790
873
  return `<ul class="dp-task-list">${lis}</ul>`;
791
874
  }
792
875
  const tag = ordered ? 'ol' : 'ul';
793
876
  const cls = ordered ? 'dp-ol' : 'dp-ul';
794
- const lis = items.map((it) => `<li>${inline(it)}</li>`).join('');
877
+ const lis = items.map((it, index) => {
878
+ if (editScope)
879
+ editScope.cursor = starts[index];
880
+ return `<li${editAttributes(it)}>${inline(it)}</li>`;
881
+ }).join('');
795
882
  return `<${tag} class="${cls}">${lis}</${tag}>`;
796
883
  }
797
884
  function renderTable(header, rows) {
798
- const head = `<tr>${header.map((c) => `<th>${inline(c)}</th>`).join('')}</tr>`;
799
- const body = rows.map((r) => `<tr>${r.map((c) => `<td>${inline(c)}</td>`).join('')}</tr>`).join('');
885
+ const head = `<tr>${header.map((c) => `<th${editAttributes(c)}>${inline(c)}</th>`).join('')}</tr>`;
886
+ const body = rows.map((r) => `<tr>${r.map((c) => `<td${editAttributes(c)}>${inline(c)}</td>`).join('')}</tr>`).join('');
800
887
  return `<div class="dp-table-wrap"><table class="dp-table"><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
801
888
  }
802
889
  // ---------------------------------------------------------------------------
@@ -809,7 +896,7 @@ function inline(text) {
809
896
  const html = tag === 'Badge'
810
897
  ? `<span class="dp-inline-badge">${esc(str(props.text) || (body ?? '').trim())}</span>`
811
898
  : `<span class="dp-inline-badge">${esc(iconGlyph(str(props.name)))}</span>`;
812
- tokens.push(html);
899
+ tokens.push(editableSource !== undefined ? `<span contenteditable="false" data-md-atom="${escAttr(_m)}">${html}</span>` : html);
813
900
  return `${TOK_OPEN}${tokens.length - 1}${TOK_CLOSE}`;
814
901
  });
815
902
  s = esc(s);
@@ -821,6 +908,20 @@ function inline(text) {
821
908
  s = s.replace(/~~([^~]+)~~/g, '<s>$1</s>');
822
909
  s = s.replace(/(^|[^*])\*([^*]+)\*/g, '$1<em>$2</em>');
823
910
  s = s.replace(/(^|[^_\w])_([^_]+)_/g, '$1<em>$2</em>');
911
+ if (editableSource !== undefined) {
912
+ const links = [...text.matchAll(/(?<!!)\[([^\]]+)\](\([^)\s]+[^)]*\))/g)];
913
+ let linkIndex = 0;
914
+ s = s.replace(/<a class="df-inline-link"[^>]*>/g, (tag) => {
915
+ const link = links[linkIndex++];
916
+ return link ? tag.replace(/>$/, ` data-md-suffix="${escAttr(']' + link[2])}">`) : tag;
917
+ });
918
+ const images = [...text.matchAll(/!\[([^\]]*)\]\([^)\s]+[^)]*\)/g)];
919
+ let imageIndex = 0;
920
+ s = s.replace(/<img [^>]*\/>/g, (tag) => {
921
+ const source = images[imageIndex++]?.[0];
922
+ return source ? `<span contenteditable="false" data-md-atom="${escAttr(source)}">${tag}</span>` : tag;
923
+ });
924
+ }
824
925
  if (tokens.length) {
825
926
  s = s.replace(new RegExp(`${TOK_OPEN}(\\d+)${TOK_CLOSE}`, 'g'), (_m, k) => tokens[Number(k)] ?? '');
826
927
  }
@@ -0,0 +1,10 @@
1
+ import type { DoxloopProject, DriftResult, SourceChange } from './types.js';
2
+ /**
3
+ * Answer "which documentation pages no longer match the product?" without
4
+ * starting an agent. Detection is deterministic and safe to run on a schedule
5
+ * or in continuous integration.
6
+ */
7
+ export declare function computeDrift(root: string, project: DoxloopProject): Promise<DriftResult>;
8
+ export declare function computeDriftFromChanges(root: string, project: DoxloopProject, changes: SourceChange[]): Promise<DriftResult>;
9
+ export declare function formatDrift(result: DriftResult): string;
10
+ //# sourceMappingURL=drift.d.ts.map
package/dist/drift.js ADDED
@@ -0,0 +1,164 @@
1
+ import { pagesForChange, readEvidenceMap } from './evidence.js';
2
+ import { isWatchedPath } from './globs.js';
3
+ import { changedSourcePaths, collectSourceChanges, readSyncState } from './sync.js';
4
+ /**
5
+ * Answer "which documentation pages no longer match the product?" without
6
+ * starting an agent. Detection is deterministic and safe to run on a schedule
7
+ * or in continuous integration.
8
+ */
9
+ export async function computeDrift(root, project) {
10
+ const changes = await collectSourceChanges(root, project.sources);
11
+ return computeDriftFromChanges(root, project, changes);
12
+ }
13
+ export async function computeDriftFromChanges(root, project, changes) {
14
+ const map = await readEvidenceMap(root);
15
+ const notes = [];
16
+ const sources = [];
17
+ const reasonsByPage = new Map();
18
+ let unresolved = false;
19
+ for (const change of changes) {
20
+ const rawPaths = changedSourcePaths(change);
21
+ const changedPaths = rawPaths.filter((path) => isWatchedPath(path, project.sync.watch, project.sync.ignore));
22
+ sources.push({
23
+ name: change.name,
24
+ path: change.path,
25
+ kind: change.kind,
26
+ changedPaths,
27
+ filteredPaths: rawPaths.length - changedPaths.length,
28
+ ...('baseline' in change ? { baseline: change.baseline } : {}),
29
+ ...('head' in change ? { head: change.head } : {}),
30
+ ...(change.scope ? { scope: change.scope } : {}),
31
+ });
32
+ const note = unresolvedNote(change);
33
+ if (note) {
34
+ notes.push(note);
35
+ unresolved = true;
36
+ continue;
37
+ }
38
+ if (changedPaths.length === 0)
39
+ continue;
40
+ if (!map) {
41
+ notes.push(`Source "${change.name}" changed, but no evidence map exists yet, so affected pages cannot be named. Run \`doxloop update\` to build one.`);
42
+ unresolved = true;
43
+ continue;
44
+ }
45
+ const matched = pagesForChange(map, change.name, changedPaths);
46
+ for (const [page, paths] of matched) {
47
+ const reasons = reasonsByPage.get(page) ?? [];
48
+ reasons.push({
49
+ source: change.name,
50
+ paths,
51
+ ...('baseline' in change ? { baseline: change.baseline } : {}),
52
+ ...('head' in change ? { head: change.head } : {}),
53
+ });
54
+ reasonsByPage.set(page, reasons);
55
+ }
56
+ const attributed = new Set([...matched.values()].flat());
57
+ const unattributed = changedPaths.filter((path) => !attributed.has(path));
58
+ if (unattributed.length > 0) {
59
+ notes.push(`${unattributed.length} changed file${unattributed.length === 1 ? '' : 's'} in "${change.name}" ${unattributed.length === 1 ? 'is' : 'are'} not referenced by any page.`);
60
+ }
61
+ }
62
+ if (map && project.sync.maxVerificationAgeDays) {
63
+ const state = await readSyncState(root);
64
+ const threshold = project.sync.maxVerificationAgeDays;
65
+ const severity = project.sync.maxVerificationAgeSeverity ?? 'warn';
66
+ for (const [page, evidence] of Object.entries(map.pages)) {
67
+ for (const entry of evidence.sources) {
68
+ const record = state.sources[entry.source];
69
+ const explicit = evidence.verifiedOn?.[entry.source];
70
+ const revisionMatches = record && evidence.verifiedAt?.[entry.source] && [record.commit, record.contentFingerprint].includes(evidence.verifiedAt[entry.source]);
71
+ const verifiedOn = explicit ?? (revisionMatches ? record.recordedAt : undefined);
72
+ const ageDays = verifiedOn ? Math.floor((Date.now() - Date.parse(verifiedOn)) / 86_400_000) : Number.POSITIVE_INFINITY;
73
+ if (ageDays <= threshold)
74
+ continue;
75
+ const ageLabel = Number.isFinite(ageDays) ? `${ageDays} days` : 'an unknown amount of time';
76
+ if (severity === 'warn') {
77
+ notes.push(`Page "${page}" was last verified against "${entry.source}" ${ageLabel} ago; policy is ${threshold} days.`);
78
+ continue;
79
+ }
80
+ const reasons = reasonsByPage.get(page) ?? [];
81
+ reasons.push({ source: entry.source, paths: [], kind: 'max-age', ...(Number.isFinite(ageDays) ? { ageDays } : {}) });
82
+ reasonsByPage.set(page, reasons);
83
+ }
84
+ }
85
+ }
86
+ const pages = [...reasonsByPage.entries()]
87
+ .map(([page, reasons]) => stalePage(page, reasons, map))
88
+ .sort((left, right) => left.page.localeCompare(right.page));
89
+ return {
90
+ status: pages.length > 0 ? 'stale' : unresolved ? 'unknown' : 'current',
91
+ pages,
92
+ trackedPages: map ? Object.keys(map.pages).length : 0,
93
+ sources,
94
+ evidenceMap: map ? 'present' : 'missing',
95
+ notes,
96
+ };
97
+ }
98
+ function stalePage(page, reasons, map) {
99
+ const recorded = map?.pages[page]?.verifiedAt;
100
+ const verifiedAt = recorded
101
+ ? recorded[reasons[0]?.source ?? '']
102
+ : undefined;
103
+ return { page, reasons, ...(verifiedAt ? { verifiedAt } : {}) };
104
+ }
105
+ /**
106
+ * Conditions where drift cannot be decided from local state. These are
107
+ * reported rather than silently treated as "documentation is current".
108
+ */
109
+ function unresolvedNote(change) {
110
+ switch (change.kind) {
111
+ case 'missing-path':
112
+ return `Source "${change.name}" is configured as ${change.path}, which does not exist.`;
113
+ case 'not-git':
114
+ return `Source "${change.name}" is not a Git repository, so changes cannot be compared.`;
115
+ case 'no-baseline':
116
+ return `Source "${change.name}" has no sync baseline yet. Accepting the next update records one.`;
117
+ case 'baseline-lost':
118
+ return `The recorded baseline for source "${change.name}" no longer exists, so changes cannot be compared.`;
119
+ case 'spec-remote':
120
+ return `The API specification "${change.name}" is remote and is not compared locally. The next update checks it.`;
121
+ default:
122
+ return undefined;
123
+ }
124
+ }
125
+ export function formatDrift(result) {
126
+ const lines = [];
127
+ if (result.status === 'stale') {
128
+ lines.push(`Documentation drift: ${result.pages.length} page${result.pages.length === 1 ? '' : 's'} stale`, '');
129
+ for (const page of result.pages) {
130
+ lines.push(` ${page.page}`);
131
+ for (const reason of page.reasons) {
132
+ if (reason.kind === 'max-age') {
133
+ lines.push(` stale because verification is ${reason.ageDays === undefined ? 'undated' : `${reason.ageDays} days old`} for source "${reason.source}"`);
134
+ continue;
135
+ }
136
+ const because = reason.paths.length === 0
137
+ ? `source "${reason.source}" changed`
138
+ : `${reason.paths.slice(0, 5).join(', ')}${reason.paths.length > 5 ? `, and ${reason.paths.length - 5} more` : ''} changed`;
139
+ lines.push(` stale because ${because}${reason.head ? ` (${reason.head.slice(0, 12)})` : ''}`);
140
+ }
141
+ if (page.verifiedAt) {
142
+ lines.push(` last verified ${page.verifiedAt.slice(0, 12)}`);
143
+ }
144
+ lines.push('');
145
+ }
146
+ const current = Math.max(result.trackedPages - result.pages.length, 0);
147
+ if (current > 0)
148
+ lines.push(` ${current} other tracked page${current === 1 ? '' : 's'} current`, '');
149
+ }
150
+ else if (result.status === 'current') {
151
+ lines.push('Documentation is current with the recorded source baseline.', '');
152
+ }
153
+ else {
154
+ lines.push('Documentation drift could not be determined.', '');
155
+ }
156
+ for (const note of result.notes)
157
+ lines.push(` Note: ${note}`);
158
+ if (result.notes.length > 0)
159
+ lines.push('');
160
+ if (result.status === 'stale')
161
+ lines.push('Fix with: doxloop update');
162
+ return lines.join('\n').trimEnd();
163
+ }
164
+ //# sourceMappingURL=drift.js.map
@@ -0,0 +1,46 @@
1
+ export declare const EVALUATION_CONTRACT_VERSION: "1.0.0";
2
+ export declare const EVALUATION_BASELINE_FILE: string;
3
+ export interface EvaluationMetric {
4
+ id: string;
5
+ score: number;
6
+ weight: number;
7
+ detail: string;
8
+ }
9
+ export interface EvaluationReport {
10
+ schemaVersion: 1;
11
+ contractVersion: '1.0.0';
12
+ generatedAt: string;
13
+ mode: 'generation' | 'update';
14
+ project: string;
15
+ score: number;
16
+ metrics: EvaluationMetric[];
17
+ durationMs?: number;
18
+ usage?: {
19
+ inputTokens?: number;
20
+ outputTokens?: number;
21
+ costUsd?: number;
22
+ };
23
+ reviewer: {
24
+ accepted: number;
25
+ revised: number;
26
+ rejected: number;
27
+ };
28
+ regression?: {
29
+ baselineScore: number;
30
+ delta: number;
31
+ threshold: number;
32
+ blocked: boolean;
33
+ };
34
+ }
35
+ export declare function evaluateWorkspace(root: string, options: {
36
+ mode: 'generation' | 'update';
37
+ before?: string;
38
+ expectedChangedPages?: string[];
39
+ maximumPages?: number;
40
+ durationMs?: number;
41
+ usage?: EvaluationReport['usage'];
42
+ regressionThreshold?: number;
43
+ }): Promise<EvaluationReport>;
44
+ export declare function approveEvaluationBaseline(root: string, report: EvaluationReport): Promise<void>;
45
+ export declare function formatEvaluation(report: EvaluationReport): string;
46
+ //# sourceMappingURL=evaluation.d.ts.map