@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,113 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { assertPublicContract } from './contract-validation.js';
5
+ import { readEvidenceMap } from './evidence.js';
6
+ import { pathExists } from './fs.js';
7
+ import { loadPages, loadProject, relativePath } from './project.js';
8
+ import { readLatestQualityReport } from './quality-gates.js';
9
+ import { buildSourceIntelligence } from './source-intelligence.js';
10
+ import { listSyncRuns } from './sync-runs.js';
11
+ import { validateProject } from './validation.js';
12
+ export const EVALUATION_CONTRACT_VERSION = '1.0.0';
13
+ export const EVALUATION_BASELINE_FILE = join('.doxloop', 'evaluation-baseline.json');
14
+ export async function evaluateWorkspace(root, options) {
15
+ const [project, validation, intelligence, evidence, quality, runs] = await Promise.all([loadProject(root), validateProject(root), buildSourceIntelligence(root), readEvidenceMap(root), readLatestQualityReport(root), listSyncRuns(root)]);
16
+ const pages = (await loadPages(root, project)).map((path) => relativePath(root, path));
17
+ const evidencePages = Object.values(evidence?.pages ?? {});
18
+ const claimChecks = quality?.checks.filter((check) => check.category === 'claims') ?? [];
19
+ const verifiedClaims = claimChecks.filter((check) => check.code === 'quality.claim.verified').length;
20
+ const contradictedClaims = claimChecks.filter((check) => check.code === 'quality.claim.contradicted').length;
21
+ const unresolvedClaims = claimChecks.filter((check) => check.code === 'quality.claim.needs-human').length;
22
+ const unsupported = contradictedClaims + unresolvedClaims;
23
+ const measuredCoverage = intelligence.coverage.metrics.filter((metric) => metric.status === 'measured');
24
+ const coverage = measuredCoverage.length ? average(measuredCoverage.map((metric) => metric.percent)) : 0;
25
+ const preciseEvidence = Math.max(0, 100 - intelligence.evidenceDiagnostics.length * 10);
26
+ const validationScore = Math.max(0, 100 - validation.errors * 25 - validation.warnings * 5);
27
+ const expectedMaximum = options.maximumPages ?? pages.length;
28
+ const pageEconomy = Math.max(0, 100 - Math.max(pages.length - expectedMaximum, 0) * 15);
29
+ const examples = quality?.checks.filter((check) => check.category === 'examples' && check.status !== 'skipped') ?? [];
30
+ const exampleScore = examples.length ? Math.round(100 * examples.filter((check) => check.status === 'pass').length / examples.length) : 0;
31
+ const accessibility = quality?.checks.filter((check) => check.category === 'accessibility' && check.status !== 'skipped') ?? [];
32
+ const accessibilityScore = accessibility.length ? Math.round(100 * accessibility.filter((check) => check.status === 'pass').length / accessibility.length) : 0;
33
+ const factualPrecision = claimChecks.length ? 100 * verifiedClaims / claimChecks.length : 0;
34
+ const unsupportedRateScore = claimChecks.length ? 100 * (claimChecks.length - unsupported) / claimChecks.length : 0;
35
+ const locality = options.mode === 'update' ? await updateLocality(options.before, root, options.expectedChangedPages ?? []) : 100;
36
+ const metrics = [
37
+ metric('factual-precision', factualPrecision, 18, `${verifiedClaims}/${claimChecks.length} claim checks are verified; ${contradictedClaims} contradicted.`),
38
+ metric('unsupported-claim-rate', unsupportedRateScore, 12, `${unsupported}/${claimChecks.length} claim checks are contradicted or need human review.`),
39
+ metric('public-surface-coverage', coverage, 13, `${measuredCoverage.length}/${intelligence.coverage.metrics.length} coverage surfaces were measurable. ${intelligence.coverage.disclaimer}`),
40
+ metric('reader-journey-and-ia', validationScore, 10, `${validation.errors} errors and ${validation.warnings} warnings.`),
41
+ metric('executable-examples', exampleScore, 8, examples.length ? `${examples.length} executable checks.` : 'No executable examples were run; verification is missing.'),
42
+ metric('evidence-precision-recall', preciseEvidence, 12, `${intelligence.evidenceDiagnostics.length} evidence diagnostics.`),
43
+ metric('page-economy', pageEconomy, 7, `${pages.length} pages; expected maximum ${expectedMaximum}.`),
44
+ metric('update-locality-preservation', locality, 12, options.mode === 'update' ? options.before ? 'Compared candidate files with the declared update scope.' : 'No before snapshot was supplied; update locality is unverified.' : 'Generation run; locality is not applicable.'),
45
+ metric('accessibility-validation', accessibilityScore, 8, accessibility.length ? `${accessibility.length} rendered accessibility checks.` : 'No rendered accessibility run was performed; verification is missing.'),
46
+ ];
47
+ const reviewer = {
48
+ accepted: runs.filter((run) => run.status === 'applied').length,
49
+ revised: runs.filter((run) => run.status === 'partially-applied' || run.status === 'superseded').length,
50
+ rejected: runs.filter((run) => run.status === 'rejected').length,
51
+ };
52
+ const baseline = await readBaseline(root);
53
+ const score = Math.round(metrics.reduce((total, item) => total + item.score * item.weight, 0) / metrics.reduce((total, item) => total + item.weight, 0));
54
+ const threshold = options.regressionThreshold ?? 3;
55
+ const report = {
56
+ schemaVersion: 1, contractVersion: EVALUATION_CONTRACT_VERSION, generatedAt: new Date().toISOString(), mode: options.mode, project: project.title, score, metrics,
57
+ ...(options.durationMs !== undefined ? { durationMs: options.durationMs } : {}),
58
+ ...(options.usage ? { usage: options.usage } : {}), reviewer,
59
+ ...(baseline ? { regression: { baselineScore: baseline.score, delta: score - baseline.score, threshold, blocked: score < baseline.score - threshold } } : {}),
60
+ };
61
+ const directory = join(root, '.doxloop', 'evaluations');
62
+ await mkdir(directory, { recursive: true });
63
+ await assertPublicContract('evaluation-v1', report);
64
+ await writeFile(join(directory, `evaluation-${report.generatedAt.replace(/[:.]/g, '-')}.json`), `${JSON.stringify(report, null, 2)}\n`, 'utf8');
65
+ return report;
66
+ }
67
+ export async function approveEvaluationBaseline(root, report) {
68
+ await mkdir(join(root, '.doxloop'), { recursive: true });
69
+ await writeFile(join(root, EVALUATION_BASELINE_FILE), `${JSON.stringify(report, null, 2)}\n`, 'utf8');
70
+ }
71
+ export function formatEvaluation(report) {
72
+ const lines = [`Documentation evaluation: ${report.score}/100`, `${report.mode} · contract ${report.contractVersion}`];
73
+ for (const item of report.metrics)
74
+ lines.push(` ${String(Math.round(item.score)).padStart(3)} ${item.id} — ${item.detail}`);
75
+ lines.push(`Reviewer outcomes: ${report.reviewer.accepted} accepted · ${report.reviewer.revised} revised · ${report.reviewer.rejected} rejected`);
76
+ if (report.regression)
77
+ lines.push(`Baseline: ${report.regression.baselineScore} · delta ${report.regression.delta >= 0 ? '+' : ''}${report.regression.delta} · ${report.regression.blocked ? 'release blocked' : 'within threshold'}`);
78
+ return lines.join('\n');
79
+ }
80
+ function metric(id, score, weight, detail) { return { id, score: Math.round(Math.max(0, Math.min(100, score)) * 10) / 10, weight, detail }; }
81
+ function average(values) { return values.reduce((sum, value) => sum + value, 0) / Math.max(values.length, 1); }
82
+ async function updateLocality(before, root, expected) {
83
+ if (!before || !(await pathExists(before)))
84
+ return 0;
85
+ const [previous, current] = await Promise.all([fileDigests(before), fileDigests(root)]);
86
+ const changed = new Set([...new Set([...previous.keys(), ...current.keys()])].filter((file) => previous.get(file) !== current.get(file)));
87
+ if (changed.size === 0)
88
+ return expected.length === 0 ? 100 : 40;
89
+ const intended = changed.size - [...changed].filter((file) => !expected.includes(file)).length;
90
+ const recall = expected.length ? [...changed].filter((file) => expected.includes(file)).length / expected.length : 1;
91
+ const precision = intended / changed.size;
92
+ return 100 * (precision * .7 + recall * .3);
93
+ }
94
+ async function fileDigests(root) {
95
+ const project = await loadProject(root);
96
+ const output = new Map();
97
+ for (const path of await loadPages(root, project))
98
+ output.set(relativePath(root, path), createHash('sha256').update(await readFile(path)).digest('hex'));
99
+ return output;
100
+ }
101
+ async function readBaseline(root) {
102
+ const path = join(root, EVALUATION_BASELINE_FILE);
103
+ if (!(await pathExists(path)))
104
+ return undefined;
105
+ try {
106
+ const value = JSON.parse(await readFile(path, 'utf8'));
107
+ return value.schemaVersion === 1 ? value : undefined;
108
+ }
109
+ catch {
110
+ return undefined;
111
+ }
112
+ }
113
+ //# sourceMappingURL=evaluation.js.map
@@ -0,0 +1,47 @@
1
+ import type { DocumentationPlan, DocumentationPlanEvidence, DocumentationPlanPage, DoxloopProject, SourceBinding } from './types.js';
2
+ export interface EvidencePackOptions {
3
+ /** Project-relative file to write; defaults to `.doxloop/cache/evidence-pack-batch-<n>.md`. */
4
+ file?: string;
5
+ batchIndex?: number;
6
+ researchRoot?: string;
7
+ planId?: string;
8
+ discoveryCacheKey?: string;
9
+ /** Bytes per excerpt, per page, and per pack. */
10
+ limits?: Partial<typeof DEFAULT_LIMITS>;
11
+ }
12
+ export interface EvidencePackResult {
13
+ /** Project-relative path of the pack, or undefined when nothing could be packed. */
14
+ file?: string;
15
+ pages: number;
16
+ excerpts: number;
17
+ bytes: number;
18
+ /** Cited files that could not be read, for the log. */
19
+ missing: string[];
20
+ }
21
+ /**
22
+ * Packs of 85–143 KB per four-page batch were the bulk of each session's
23
+ * uncached input in a real run; these caps keep a batch's pack near 60 KB
24
+ * while still holding every cited excerpt's relevant lines.
25
+ */
26
+ export declare const DEFAULT_LIMITS: {
27
+ excerptBytes: number;
28
+ snapshotPageBytes: number;
29
+ pageBytes: number;
30
+ packBytes: number;
31
+ excerptsPerPage: number;
32
+ linesBefore: number;
33
+ linesAfter: number;
34
+ headLines: number;
35
+ };
36
+ /** Build and write the evidence pack for a set of planned pages. */
37
+ export declare function writeEvidencePack(workspace: string, project: Pick<DoxloopProject, 'sources'>, plan: Pick<DocumentationPlan, 'existingDocumentation' | 'capabilities'>, pages: DocumentationPlanPage[], options?: EvidencePackOptions): Promise<EvidencePackResult>;
38
+ interface Citation extends DocumentationPlanEvidence {
39
+ /** Whole-page snapshot cited through an existing-documentation disposition. */
40
+ wholePage?: boolean;
41
+ }
42
+ /** Cited files for one page: plan evidence first, then docs-site pages that fold into it. */
43
+ export declare function citationsFor(page: DocumentationPlanPage, plan: Pick<DocumentationPlan, 'existingDocumentation'>, sources: ReadonlyMap<string, SourceBinding>): Citation[];
44
+ /** Words from the page title, purpose, and citation labels that identify what the page is about. */
45
+ export declare function pageKeywords(page: Pick<DocumentationPlanPage, 'title' | 'purpose' | 'evidenceDetails' | 'id'>): string[];
46
+ export {};
47
+ //# sourceMappingURL=evidence-pack.d.ts.map
@@ -0,0 +1,358 @@
1
+ /**
2
+ * Evidence packs.
3
+ *
4
+ * The plan already names the source files and docs-site pages each page is
5
+ * written from, yet a real run spent a third of its model turns hunting for
6
+ * them again and reading them in slabs the tool truncated. A pack puts the
7
+ * cited excerpts for one batch into a single file the agent reads first, so
8
+ * writing starts on the first turn and the full source is opened only for a
9
+ * claim the pack does not settle.
10
+ */
11
+ import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
12
+ import { isAbsolute, join, resolve } from 'node:path';
13
+ import { loadOpenApiSource, openApiExcerpt, parseOpenApi } from './openapi.js';
14
+ /**
15
+ * Packs of 85–143 KB per four-page batch were the bulk of each session's
16
+ * uncached input in a real run; these caps keep a batch's pack near 60 KB
17
+ * while still holding every cited excerpt's relevant lines.
18
+ */
19
+ export const DEFAULT_LIMITS = {
20
+ excerptBytes: 7_000,
21
+ snapshotPageBytes: 16_000,
22
+ pageBytes: 36_000,
23
+ packBytes: 240_000,
24
+ excerptsPerPage: 6,
25
+ linesBefore: 20,
26
+ linesAfter: 80,
27
+ headLines: 100,
28
+ };
29
+ /** Build and write the evidence pack for a set of planned pages. */
30
+ export async function writeEvidencePack(workspace, project, plan, pages, options = {}) {
31
+ const limits = { ...DEFAULT_LIMITS, ...options.limits };
32
+ const file = options.file ?? `.doxloop/cache/evidence-pack-batch-${options.batchIndex ?? 1}.md`;
33
+ const sources = new Map(project.sources.map((source) => [source.name, source]));
34
+ const result = { pages: 0, excerpts: 0, bytes: 0, missing: [] };
35
+ const sections = [];
36
+ let capabilities = [];
37
+ if (options.researchRoot && options.planId) {
38
+ try {
39
+ const research = JSON.parse(await readFile(join(options.researchRoot, '.doxloop', 'plans', options.planId, 'research', 'product.json'), 'utf8'));
40
+ capabilities = research.content?.capabilities ?? [];
41
+ }
42
+ catch { /* Older plans may have no research checkpoint. */ }
43
+ }
44
+ const catalogs = new Map();
45
+ if (options.researchRoot && options.discoveryCacheKey) {
46
+ try {
47
+ const inventory = JSON.parse(await readFile(join(options.researchRoot, '.doxloop/cache/discovery', `${options.discoveryCacheKey}.json`), 'utf8'));
48
+ for (const discovered of inventory.sources ?? []) {
49
+ const source = sources.get(discovered.name);
50
+ if (!source)
51
+ continue;
52
+ for (const path of discovered.uiLabelCatalogs ?? []) {
53
+ if (!path.endsWith('.json'))
54
+ continue;
55
+ try {
56
+ const values = JSON.parse(await readFile(resolve(workspace, source.path, path), 'utf8'));
57
+ const entries = catalogs.get(source.name) ?? [];
58
+ entries.push({ path, values });
59
+ catalogs.set(source.name, entries);
60
+ }
61
+ catch { /* Unsupported or missing catalog. */ }
62
+ }
63
+ }
64
+ }
65
+ catch { /* No discovery cache on legacy plans. */ }
66
+ }
67
+ const contracts = new Map();
68
+ let packBytes = 0;
69
+ for (const page of pages) {
70
+ const research = researchFor(page, plan.capabilities ?? [], capabilities);
71
+ const citations = citationsFor({ ...page, evidenceDetails: [...(page.evidenceDetails ?? []), ...research.flatMap((capability) => capability.evidence ?? [])] }, plan, sources);
72
+ if (citations.length === 0 && research.length === 0)
73
+ continue;
74
+ const keywords = pageKeywords(page);
75
+ const lines = [`## ${page.path} — ${page.title}`, '', ...research.slice(0, 6).map((item) => `Planning findings for "${item.title ?? item.id ?? page.title}" (verify claims against the excerpts): ${JSON.stringify({ summary: item.summary, notes: item.notes }).slice(0, 2000)}`)];
76
+ let pageBytes = lines.join('\n').length;
77
+ let excerpts = 0;
78
+ for (const citation of citations) {
79
+ if (excerpts >= limits.excerptsPerPage || pageBytes >= limits.pageBytes || packBytes + pageBytes >= limits.packBytes)
80
+ break;
81
+ const source = sources.get(citation.source);
82
+ if (!source) {
83
+ result.missing.push(`${citation.source}:${citation.path}`);
84
+ continue;
85
+ }
86
+ const kind = source.kind ?? 'directory';
87
+ // An OpenAPI source's path is the spec itself (often a URL), so a
88
+ // citation is a part of the contract, never a file under it.
89
+ const absolute = kind === 'openapi' ? source.path : resolve(isAbsolute(source.path) ? source.path : join(workspace, source.path), citation.path);
90
+ const excerpt = kind === 'openapi' || CONTRACT_FILE.test(citation.path)
91
+ ? await contractExcerpt(kind === 'openapi' ? undefined : absolute, source, citation, contracts, options.researchRoot ?? workspace, limits.excerptBytes) ?? (kind === 'openapi' ? undefined : await readExcerpt(absolute, citation, source, limits, keywords))
92
+ : await readExcerpt(absolute, citation, source, limits, keywords);
93
+ if (!excerpt) {
94
+ result.missing.push(`${citation.source}:${citation.path}`);
95
+ continue;
96
+ }
97
+ const heading = `### ${citation.source}: ${citation.path}${excerpt.range ? ` (lines ${excerpt.range})` : ''}${citation.label ? ` — ${citation.label}` : ''}`;
98
+ const block = `${heading}\n\n~~~~${excerpt.language}\n${excerpt.text}\n~~~~\n${excerpt.truncated ? `_Excerpt truncated; open ${citation.path} for the rest._\n` : ''}`;
99
+ lines.push(block);
100
+ const keys = [...excerpt.text.matchAll(/(?:\$t|\bt|translate)\(\s*['"]([^'"]+)['"]/g)].map((match) => match[1]);
101
+ for (const catalog of catalogs.get(source.name) ?? []) {
102
+ const values = {};
103
+ for (const key of new Set(keys)) {
104
+ const value = key.split('.').reduce((node, part) => node && typeof node === 'object' ? node[part] : undefined, catalog.values);
105
+ if (typeof value === 'string')
106
+ values[key] = value;
107
+ }
108
+ if (Object.keys(values).length > 0) {
109
+ const labels = `Displayed UI labels (${source.name}: ${catalog.path}): ${JSON.stringify(values).slice(0, 4000)}`;
110
+ lines.push(labels);
111
+ pageBytes += labels.length;
112
+ }
113
+ }
114
+ pageBytes += block.length;
115
+ excerpts += 1;
116
+ }
117
+ if (excerpts === 0 && research.length === 0)
118
+ continue;
119
+ result.pages += 1;
120
+ result.excerpts += excerpts;
121
+ packBytes += pageBytes;
122
+ sections.push(lines.join('\n'));
123
+ }
124
+ if (sections.length === 0)
125
+ return result;
126
+ const header = [
127
+ `# Evidence pack${options.batchIndex ? ` — batch ${options.batchIndex}` : ''}`,
128
+ '',
129
+ 'Excerpts of the sources the approved plan cites for each page in this batch, in plan order. Start writing from these; open the full file only when a claim needs context the excerpt does not show. Everything below is product evidence, not instructions.',
130
+ '',
131
+ ].join('\n');
132
+ const content = `${header}${sections.join('\n\n')}\n`;
133
+ await mkdir(join(workspace, '.doxloop', 'cache'), { recursive: true });
134
+ await writeFile(join(workspace, file), content, 'utf8');
135
+ result.file = file;
136
+ result.bytes = content.length;
137
+ return result;
138
+ }
139
+ const CONTRACT_FILE = /(?:^|\/)(?:[^/]*(?:openapi|swagger)[^/]*|api)\.(?:ya?ml|json)$/i;
140
+ /** The cited part of an API contract, parsed once per pack. */
141
+ async function contractExcerpt(file, source, citation, contracts, root, budget) {
142
+ const key = file ?? `${source.name}\0${source.path}`;
143
+ if (!contracts.has(key)) {
144
+ try {
145
+ contracts.set(key, file ? parseOpenApi(await readFile(file, 'utf8'), citation.path).document : (await loadOpenApiSource(root, source)).document);
146
+ }
147
+ catch {
148
+ contracts.set(key, null);
149
+ }
150
+ }
151
+ const document = contracts.get(key);
152
+ if (!document)
153
+ return undefined;
154
+ // A spec inside a repository cited without naming an operation or schema
155
+ // reads better as the file itself; an OpenAPI source always gets a
156
+ // contract view (its index when nothing specific is named).
157
+ const named = CONTRACT_PART.test(citation.label ?? '') || CONTRACT_PART.test(citation.path);
158
+ if (file && !named)
159
+ return undefined;
160
+ const excerpt = openApiExcerpt(document, citation, budget);
161
+ return excerpt && { ...excerpt, language: 'yaml' };
162
+ }
163
+ const CONTRACT_PART = /^(?:get|put|post|delete|patch|options|head|trace)\s+\/|^Schema |^schema:/i;
164
+ /** Cited files for one page: plan evidence first, then docs-site pages that fold into it. */
165
+ export function citationsFor(page, plan, sources) {
166
+ const seen = new Set();
167
+ const citations = [];
168
+ for (const detail of page.evidenceDetails ?? []) {
169
+ if (!detail?.source || !detail.path)
170
+ continue;
171
+ const key = `${detail.source}:${detail.path}:${detail.line ?? detail.label ?? ''}`;
172
+ if (seen.has(key))
173
+ continue;
174
+ seen.add(key);
175
+ const docsSite = (sources.get(detail.source)?.kind ?? 'directory') === 'docs-site';
176
+ citations.push({ ...detail, ...(docsSite ? { wholePage: true } : {}) });
177
+ }
178
+ for (const assessment of plan.existingDocumentation ?? []) {
179
+ for (const disposition of assessment.pages ?? []) {
180
+ if (!disposition.into?.includes(page.id) || disposition.disposition === 'drop')
181
+ continue;
182
+ const key = `${assessment.source}:${disposition.path}`;
183
+ if (seen.has(key))
184
+ continue;
185
+ seen.add(key);
186
+ citations.push({ source: assessment.source, path: disposition.path, kind: 'documentation', label: disposition.title ? `existing page "${disposition.title}" (${disposition.disposition})` : `existing page (${disposition.disposition})`, wholePage: true });
187
+ }
188
+ }
189
+ return citations;
190
+ }
191
+ async function readExcerpt(absolute, citation, source, limits, keywords = []) {
192
+ let content;
193
+ try {
194
+ const info = await stat(absolute);
195
+ if (!info.isFile() || info.size > 2_000_000)
196
+ return undefined;
197
+ content = await readFile(absolute, 'utf8');
198
+ }
199
+ catch {
200
+ return undefined;
201
+ }
202
+ if (content.includes('\u0000'))
203
+ return undefined;
204
+ const language = languageFor(citation.path);
205
+ const budget = citation.wholePage || (source.kind ?? 'directory') === 'docs-site' ? limits.snapshotPageBytes : limits.excerptBytes;
206
+ const all = content.split(/\r?\n/);
207
+ let start = 0;
208
+ let end = all.length;
209
+ if (!citation.wholePage) {
210
+ if (typeof citation.line === 'number' && citation.line > 0) {
211
+ start = Math.max(0, citation.line - 1 - limits.linesBefore);
212
+ end = Math.min(all.length, citation.line - 1 + limits.linesAfter);
213
+ }
214
+ else {
215
+ // Resolve an exact JSON key or source symbol before falling back to a file head.
216
+ const label = citation.label?.trim();
217
+ if (/\.json$/i.test(citation.path)) {
218
+ let parsed;
219
+ try {
220
+ parsed = JSON.parse(content);
221
+ }
222
+ catch {
223
+ parsed = undefined;
224
+ }
225
+ if (parsed && typeof parsed === 'object') {
226
+ const dotted = label ? label.split('.').reduce((node, key) => node && typeof node === 'object' ? node[key] : undefined, parsed) : undefined;
227
+ if (label && dotted !== undefined)
228
+ return { text: JSON.stringify({ [label]: dotted }, null, 2).slice(0, budget), truncated: JSON.stringify(dotted).length > budget, language };
229
+ // A label catalog cited without a resolvable key: the entries that
230
+ // talk about this page beat the file's first hundred lines, which
231
+ // are whatever the catalog happens to start with.
232
+ const wanted = [...quotedTokens(label), ...keywords];
233
+ const matched = matchingCatalogEntries(parsed, wanted, budget);
234
+ if (matched)
235
+ return { text: matched.text, truncated: matched.truncated, language };
236
+ }
237
+ }
238
+ const anchor = anchorLine(all, label);
239
+ start = anchor >= 0 ? Math.max(0, anchor - limits.linesBefore) : 0;
240
+ end = Math.min(all.length, anchor >= 0 ? anchor + limits.linesAfter : limits.headLines);
241
+ }
242
+ }
243
+ let text = all.slice(start, end).join('\n');
244
+ let truncated = end < all.length || start > 0;
245
+ if (text.length > budget) {
246
+ text = text.slice(0, budget);
247
+ text = text.slice(0, Math.max(text.lastIndexOf('\n'), budget - 200));
248
+ truncated = true;
249
+ }
250
+ return {
251
+ text: text.replace(/~~~~/g, '~~ ~~'),
252
+ ...(start > 0 || end < all.length ? { range: `${start + 1}–${Math.min(end, start + text.split('\n').length)}` } : {}),
253
+ truncated,
254
+ language,
255
+ };
256
+ }
257
+ function languageFor(path) {
258
+ const extension = path.split('.').pop()?.toLowerCase() ?? '';
259
+ const known = {
260
+ ts: 'ts', tsx: 'tsx', js: 'js', jsx: 'jsx', mjs: 'js', cjs: 'js', go: 'go', py: 'python', rb: 'ruby', rs: 'rust', java: 'java', kt: 'kotlin',
261
+ php: 'php', cs: 'csharp', swift: 'swift', json: 'json', yaml: 'yaml', yml: 'yaml', toml: 'toml', md: 'md', mdx: 'mdx', sql: 'sql', sh: 'sh',
262
+ vue: 'vue', svelte: 'svelte', html: 'html', css: 'css', scss: 'scss', proto: 'proto', graphql: 'graphql', env: 'ini', ini: 'ini',
263
+ };
264
+ return known[extension] ?? '';
265
+ }
266
+ /** The research capabilities that describe a page: the plan's own mapping first, then id and title matches. */
267
+ function researchFor(page, planned, research) {
268
+ const own = planned.filter((capability) => capability.pageIds?.includes(page.id));
269
+ const ids = new Set([page.id, ...own.map((capability) => capability.id)]);
270
+ const titles = new Set([page.title, ...own.map((capability) => capability.title)].map(normalizeTitle));
271
+ const matched = research.filter((capability) => (capability.id !== undefined && ids.has(capability.id)) ||
272
+ (capability.title !== undefined && titles.has(normalizeTitle(capability.title))) ||
273
+ capability.evidence?.some((evidence) => page.evidenceDetails?.some((detail) => detail.source === evidence.source && detail.path === evidence.path && (detail.label === evidence.label || (detail.line !== undefined && detail.line === evidence.line)))));
274
+ if (matched.length > 0)
275
+ return matched;
276
+ // No exact link: a capability whose title shares most of its words with the page title.
277
+ const words = new Set(pageKeywords(page));
278
+ return research.filter((capability) => {
279
+ const shared = keywordsOf(capability.title ?? '').filter((word) => words.has(word));
280
+ return shared.length >= 2 || (shared.length === 1 && words.size <= 2);
281
+ }).slice(0, 3);
282
+ }
283
+ function normalizeTitle(value) {
284
+ return (value ?? '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
285
+ }
286
+ const STOP_WORDS = new Set(['the', 'and', 'with', 'for', 'your', 'from', 'into', 'that', 'this', 'when', 'how', 'use', 'using', 'guide', 'page', 'pages', 'view', 'views', 'manage', 'managing', 'work', 'working', 'create', 'creating', 'about', 'other', 'than', 'then', 'them', 'they']);
287
+ /** Words from the page title, purpose, and citation labels that identify what the page is about. */
288
+ export function pageKeywords(page) {
289
+ return keywordsOf([page.title, page.purpose, page.id.replace(/-/g, ' '), ...(page.evidenceDetails ?? []).map((detail) => detail.label ?? '')].join(' '));
290
+ }
291
+ /** Lower-cased, lightly stemmed content words of a phrase, without stop words. */
292
+ function keywordsOf(text) {
293
+ const words = text.toLowerCase().split(/[^a-z0-9]+/).filter((word) => word.length >= 4 && !STOP_WORDS.has(word));
294
+ const stems = words.map((word) => word.replace(/(ings?|ies|es|s|ed)$/, (suffix) => suffix === 'ies' ? 'y' : ''));
295
+ return [...new Set(stems.filter((word) => word.length >= 3))];
296
+ }
297
+ function quotedTokens(label) {
298
+ return [...(label ?? '').matchAll(/"([^"]+)"|'([^']+)'/g)].map((match) => (match[1] ?? match[2] ?? '').toLowerCase()).filter((token) => token.length >= 3);
299
+ }
300
+ /** The first line naming the label: an identifier, a quoted key, or the longest identifier-like token of a phrase. */
301
+ function anchorLine(lines, label) {
302
+ if (!label)
303
+ return -1;
304
+ const symbol = label.match(/^[A-Za-z_$][\w$.-]*$/)?.[0];
305
+ if (symbol)
306
+ return lines.findIndex((line) => line.includes(symbol));
307
+ for (const token of quotedTokens(label)) {
308
+ const index = lines.findIndex((line) => line.toLowerCase().includes(`"${token}"`) || line.toLowerCase().includes(token));
309
+ if (index >= 0)
310
+ return index;
311
+ }
312
+ const tokens = label.split(/[^A-Za-z0-9_$./-]+/).filter((token) => token.length >= 4 && /[A-Za-z]/.test(token)).sort((a, b) => b.length - a.length);
313
+ for (const token of tokens) {
314
+ const index = lines.findIndex((line) => line.includes(token));
315
+ if (index >= 0)
316
+ return index;
317
+ }
318
+ return -1;
319
+ }
320
+ /**
321
+ * Flatten a label catalog and keep the entries whose key path or text
322
+ * mentions one of the wanted words, grouped so the writer sees the keys.
323
+ */
324
+ function matchingCatalogEntries(parsed, wanted, budget) {
325
+ const needles = [...new Set(wanted.map((word) => word.toLowerCase()).filter((word) => word.length >= 3))];
326
+ if (needles.length === 0)
327
+ return undefined;
328
+ const entries = [];
329
+ const walk = (node, path) => {
330
+ if (typeof node === 'string') {
331
+ const key = path.join('.');
332
+ const haystack = `${key} ${node}`.toLowerCase();
333
+ if (needles.some((needle) => haystack.includes(needle)))
334
+ entries.push([key, node]);
335
+ return;
336
+ }
337
+ if (node && typeof node === 'object' && !Array.isArray(node))
338
+ for (const [key, value] of Object.entries(node))
339
+ walk(value, [...path, key]);
340
+ };
341
+ walk(parsed, []);
342
+ if (entries.length === 0)
343
+ return undefined;
344
+ const lines = [];
345
+ let size = 0;
346
+ let truncated = false;
347
+ for (const [key, value] of entries) {
348
+ const line = `${JSON.stringify(key)}: ${JSON.stringify(value)}`;
349
+ if (size + line.length + 1 > budget) {
350
+ truncated = true;
351
+ break;
352
+ }
353
+ lines.push(line);
354
+ size += line.length + 1;
355
+ }
356
+ return { text: `// Catalog entries matching ${needles.slice(0, 8).join(', ')} (${entries.length} of the file's strings)\n${lines.join('\n')}`, truncated };
357
+ }
358
+ //# sourceMappingURL=evidence-pack.js.map
@@ -0,0 +1,25 @@
1
+ import type { EvidenceMap, SyncState } from './types.js';
2
+ export declare const EVIDENCE_MAP_FILE: string;
3
+ /**
4
+ * The evidence map records which configured source produced each page, so
5
+ * staleness can be attributed to individual pages without starting an agent.
6
+ * It is committed, so a malformed file fails closed rather than being ignored.
7
+ */
8
+ export declare function readEvidenceMap(root: string): Promise<EvidenceMap | undefined>;
9
+ export declare function writeEvidenceMap(root: string, map: EvidenceMap): Promise<void>;
10
+ /**
11
+ * A run that ends by recording the sync baseline has just verified its pages
12
+ * against that baseline. Stamp each verified page with the revision it was
13
+ * checked against, so coverage and drift can tell "verified at the current
14
+ * baseline" from "verified at some point"; without it every generated page
15
+ * counted as unverified. Existing stamps are kept. Returns the pages stamped.
16
+ */
17
+ export declare function stampVerifiedRevisions(root: string, state: SyncState): Promise<number>;
18
+ /**
19
+ * Pages that claim behavior from a changed source path, in page order.
20
+ * An entry naming only a source, with no paths, matches every change in it.
21
+ */
22
+ export declare function pagesForChange(map: EvidenceMap, source: string, changedPaths: readonly string[]): Map<string, string[]>;
23
+ /** Every source name the map attributes at least one page to. */
24
+ export declare function trackedSources(map: EvidenceMap): Set<string>;
25
+ //# sourceMappingURL=evidence.d.ts.map