@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
package/dist/sync.d.ts CHANGED
@@ -1,7 +1,42 @@
1
1
  import type { SourceBinding, SourceChange, SyncState } from './types.js';
2
+ /**
3
+ * Baseline marker for a local folder that is not a Git repository. Such a
4
+ * source is compared by content: the recorded per-file digests name exactly
5
+ * which files changed, so drift can still be traced to individual pages.
6
+ */
7
+ export declare const LOCAL_CONTENT_BASELINE = "local-content";
8
+ /**
9
+ * Baseline marker for a crawled documentation-site snapshot. The snapshot is
10
+ * frozen, so a change means the site was re-crawled and the recorded per-page
11
+ * digests name exactly which existing pages changed.
12
+ */
13
+ export declare const DOCS_SITE_BASELINE = "docs-site";
2
14
  export declare const SYNC_STATE_FILE: string;
3
15
  export declare function readSyncState(root: string): Promise<SyncState>;
4
16
  export declare function recordSyncState(root: string, sources: SourceBinding[]): Promise<SyncState>;
5
17
  export declare function collectSourceChanges(root: string, sources: SourceBinding[]): Promise<SourceChange[]>;
18
+ /** Name-status lines, in Git's format, describing how one manifest became another. */
19
+ export declare function manifestChanges(before: Record<string, string>, after: Record<string, string>): string[];
20
+ /** Stable content fingerprints used to invalidate an approved planning checkpoint. */
21
+ export declare function sourceSnapshotFingerprints(root: string, sources: SourceBinding[]): Promise<Record<string, string | null>>;
22
+ /**
23
+ * Source-relative paths touched by a change, parsed from the same Git output
24
+ * that produced the human-readable lists. Rename and copy entries contribute
25
+ * both the old and the new path so either one can match documented evidence.
26
+ */
27
+ export declare function changedSourcePaths(change: SourceChange): string[];
6
28
  export declare function formatSourceChanges(changes: SourceChange[]): string;
29
+ export interface LocalSourceManifest {
30
+ /** Source-relative portable paths to their content digests. */
31
+ files: Record<string, string>;
32
+ /** One digest over every path and content, in the historical format. */
33
+ fingerprint: string;
34
+ }
35
+ /**
36
+ * Every file that counts as source content, with its digest. Git repositories
37
+ * contribute tracked and untracked-but-not-ignored files; a plain folder is
38
+ * walked directly with the same exclusions a repository would apply by
39
+ * convention. Secrets are never read.
40
+ */
41
+ export declare function localSourceManifest(path: string): Promise<LocalSourceManifest>;
7
42
  //# sourceMappingURL=sync.d.ts.map
package/dist/sync.js CHANGED
@@ -1,11 +1,25 @@
1
1
  import { execFile } from 'node:child_process';
2
2
  import { createHash } from 'node:crypto';
3
- import { lstat, readFile, readlink, writeFile } from 'node:fs/promises';
4
- import { join, resolve } from 'node:path';
3
+ import { lstat, readFile, readdir, readlink, writeFile } from 'node:fs/promises';
4
+ import { basename, join, relative, resolve } from 'node:path';
5
5
  import { promisify } from 'node:util';
6
6
  import { pathExists, readJson } from './fs.js';
7
+ import { diffOpenApi, loadOpenApiSource, openApiChangedIdentifiers } from './openapi.js';
7
8
  import { isSpecUrl, sourceKind } from './project.js';
9
+ import { remoteHead } from './remote-source.js';
8
10
  const SPEC_BASELINE = 'openapi-spec';
11
+ /**
12
+ * Baseline marker for a local folder that is not a Git repository. Such a
13
+ * source is compared by content: the recorded per-file digests name exactly
14
+ * which files changed, so drift can still be traced to individual pages.
15
+ */
16
+ export const LOCAL_CONTENT_BASELINE = 'local-content';
17
+ /**
18
+ * Baseline marker for a crawled documentation-site snapshot. The snapshot is
19
+ * frozen, so a change means the site was re-crawled and the recorded per-page
20
+ * digests name exactly which existing pages changed.
21
+ */
22
+ export const DOCS_SITE_BASELINE = 'docs-site';
9
23
  export const SYNC_STATE_FILE = join('.doxloop', 'sync-state.json');
