@doxbrix/doxloop 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (357) hide show
  1. package/CHANGELOG.md +1145 -0
  2. package/README.md +422 -227
  3. package/assets/doxbrix-preview.css +77 -15
  4. package/contracts/agent-events-v1.schema.json +1 -0
  5. package/contracts/coverage-v1.schema.json +59 -0
  6. package/contracts/drift-v1.schema.json +1 -0
  7. package/contracts/evaluation-v1.schema.json +62 -0
  8. package/contracts/quality-codes-v1.json +1 -0
  9. package/contracts/quality-report-v1.schema.json +1 -0
  10. package/contracts/validation-v1.schema.json +1 -0
  11. package/dist/agent-failure.d.ts +36 -0
  12. package/dist/agent-failure.js +98 -0
  13. package/dist/agent-isolation.d.ts +17 -0
  14. package/dist/agent-isolation.js +94 -0
  15. package/dist/agent-log.d.ts +197 -0
  16. package/dist/agent-log.js +1094 -0
  17. package/dist/agent-process.d.ts +63 -0
  18. package/dist/agent-process.js +128 -0
  19. package/dist/agent-reply.d.ts +47 -0
  20. package/dist/agent-reply.js +315 -0
  21. package/dist/agents.d.ts +24 -5
  22. package/dist/agents.js +60 -9
  23. package/dist/api-coverage.d.ts +11 -0
  24. package/dist/api-coverage.js +72 -0
  25. package/dist/application-probe.d.ts +28 -0
  26. package/dist/application-probe.js +93 -0
  27. package/dist/application-url.d.ts +14 -0
  28. package/dist/application-url.js +26 -0
  29. package/dist/args.js +15 -0
  30. package/dist/artifact-deploy.d.ts +9 -0
  31. package/dist/artifact-deploy.js +28 -8
  32. package/dist/assets.d.ts +72 -0
  33. package/dist/assets.js +382 -0
  34. package/dist/auth.d.ts +10 -0
  35. package/dist/auth.js +38 -9
  36. package/dist/author.d.ts +206 -4
  37. package/dist/author.js +1650 -52
  38. package/dist/authoring-batches.d.ts +190 -0
  39. package/dist/authoring-batches.js +489 -0
  40. package/dist/authoring-postpass.d.ts +79 -0
  41. package/dist/authoring-postpass.js +1289 -0
  42. package/dist/authoring-progress.d.ts +95 -0
  43. package/dist/authoring-progress.js +269 -0
  44. package/dist/autosync.d.ts +56 -0
  45. package/dist/autosync.js +442 -0
  46. package/dist/batch-artifacts.d.ts +81 -0
  47. package/dist/batch-artifacts.js +412 -0
  48. package/dist/batch-limits.d.ts +37 -0
  49. package/dist/batch-limits.js +80 -0
  50. package/dist/branding.d.ts +41 -0
  51. package/dist/branding.js +229 -0
  52. package/dist/bundle-upload.d.ts +26 -0
  53. package/dist/bundle-upload.js +99 -0
  54. package/dist/capture-auth.d.ts +95 -0
  55. package/dist/capture-auth.js +204 -0
  56. package/dist/capture.d.ts +48 -0
  57. package/dist/capture.js +20 -1
  58. package/dist/cli.js +839 -32
  59. package/dist/content-links.d.ts +10 -0
  60. package/dist/content-links.js +49 -0
  61. package/dist/contract-validation.d.ts +4 -0
  62. package/dist/contract-validation.js +25 -0
  63. package/dist/coverage-actions.d.ts +8 -0
  64. package/dist/coverage-actions.js +71 -0
  65. package/dist/coverage-resolutions.d.ts +16 -0
  66. package/dist/coverage-resolutions.js +58 -0
  67. package/dist/db.d.ts +37 -0
  68. package/dist/db.js +288 -0
  69. package/dist/demo.d.ts +12 -0
  70. package/dist/demo.js +122 -0
  71. package/dist/deploy-credentials.d.ts +7 -0
  72. package/dist/deploy-credentials.js +76 -0
  73. package/dist/deploy-targets/github-pages.d.ts +3 -0
  74. package/dist/deploy-targets/github-pages.js +100 -0
  75. package/dist/deploy-targets/index.d.ts +8 -0
  76. package/dist/deploy-targets/index.js +78 -0
  77. package/dist/deploy-targets/netlify.d.ts +3 -0
  78. package/dist/deploy-targets/netlify.js +49 -0
  79. package/dist/deploy-targets/types.d.ts +32 -0
  80. package/dist/deploy-targets/types.js +2 -0
  81. package/dist/deploy-targets/vercel.d.ts +3 -0
  82. package/dist/deploy-targets/vercel.js +95 -0
  83. package/dist/deploy.d.ts +9 -0
  84. package/dist/deploy.js +87 -13
  85. package/dist/deterministic-capture.d.ts +90 -0
  86. package/dist/deterministic-capture.js +435 -0
  87. package/dist/direct-edit.d.ts +40 -0
  88. package/dist/direct-edit.js +159 -0
  89. package/dist/docs-crawl.d.ts +86 -0
  90. package/dist/docs-crawl.js +536 -0
  91. package/dist/docs-site.d.ts +49 -0
  92. package/dist/docs-site.js +233 -0
  93. package/dist/doctor.js +8 -0
  94. package/dist/documentation-collections.d.ts +23 -0
  95. package/dist/documentation-collections.js +191 -0
  96. package/dist/documentation-plan.d.ts +253 -0
  97. package/dist/documentation-plan.js +2563 -0
  98. package/dist/doxbrix-build.d.ts +19 -0
  99. package/dist/doxbrix-build.js +167 -0
  100. package/dist/doxbrix-markdown.d.ts +9 -0
  101. package/dist/doxbrix-markdown.js +115 -14
  102. package/dist/drift.d.ts +10 -0
  103. package/dist/drift.js +164 -0
  104. package/dist/evaluation.d.ts +46 -0
  105. package/dist/evaluation.js +113 -0
  106. package/dist/evidence-pack.d.ts +47 -0
  107. package/dist/evidence-pack.js +358 -0
  108. package/dist/evidence.d.ts +25 -0
  109. package/dist/evidence.js +175 -0
  110. package/dist/fs.d.ts +8 -2
  111. package/dist/fs.js +41 -11
  112. package/dist/generator-api.d.ts +89 -0
  113. package/dist/generator-preflight.d.ts +27 -0
  114. package/dist/generator-preflight.js +105 -0
  115. package/dist/generator-runtime.d.ts +7 -0
  116. package/dist/generator-runtime.js +17 -1
  117. package/dist/generators.d.ts +24 -2
  118. package/dist/generators.js +54 -1
  119. package/dist/git-delivery.d.ts +17 -0
  120. package/dist/git-delivery.js +123 -0
  121. package/dist/globs.d.ts +16 -0
  122. package/dist/globs.js +65 -0
  123. package/dist/glossary.d.ts +26 -0
  124. package/dist/glossary.js +179 -0
  125. package/dist/history.d.ts +106 -0
  126. package/dist/history.js +600 -0
  127. package/dist/html-markdown.d.ts +46 -0
  128. package/dist/html-markdown.js +423 -0
  129. package/dist/interactive.js +3 -0
  130. package/dist/job-events.d.ts +74 -0
  131. package/dist/job-events.js +377 -0
  132. package/dist/keep-awake.d.ts +50 -0
  133. package/dist/keep-awake.js +123 -0
  134. package/dist/local-source-snapshot.d.ts +20 -0
  135. package/dist/local-source-snapshot.js +61 -0
  136. package/dist/mintlify-detect.d.ts +3 -0
  137. package/dist/mintlify-detect.js +18 -0
  138. package/dist/mintlify-import.d.ts +75 -0
  139. package/dist/mintlify-import.js +190 -0
  140. package/dist/navigation.d.ts +98 -0
  141. package/dist/navigation.js +310 -0
  142. package/dist/openapi.d.ts +60 -0
  143. package/dist/openapi.js +439 -0
  144. package/dist/page-editor-bridge.d.ts +3 -0
  145. package/dist/page-editor-bridge.js +109 -0
  146. package/dist/page-editor-preview.d.ts +10 -0
  147. package/dist/page-editor-preview.js +55 -0
  148. package/dist/page-extension.d.ts +9 -0
  149. package/dist/page-extension.js +15 -0
  150. package/dist/page-metadata.d.ts +28 -0
  151. package/dist/page-metadata.js +166 -0
  152. package/dist/page-operations.d.ts +34 -0
  153. package/dist/page-operations.js +215 -0
  154. package/dist/page-routes.d.ts +4 -0
  155. package/dist/page-routes.js +61 -0
  156. package/dist/pages.d.ts +20 -0
  157. package/dist/pages.js +184 -0
  158. package/dist/plan-generator.d.ts +3 -0
  159. package/dist/plan-generator.js +21 -0
  160. package/dist/plan-navigation.d.ts +11 -0
  161. package/dist/plan-navigation.js +30 -0
  162. package/dist/planning-captures.d.ts +20 -0
  163. package/dist/planning-captures.js +143 -0
  164. package/dist/planning-research.d.ts +135 -0
  165. package/dist/planning-research.js +472 -0
  166. package/dist/planning-triage.d.ts +23 -0
  167. package/dist/planning-triage.js +131 -0
  168. package/dist/preview.d.ts +24 -0
  169. package/dist/preview.js +280 -29
  170. package/dist/project-detect.d.ts +36 -0
  171. package/dist/project-detect.js +251 -0
  172. package/dist/project-import.d.ts +54 -0
  173. package/dist/project-import.js +157 -0
  174. package/dist/project-lock.d.ts +6 -0
  175. package/dist/project-lock.js +96 -0
  176. package/dist/project-registry.d.ts +25 -0
  177. package/dist/project-registry.js +79 -0
  178. package/dist/project.d.ts +35 -3
  179. package/dist/project.js +388 -38
  180. package/dist/prompts.d.ts +9 -0
  181. package/dist/prompts.js +25 -0
  182. package/dist/proposal-replay.d.ts +32 -0
  183. package/dist/proposal-replay.js +99 -0
  184. package/dist/quality-claims.d.ts +8 -0
  185. package/dist/quality-claims.js +168 -0
  186. package/dist/quality-config.d.ts +5 -0
  187. package/dist/quality-config.js +84 -0
  188. package/dist/quality-contract.d.ts +37 -0
  189. package/dist/quality-contract.js +46 -0
  190. package/dist/quality-examples.d.ts +4 -0
  191. package/dist/quality-examples.js +233 -0
  192. package/dist/quality-gates.d.ts +16 -0
  193. package/dist/quality-gates.js +192 -0
  194. package/dist/quality-links.d.ts +7 -0
  195. package/dist/quality-links.js +149 -0
  196. package/dist/quality-lint.d.ts +6 -0
  197. package/dist/quality-lint.js +124 -0
  198. package/dist/quality-rendered.d.ts +11 -0
  199. package/dist/quality-rendered.js +222 -0
  200. package/dist/quality-schema.d.ts +3 -0
  201. package/dist/quality-schema.js +71 -0
  202. package/dist/release-notes.d.ts +44 -0
  203. package/dist/release-notes.js +183 -0
  204. package/dist/remote-monitor.d.ts +16 -0
  205. package/dist/remote-monitor.js +74 -0
  206. package/dist/remote-source.d.ts +34 -0
  207. package/dist/remote-source.js +426 -0
  208. package/dist/review-diff.d.ts +82 -0
  209. package/dist/review-diff.js +400 -0
  210. package/dist/review-learning.d.ts +11 -0
  211. package/dist/review-learning.js +60 -0
  212. package/dist/review-render.d.ts +38 -0
  213. package/dist/review-render.js +224 -0
  214. package/dist/review-report.d.ts +9 -0
  215. package/dist/review-report.js +89 -0
  216. package/dist/review-ui.d.ts +14 -0
  217. package/dist/review-ui.js +1248 -0
  218. package/dist/schedule.d.ts +78 -0
  219. package/dist/schedule.js +480 -0
  220. package/dist/screen-capture-provider.d.ts +52 -0
  221. package/dist/screen-capture-provider.js +218 -0
  222. package/dist/screenshot-workflow.d.ts +167 -0
  223. package/dist/screenshot-workflow.js +1237 -0
  224. package/dist/settings.d.ts +1 -1
  225. package/dist/settings.js +95 -7
  226. package/dist/site-export.d.ts +18 -0
  227. package/dist/site-export.js +87 -0
  228. package/dist/source-connectors.d.ts +33 -0
  229. package/dist/source-connectors.js +268 -0
  230. package/dist/source-discovery.d.ts +132 -0
  231. package/dist/source-discovery.js +823 -0
  232. package/dist/source-intelligence.d.ts +9 -0
  233. package/dist/source-intelligence.js +306 -0
  234. package/dist/sync-review.d.ts +28 -0
  235. package/dist/sync-review.js +264 -0
  236. package/dist/sync-runs.d.ts +192 -0
  237. package/dist/sync-runs.js +2244 -0
  238. package/dist/sync.d.ts +35 -0
  239. package/dist/sync.js +298 -32
  240. package/dist/text-diff.d.ts +9 -0
  241. package/dist/text-diff.js +59 -0
  242. package/dist/types.d.ts +946 -1
  243. package/dist/ui/assets/doxloop-logo-light-De7Nx7j7.png +0 -0
  244. package/dist/ui/assets/index-BHBYU2aG.css +1 -0
  245. package/dist/ui/assets/index-Cq3RPQiC.js +33 -0
  246. package/dist/ui/index.html +18 -0
  247. package/dist/ui-server.d.ts +83 -0
  248. package/dist/ui-server.js +3532 -0
  249. package/dist/usage-budget.d.ts +28 -0
  250. package/dist/usage-budget.js +90 -0
  251. package/dist/validation.d.ts +25 -1
  252. package/dist/validation.js +312 -26
  253. package/dist/workspace-tools.d.ts +54 -0
  254. package/dist/workspace-tools.js +123 -0
  255. package/docs/agent-compatibility.md +49 -32
  256. package/docs/ci-and-automation.md +105 -66
  257. package/docs/doxbrix-http-api.md +8 -1
  258. package/docs/existing-documentation.md +80 -0
  259. package/docs/generation-performance.md +108 -0
  260. package/docs/generator-authoring.md +68 -5
  261. package/docs/generator-selection.md +50 -13
  262. package/docs/mintlify-import.md +71 -0
  263. package/docs/openapi-security.md +25 -0
  264. package/docs/project-format.md +331 -36
  265. package/docs/release-quality.md +158 -0
  266. package/docs/releasing.md +72 -0
  267. package/docs/review-workflows.md +51 -0
  268. package/docs/security-model.md +102 -39
  269. package/docs/troubleshooting.md +226 -93
  270. package/package.json +46 -15
  271. package/scripts/test-auto-screenshot.mjs +172 -0
  272. package/skills/doxloop-authoring/SKILL.md +242 -361
  273. package/skills/doxloop-authoring/references/existing-documentation.md +94 -0
  274. package/skills/doxloop-authoring/references/navigation-architecture.md +18 -7
  275. package/skills/doxloop-authoring/references/page-depth.md +169 -0
  276. package/skills/doxloop-authoring/references/project-format.md +106 -7
  277. package/skills/doxloop-authoring/references/quality.md +10 -0
  278. package/skills/doxloop-authoring/references/screenshot-manifest.md +113 -0
  279. package/skills/doxloop-authoring/references/screenshots.md +149 -235
  280. package/skills/doxloop-authoring/references/workflows.md +76 -0
  281. package/skills/doxloop-doxbrix/SKILL.md +38 -22
  282. package/skills/doxloop-doxbrix/references/api-endpoints.md +15 -14
  283. package/skills/doxloop-doxbrix/references/components.md +30 -3
  284. package/skills/doxloop-doxbrix/references/manifest.md +3 -2
  285. package/vendor/doxbrix-import/LICENSE +202 -0
  286. package/vendor/doxbrix-import/README.md +17 -0
  287. package/vendor/doxbrix-import/UPSTREAM.json +47 -0
  288. package/vendor/doxbrix-import/dist/docs/frontmatter.d.ts +13 -0
  289. package/vendor/doxbrix-import/dist/docs/frontmatter.js +83 -0
  290. package/vendor/doxbrix-import/dist/docs/import.d.ts +21 -0
  291. package/vendor/doxbrix-import/dist/docs/import.js +147 -0
  292. package/vendor/doxbrix-import/dist/docs/manifest.d.ts +163 -0
  293. package/vendor/doxbrix-import/dist/docs/manifest.js +64 -0
  294. package/vendor/doxbrix-import/dist/docs/project.d.ts +25 -0
  295. package/vendor/doxbrix-import/dist/docs/project.js +77 -0
  296. package/vendor/doxbrix-import/dist/docs/starter.d.ts +4 -0
  297. package/vendor/doxbrix-import/dist/docs/starter.js +11 -0
  298. package/vendor/doxbrix-import/dist/importer.d.ts +227 -0
  299. package/vendor/doxbrix-import/dist/importer.js +1567 -0
  300. package/vendor/doxbrix-import/dist/mintlify-openapi.d.ts +37 -0
  301. package/vendor/doxbrix-import/dist/mintlify-openapi.js +305 -0
  302. package/vendor/doxbrix-import/dist/safe-path.d.ts +9 -0
  303. package/vendor/doxbrix-import/dist/safe-path.js +47 -0
  304. package/dist/agents.d.ts.map +0 -1
  305. package/dist/agents.js.map +0 -1
  306. package/dist/args.d.ts.map +0 -1
  307. package/dist/args.js.map +0 -1
  308. package/dist/artifact-deploy.d.ts.map +0 -1
  309. package/dist/artifact-deploy.js.map +0 -1
  310. package/dist/auth.d.ts.map +0 -1
  311. package/dist/auth.js.map +0 -1
  312. package/dist/author.d.ts.map +0 -1
  313. package/dist/author.js.map +0 -1
  314. package/dist/capture.d.ts.map +0 -1
  315. package/dist/capture.js.map +0 -1
  316. package/dist/cli.d.ts.map +0 -1
  317. package/dist/cli.js.map +0 -1
  318. package/dist/deploy.d.ts.map +0 -1
  319. package/dist/deploy.js.map +0 -1
  320. package/dist/deployment-visibility.d.ts.map +0 -1
  321. package/dist/deployment-visibility.js.map +0 -1
  322. package/dist/doctor.d.ts.map +0 -1
  323. package/dist/doctor.js.map +0 -1
  324. package/dist/doxbrix-markdown.d.ts.map +0 -1
  325. package/dist/doxbrix-markdown.js.map +0 -1
  326. package/dist/errors.d.ts.map +0 -1
  327. package/dist/errors.js.map +0 -1
  328. package/dist/fs.d.ts.map +0 -1
  329. package/dist/fs.js.map +0 -1
  330. package/dist/generator-api.d.ts.map +0 -1
  331. package/dist/generator-api.js.map +0 -1
  332. package/dist/generator-manager.d.ts.map +0 -1
  333. package/dist/generator-manager.js.map +0 -1
  334. package/dist/generator-runtime.d.ts.map +0 -1
  335. package/dist/generator-runtime.js.map +0 -1
  336. package/dist/generators.d.ts.map +0 -1
  337. package/dist/generators.js.map +0 -1
  338. package/dist/interactive.d.ts.map +0 -1
  339. package/dist/interactive.js.map +0 -1
  340. package/dist/preview.d.ts.map +0 -1
  341. package/dist/preview.js.map +0 -1
  342. package/dist/progress.d.ts.map +0 -1
  343. package/dist/progress.js.map +0 -1
  344. package/dist/project.d.ts.map +0 -1
  345. package/dist/project.js.map +0 -1
  346. package/dist/prompts.d.ts.map +0 -1
  347. package/dist/prompts.js.map +0 -1
  348. package/dist/settings.d.ts.map +0 -1
  349. package/dist/settings.js.map +0 -1
  350. package/dist/sync.d.ts.map +0 -1
  351. package/dist/sync.js.map +0 -1
  352. package/dist/types.d.ts.map +0 -1
  353. package/dist/types.js.map +0 -1
  354. package/dist/validation.d.ts.map +0 -1
  355. package/dist/validation.js.map +0 -1
  356. package/dist/version.d.ts.map +0 -1
  357. package/dist/version.js.map +0 -1
