@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,600 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { join, relative } from 'node:path';
4
+ import { openHistory, withHistory } from './db.js';
5
+ import { readEvidenceMap } from './evidence.js';
6
+ import { pathExists } from './fs.js';
7
+ import { loadPages, loadProject } from './project.js';
8
+ import { lineHunks } from './text-diff.js';
9
+ /** Open a request row before the agent starts, so an interrupted run is still visible. */
10
+ export async function startRequest(root, input) {
11
+ const id = `req-${randomUUID().slice(0, 12)}`;
12
+ const recorded = await withHistory(root, (database) => {
13
+ database
14
+ .prepare(`INSERT INTO requests (id, created_at, kind, trigger, request_text, agent, model,
15
+ reasoning_effort, status)
16
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'running')`)
17
+ .run(id, now(), input.kind, input.trigger ?? 'manual', text(input.requestText), text(input.agent), text(input.model), text(input.reasoningEffort));
18
+ return id;
19
+ });
20
+ return recorded;
21
+ }
22
+ export async function finishRequest(root, id, input) {
23
+ if (!id)
24
+ return;
25
+ await withHistory(root, (database) => {
26
+ database
27
+ .prepare(`UPDATE requests
28
+ SET finished_at = ?,
29
+ duration_ms = CAST((julianday(?) - julianday(created_at)) * 86400000 AS INTEGER),
30
+ status = ?,
31
+ pages_changed = COALESCE(?, pages_changed),
32
+ lines_added = COALESCE(?, lines_added),
33
+ lines_removed = COALESCE(?, lines_removed),
34
+ validation_pages = ?,
35
+ validation_errors = ?,
36
+ validation_warnings = ?,
37
+ error_message = ?,
38
+ input_tokens = COALESCE(?, input_tokens),
39
+ output_tokens = COALESCE(?, output_tokens),
40
+ cache_read_tokens = COALESCE(?, cache_read_tokens),
41
+ cache_creation_tokens = COALESCE(?, cache_creation_tokens),
42
+ cost_usd = COALESCE(?, cost_usd),
43
+ agent_turns = COALESCE(?, agent_turns),
44
+ agent_sessions = COALESCE(?, agent_sessions),
45
+ max_context_tokens = COALESCE(?, max_context_tokens)
46
+ WHERE id = ?`)
47
+ .run(now(), now(), input.status, input.pagesChanged ?? null, input.linesAdded ?? null, input.linesRemoved ?? null, input.validation?.pages ?? null, input.validation?.errors ?? null, input.validation?.warnings ?? null, text(input.error), ...usageColumns(input.usage), id);
48
+ });
49
+ }
50
+ /**
51
+ * Capture page content so a direct authoring run can be measured afterwards.
52
+ * Held in memory rather than read back from the page registry: the UI server
53
+ * backfills that registry on startup, which would otherwise claim the pages a
54
+ * still-running agent is in the middle of writing.
55
+ */
56
+ export async function snapshotPages(root, project) {
57
+ const snapshot = new Map();
58
+ try {
59
+ const resolved = project ?? (await loadProject(root));
60
+ for (const absolute of await loadPages(root, resolved)) {
61
+ snapshot.set(portable(relative(root, absolute)), await readFile(absolute, 'utf8'));
62
+ }
63
+ }
64
+ catch {
65
+ // A project with no content directory yet simply starts from nothing.
66
+ }
67
+ return snapshot;
68
+ }
69
+ /**
70
+ * Record what an authoring run changed. `create` and `update` write straight to
71
+ * disk instead of producing a proposal, so the diff against the pre-run
72
+ * snapshot is the only account of which pages the request touched.
73
+ */
74
+ export async function recordAuthoredPages(root, requestId, before, project) {
75
+ const result = { paths: new Set(), linesAdded: 0, linesRemoved: 0 };
76
+ const after = await snapshotPages(root, project);
77
+ const changes = [];
78
+ const paths = new Set([...before.keys(), ...after.keys()]);
79
+ for (const path of [...paths].sort()) {
80
+ const previous = before.get(path);
81
+ const current = after.get(path);
82
+ if (previous === current)
83
+ continue;
84
+ let added = 0;
85
+ let removed = 0;
86
+ for (const hunk of lineHunks(previous ?? '', current ?? '', path)) {
87
+ added += hunk.newLines.length;
88
+ removed += hunk.oldLines.length;
89
+ }
90
+ result.paths.add(path);
91
+ result.linesAdded += added;
92
+ result.linesRemoved += removed;
93
+ changes.push({
94
+ path,
95
+ kind: current === undefined ? 'deleted' : previous === undefined ? 'added' : 'modified',
96
+ title: text(frontmatterTitle(current ?? previous ?? '')),
97
+ added,
98
+ removed,
99
+ });
100
+ }
101
+ if (!requestId || changes.length === 0)
102
+ return result;
103
+ await withHistory(root, (database) => {
104
+ const timestamp = now();
105
+ // Authoring writes straight to the working tree, so every page it touched is
106
+ // already in effect — there is no pending decision to make later.
107
+ const upsert = database.prepare(`INSERT INTO request_pages (request_id, path, title, change_kind, category, decision,
108
+ decided_at, lines_added, lines_removed)
109
+ VALUES (?, ?, ?, ?, 'page', 'accepted', ?, ?, ?)
110
+ ON CONFLICT (request_id, path) DO UPDATE SET
111
+ title = excluded.title,
112
+ change_kind = excluded.change_kind,
113
+ lines_added = excluded.lines_added,
114
+ lines_removed = excluded.lines_removed`);
115
+ for (const change of changes) {
116
+ upsert.run(requestId, change.path, change.title, change.kind, timestamp, change.added, change.removed);
117
+ }
118
+ });
119
+ return result;
120
+ }
121
+ /**
122
+ * Mirror a proposal into history. Called on every status change, so the row
123
+ * survives even when the run directory is later pruned.
124
+ */
125
+ export async function recordSyncRun(root, run, context = {}) {
126
+ await withHistory(root, (database) => {
127
+ const counts = lineCounts(run.changes);
128
+ const finishedAt = run.appliedAt ?? run.rejectedAt ?? run.completedAt ?? null;
129
+ database
130
+ .prepare(`INSERT INTO requests (id, created_at, finished_at, duration_ms, kind, trigger,
131
+ request_text, agent, model, reasoning_effort, status, pages_changed, lines_added,
132
+ lines_removed, validation_pages, validation_errors, validation_warnings,
133
+ source_summary, stale_pages_count, error_message, run_dir, input_tokens,
134
+ output_tokens, cache_read_tokens, cache_creation_tokens, cost_usd, agent_turns,
135
+ agent_sessions, max_context_tokens)
136
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
137
+ ON CONFLICT (id) DO UPDATE SET
138
+ finished_at = excluded.finished_at,
139
+ duration_ms = excluded.duration_ms,
140
+ kind = excluded.kind,
141
+ status = excluded.status,
142
+ pages_changed = excluded.pages_changed,
143
+ lines_added = excluded.lines_added,
144
+ lines_removed = excluded.lines_removed,
145
+ validation_pages = excluded.validation_pages,
146
+ validation_errors = excluded.validation_errors,
147
+ validation_warnings = excluded.validation_warnings,
148
+ source_summary = excluded.source_summary,
149
+ stale_pages_count = excluded.stale_pages_count,
150
+ error_message = excluded.error_message,
151
+ request_text = COALESCE(excluded.request_text, requests.request_text),
152
+ agent = COALESCE(excluded.agent, requests.agent),
153
+ model = COALESCE(excluded.model, requests.model),
154
+ input_tokens = COALESCE(excluded.input_tokens, requests.input_tokens),
155
+ output_tokens = COALESCE(excluded.output_tokens, requests.output_tokens),
156
+ cache_read_tokens = COALESCE(excluded.cache_read_tokens, requests.cache_read_tokens),
157
+ cache_creation_tokens = COALESCE(excluded.cache_creation_tokens, requests.cache_creation_tokens),
158
+ cost_usd = COALESCE(excluded.cost_usd, requests.cost_usd),
159
+ agent_turns = COALESCE(excluded.agent_turns, requests.agent_turns),
160
+ agent_sessions = COALESCE(excluded.agent_sessions, requests.agent_sessions),
161
+ max_context_tokens = COALESCE(excluded.max_context_tokens, requests.max_context_tokens)`)
162
+ .run(run.id, run.createdAt, finishedAt, finishedAt ? elapsed(run.createdAt, finishedAt) : null, run.editRequest ? 'edit' : run.authoringMode ?? 'update', run.trigger, text(run.editRequest?.followUps.at(-1)?.instruction ?? run.editRequest?.instruction ?? context.requestText), text(context.agent), text(context.model), text(context.reasoningEffort), run.status, run.changes.filter((change) => change.category === 'page').length, counts.added, counts.removed, run.validation?.pages ?? null, run.validation?.errors ?? null, run.validation?.warnings ?? null, text(run.sourceSummary), run.stalePages.length, text(run.error), text(context.runDir), ...usageColumns(run.usage));
163
+ const upsertPage = database.prepare(`INSERT INTO request_pages (request_id, path, title, change_kind, category, decision,
164
+ decided_at, lines_added, lines_removed)
165
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
166
+ ON CONFLICT (request_id, path) DO UPDATE SET
167
+ title = COALESCE(excluded.title, request_pages.title),
168
+ change_kind = excluded.change_kind,
169
+ category = excluded.category,
170
+ decision = excluded.decision,
171
+ decided_at = excluded.decided_at,
172
+ lines_added = excluded.lines_added,
173
+ lines_removed = excluded.lines_removed`);
174
+ if (run.editRequest) {
175
+ for (const path of run.editRequest.paths) {
176
+ if (run.changes.some((change) => change.path === path))
177
+ continue;
178
+ upsertPage.run(run.id, path, null, 'modified', 'page', 'pending', null, 0, 0);
179
+ }
180
+ }
181
+ for (const change of run.changes) {
182
+ const decision = changeDecision(change);
183
+ const changeCounts = lineCounts([change]);
184
+ upsertPage.run(run.id, change.path, text(change.title), change.kind, change.category, decision.state, decision.at, changeCounts.added, changeCounts.removed);
185
+ }
186
+ });
187
+ }
188
+ /** Append the baselines a run was written against. Ignores repeats. */
189
+ export async function recordSourceSyncs(root, state, requestId) {
190
+ if (!state)
191
+ return;
192
+ await withHistory(root, (database) => {
193
+ const insert = database.prepare(`INSERT OR IGNORE INTO source_syncs (source_name, commit_hash, fingerprint, recorded_at,
194
+ request_id)
195
+ VALUES (?, ?, ?, ?, ?)`);
196
+ for (const [name, record] of Object.entries(state.sources)) {
197
+ insert.run(name, record.commit ?? '', record.contentFingerprint ?? '', record.recordedAt, text(requestId));
198
+ }
199
+ });
200
+ }
201
+ export async function recordDeployment(root, input) {
202
+ await withHistory(root, (database) => {
203
+ const finishedAt = now();
204
+ database
205
+ .prepare(`INSERT INTO deployments (started_at, finished_at, duration_ms, target, name, slug,
206
+ visibility, url, status, pages_count, media_count, bytes, pages_created,
207
+ pages_updated, pages_deleted, error_message)
208
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
209
+ .run(input.startedAt, finishedAt, elapsed(input.startedAt, finishedAt), input.target, text(input.name), text(input.slug), text(input.visibility), text(input.url), input.status, input.pagesCount ?? null, input.mediaCount ?? null, input.bytes ?? null, input.pagesCreated ?? null, input.pagesUpdated ?? null, input.pagesDeleted ?? null, text(input.error));
210
+ });
211
+ }
212
+ /**
213
+ * Reconcile the page registry with what is on disk. Content hashes make edits
214
+ * that bypassed Doxloop visible instead of silently absent from history.
215
+ */
216
+ export async function syncPageRegistry(root, project, requestId, authored) {
217
+ const database = await openHistory(root);
218
+ if (!database)
219
+ return;
220
+ try {
221
+ const resolved = project ?? (await loadProject(root));
222
+ const contentRoot = join(root, resolved.contentDir);
223
+ const absolutePaths = await loadPages(root, resolved);
224
+ const evidence = await readEvidenceMap(root);
225
+ const seen = new Set();
226
+ const timestamp = now();
227
+ for (const absolute of absolutePaths) {
228
+ const path = portable(relative(root, absolute));
229
+ seen.add(path);
230
+ const content = await readFile(absolute, 'utf8');
231
+ const hash = createHash('sha256').update(content).digest('hex');
232
+ const title = frontmatterTitle(content) ?? portable(relative(contentRoot, absolute));
233
+ const confidence = evidence?.pages[path]?.confidence;
234
+ const existing = database
235
+ .prepare('SELECT content_hash FROM pages WHERE path = ?')
236
+ .get(path);
237
+ if (existing?.content_hash === hash) {
238
+ database
239
+ .prepare('UPDATE pages SET title = ?, evidence_confidence = ?, status = ? WHERE path = ?')
240
+ .run(title, text(confidence), 'active', path);
241
+ // A registry refresh may have recorded this content first — the UI server
242
+ // backfills on startup, mid-run. The hash then matches even though this
243
+ // request is what wrote the page, so attribution has to be set explicitly.
244
+ if (requestId && authored?.has(path)) {
245
+ database
246
+ .prepare('UPDATE pages SET last_request_id = ? WHERE path = ?')
247
+ .run(requestId, path);
248
+ }
249
+ continue;
250
+ }
251
+ database
252
+ .prepare(`INSERT INTO pages (path, title, created_at, updated_at, last_request_id, content_hash,
253
+ change_count, evidence_confidence, status)
254
+ VALUES (?, ?, ?, ?, ?, ?, 1, ?, 'active')
255
+ ON CONFLICT (path) DO UPDATE SET
256
+ title = excluded.title,
257
+ updated_at = excluded.updated_at,
258
+ last_request_id = excluded.last_request_id,
259
+ content_hash = excluded.content_hash,
260
+ change_count = pages.change_count + 1,
261
+ evidence_confidence = excluded.evidence_confidence,
262
+ status = 'active'`)
263
+ .run(path, title, timestamp, timestamp, text(requestId), hash, text(confidence));
264
+ }
265
+ const known = database.prepare("SELECT path FROM pages WHERE status = 'active'").all();
266
+ for (const row of known) {
267
+ if (seen.has(row.path))
268
+ continue;
269
+ database
270
+ .prepare("UPDATE pages SET status = 'deleted', updated_at = ?, last_request_id = ? WHERE path = ?")
271
+ .run(timestamp, text(requestId), row.path);
272
+ }
273
+ }
274
+ catch {
275
+ // A registry refresh is best effort; documentation commands must not fail on it.
276
+ }
277
+ }
278
+ export async function listRequests(root, limit = 20) {
279
+ const rows = await withHistory(root, (database) => database
280
+ .prepare(`SELECT id, created_at, finished_at, duration_ms, kind, trigger, request_text, agent,
281
+ model, status, pages_changed, lines_added, lines_removed, validation_errors,
282
+ validation_warnings, source_summary, error_message, input_tokens, output_tokens,
283
+ cache_read_tokens, cache_creation_tokens, cost_usd, agent_turns, agent_sessions,
284
+ max_context_tokens
285
+ FROM requests
286
+ ORDER BY created_at DESC
287
+ LIMIT ?`)
288
+ .all(limit));
289
+ return (rows ?? []).map(toRequest);
290
+ }
291
+ /**
292
+ * The pages each request touched, keyed by request id. Attached to a request
293
+ * list so the workspace can show what changed without a second round trip.
294
+ */
295
+ export async function requestPages(root, requestIds) {
296
+ if (requestIds.length === 0)
297
+ return {};
298
+ const rows = await withHistory(root, (database) => database
299
+ .prepare(`SELECT request_id, path, title, change_kind, decision, lines_added, lines_removed
300
+ FROM request_pages
301
+ WHERE category = 'page'
302
+ AND request_id IN (${requestIds.map(() => '?').join(', ')})
303
+ ORDER BY path`)
304
+ .all(...requestIds));
305
+ const grouped = {};
306
+ for (const row of rows ?? []) {
307
+ const record = row;
308
+ const id = String(record.request_id);
309
+ const pages = grouped[id] ?? (grouped[id] = []);
310
+ pages.push({
311
+ path: String(record.path),
312
+ title: optional(record.title),
313
+ changeKind: String(record.change_kind),
314
+ decision: String(record.decision),
315
+ linesAdded: Number(record.lines_added ?? 0),
316
+ linesRemoved: Number(record.lines_removed ?? 0),
317
+ });
318
+ }
319
+ return grouped;
320
+ }
321
+ export async function pageHistory(root, path, limit = 20) {
322
+ const rows = await withHistory(root, (database) => database
323
+ .prepare(`SELECT rp.request_id, rp.path, rp.title, rp.change_kind, rp.decision, rp.decided_at,
324
+ rp.lines_added, rp.lines_removed, r.created_at, r.request_text, r.agent, r.status
325
+ FROM request_pages rp
326
+ JOIN requests r ON r.id = rp.request_id
327
+ WHERE rp.path = ?
328
+ ORDER BY r.created_at DESC
329
+ LIMIT ?`)
330
+ .all(path, limit));
331
+ return (rows ?? []).map((row) => {
332
+ const record = row;
333
+ return {
334
+ requestId: String(record.request_id),
335
+ path: String(record.path),
336
+ title: optional(record.title),
337
+ changeKind: String(record.change_kind),
338
+ decision: String(record.decision),
339
+ decidedAt: optional(record.decided_at),
340
+ linesAdded: Number(record.lines_added ?? 0),
341
+ linesRemoved: Number(record.lines_removed ?? 0),
342
+ requestedAt: String(record.created_at),
343
+ requestText: optional(record.request_text),
344
+ agent: optional(record.agent),
345
+ requestStatus: String(record.status),
346
+ };
347
+ });
348
+ }
349
+ export async function listPages(root) {
350
+ const rows = await withHistory(root, (database) => database
351
+ .prepare(`SELECT path, title, created_at, updated_at, change_count, evidence_confidence, status
352
+ FROM pages
353
+ ORDER BY updated_at DESC`)
354
+ .all());
355
+ return (rows ?? []).map((row) => {
356
+ const record = row;
357
+ return {
358
+ path: String(record.path),
359
+ title: optional(record.title),
360
+ createdAt: String(record.created_at),
361
+ updatedAt: String(record.updated_at),
362
+ changeCount: Number(record.change_count ?? 0),
363
+ evidenceConfidence: optional(record.evidence_confidence),
364
+ status: String(record.status),
365
+ };
366
+ });
367
+ }
368
+ export async function listDeployments(root, limit = 20) {
369
+ const rows = await withHistory(root, (database) => database
370
+ .prepare(`SELECT started_at, finished_at, duration_ms, target, name, slug, visibility, url, status,
371
+ pages_count, pages_created, pages_updated, pages_deleted, error_message
372
+ FROM deployments
373
+ ORDER BY started_at DESC
374
+ LIMIT ?`)
375
+ .all(limit));
376
+ return (rows ?? []).map((row) => {
377
+ const record = row;
378
+ return {
379
+ startedAt: String(record.started_at),
380
+ finishedAt: optional(record.finished_at),
381
+ durationMs: record.duration_ms === null ? undefined : Number(record.duration_ms),
382
+ target: String(record.target),
383
+ name: optional(record.name),
384
+ slug: optional(record.slug),
385
+ visibility: optional(record.visibility),
386
+ url: optional(record.url),
387
+ status: String(record.status),
388
+ pagesCount: record.pages_count === null ? undefined : Number(record.pages_count),
389
+ pagesCreated: record.pages_created === null ? undefined : Number(record.pages_created),
390
+ pagesUpdated: record.pages_updated === null ? undefined : Number(record.pages_updated),
391
+ pagesDeleted: record.pages_deleted === null ? undefined : Number(record.pages_deleted),
392
+ error: optional(record.error_message),
393
+ };
394
+ });
395
+ }
396
+ /**
397
+ * Import the JSON state written before history existed, so an upgraded project
398
+ * does not start empty. Runs once; the marker keeps later commands cheap.
399
+ */
400
+ export async function backfillHistory(root, force = false) {
401
+ const database = await openHistory(root);
402
+ if (!database)
403
+ return 0;
404
+ try {
405
+ if (!force) {
406
+ // Version 2 re-imports retained run manifests once so older create runs
407
+ // that were incorrectly stored as updates receive the right action.
408
+ const marker = database.prepare("SELECT value FROM meta WHERE key = 'backfilled_at_v2'").get();
409
+ if (marker)
410
+ return 0;
411
+ }
412
+ let imported = 0;
413
+ const runsRoot = join(root, '.doxloop', 'runs');
414
+ if (await pathExists(runsRoot)) {
415
+ const { readdir } = await import('node:fs/promises');
416
+ const entries = await readdir(runsRoot, { withFileTypes: true });
417
+ for (const entry of entries) {
418
+ if (!entry.isDirectory())
419
+ continue;
420
+ const manifest = join(runsRoot, entry.name, 'run.json');
421
+ if (!(await pathExists(manifest)))
422
+ continue;
423
+ try {
424
+ const raw = JSON.parse(await readFile(manifest, 'utf8'));
425
+ if ((raw.schemaVersion !== 1 && raw.schemaVersion !== 2) || !Array.isArray(raw.changes))
426
+ continue;
427
+ await recordSyncRun(root, raw, { runDir: join('.doxloop', 'runs', entry.name) });
428
+ imported += 1;
429
+ }
430
+ catch {
431
+ // An interrupted run directory is not history worth importing.
432
+ }
433
+ }
434
+ }
435
+ const syncState = join(root, '.doxloop', 'sync-state.json');
436
+ if (await pathExists(syncState)) {
437
+ try {
438
+ await recordSourceSyncs(root, JSON.parse(await readFile(syncState, 'utf8')));
439
+ }
440
+ catch {
441
+ // A malformed baseline file simply contributes nothing.
442
+ }
443
+ }
444
+ await syncPageRegistry(root);
445
+ database
446
+ .prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('backfilled_at_v2', ?)")
447
+ .run(now());
448
+ return imported;
449
+ }
450
+ catch {
451
+ return 0;
452
+ }
453
+ }
454
+ export function formatRequestHistory(requests) {
455
+ if (requests.length === 0) {
456
+ return 'No documentation history recorded yet.';
457
+ }
458
+ const lines = requests.map((request) => {
459
+ const when = request.createdAt.replace('T', ' ').slice(0, 16);
460
+ const changes = request.pagesChanged > 0
461
+ ? `${request.pagesChanged} page${request.pagesChanged === 1 ? '' : 's'} (+${request.linesAdded}/-${request.linesRemoved})`
462
+ : 'no changes';
463
+ const detail = request.requestText
464
+ ? `\n "${truncate(request.requestText, 96)}"`
465
+ : request.sourceSummary
466
+ ? `\n ${truncate(request.sourceSummary, 96)}`
467
+ : '';
468
+ return ` ${statusMark(request.status)} ${when} ${request.kind.padEnd(6)} ${changes}${request.agent ? ` · ${request.agent}` : ''}${detail}`;
469
+ });
470
+ return `Documentation history (${requests.length} most recent)\n${lines.join('\n')}`;
471
+ }
472
+ function statusMark(status) {
473
+ if (status === 'applied')
474
+ return '✓';
475
+ if (status === 'rejected')
476
+ return '✗';
477
+ if (status === 'failed')
478
+ return '!';
479
+ if (status === 'running' || status === 'generating')
480
+ return '·';
481
+ return '○';
482
+ }
483
+ function toRequest(row) {
484
+ const record = row;
485
+ return {
486
+ id: String(record.id),
487
+ createdAt: String(record.created_at),
488
+ finishedAt: optional(record.finished_at),
489
+ durationMs: record.duration_ms === null ? undefined : Number(record.duration_ms),
490
+ kind: String(record.kind),
491
+ trigger: String(record.trigger),
492
+ requestText: optional(record.request_text),
493
+ agent: optional(record.agent),
494
+ model: optional(record.model),
495
+ status: String(record.status),
496
+ pagesChanged: Number(record.pages_changed ?? 0),
497
+ linesAdded: Number(record.lines_added ?? 0),
498
+ linesRemoved: Number(record.lines_removed ?? 0),
499
+ validationErrors: record.validation_errors === null ? undefined : Number(record.validation_errors),
500
+ validationWarnings: record.validation_warnings === null ? undefined : Number(record.validation_warnings),
501
+ sourceSummary: optional(record.source_summary),
502
+ error: optional(record.error_message),
503
+ usage: usageFromRow(record),
504
+ };
505
+ }
506
+ /** The eight usage columns in table order, all null when the agent reported no usage. */
507
+ function usageColumns(usage) {
508
+ if (!usage)
509
+ return [null, null, null, null, null, null, null, null];
510
+ return [
511
+ usage.inputTokens,
512
+ usage.outputTokens,
513
+ usage.cacheReadTokens,
514
+ usage.cacheCreationTokens,
515
+ usage.costUsd ?? null,
516
+ usage.turns,
517
+ usage.sessions,
518
+ usage.maxContextTokens,
519
+ ];
520
+ }
521
+ /** Rebuild the usage from a request row; undefined when no usage was ever recorded for it. */
522
+ function usageFromRow(record) {
523
+ const count = (value) => value === null || value === undefined ? undefined : Number(value);
524
+ const input = count(record.input_tokens);
525
+ const output = count(record.output_tokens);
526
+ const cacheRead = count(record.cache_read_tokens);
527
+ const cacheCreation = count(record.cache_creation_tokens);
528
+ if (input === undefined && output === undefined && cacheRead === undefined && cacheCreation === undefined) {
529
+ return undefined;
530
+ }
531
+ const cost = count(record.cost_usd);
532
+ return {
533
+ inputTokens: input ?? 0,
534
+ outputTokens: output ?? 0,
535
+ cacheReadTokens: cacheRead ?? 0,
536
+ cacheCreationTokens: cacheCreation ?? 0,
537
+ totalTokens: (input ?? 0) + (output ?? 0) + (cacheRead ?? 0) + (cacheCreation ?? 0),
538
+ ...(cost !== undefined ? { costUsd: cost } : {}),
539
+ turns: count(record.agent_turns) ?? 0,
540
+ sessions: count(record.agent_sessions) ?? 1,
541
+ maxContextTokens: count(record.max_context_tokens) ?? 0,
542
+ durationMs: count(record.duration_ms) ?? 0,
543
+ };
544
+ }
545
+ function changeDecision(change) {
546
+ const decided = change.hunks.filter((hunk) => hunk.acceptedAt || hunk.rejectedAt);
547
+ if (decided.length === 0)
548
+ return { state: 'pending', at: null };
549
+ const accepted = change.hunks.filter((hunk) => hunk.acceptedAt);
550
+ const at = decided
551
+ .map((hunk) => hunk.acceptedAt ?? hunk.rejectedAt ?? '')
552
+ .sort()
553
+ .at(-1) ?? null;
554
+ if (accepted.length === change.hunks.length)
555
+ return { state: 'accepted', at };
556
+ if (accepted.length === 0 && decided.length === change.hunks.length) {
557
+ return { state: 'rejected', at };
558
+ }
559
+ return { state: 'partial', at };
560
+ }
561
+ function lineCounts(changes) {
562
+ let added = 0;
563
+ let removed = 0;
564
+ for (const change of changes) {
565
+ for (const hunk of change.hunks) {
566
+ added += hunk.newLines.length;
567
+ removed += hunk.oldLines.length;
568
+ }
569
+ }
570
+ return { added, removed };
571
+ }
572
+ function frontmatterTitle(content) {
573
+ const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
574
+ if (!match)
575
+ return undefined;
576
+ const title = /^title:\s*(.+)$/m.exec(match[1] ?? '');
577
+ return title?.[1]?.trim().replace(/^["']|["']$/g, '') || undefined;
578
+ }
579
+ function elapsed(from, to) {
580
+ const start = Date.parse(from);
581
+ const end = Date.parse(to);
582
+ return Number.isFinite(start) && Number.isFinite(end) ? Math.max(0, end - start) : null;
583
+ }
584
+ function truncate(value, length) {
585
+ const single = value.replace(/\s+/g, ' ').trim();
586
+ return single.length > length ? `${single.slice(0, length - 1)}…` : single;
587
+ }
588
+ function portable(path) {
589
+ return path.split('\\').join('/');
590
+ }
591
+ function optional(value) {
592
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
593
+ }
594
+ function text(value) {
595
+ return value === undefined || value === '' ? null : value;
596
+ }
597
+ function now() {
598
+ return new Date().toISOString();
599
+ }
600
+ //# sourceMappingURL=history.js.map
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Dependency-free HTML to Markdown conversion for crawled documentation pages.
3
+ *
4
+ * The converter is intentionally tolerant: documentation sites ship imperfect
5
+ * HTML, and the output feeds an agent that reads Markdown for context rather
6
+ * than a renderer that needs exact fidelity. Navigation chrome, scripts,
7
+ * styles, and hidden elements are dropped before conversion.
8
+ */
9
+ export interface HtmlNode {
10
+ type: 'element' | 'text';
11
+ name?: string;
12
+ attributes: Record<string, string>;
13
+ children: HtmlNode[];
14
+ text?: string;
15
+ }
16
+ export interface HtmlDocumentSummary {
17
+ title?: string;
18
+ description?: string;
19
+ canonical?: string;
20
+ generator?: string;
21
+ language?: string;
22
+ /** Absolute or relative link targets found in the main content and navigation. */
23
+ links: string[];
24
+ /** Image sources found in the main content. */
25
+ images: string[];
26
+ headings: Array<{
27
+ level: number;
28
+ text: string;
29
+ }>;
30
+ markdown: string;
31
+ /** Approximate word count of the converted main content. */
32
+ words: number;
33
+ }
34
+ export declare function decodeEntities(value: string): string;
35
+ /** Parse tolerant HTML into a lightweight tree. */
36
+ export declare function parseHtml(html: string): HtmlNode;
37
+ /** Convert an HTML document into a Markdown summary of its main content. */
38
+ export declare function summarizeHtmlDocument(html: string): HtmlDocumentSummary;
39
+ /**
40
+ * The same summary for a page served as Markdown. Mintlify, GitBook, and
41
+ * other client-rendered docs sites send an empty HTML shell to a crawler but
42
+ * serve each page's source at `<page>.md` (and list those URLs in llms.txt).
43
+ * Their own "documentation index" preamble is dropped.
44
+ */
45
+ export declare function summarizeMarkdownDocument(source: string): HtmlDocumentSummary;
46
+ //# sourceMappingURL=html-markdown.d.ts.map