10
24
  const MAX_LISTED_FILES = 40;
11
25
  const run = promisify(execFile);
@@ -23,26 +37,58 @@ export async function recordSyncState(root, sources) {
23
37
  const state = { schemaVersion: 1, sources: {} };
24
38
  for (const source of sources) {
25
39
  if (sourceKind(source) === 'openapi') {
26
- if (isSpecUrl(source.path))
40
+ const loaded = await loadOpenApiSource(root, source);
41
+ state.sources[source.name] = {
42
+ commit: SPEC_BASELINE,
43
+ recordedAt: new Date().toISOString(),
44
+ contentFingerprint: loaded.hash,
45
+ connector: {
46
+ id: 'openapi',
47
+ version: 1,
48
+ ...(loaded.etag ? { etag: loaded.etag } : {}),
49
+ ...(loaded.lastModified ? { lastModified: loaded.lastModified } : {}),
50
+ openapi: loaded.snapshot,
51
+ },
52
+ };
53
+ continue;
54
+ }
55
+ if (source.remote) {
56
+ state.sources[source.name] = {
57
+ commit: await remoteHead(source.remote),
58
+ recordedAt: new Date().toISOString(),
59
+ };
60
+ continue;
61
+ }
62
+ const sourcePath = resolve(root, source.path);
63
+ if (sourceKind(source) === 'docs-site') {
64
+ if (!(await pathExists(sourcePath)))
27
65
  continue;
28
- const fingerprint = await specFileFingerprint(resolve(root, source.path));
29
- if (fingerprint) {
30
- state.sources[source.name] = {
31
- commit: SPEC_BASELINE,
32
- recordedAt: new Date().toISOString(),
33
- contentFingerprint: fingerprint,
34
- };
35
- }
66
+ const manifest = await localSourceManifest(sourcePath);
67
+ state.sources[source.name] = {
68
+ commit: DOCS_SITE_BASELINE,
69
+ recordedAt: new Date().toISOString(),
70
+ contentFingerprint: manifest.fingerprint,
71
+ files: manifest.files,
72
+ };
36
73
  continue;
37
74
  }
38
- const commit = await headCommit(resolve(root, source.path));
75
+ const commit = await headCommit(sourcePath);
39
76
  if (commit) {
40
- const sourcePath = resolve(root, source.path);
41
77
  state.sources[source.name] = {
42
78
  commit,
43
79
  recordedAt: new Date().toISOString(),
44
80
  contentFingerprint: await sourceContentFingerprint(sourcePath),
45
81
  };
82
+ continue;
83
+ }
84
+ if (await pathExists(sourcePath)) {
85
+ const manifest = await localSourceManifest(sourcePath);
86
+ state.sources[source.name] = {
87
+ commit: LOCAL_CONTENT_BASELINE,
88
+ recordedAt: new Date().toISOString(),
89
+ contentFingerprint: manifest.fingerprint,
90
+ files: manifest.files,
91
+ };
46
92
  }
47
93
  }
48
94
  await writeFile(join(root, SYNC_STATE_FILE), `${JSON.stringify(state, null, 2)}\n`, 'utf8');
@@ -61,9 +107,23 @@ export async function collectSourceChanges(root, sources) {
61
107
  changes.push({ ...source, kind: 'missing-path' });
62
108
  continue;
63
109
  }
110
+ if (sourceKind(source) === 'docs-site') {
111
+ changes.push(await collectLocalContentChange(source, path, state, DOCS_SITE_BASELINE));
112
+ continue;
113
+ }
64
114
  const head = await headCommit(path);
65
115
  if (!head) {
66
- changes.push({ ...source, kind: 'not-git' });
116
+ // A remote source materialized from a provider is an immutable snapshot
117
+ // pinned at the recorded commit: the folder is named after that commit
118
+ // and nothing inside it can drift, so it is current by construction.
119
+ // Treating it as a baseline-free plain folder made every page of a
120
+ // GitHub-sourced project count as unverified.
121
+ const record = state.sources[source.name];
122
+ if (source.remote && record && record.commit !== LOCAL_CONTENT_BASELINE && basename(path) === record.commit) {
123
+ changes.push({ ...source, kind: 'unchanged', baseline: record.commit, head: record.commit, changedFiles: [], uncommittedFiles: [] });
124
+ continue;
125
+ }
126
+ changes.push(await collectLocalContentChange(source, path, state));
67
127
  continue;
68
128
  }
69
129
  const uncommittedFiles = await uncommittedChanges(path);
@@ -95,32 +155,150 @@ export async function collectSourceChanges(root, sources) {
95
155
  }
96
156
  return changes;
97
157
  }