@@ -0,0 +1,489 @@
1
+ /**
2
+ * Batched authoring.
3
+ *
4
+ * One agent session that writes forty pages runs for 800 turns, and every
5
+ * turn re-reads a context that has grown to 600k tokens: real runs spent
6
+ * 99% of their tokens on cache reads and 45 minutes before a single defect
7
+ * failed the whole run. Splitting the approved plan into short batches keeps
8
+ * each session's context small, bounds the blast radius of a failure to one
9
+ * batch, and lets Doxloop validate and repair between batches instead of
10
+ * once at the end.
11
+ */
12
+ import { preferredPageExtension } from './page-extension.js';
13
+ export const DEFAULT_BATCH_PAGES = 4;
14
+ export const DEFAULT_BATCH_CAPTURES = 18;
15
+ /** Pages the run writes: approved, not deferred, created or updated. */
16
+ export function writablePlanPages(plan) {
17
+ return plan.pages.filter((page) => page.priority !== 'later' && (page.action === 'create' || page.action === 'update'));
18
+ }
19
+ export function plannedCaptures(page) {
20
+ if (!page.visuals || page.visuals.mode === 'none')
21
+ return 0;
22
+ return Math.max(page.visuals.captureSequence?.length ?? 0, page.visuals.estimatedCaptures ?? 0, 1);
23
+ }
24
+ /** Batch size from the environment, for operators tuning a slow application. */
25
+ export function batchOptionsFromEnvironment(env = process.env) {
26
+ const pages = Number(env.DOXLOOP_AUTHORING_BATCH_PAGES);
27
+ const captures = Number(env.DOXLOOP_AUTHORING_BATCH_CAPTURES);
28
+ return {
29
+ maxPages: Number.isInteger(pages) && pages > 0 ? pages : DEFAULT_BATCH_PAGES,
30
+ maxCaptures: Number.isInteger(captures) && captures > 0 ? captures : DEFAULT_BATCH_CAPTURES,
31
+ };
32
+ }
33
+ /**
34
+ * Split pages into batches in plan order, keeping navigation sections together
35
+ * where the size allows: pages that share a section read each other's
36
+ * terminology, so writing them in one session keeps them consistent.
37
+ */
38
+ export function planAuthoringBatches(plan, pages, options = {}) {
39
+ const maxPages = Math.max(1, options.maxPages ?? DEFAULT_BATCH_PAGES);
40
+ const maxCaptures = Math.max(1, options.maxCaptures ?? DEFAULT_BATCH_CAPTURES);
41
+ if (pages.length === 0)
42
+ return [];
43
+ // Order by navigation section first so a batch boundary falls between
44
+ // sections rather than through one; pages in no section keep plan order.
45
+ const sectionOrder = new Map();
46
+ for (const [index, section] of (plan.navigation?.sections ?? []).entries()) {
47
+ for (const id of section.pageIds)
48
+ if (!sectionOrder.has(id))
49
+ sectionOrder.set(id, index);
50
+ }
51
+ const planIndex = new Map(plan.pages.map((page, index) => [page.id, index]));
52
+ const landing = (page) => /^(?:index|overview|home|start-here)$/i.test(page.path.split('/').pop() ?? '');
53
+ const ordered = [...pages].sort((left, right) => {
54
+ // The landing page goes first: it links to everything else and sets the tone.
55
+ const landingDelta = Number(landing(right)) - Number(landing(left));
56
+ if (landingDelta !== 0)
57
+ return landingDelta;
58
+ const sectionDelta = (sectionOrder.get(left.id) ?? Number.MAX_SAFE_INTEGER) - (sectionOrder.get(right.id) ?? Number.MAX_SAFE_INTEGER);
59
+ if (sectionDelta !== 0)
60
+ return sectionDelta;
61
+ return (planIndex.get(left.id) ?? 0) - (planIndex.get(right.id) ?? 0);
62
+ });
63
+ const groups = [];
64
+ let current = [];
65
+ let currentCaptures = 0;
66
+ for (const page of ordered) {
67
+ const captures = plannedCaptures(page);
68
+ const overflow = current.length >= maxPages || (current.length > 0 && currentCaptures + captures > maxCaptures);
69
+ if (overflow) {
70
+ groups.push(current);
71
+ current = [];
72
+ currentCaptures = 0;
73
+ }
74
+ current.push(page);
75
+ currentCaptures += captures;
76
+ }
77
+ if (current.length > 0)
78
+ groups.push(current);
79
+ // A trailing batch of one page is a whole session for a single file; fold
80
+ // it into the previous batch when that stays within the page limit + 1.
81
+ if (groups.length > 1 && groups.at(-1).length === 1 && groups.at(-2).length <= maxPages) {
82
+ const last = groups.pop();
83
+ groups.at(-1).push(...last);
84
+ }
85
+ return groups.map((group, index) => ({
86
+ index: index + 1,
87
+ total: groups.length,
88
+ pages: group,
89
+ captures: group.reduce((sum, page) => sum + plannedCaptures(page), 0),
90
+ }));
91
+ }
92
+ /** Claude turn cap for one batch session. */
93
+ export function batchTurnBudget(batch, env = process.env) {
94
+ const override = Number(env.DOXLOOP_AGENT_MAX_TURNS);
95
+ if (Number.isInteger(override) && override > 0)
96
+ return override;
97
+ return Math.max(60, batch.pages.length * 30 + batch.captures * 12);
98
+ }
99
+ /** Turn cap for a targeted fix session over a few pages. */
100
+ export function fixTurnBudget(files, issues) {
101
+ return Math.max(30, 20 + files * 15 + issues * 4);
102
+ }
103
+ /** Wall-clock cap for one batch, so a stuck session cannot eat the run's budget. */
104
+ export function batchMinutes(batch) {
105
+ return Math.min(45, Math.max(10, batch.pages.length * 4 + batch.captures * 1.5));
106
+ }
107
+ export function describeBatchPage(page) {
108
+ const parts = [`- ${page.path} — "${page.title}" (${page.type}, ${page.action})`];
109
+ if (page.purpose)
110
+ parts.push(` purpose: ${page.purpose}`);
111
+ const evidence = page.evidenceDetails?.length
112
+ ? page.evidenceDetails.slice(0, 6).map((item) => {
113
+ const detail = item;
114
+ const source = typeof detail.source === 'string' ? detail.source : '';
115
+ const path = typeof detail.path === 'string' ? detail.path : '';
116
+ const label = typeof detail.label === 'string' ? detail.label : '';
117
+ return [source, path].filter(Boolean).join(': ') + (label ? ` (${label})` : '');
118
+ }).filter(Boolean)
119
+ : page.evidence?.slice(0, 6) ?? [];
120
+ if (evidence.length > 0)
121
+ parts.push(` evidence: ${evidence.join('; ')}`);
122
+ const visuals = page.visuals;
123
+ if (visuals && visuals.mode !== 'none') {
124
+ parts.push(` screenshots: ${visuals.mode}, start at ${visuals.startPath ?? '/'}, ${plannedCaptures(page)} planned capture${plannedCaptures(page) === 1 ? '' : 's'}`);
125
+ }
126
+ if (page.diagram === 'required')
127
+ parts.push(' diagram: required (Mermaid)');
128
+ return parts.join('\n');
129
+ }
130
+ export const BATCH_SLICE_FILE = '.doxloop/cache/authoring-batch.json';
131
+ /**
132
+ * The part of the plan one batch needs, written to a small file so the agent
133
+ * reads six pages' worth of plan instead of the whole 90 KB document (which a
134
+ * real run re-read, truncated, in every session).
135
+ */
136
+ export function batchPlanSlice(plan, batch) {
137
+ const ids = new Set(batch.pages.map((page) => page.id));
138
+ return {
139
+ batch: batch.index,
140
+ of: batch.total,
141
+ planId: plan.id,
142
+ productProfile: plan.productProfile,
143
+ audiences: plan.audiences,
144
+ terminology: plan.terminology,
145
+ exclusions: plan.exclusions,
146
+ styleGuide: plan.styleGuide,
147
+ locale: plan.locale,
148
+ accessibilityTarget: plan.accessibilityTarget,
149
+ experienceLevel: plan.experienceLevel,
150
+ preferredExamples: plan.preferredExamples,
151
+ instructions: plan.instructions,
152
+ navigation: {
153
+ top: plan.navigation.top,
154
+ sections: plan.navigation.sections
155
+ .filter((section) => section.pageIds.some((id) => ids.has(id)))
156
+ .map((section) => ({ ...section, pageIds: section.pageIds.filter((id) => ids.has(id)) })),
157
+ },
158
+ target: plan.target,
159
+ // One extension for every new page: a writer that followed the first
160
+ // entry of target.pageExtensions (".md" for Doxbrix) wrote 12 of 62
161
+ // pages as .md while the rest were .mdx.
162
+ pageExtension: preferredPageExtension(plan.target),
163
+ pages: batch.pages,
164
+ ...(plan.existingDocumentation
165
+ ? {
166
+ existingDocumentation: plan.existingDocumentation.map((assessment) => ({
167
+ ...assessment,
168
+ pages: assessment.pages?.filter((entry) => (entry.into ?? []).some((id) => ids.has(id))) ?? [],
169
+ })),
170
+ }
171
+ : {}),
172
+ };
173
+ }
174
+ export function batchContract(input) {
175
+ const { batch } = input;
176
+ const artifacts = input.artifacts ?? {};
177
+ const lines = [];
178
+ lines.push(`BATCH ${batch.index} OF ${batch.total}. This session writes only the pages listed under "Pages in this batch". Doxloop runs the other batches in separate sessions${input.concurrent ? ' (several at the same time in this workspace)' : ''}, validates the workspace after each one, and sends any defect back to a short follow-up session, so do not spend turns on anything outside this list.`);
179
+ if (artifacts.pack) {
180
+ lines.push('');
181
+ lines.push(`Start from the evidence pack at ${artifacts.pack}: it holds the excerpts of every source file and existing-documentation page the plan cites for these pages. Write from the pack; open a full source file only for a claim the pack does not settle, and then read the specific lines you need.`);
182
+ }
183
+ if (artifacts.slice) {
184
+ lines.push('');
185
+ lines.push(`Read ${artifacts.slice} for this batch's full page entries (evidence, visuals, navigation section, terminology, style). Do not read .doxloop/documentation-plan.json, .doxloop/project.json, or the whole screenshot manifest: everything this batch needs from them is in the slice and in this prompt, and reading the full files costs more than writing a page.`);
186
+ }
187
+ lines.push('');
188
+ lines.push('Read evidence economically: locate what you need with grep -n first, then read at most 120 lines at a time with sed -n; a larger read is truncated by the tool and the rest is lost, so it costs tokens and shows you nothing. Read each skill reference once per session, not once per page.');
189
+ if (input.references && input.references.length > 0) {
190
+ lines.push(`The skill references this batch needs are already in this prompt, above the task (${input.references.join(', ')}): do not open them or the skill files again. The only reference left to read is the type playbook for a page type in this batch (references/type-*.md), once.`);
191
+ }
192
+ lines.push(sourceSearchGuidance(batch.pages));
193
+ lines.push('');
194
+ lines.push('Pages in this batch (write each one completely, to the depth contract for its type):');
195
+ lines.push(...batch.pages.map(describeBatchPage));
196
+ lines.push(input.pageExtension
197
+ ? `Save every new page as its path plus ${input.pageExtension} (for example ${batch.pages[0]?.path ?? 'guides/page'}${input.pageExtension}), whatever extension another file or evidence key shows; a page that already exists keeps its file name.`
198
+ : `Save every new page as its path plus the extension named by pageExtension in the batch slice, whatever extension another file or evidence key shows; a page that already exists keeps its file name.`);
199
+ lines.push('Write a new page as one whole-file add (a single operation that creates the file with its full content), and replace an existing page by rewriting the whole file rather than editing lines inside it: line-level hunks against long prose fail verification and cost a retry. Save each page with its own edit the moment it is complete, then move to the next one. Do not hold the pages back for one final edit at the end: a session that is stopped early keeps only the pages already saved, and Doxloop retries just the missing ones.');
200
+ if (input.completed.length > 0) {
201
+ lines.push('');
202
+ lines.push(`Pages already written in earlier batches (link to them by their path; do not rewrite, reformat, or re-read them unless a link target needs checking): ${input.completed.map((page) => `/${page.path}`).join(', ')}`);
203
+ }
204
+ if (input.written && input.written.length > 0) {
205
+ lines.push(`Their frontmatter, so yours matches without opening them: ${input.written.map((page) => `${page.path} (title "${page.title ?? ''}"${page.icon ? `, icon ${page.icon}` : ''})`).join('; ')}. Do not list or grep the workspace to check conventions.`);
206
+ }
207
+ if (input.upcoming.length > 0) {
208
+ lines.push('');
209
+ lines.push(`Pages that other batches write${input.concurrent ? ', some of them right now' : ' later'} (link to these planned paths where a reader needs them; do not create or edit them): ${input.upcoming.map((page) => `/${page.path} ("${page.title}")`).join(', ')}`);
210
+ }
211
+ lines.push('');
212
+ const evidenceTarget = artifacts.evidence ?? '.doxloop/evidence-map.json';
213
+ if (artifacts.evidence)
214
+ lines.push(`${artifacts.evidence} already holds an entry per page of this batch, keyed by the page file and seeded from the plan's citations: add the claims worth re-checking and any further source paths you actually read, keep the schema, and do not read the main evidence map or other batches' evidence files.`);
215
+ lines.push(`Doxloop does the bookkeeping for you after this session: it adds missing frontmatter title/description from the plan, adds pages to the navigation, repairs local links whose target it can identify, seeds the evidence map for each page, and validates everything. So: do not run \`doxloop test\`, node, or python; do not count files, list directories to check your work, validate JSON, or grep for unclosed tags; do not write a completion report longer than three sentences. Record each page's evidence sources and claims in ${evidenceTarget} (same schema as the evidence map: a pages object keyed by project-relative page file)${artifacts.evidence ? '; Doxloop merges it into .doxloop/evidence-map.json, which you must not edit yourself in this session' : ''}.`);
216
+ if (input.exclusive) {
217
+ lines.push('Put each page in the navigation group the plan names when you can; Doxloop fills any gap.');
218
+ }
219
+ else {
220
+ lines.push('Do not edit the navigation or site configuration (docs.json or its equivalent) in this session: other sessions may be saving it at the same time, and Doxloop adds every page of this batch to the navigation group the plan names as soon as the session ends.');
221
+ }
222
+ if (input.screenshots) {
223
+ const captured = input.precaptured ?? 0;
224
+ const manifest = artifacts.manifest;
225
+ lines.push('');
226
+ lines.push(`Screenshots for this batch: ${manifest ? `${manifest} lists this batch's approved guides and steps (a slice of the main manifest). Read its saved image paths, observed labels and state descriptions; do not edit it or the main manifest.` : 'the manifest .doxloop/screenshot-manifest.json already lists every approved guide and step.'}${captured > 0 ? ` Doxloop has already captured ${captured} entry-screen step${captured === 1 ? '' : 's'} for these pages (status verified with a file): keep those rows and embed those images.` : ''} Choose and embed the saved images that illustrate these pages, using their recorded state and alt text. Browser capture is a separate stage: never navigate, capture, or change capture status while writing. Leave missing states for Doxloop to report.`);
227
+ }
228
+ if (input.mode === 'create' && batch.index === 1 && input.exclusive) {
229
+ lines.push('');
230
+ lines.push('This is the first batch of a new documentation set and it runs alone: also configure the generator theme and site identity from public brand evidence in the sources (name, logo, colors, fonts) and make sure the navigation spaces and groups the plan names exist in the generator configuration so the other batches can slot pages into them.');
231
+ }
232
+ return lines.join('\n');
233
+ }
234
+ /**
235
+ * Concurrent batch sessions, from the environment; 1 runs batches one after
236
+ * another. A run's wall time is almost entirely model time (a measured run:
237
+ * 43 min model, 1 min tools), so concurrency is the only lever on it, and it
238
+ * spends no extra tokens: each batch costs the same whether or not another
239
+ * is running.
240
+ */
241
+ export function parallelismFromEnvironment(env = process.env) {
242
+ const value = Number(env.DOXLOOP_AUTHORING_PARALLEL);
243
+ if (Number.isInteger(value) && value >= 1)
244
+ return Math.min(value, 6);
245
+ return DEFAULT_PARALLEL_BATCHES;
246
+ }
247
+ /**
248
+ * Two, not three: with three Codex sessions in flight a real run saw all
249
+ * three model streams hang at the same second and lose their whole batch.
250
+ */
251
+ export const DEFAULT_PARALLEL_BATCHES = 2;
252
+ /** How often the inactivity watchdog checks a session's output. */
253
+ export const AGENT_IDLE_CHECK_MS = 15_000;
254
+ /**
255
+ * Default quiet time before a session is stopped and retried. Long enough for
256
+ * a writer to think and then emit four pages in one edit (measured under five
257
+ * minutes), short enough that a hung model stream costs minutes, not a cap.
258
+ */
259
+ export const DEFAULT_AGENT_IDLE_MINUTES = 6;
260
+ /**
261
+ * Quiet time after which a session is stopped, in milliseconds; 0 disables
262
+ * the watchdog. `DOXLOOP_AGENT_IDLE_MINUTES` overrides the default.
263
+ */
264
+ export function agentIdleLimitMs(env = process.env) {
265
+ const raw = env.DOXLOOP_AGENT_IDLE_MINUTES;
266
+ if (raw !== undefined && raw.trim() !== '') {
267
+ const value = Number(raw);
268
+ if (Number.isFinite(value) && value >= 0)
269
+ return Math.round(value * 60_000);
270
+ }
271
+ return DEFAULT_AGENT_IDLE_MINUTES * 60_000;
272
+ }
273
+ export function formatIdleMinutes(ms) {
274
+ const minutes = Math.round(ms / 6_000) / 10;
275
+ return `${minutes} minute${minutes === 1 ? '' : 's'}`;
276
+ }
277
+ /**
278
+ * Whether a batch has to run before the others start: the landing page links
279
+ * to every section, and a new site's theme and spaces must exist before
280
+ * concurrent sessions try to use them.
281
+ */
282
+ export function batchNeedsExclusiveStart(batch, mode) {
283
+ if (mode === 'create' && batch.index === 1)
284
+ return true;
285
+ return batch.pages.some((page) => /^(?:index|overview|home|start-here)$/i.test(page.path.split('/').pop() ?? ''));
286
+ }
287
+ /** Split issues into fix-session chunks of at most `maxFiles` files each. */
288
+ export function chunkIssuesByFile(issues, maxFiles) {
289
+ const byFile = new Map();
290
+ for (const issue of issues) {
291
+ const key = issue.file ?? '(project)';
292
+ byFile.set(key, [...(byFile.get(key) ?? []), issue]);
293
+ }
294
+ const chunks = [];
295
+ let current = [];
296
+ let files = 0;
297
+ for (const group of byFile.values()) {
298
+ if (files >= Math.max(1, maxFiles)) {
299
+ chunks.push(current);
300
+ current = [];
301
+ files = 0;
302
+ }
303
+ current.push(...group);
304
+ files += 1;
305
+ }
306
+ if (current.length > 0)
307
+ chunks.push(current);
308
+ return chunks;
309
+ }
310
+ /**
311
+ * The follow-up prompt for defects Doxloop found in a batch. Each issue names
312
+ * its file, so the agent can go straight to the fix.
313
+ */
314
+ export function fixContract(input) {
315
+ const byFile = new Map();
316
+ for (const issue of input.issues) {
317
+ const key = issue.file ?? '(project)';
318
+ byFile.set(key, [...(byFile.get(key) ?? []), issue]);
319
+ }
320
+ const sections = [...byFile.entries()].map(([file, issues]) => `${file}:\n${issues.map((issue) => ` - ${issue.severity} ${issue.code}: ${issue.message}`).join('\n')}`);
321
+ const depth = input.issues.some((issue) => issue.code === 'thin-page' || issue.code === 'thin-procedure');
322
+ return `FIX ROUND ${input.round} OF ${input.maxRounds}. Doxloop validated the pages this run wrote and found the problems below. Fix exactly these problems in exactly these files and nothing else; do not rewrite passages that are not named, do not touch other pages, and do not run validation yourself — Doxloop re-validates the moment you finish. Do not re-read the skill, the plan, or the project configuration for this: ${depth ? 'read only the authoring skill\'s references/page-depth.md for the depth contract, then the named pages and the evidence they cite' : 'read only the named pages and the evidence they cite'}. A thin-page or thin-procedure warning means the page is missing parts of the depth contract (outcome-led opening, prerequisites, every step with its observable result, verification, evidence-backed troubleshooting, next step): add only the missing parts, written for this page's own task from the evidence it cites. If every part is already present and the evidence offers nothing more, leave the page unchanged and say so in your reply: never pad with generic advice, testing tips, restated steps, repeated cautions, or fixture details from the capture application, because a short accurate page beats a long padded one and the warning is then accepted as is. A broken-link error names a target that does not exist: point the link at the real page path or remove it. When you finish, reply with one sentence per file saying what changed.
323
+
324
+ Files: ${input.files.join(', ')}
325
+
326
+ ${sections.join('\n\n')}`;
327
+ }
328
+ /** Validation issues that belong to the given page files, errors first. */
329
+ export function issuesForFiles(issues, files, options = {}) {
330
+ // The post-pass may rename a page from .md to .mdx after the batch's file
331
+ // list was taken; the page is the same one.
332
+ const stem = (file) => file.replace(/\.mdx?$/i, '');
333
+ const stems = new Set([...files].map(stem));
334
+ const relevant = issues.filter((issue) => {
335
+ if (!issue.file || !(files.has(issue.file) || stems.has(stem(issue.file))))
336
+ return false;
337
+ if (issue.severity === 'error')
338
+ return true;
339
+ return options.includeWarnings === true && FIXABLE_WARNINGS.has(issue.code);
340
+ });
341
+ return relevant.sort((left, right) => Number(right.severity === 'error') - Number(left.severity === 'error'));
342
+ }
343
+ /** Warnings worth a fix session: they are the depth contract the writer owes, not taste. */
344
+ export const FIXABLE_WARNINGS = new Set(['thin-page', 'thin-procedure', 'missing-diagram', 'api-endpoint-param-example']);
345
+ const SUGGESTION_LABELS = {
346
+ 'thin-page': 'thin pages',
347
+ 'thin-procedure': 'procedures missing parts',
348
+ 'missing-diagram': 'missing diagrams',
349
+ 'api-endpoint-param-example': 'missing parameter examples',
350
+ 'api-endpoint-missing': 'endpoints without a reference block',
351
+ };
352
+ function plural(count, noun) {
353
+ return `${count} ${noun}${count === 1 ? '' : 's'}`;
354
+ }
355
+ /**
356
+ * How a set of validation issues reads in the run log. Errors block the
357
+ * proposal; the fixable warnings are suggestions, and calling 204 missing
358
+ * parameter examples "204 issues" right after every batch "passed" read as
359
+ * a run that had gone wrong.
360
+ */
361
+ export function describeIssues(issues) {
362
+ const errors = issues.filter((issue) => issue.severity === 'error').length;
363
+ const suggestions = issues.length - errors;
364
+ const pages = new Set(issues.map((issue) => issue.file ?? '(project)')).size;
365
+ const kinds = [...new Set(issues.filter((issue) => issue.severity !== 'error').map((issue) => SUGGESTION_LABELS[issue.code] ?? issue.code))];
366
+ const parts = [
367
+ ...(errors > 0 ? [plural(errors, 'error')] : []),
368
+ ...(suggestions > 0 ? [`${plural(suggestions, errors > 0 ? 'suggestion' : 'minor suggestion')}${kinds.length > 0 ? ` (${kinds.join(', ')})` : ''}`] : []),
369
+ ];
370
+ return `${parts.join(' and ')} in ${plural(pages, 'page')}`;
371
+ }
372
+ /** The line that opens the end-of-run pass: "Final polish" when nothing blocks the proposal. */
373
+ export function finalCheckAnnouncement(issues, sessions, parallel) {
374
+ const blocking = issues.some((issue) => issue.severity === 'error');
375
+ const concurrency = sessions > 1 && parallel > 1 ? `, up to ${Math.min(parallel, sessions)} at a time` : '';
376
+ return `${blocking ? 'Final check' : 'Final polish'}: ${describeIssues(issues)}; fixing in ${plural(sessions, 'session')}${concurrency}.`;
377
+ }
378
+ /**
379
+ * What a batch's fix round works on. The batch and the end-of-run pass
380
+ * validate with the same rules: errors always go to a fix session, and the
381
+ * fixable warnings ride along with them, because the session is already open
382
+ * on those pages. A batch with warnings alone passes and leaves them for the
383
+ * one consolidated polish at the end, where they used to arrive unannounced
384
+ * (204 of them after sixteen batches that had all "passed").
385
+ */
386
+ export function splitBatchIssues(issues) {
387
+ const hasErrors = issues.some((issue) => issue.severity === 'error');
388
+ return hasErrors ? { fix: issues, deferred: [] } : { fix: [], deferred: issues };
389
+ }
390
+ /**
391
+ * A batch's result line. A batch passes on errors alone; its fixable
392
+ * warnings are counted here so the end-of-run polish is no surprise.
393
+ */
394
+ export function batchPassLine(label, pageCount, deferred) {
395
+ const suggestions = deferred.filter((issue) => issue.severity !== 'error');
396
+ const tail = suggestions.length > 0 ? ` (${plural(suggestions.length, 'minor suggestion')} left for the final polish)` : '';
397
+ return `${label}: ${plural(pageCount, 'page')} pass${pageCount === 1 ? 'es' : ''} validation${tail}.`;
398
+ }
399
+ /** Codes Doxloop repairs itself between batches; a fix session never needs them. */
400
+ export const REPAIRED_BY_POSTPASS = new Set(['missing-title', 'missing-description', 'unnavigated-page', 'code-language']);
401
+ /**
402
+ * A "broken link" whose target is a planned page another batch has not
403
+ * written yet is not a defect: the contract tells the writer to link to
404
+ * those paths, and the end-of-run check sees them once every batch is done.
405
+ * A per-batch fix session given such an issue removes the cross-link, which
406
+ * costs a session and loses the link for good.
407
+ */
408
+ export function isForwardLinkIssue(issue, pendingPaths) {
409
+ if (issue.code !== 'broken-link')
410
+ return false;
411
+ const target = /:\s*(\S+)\s*$/.exec(issue.message)?.[1];
412
+ if (!target)
413
+ return false;
414
+ const normalized = normalizeLinkPath(target);
415
+ if (!normalized)
416
+ return false;
417
+ for (const path of pendingPaths) {
418
+ const planned = normalizeLinkPath(path);
419
+ if (planned && (normalized === planned || normalized.endsWith(`/${planned}`)))
420
+ return true;
421
+ }
422
+ return false;
423
+ }
424
+ function normalizeLinkPath(value) {
425
+ return value
426
+ .split(/[?#]/)[0]
427
+ .replace(/^\.?\//, '')
428
+ .replace(/\/+$/, '')
429
+ .replace(/\.(mdx?|rst|html?)$/i, '')
430
+ .replace(/\/index$/, '')
431
+ .replace(/^\/+/, '');
432
+ }
433
+ /**
434
+ * Where the writer should look, and where it should not. Search results from
435
+ * tests, fixtures, generated bundles, migrations and lockfiles are the bulk
436
+ * of what a broad grep over a product repository returns, and every hit the
437
+ * agent reads is paid for in context. The batch's own citations name the
438
+ * directories that matter.
439
+ */
440
+ export const SOURCE_NOISE_DIRECTORIES = ['node_modules', 'dist', 'build', 'out', 'vendor', 'coverage', 'target', '.git', '__snapshots__', 'testdata', 'fixtures', 'migrations', 'migration', 'swagger', 'openapi', 'locales', 'i18n/lang', 'e2e', 'cypress', '__tests__', 'test', 'tests', 'mocks', '__mocks__', 'generated', 'gen', 'proto-gen', 'public/emojis'];
441
+ export const SOURCE_NOISE_FILES = ['*_test.go', '*.test.*', '*.spec.*', '*.min.js', '*.standalone.js', '*.map', '*.lock', '*-lock.*', '*.snap', '*.pb.go', '*.generated.*', 'CHANGELOG*'];
442
+ export function sourceSearchGuidance(pages) {
443
+ const directories = new Set();
444
+ for (const page of pages) {
445
+ for (const detail of page.evidenceDetails ?? []) {
446
+ if (!detail?.path)
447
+ continue;
448
+ const parts = detail.path.split('/').filter(Boolean);
449
+ if (parts.length > 1)
450
+ directories.add(`${detail.source}: ${parts.slice(0, Math.min(2, parts.length - 1)).join('/')}`);
451
+ }
452
+ }
453
+ const excludeDirs = SOURCE_NOISE_DIRECTORIES.filter((entry) => !entry.includes('/')).map((entry) => `--exclude-dir=${entry}`).join(' ');
454
+ const excludeFiles = SOURCE_NOISE_FILES.map((entry) => `--exclude='${entry}'`).join(' ');
455
+ const lines = [
456
+ `When the pack does not settle a claim, search the source narrowly. Grep only the directory the citation points at${directories.size > 0 ? ` (this batch's evidence lives under ${[...directories].slice(0, 6).join(', ')})` : ''}, never a whole repository, and exclude what is not product behaviour: ${excludeDirs} ${excludeFiles}. Tests, fixtures, migrations, generated API bundles, translation files for other languages, minified vendor scripts and changelogs are not evidence for a how-to page; open a test only for a concrete example the pack lacks.`,
457
+ 'Read the label catalog (the English UI strings file) through the values quoted in the pack or with grep -n for one key; never cat it.',
458
+ ];
459
+ return lines.join(' ');
460
+ }
461
+ /** Guides per capture session; the browser signs in once per session. */
462
+ export const DEFAULT_CAPTURE_GUIDES_PER_SESSION = 3;
463
+ export const DEFAULT_CAPTURE_STEPS_PER_SESSION = 8;
464
+ /**
465
+ * Group guides that still need captures into sessions: a few guides each,
466
+ * bounded by their planned steps so one session never drives a long tour,
467
+ * in plan order so a session's guides are neighbours in the product.
468
+ */
469
+ export function captureSessionGroups(pending, env = process.env) {
470
+ const guides = Number(env.DOXLOOP_CAPTURE_GUIDES_PER_SESSION);
471
+ const maxGuides = Number.isInteger(guides) && guides > 0 ? guides : DEFAULT_CAPTURE_GUIDES_PER_SESSION;
472
+ const groups = [];
473
+ let current = [];
474
+ let steps = 0;
475
+ for (const item of pending) {
476
+ const own = item.guide.steps.filter((step) => step.status !== 'verified').length;
477
+ if (current.length > 0 && (current.length >= maxGuides || steps + own > DEFAULT_CAPTURE_STEPS_PER_SESSION)) {
478
+ groups.push(current);
479
+ current = [];
480
+ steps = 0;
481
+ }
482
+ current.push(item);
483
+ steps += own;
484
+ }
485
+ if (current.length > 0)
486
+ groups.push(current);
487
+ return groups;
488
+ }
489
+ //# sourceMappingURL=authoring-batches.js.map
@@ -0,0 +1,79 @@
1
+ import type { ValidationIssue, DocumentationPlan, DocumentationPlanPage, DoxloopProject, PageEvidence } from './types.js';
2
+ /**
3
+ * Deterministic bookkeeping repairs that run after each authoring batch,
4
+ * before validation. Agents write good prose and then miss the mechanical
5
+ * parts: frontmatter fields, navigation entries, link paths that assume a
6
+ * folder the page does not live in, and evidence-map entries. Each repair
7
+ * here is idempotent, stays inside the workspace, and never rewrites prose.
8
+ */
9
+ export interface PostPassInput {
10
+ workspace: string;
11
+ project: DoxloopProject;
12
+ plan: DocumentationPlan;
13
+ /** The pages of the batch that just finished; other planned pages are only used as link targets. */
14
+ pages: DocumentationPlanPage[];
15
+ /**
16
+ * Turn a local link that resolves to no page, planned or existing, into
17
+ * plain text. Only the end-of-run pass sets this: a broken link is a
18
+ * validation error that blocks the whole proposal, and a page that mentions
19
+ * a topic without linking it is still a usable page.
20
+ */
21
+ unlinkUnresolved?: boolean;
22
+ /**
23
+ * Remove spaces no page landed in. Only the end-of-run pass sets this: the
24
+ * first batch of a new site creates the spaces later batches slot pages
25
+ * into, and pruning them between batches sends every later page to the
26
+ * first space.
27
+ */
28
+ pruneEmptySpaces?: boolean;
29
+ }
30
+ export interface PostPassReport {
31
+ repairs: string[];
32
+ problems: string[];
33
+ }
34
+ export declare function applyAuthoringPostPass(input: PostPassInput): Promise<PostPassReport>;
35
+ /**
36
+ * The starter file a planned page was written over, mirroring the contract
37
+ * `starterReplacements` gives the writer: a landing page (index, overview,
38
+ * home, start-here) is the site's index; any other page replaces the starter
39
+ * whose file name matches its last path segment.
40
+ */
41
+ export declare function starterReplacementId(planId: string, workspacePages: readonly string[]): string | undefined;
42
+ /**
43
+ * Give an unlabeled opening fence the `text` language. The validator warns on
44
+ * every bare fence and a real run spent two fix sessions adding `ini` and
45
+ * `text` by hand; `text` renders the block exactly as written, so it is never
46
+ * wrong, and a writer that knows better still labels its blocks itself.
47
+ */
48
+ export declare function labelBareCodeFences(content: string): string;
49
+ /**
50
+ * Insert or replace top-level frontmatter keys while leaving every other byte
51
+ * of the file alone. A blank existing key (`title:`) is replaced in place so
52
+ * the YAML never gains a duplicate mapping key.
53
+ */
54
+ export declare function insertFrontmatterFields(content: string, fields: Array<[string, string]>): string;
55
+ export declare function trimAtWordBoundary(text: string, limit: number): string;
56
+ /**
57
+ * Replace every Markdown link to `href` with its link text, leaving fenced
58
+ * code and comments untouched. Reference definitions and HTML attributes are
59
+ * left alone: a definition is harmless, and an `href` inside a component is
60
+ * the author's to fix.
61
+ */
62
+ export declare function unlinkTarget(content: string, href: string): string;
63
+ /**
64
+ * Replace one link target everywhere it appears as a Markdown destination, a
65
+ * reference definition, or an `href`/`src` attribute, leaving fenced code and
66
+ * comments untouched so examples keep their literal text.
67
+ */
68
+ export declare function rewriteLink(content: string, from: string, to: string): string;
69
+ export declare function seedEvidence(page: DocumentationPlanPage): PageEvidence;
70
+ /** Strip `./`, a leading slash, and a page extension so plan paths compare to page ids. */
71
+ export declare function normalizePlanPath(path: string): string;
72
+ /**
73
+ * Delete the generated starter pages behind unplanned-page errors when they
74
+ * still carry the starter marker. A plan that keeps no page at a starter's
75
+ * path superseded it; handing it to a fix session produced a rewritten page
76
+ * that was still in no navigation.
77
+ */
78
+ export declare function removeSupersededStarterPages(root: string, issues: ValidationIssue[], report?: PostPassReport): Promise<string[]>;
79
+ //# sourceMappingURL=authoring-postpass.d.ts.map