158
+ /**
159
+ * A folder without Git history is compared against the file digests recorded
160
+ * at the last sync, so a scheduled check on a plain folder still names the
161
+ * files that changed.
162
+ */
163
+ async function collectLocalContentChange(source, path, state, baseline = LOCAL_CONTENT_BASELINE) {
164
+ const record = state.sources[source.name];
165
+ if (!record || record.commit !== baseline || !record.files) {
166
+ return { ...source, kind: 'no-baseline', head: baseline, uncommittedFiles: [] };
167
+ }
168
+ const manifest = await localSourceManifest(path);
169
+ const changedFiles = record.contentFingerprint === manifest.fingerprint
170
+ ? []
171
+ : manifestChanges(record.files, manifest.files);
172
+ return {
173
+ ...source,
174
+ kind: changedFiles.length === 0 ? 'unchanged' : 'changed',
175
+ baseline,
176
+ head: baseline,
177
+ changedFiles,
178
+ uncommittedFiles: [],
179
+ };
180
+ }
181
+ /** Name-status lines, in Git's format, describing how one manifest became another. */
182
+ export function manifestChanges(before, after) {
183
+ const lines = [];
184
+ for (const file of Object.keys(after).sort()) {
185
+ if (!(file in before))
186
+ lines.push(`A\t${file}`);
187
+ else if (before[file] !== after[file])
188
+ lines.push(`M\t${file}`);
189
+ }
190
+ for (const file of Object.keys(before).sort()) {
191
+ if (!(file in after))
192
+ lines.push(`D\t${file}`);
193
+ }
194
+ return lines;
195
+ }
196
+ /** Stable content fingerprints used to invalidate an approved planning checkpoint. */
197
+ export async function sourceSnapshotFingerprints(root, sources) {
198
+ const fingerprints = {};
199
+ for (const source of sources) {
200
+ if (sourceKind(source) === 'openapi') {
201
+ try {
202
+ fingerprints[source.name] = (await loadOpenApiSource(root, source)).hash;
203
+ }
204
+ catch {
205
+ fingerprints[source.name] = null;
206
+ }
207
+ continue;
208
+ }
209
+ try {
210
+ fingerprints[source.name] = await sourceContentFingerprint(resolve(root, source.path));
211
+ }
212
+ catch {
213
+ fingerprints[source.name] = null;
214
+ }
215
+ }
216
+ return fingerprints;
217
+ }
98
218
  async function collectSpecChange(root, source, state) {
99
- if (isSpecUrl(source.path))
100
- return { ...source, kind: 'spec-remote' };
101
- const fingerprint = await specFileFingerprint(resolve(root, source.path));
102
- if (!fingerprint)
103
- return { ...source, kind: 'missing-path' };
104
219
  const record = state.sources[source.name];
105
- if (record?.contentFingerprint === fingerprint) {
106
- return { ...source, kind: 'spec-unchanged' };
220
+ let loaded;
221
+ try {
222
+ loaded = await loadOpenApiSource(root, source, record?.connector ? {
223
+ ...(record.connector.etag ? { etag: record.connector.etag } : {}),
224
+ ...(record.connector.lastModified ? { lastModified: record.connector.lastModified } : {}),
225
+ } : undefined);
107
226
  }
108
- return { ...source, kind: 'spec-changed' };
227
+ catch (error) {
228
+ if (!isSpecUrl(source.path) && !(await pathExists(resolve(root, source.path)))) {
229
+ return { ...source, kind: 'missing-path' };
230
+ }
231
+ throw error;
232
+ }
233
+ if (record?.contentFingerprint === loaded.hash || loaded.notModified) {
234
+ return { ...source, kind: 'spec-unchanged', head: loaded.hash, summary: loaded.summary };
235
+ }
236
+ const apiDiff = diffOpenApi(record?.connector?.openapi, loaded.snapshot);
237
+ return {
238
+ ...source,
239
+ kind: 'spec-changed',
240
+ ...(record?.contentFingerprint ? { baseline: record.contentFingerprint } : {}),
241
+ head: loaded.hash,
242
+ summary: loaded.summary,
243
+ apiDiff,
244
+ changedIdentifiers: openApiChangedIdentifiers(apiDiff),
245
+ };
246
+ }
247
+ /**
248
+ * Source-relative paths touched by a change, parsed from the same Git output
249
+ * that produced the human-readable lists. Rename and copy entries contribute
250
+ * both the old and the new path so either one can match documented evidence.
251
+ */
252
+ export function changedSourcePaths(change) {
253
+ if (change.kind === 'spec-changed')
254
+ return change.changedIdentifiers;
255
+ if (change.kind !== 'changed' && change.kind !== 'no-baseline' && change.kind !== 'baseline-lost') {
256
+ return [];
257
+ }
258
+ const paths = new Set();
259
+ if (change.kind === 'changed') {
260
+ for (const line of change.changedFiles) {
261
+ for (const path of parseNameStatus(line))
262
+ paths.add(path);
263
+ }
264
+ }
265
+ for (const line of change.uncommittedFiles) {
266
+ for (const path of parsePorcelain(line))
267
+ paths.add(path);
268
+ }
269
+ return [...paths];
270
+ }
271
+ /** `M<tab>src/auth.ts` or `R100<tab>old.ts<tab>new.ts` */
272
+ function parseNameStatus(line) {
273
+ const fields = line.split('\t');
274
+ return fields.slice(1).map(unquotePath).filter(Boolean);
109
275
  }
110
- async function specFileFingerprint(path) {
276
+ /** `M src/auth.ts`, `?? new.ts`, or `R old.ts -> new.ts`, already trimmed. */
277
+ function parsePorcelain(line) {
278
+ const match = /^\S+\s+(.*)$/.exec(line);
279
+ if (!match?.[1])
280
+ return [];
281
+ return match[1]
282
+ .split(' -> ')
283
+ .map((part) => unquotePath(part))
284
+ .filter(Boolean);
285
+ }
286
+ function unquotePath(value) {
287
+ const path = value.trim();
288
+ if (!path.startsWith('"') || !path.endsWith('"'))
289
+ return path;
111
290
  try {
112
- const content = await readFile(path);
113
- return createHash('sha256').update(content).digest('hex');
291
+ return String(JSON.parse(path));
114
292
  }
115
293
  catch {
116
- return undefined;
294
+ return path.slice(1, -1);
117
295
  }
118
296
  }
119
297
  export function formatSourceChanges(changes) {
120
298
  if (changes.length === 0)
121
299
  return '';
122
300
  const sections = changes.map((change) => {
123
- const heading = `Source "${change.name}" (${change.path})`;
301
+ const heading = `Source "${change.name}" (${change.site?.url ?? change.path})`;
124
302
  switch (change.kind) {
125
303
  case 'missing-path':
126
304
  return `${heading}: the configured path does not exist. Report this instead of guessing.`;
@@ -129,16 +307,35 @@ export function formatSourceChanges(changes) {
129
307
  case 'spec-unchanged':
130
308
  return `${heading}: the API specification is unchanged since the last documentation sync.`;
131
309
  case 'spec-changed':
132
- return `${heading}: the API specification changed since the last documentation sync, or no baseline is recorded yet. Compare it with the documented endpoints and update every affected page; the baseline is recorded when this task completes.`;
310
+ return `${heading}: the API specification changed${change.baseline ? ` (${short(change.baseline)} -> ${short(change.head)})` : ' and has no recorded baseline'}.
311
+ ${formatApiDelta(change.apiDiff)}
312
+ Update only documentation affected by this structural delta; the new baseline is recorded after acceptance.`;
133
313
  case 'not-git':
134
314
  return `${heading}: not a Git repository, so no change baseline is available. Inspect the source directly.`;
135
315
  case 'no-baseline':
316
+ if (change.head === DOCS_SITE_BASELINE) {
317
+ return `${heading}: existing documentation site, crawled into the read-only snapshot at ${change.path}. No documentation sync baseline is recorded yet; read the snapshot's index.md and page files directly. A baseline is recorded when this task completes.`;
318
+ }
136
319
  return withUncommitted(`${heading}: no documentation sync baseline is recorded yet. Inspect the source directly; a baseline is recorded when this task completes.`, change.uncommittedFiles);
137
320
  case 'baseline-lost':
138
321
  return withUncommitted(`${heading}: the recorded baseline commit no longer exists (history rewritten?). Inspect the source directly; the baseline is re-recorded when this task completes.`, change.uncommittedFiles);
139
322
  case 'unchanged':
140
- return withUncommitted(`${heading}: no source-content changes since the last documentation sync (${short(change.baseline)}).`, change.uncommittedFiles);
323
+ if (change.baseline === DOCS_SITE_BASELINE) {
324
+ return `${heading}: the existing documentation snapshot at ${change.path} is unchanged since the last documentation sync.`;
325
+ }
326
+ return withUncommitted(`${heading}: no source-content changes since the last documentation sync (${change.baseline === LOCAL_CONTENT_BASELINE ? 'compared by content' : short(change.baseline)}).`, change.uncommittedFiles);
141
327
  case 'changed':
328
+ if (change.baseline === DOCS_SITE_BASELINE) {
329
+ return `${heading}: the existing documentation site was re-crawled and these snapshot pages changed:\n${fileList(change.changedFiles)}\nRead the changed pages in the snapshot at ${change.path} and decide whether the rewritten documentation must follow.`;
330
+ }
331
+ if (change.remote) {
332
+ return change.changedFiles.length > 0
333
+ ? `${heading}: the remote repository changed (${short(change.baseline)} -> ${short(change.head)}).\nChanged files reported by ${change.remote.provider}:\n${fileList(change.changedFiles)}\nThe current commit was downloaded into this isolated read-only evidence snapshot; inspect files there directly.`
334
+ : `${heading}: the remote repository changed (${short(change.baseline)} -> ${short(change.head)}). Inspect the isolated evidence snapshot directly.`;
335
+ }
336
+ if (change.baseline === LOCAL_CONTENT_BASELINE) {
337
+ return `${heading}: files changed since the last documentation sync (compared by content; this folder has no Git history).\nChanged files:\n${fileList(change.changedFiles)}\nInspect the current files directly.`;
338
+ }
142
339
  return withUncommitted(change.changedFiles.length > 0
143
340
  ? `${heading}: changed since the last documentation sync (${short(change.baseline)} -> ${short(change.head)}).\nCommitted changes:\n${fileList(change.changedFiles)}\nInspect details with \`git -C ${change.path} diff ${short(change.baseline)}..HEAD -- <file>\`.`
144
341
  : `${heading}: the working tree changed since the last documentation sync (${short(change.baseline)}).`, change.uncommittedFiles);
@@ -146,6 +343,28 @@ export function formatSourceChanges(changes) {
146
343
  });
147
344
  return `Source changes since the last documentation sync:\n\n${sections.join('\n\n')}`;
148
345
  }
346
+ function formatApiDelta(diff) {
347
+ const lines = ['Structural API delta:'];
348
+ for (const item of diff.operations.added)
349
+ lines.push(`- operation added: ${item}`);
350
+ for (const item of diff.operations.removed)
351
+ lines.push(`- operation removed: ${item}`);
352
+ for (const item of diff.operations.changed)
353
+ lines.push(`- operation changed: ${item.id} (${item.facets.join(', ')})`);
354
+ for (const item of diff.schemas.added)
355
+ lines.push(`- schema added: ${item}`);
356
+ for (const item of diff.schemas.removed)
357
+ lines.push(`- schema removed: ${item}`);
358
+ for (const item of diff.schemas.changed)
359
+ lines.push(`- schema changed: ${item.id}`);
360
+ for (const item of diff.securitySchemes.added)
361
+ lines.push(`- authentication added: ${item}`);
362
+ for (const item of diff.securitySchemes.removed)
363
+ lines.push(`- authentication removed: ${item}`);
364
+ for (const item of diff.securitySchemes.changed)
365
+ lines.push(`- authentication changed: ${item.id}`);
366
+ return lines.length === 1 ? `${lines[0]} no public structural changes detected.` : lines.join('\n');
367
+ }
149
368
  function withUncommitted(text, uncommittedFiles) {
150
369
  if (uncommittedFiles.length === 0)
151
370
  return text;
@@ -183,14 +402,26 @@ async function uncommittedChanges(path) {
183
402
  return lines(output).map((line) => line.trim());
184
403
  }
185
404
  async function sourceContentFingerprint(path) {
405
+ return (await localSourceManifest(path)).fingerprint;
406
+ }
407
+ /**
408
+ * Every file that counts as source content, with its digest. Git repositories
409
+ * contribute tracked and untracked-but-not-ignored files; a plain folder is
410
+ * walked directly with the same exclusions a repository would apply by
411
+ * convention. Secrets are never read.
412
+ */
413
+ export async function localSourceManifest(path) {
186
414
  const output = await git(path, [
187
415
  'ls-files',
188
416
  '-co',
189
417
  '--exclude-standard',
190
418
  '-z',
191
419
  ]);
192
- const files = (output ?? '').split('\0').filter(Boolean).sort();
420
+ const files = output === undefined
421
+ ? await walkPlainFolder(path)
422
+ : output.split('\0').filter(Boolean).sort();
193
423
  const hash = createHash('sha256');
424
+ const manifest = {};
194
425
  for (const file of files) {
195
426
  if (isSensitiveSourcePath(file))
196
427
  continue;
@@ -215,8 +446,34 @@ async function sourceContentFingerprint(path) {
215
446
  if (content !== undefined)
216
447
  hash.update(content);
217
448
  hash.update('\0');
449
+ manifest[file] = createHash('sha256').update(content ?? '').digest('hex');
218
450
  }
219
- return hash.digest('hex');
451
+ return { files: manifest, fingerprint: hash.digest('hex') };
452
+ }
453
+ const PLAIN_FOLDER_IGNORED = new Set(['.git', 'node_modules', '.doxloop-sources', '.doxloop']);
454
+ async function walkPlainFolder(root) {
455
+ const files = [];
456
+ const visit = async (directory) => {
457
+ let entries;
458
+ try {
459
+ entries = await readdir(directory, { withFileTypes: true });
460
+ }
461
+ catch {
462
+ return;
463
+ }
464
+ for (const entry of entries) {
465
+ if (PLAIN_FOLDER_IGNORED.has(entry.name))
466
+ continue;
467
+ const absolute = join(directory, entry.name);
468
+ if (entry.isDirectory())
469
+ await visit(absolute);
470
+ else if (entry.isFile() || entry.isSymbolicLink()) {
471
+ files.push(relative(root, absolute).split('\\').join('/'));
472
+ }
473
+ }
474
+ };
475
+ await visit(root);
476
+ return files.sort();
220
477
  }
221
478
  function isSensitiveSourcePath(path) {
222
479
  const segments = path.toLowerCase().split(/[\\/]/);
@@ -262,6 +519,15 @@ function isSourceRecords(value) {
262
519
  (record.contentFingerprint ===
263
520
  undefined ||
264
521
  typeof record
265
- .contentFingerprint === 'string')));
522
+ .contentFingerprint === 'string') &&
523
+ isFileManifest(record.files)));
524
+ }
525
+ function isFileManifest(value) {
526
+ if (value === undefined)
527
+ return true;
528
+ return (value !== null &&
529
+ typeof value === 'object' &&
530
+ !Array.isArray(value) &&
531
+ Object.values(value).every((digest) => typeof digest === 'string'));
266
532
  }
267
533
  //# sourceMappingURL=sync.js.map
@@ -0,0 +1,9 @@
1
+ import type { SyncChangeHunk } from './types.js';
2
+ /**
3
+ * Line diffing shared by the proposal pipeline and the history index. It lives
4
+ * on its own so history can measure an agent's edits without importing the
5
+ * synchronization module that already records into history.
6
+ */
7
+ export declare function textLines(text: string): string[];
8
+ export declare function lineHunks(before: string, after: string, prefix: string): SyncChangeHunk[];
9
+ //# sourceMappingURL=text-diff.d.ts.map
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Line diffing shared by the proposal pipeline and the history index. It lives
3
+ * on its own so history can measure an agent's edits without importing the
4
+ * synchronization module that already records into history.
5
+ */
6
+ export function textLines(text) {
7
+ if (text === '')
8
+ return [];
9
+ const normalized = text.replace(/\r\n/g, '\n');
10
+ return normalized.endsWith('\n') ? normalized.slice(0, -1).split('\n') : normalized.split('\n');
11
+ }
12
+ export function lineHunks(before, after, prefix) {
13
+ if (before === after)
14
+ return [];
15
+ const oldLines = textLines(before);
16
+ const newLines = textLines(after);
17
+ if (oldLines.length * newLines.length > 2_000_000) {
18
+ return [{ id: `${prefix}-1`, oldStart: 0, oldLines, newStart: 0, newLines }];
19
+ }
20
+ const table = Array.from({ length: oldLines.length + 1 }, () => new Uint32Array(newLines.length + 1));
21
+ for (let old = oldLines.length - 1; old >= 0; old -= 1) {
22
+ for (let next = newLines.length - 1; next >= 0; next -= 1) {
23
+ table[old][next] = oldLines[old] === newLines[next]
24
+ ? table[old + 1][next + 1] + 1
25
+ : Math.max(table[old + 1][next], table[old][next + 1]);
26
+ }
27
+ }
28
+ const hunks = [];
29
+ let old = 0;
30
+ let next = 0;
31
+ let current;
32
+ const flush = () => {
33
+ if (!current)
34
+ return;
35
+ current.id = `${prefix}-${hunks.length + 1}`;
36
+ hunks.push(current);
37
+ current = undefined;
38
+ };
39
+ while (old < oldLines.length || next < newLines.length) {
40
+ if (old < oldLines.length && next < newLines.length && oldLines[old] === newLines[next]) {
41
+ flush();
42
+ old += 1;
43
+ next += 1;
44
+ continue;
45
+ }
46
+ current ??= { id: '', oldStart: old, oldLines: [], newStart: next, newLines: [] };
47
+ if (next < newLines.length && (old >= oldLines.length || table[old][next + 1] >= table[old + 1][next])) {
48
+ current.newLines.push(newLines[next]);
49
+ next += 1;
50
+ }
51
+ else if (old < oldLines.length) {
52
+ current.oldLines.push(oldLines[old]);
53
+ old += 1;
54
+ }
55
+ }
56
+ flush();
57
+ return hunks;
58
+ }
59
+ //# sourceMappingURL=text-diff.js.map