blume 2.0.1 → 2.0.2

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 (410) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/dist/cli/{chunk-vtk4a6dg.js → chunk-00gs3wqs.js} +1 -1
  3. package/dist/cli/{chunk-fa25z98p.js → chunk-273ygyr4.js} +4 -3
  4. package/dist/cli/chunk-273ygyr4.js.map +11 -0
  5. package/dist/cli/{chunk-yw7dm696.js → chunk-3yce002v.js} +21 -8
  6. package/dist/cli/{chunk-yw7dm696.js.map → chunk-3yce002v.js.map} +3 -3
  7. package/dist/cli/{chunk-yt5n7ppj.js → chunk-4e9b9ra6.js} +17 -7
  8. package/dist/cli/chunk-4e9b9ra6.js.map +10 -0
  9. package/dist/cli/{chunk-fs23ddbb.js → chunk-5r8g91qn.js} +352 -676
  10. package/dist/cli/chunk-5r8g91qn.js.map +34 -0
  11. package/dist/cli/{chunk-6vm74dry.js → chunk-6dtt0zfn.js} +8 -8
  12. package/dist/cli/{chunk-qwsrynx5.js → chunk-6k4ftwze.js} +38 -19
  13. package/dist/cli/{chunk-qwsrynx5.js.map → chunk-6k4ftwze.js.map} +4 -4
  14. package/dist/cli/{chunk-qs4q5p4e.js → chunk-7mbqtmgb.js} +16 -7
  15. package/dist/cli/chunk-7mbqtmgb.js.map +10 -0
  16. package/dist/cli/{chunk-q5163e60.js → chunk-7vtckvaw.js} +21 -19
  17. package/dist/cli/chunk-7vtckvaw.js.map +11 -0
  18. package/dist/cli/{chunk-kdp5q7ke.js → chunk-8g8ytmgx.js} +17 -18
  19. package/dist/cli/{chunk-kdp5q7ke.js.map → chunk-8g8ytmgx.js.map} +2 -2
  20. package/dist/cli/{chunk-epjnccmv.js → chunk-91ws1n6j.js} +18 -15
  21. package/dist/cli/chunk-91ws1n6j.js.map +10 -0
  22. package/dist/cli/{chunk-fxypxtvm.js → chunk-bbnwccaz.js} +2 -2
  23. package/dist/cli/{chunk-m3vmjgmq.js → chunk-bfwp9vp6.js} +16 -8
  24. package/dist/cli/chunk-bfwp9vp6.js.map +10 -0
  25. package/dist/cli/{chunk-f2z5v128.js → chunk-d1v5rhy0.js} +14 -15
  26. package/dist/cli/{chunk-f2z5v128.js.map → chunk-d1v5rhy0.js.map} +2 -2
  27. package/dist/cli/{chunk-6crbhc3x.js → chunk-ddndchfr.js} +21 -6
  28. package/dist/cli/chunk-ddndchfr.js.map +14 -0
  29. package/dist/cli/{chunk-ce574jw2.js → chunk-esh98wmb.js} +1 -1
  30. package/dist/cli/{chunk-zxcczpyx.js → chunk-fsmrqk8a.js} +1 -1
  31. package/dist/cli/{chunk-hdpx1tax.js → chunk-g698a744.js} +5 -5
  32. package/dist/cli/{chunk-jts8mvcz.js → chunk-gs7r695n.js} +9 -3
  33. package/dist/cli/{chunk-jts8mvcz.js.map → chunk-gs7r695n.js.map} +3 -3
  34. package/dist/cli/{chunk-zxh4d9vy.js → chunk-h2ez8dzb.js} +4 -4
  35. package/dist/cli/{chunk-5shv93fd.js → chunk-h7k3nq3v.js} +2 -2
  36. package/dist/cli/{chunk-79jhk4py.js → chunk-hqp2ajnh.js} +251 -122
  37. package/dist/cli/chunk-hqp2ajnh.js.map +35 -0
  38. package/dist/cli/{chunk-2hn4b8z7.js → chunk-hr8ne106.js} +109 -40
  39. package/dist/cli/chunk-hr8ne106.js.map +13 -0
  40. package/dist/cli/{chunk-6hsn950k.js → chunk-j85scx15.js} +62 -18
  41. package/dist/cli/chunk-j85scx15.js.map +10 -0
  42. package/dist/cli/{chunk-s1p84fyh.js → chunk-k7pj68a8.js} +63 -22
  43. package/dist/cli/chunk-k7pj68a8.js.map +11 -0
  44. package/dist/cli/{chunk-ah61y8py.js → chunk-mqc662a6.js} +2 -2
  45. package/dist/cli/{chunk-ch6g3ar0.js → chunk-n1yg3tj3.js} +4 -4
  46. package/dist/cli/{chunk-mb2919y2.js → chunk-nfcyttvj.js} +17 -6
  47. package/dist/cli/chunk-nfcyttvj.js.map +10 -0
  48. package/dist/cli/{chunk-kpf8rrjc.js → chunk-pbg5a4s3.js} +56 -24
  49. package/dist/cli/chunk-pbg5a4s3.js.map +19 -0
  50. package/dist/cli/{chunk-jwyddg7y.js → chunk-ppzjqwx2.js} +21 -14
  51. package/dist/cli/{chunk-jwyddg7y.js.map → chunk-ppzjqwx2.js.map} +4 -4
  52. package/dist/cli/{chunk-dh8cwk36.js → chunk-qkb5a8sa.js} +24 -9
  53. package/dist/cli/chunk-qkb5a8sa.js.map +10 -0
  54. package/dist/cli/{chunk-wm7js3j9.js → chunk-sqw4ekg1.js} +2 -2
  55. package/dist/cli/{chunk-qkqwkpte.js → chunk-v6ya5kcb.js} +3083 -953
  56. package/dist/cli/chunk-v6ya5kcb.js.map +189 -0
  57. package/dist/cli/{chunk-fz5wtpmh.js → chunk-w4bxdvsa.js} +18 -15
  58. package/dist/cli/chunk-w4bxdvsa.js.map +10 -0
  59. package/dist/cli/{chunk-27g6wdth.js → chunk-wdrt2k2v.js} +2 -2
  60. package/dist/cli/{chunk-wgm7m9qk.js → chunk-xh43dwgw.js} +158 -43
  61. package/dist/cli/chunk-xh43dwgw.js.map +36 -0
  62. package/dist/cli/{chunk-s6jhgk0q.js → chunk-zp79m0ts.js} +2 -2
  63. package/dist/cli/index.js +160 -35
  64. package/dist/cli/index.js.map +4 -4
  65. package/dist/types/ai/agent-surface.d.ts +32 -0
  66. package/dist/types/ai/api-catalog.d.ts +7 -1
  67. package/dist/types/ai/ask-context.d.ts +7 -0
  68. package/dist/types/ai/component-markdown.d.ts +4 -4
  69. package/dist/types/ai/relative-links.d.ts +9 -4
  70. package/dist/types/ai/static-expression.d.ts +28 -0
  71. package/dist/types/analytics/databuddy.d.ts +43 -0
  72. package/dist/types/analytics/index.d.ts +2 -0
  73. package/dist/types/analytics/schema.d.ts +14 -0
  74. package/dist/types/cli/env.d.ts +5 -0
  75. package/dist/types/cli/init/scaffold.d.ts +19 -3
  76. package/dist/types/cli/init/starter-spec.d.ts +11 -0
  77. package/dist/types/core/base-path.d.ts +9 -0
  78. package/dist/types/core/config-input.d.ts +4 -4
  79. package/dist/types/core/config.d.ts +2 -2
  80. package/dist/types/core/graph.d.ts +2 -0
  81. package/dist/types/core/i18n-ui.d.ts +31 -0
  82. package/dist/types/core/i18n.d.ts +7 -1
  83. package/dist/types/core/links.d.ts +3 -1
  84. package/dist/types/core/load-module.d.ts +10 -0
  85. package/dist/types/core/locale-links.d.ts +12 -2
  86. package/dist/types/core/meta.d.ts +5 -1
  87. package/dist/types/core/nav-diagnostics.d.ts +10 -0
  88. package/dist/types/core/navigation.d.ts +38 -0
  89. package/dist/types/core/ordering-prefix.d.ts +4 -0
  90. package/dist/types/core/safe-links.d.ts +3 -1
  91. package/dist/types/core/schema.d.ts +52 -13
  92. package/dist/types/core/sources/github-releases.d.ts +5 -0
  93. package/dist/types/core/sources/lower.d.ts +14 -2
  94. package/dist/types/core/sources/normalize.d.ts +10 -1
  95. package/dist/types/core/sources/remote.d.ts +11 -1
  96. package/dist/types/core/sources/resolve.d.ts +12 -0
  97. package/dist/types/core/sources/types.d.ts +31 -0
  98. package/dist/types/core/types.d.ts +9 -0
  99. package/dist/types/deploy/adapters/node.d.ts +5 -2
  100. package/dist/types/deploy/adapters/types.d.ts +7 -0
  101. package/dist/types/deploy/cloudflare-negotiation.d.ts +3 -2
  102. package/dist/types/deploy/headers.d.ts +31 -7
  103. package/dist/types/deploy/node-headers.d.ts +43 -8
  104. package/dist/types/deploy/platforms/netlify.d.ts +27 -2
  105. package/dist/types/deploy/platforms/node.d.ts +6 -5
  106. package/dist/types/deploy/platforms/types.d.ts +7 -0
  107. package/dist/types/deploy/platforms/vercel.d.ts +3 -2
  108. package/dist/types/deploy/redirects.d.ts +7 -2
  109. package/dist/types/deploy/vercel-negotiation.d.ts +3 -2
  110. package/dist/types/openapi/model.d.ts +20 -6
  111. package/dist/types/search/sync/algolia.d.ts +3 -1
  112. package/docs/01-quickstart.mdx +3 -2
  113. package/docs/02-deployment.mdx +17 -8
  114. package/docs/08-faq.mdx +9 -2
  115. package/docs/advanced/changelog.mdx +1 -1
  116. package/docs/advanced/custom-pages.mdx +4 -2
  117. package/docs/cli/audit.mdx +2 -2
  118. package/docs/cli/doctor.mdx +2 -2
  119. package/docs/cli/evals.mdx +2 -2
  120. package/docs/cli/index.mdx +4 -1
  121. package/docs/cli/translate.mdx +1 -1
  122. package/docs/configuration/analytics.mdx +23 -2
  123. package/docs/configuration/assistant.mdx +1 -1
  124. package/docs/configuration/customization.mdx +4 -3
  125. package/docs/configuration/index.mdx +5 -3
  126. package/docs/configuration/search.mdx +1 -1
  127. package/docs/content/components.mdx +1 -1
  128. package/docs/content/frontmatter.mdx +5 -1
  129. package/docs/content/i18n.mdx +1 -1
  130. package/docs/content/index.mdx +4 -2
  131. package/docs/content/meta.mdx +5 -3
  132. package/docs/content/navigation.mdx +29 -4
  133. package/docs/content/sources.mdx +18 -12
  134. package/docs/content/versioning.mdx +1 -0
  135. package/docs/discoverability/agent-discovery.mdx +21 -7
  136. package/docs/discoverability/index.mdx +2 -2
  137. package/docs/discoverability/llms-txt.mdx +2 -5
  138. package/docs/discoverability/markdown.mdx +5 -3
  139. package/docs/discoverability/mcp.mdx +4 -0
  140. package/docs/discoverability/metadata.mdx +3 -2
  141. package/docs/discoverability/open-graph.mdx +6 -4
  142. package/docs/discoverability/rss.mdx +3 -1
  143. package/docs/references/asyncapi.mdx +1 -1
  144. package/docs/references/graphql.mdx +2 -2
  145. package/docs/references/openapi.mdx +3 -3
  146. package/package.json +1 -1
  147. package/skills/blume-migrate/SKILL.md +6 -6
  148. package/skills/blume-migrate/references/docusaurus.md +6 -6
  149. package/skills/blume-migrate/references/mintlify.md +3 -3
  150. package/skills/blume-migrate/references/monorepo.md +1 -1
  151. package/skills/blume-migrate/references/nextra.md +1 -1
  152. package/skills/blume-migrate/references/starlight.md +5 -5
  153. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +2 -2
  154. package/skills/blume-update-docs/references/audit-checklist.md +1 -1
  155. package/src/ai/agent-surface.ts +56 -0
  156. package/src/ai/api/spec.ts +15 -2
  157. package/src/ai/api-catalog.ts +7 -1
  158. package/src/ai/ask-context.ts +14 -2
  159. package/src/ai/ask-data.ts +26 -12
  160. package/src/ai/component-markdown.ts +43 -37
  161. package/src/ai/link-headers.ts +8 -3
  162. package/src/ai/llms.ts +4 -3
  163. package/src/ai/markdown.ts +23 -11
  164. package/src/ai/mcp/server.ts +78 -7
  165. package/src/ai/relative-links.ts +78 -10
  166. package/src/ai/static-expression.ts +416 -0
  167. package/src/ai/visibility.ts +45 -14
  168. package/src/analytics/databuddy.ts +67 -0
  169. package/src/analytics/head.ts +4 -0
  170. package/src/analytics/index.ts +2 -0
  171. package/src/analytics/posthog.ts +21 -3
  172. package/src/analytics/schema.ts +2 -0
  173. package/src/astro/generate.ts +71 -27
  174. package/src/astro/templates.ts +61 -23
  175. package/src/audit/catalog.ts +2 -2
  176. package/src/audit/checks/assets.ts +25 -5
  177. package/src/audit/checks/content.ts +20 -2
  178. package/src/audit/checks/i18n.ts +46 -8
  179. package/src/audit/checks/indexability.ts +39 -19
  180. package/src/audit/checks/links.ts +14 -0
  181. package/src/audit/checks/llms.ts +6 -3
  182. package/src/audit/checks/network.ts +3 -1
  183. package/src/audit/checks/og-image.ts +10 -0
  184. package/src/audit/checks/robots.ts +6 -1
  185. package/src/audit/checks/sitemap.ts +56 -31
  186. package/src/audit/checks/social.ts +21 -2
  187. package/src/audit/crawl.ts +88 -15
  188. package/src/audit/report.ts +54 -17
  189. package/src/audit/run.ts +1 -0
  190. package/src/audit/types.ts +18 -0
  191. package/src/audit/url.ts +37 -6
  192. package/src/cli/build-failure.ts +50 -0
  193. package/src/cli/commands/audit.ts +7 -0
  194. package/src/cli/commands/build.ts +19 -5
  195. package/src/cli/commands/check.ts +2 -0
  196. package/src/cli/commands/eval.ts +1 -7
  197. package/src/cli/commands/init.ts +15 -5
  198. package/src/cli/commands/preview.ts +15 -0
  199. package/src/cli/commands/sync.ts +2 -0
  200. package/src/cli/commands/translate.ts +9 -6
  201. package/src/cli/commands/upgrade.ts +11 -0
  202. package/src/cli/eject-scripts.ts +32 -7
  203. package/src/cli/env.ts +12 -1
  204. package/src/cli/init/scaffold.ts +117 -11
  205. package/src/cli/init/starter-spec.ts +235 -0
  206. package/src/components/content/AccordionItem.astro +26 -22
  207. package/src/components/content/Badge.astro +2 -9
  208. package/src/components/content/Card.astro +2 -2
  209. package/src/components/content/Component.astro +9 -3
  210. package/src/components/content/Expandable.astro +5 -1
  211. package/src/components/content/Frame.astro +2 -7
  212. package/src/components/content/GithubInfo.astro +12 -2
  213. package/src/components/content/Prompt.astro +2 -7
  214. package/src/components/content/Tabs.astro +54 -5
  215. package/src/components/content/Tile.astro +1 -1
  216. package/src/components/content/Tooltip.astro +69 -7
  217. package/src/components/content/Tree.astro +7 -2
  218. package/src/components/content/TypeTable.astro +10 -5
  219. package/src/components/content/Update.astro +8 -2
  220. package/src/components/content/auto-type-table.ts +4 -1
  221. package/src/components/content/badge-color.ts +17 -0
  222. package/src/components/content/base-href.ts +18 -3
  223. package/src/components/content/inline-markdown.ts +27 -7
  224. package/src/components/copy-feedback.ts +35 -8
  225. package/src/components/islands/assistant.tsx +16 -2
  226. package/src/components/islands/hooks.ts +7 -2
  227. package/src/components/islands/webmcp.ts +12 -8
  228. package/src/components/layout/Banner.astro +23 -4
  229. package/src/components/layout/Header.astro +22 -9
  230. package/src/components/layout/Logo.astro +5 -0
  231. package/src/components/layout/NavSelector.astro +6 -1
  232. package/src/components/layout/NavTabMenu.astro +133 -0
  233. package/src/components/layout/NavTree.astro +15 -6
  234. package/src/components/layout/NavTreeCache.astro +5 -2
  235. package/src/components/layout/NavTreeScript.astro +45 -5
  236. package/src/components/layout/PageLayout.astro +22 -8
  237. package/src/components/layout/ReferenceLayout.astro +4 -1
  238. package/src/components/layout/RootLayout.astro +21 -8
  239. package/src/components/layout/Search.astro +5 -1
  240. package/src/components/layout/analytics-client.ts +2 -0
  241. package/src/components/openapi/GraphqlType.astro +11 -3
  242. package/src/components/openapi/MessageComposer.astro +1 -1
  243. package/src/components/openapi/Operation.astro +21 -5
  244. package/src/components/openapi/PanelTabs.astro +4 -1
  245. package/src/components/openapi/Playground.astro +8 -2
  246. package/src/components/openapi/RequestPanel.astro +9 -5
  247. package/src/components/openapi/SchemaProperty.astro +11 -33
  248. package/src/components/openapi/SchemaTable.astro +27 -52
  249. package/src/components/openapi/helpers.ts +90 -12
  250. package/src/components/openapi/message-composer.ts +8 -0
  251. package/src/components/openapi/message-model.ts +12 -2
  252. package/src/components/openapi/message.ts +4 -1
  253. package/src/components/openapi/operation-model.ts +40 -8
  254. package/src/components/openapi/panel.ts +29 -5
  255. package/src/components/openapi/playground-client.ts +37 -7
  256. package/src/components/openapi/playground-schema.ts +25 -6
  257. package/src/components/openapi/request.ts +2 -0
  258. package/src/components/openapi/schema-tree.ts +203 -0
  259. package/src/components/openapi/snippets.ts +30 -15
  260. package/src/components/openapi/validate-json.ts +1 -1
  261. package/src/core/base-path.ts +15 -0
  262. package/src/core/config-input.ts +4 -4
  263. package/src/core/config.ts +18 -4
  264. package/src/core/diagnostics.ts +217 -30
  265. package/src/core/graph.ts +120 -11
  266. package/src/core/i18n-ui.ts +39 -2
  267. package/src/core/i18n.ts +13 -2
  268. package/src/core/links.ts +7 -4
  269. package/src/core/load-module.ts +20 -0
  270. package/src/core/locale-links.ts +17 -17
  271. package/src/core/manifest.ts +3 -2
  272. package/src/core/meta.ts +69 -6
  273. package/src/core/nav-diagnostics.ts +56 -1
  274. package/src/core/navigation.ts +245 -76
  275. package/src/core/ordering-prefix.ts +27 -0
  276. package/src/core/project-graph.ts +23 -1
  277. package/src/core/safe-href.ts +53 -1
  278. package/src/core/safe-links.ts +11 -2
  279. package/src/core/schema.ts +70 -14
  280. package/src/core/sources/assets.ts +83 -41
  281. package/src/core/sources/contentful-rich-text.ts +3 -2
  282. package/src/core/sources/contentful.ts +25 -12
  283. package/src/core/sources/filesystem.ts +5 -1
  284. package/src/core/sources/github-releases.ts +106 -5
  285. package/src/core/sources/lexical.ts +9 -4
  286. package/src/core/sources/lower.ts +79 -26
  287. package/src/core/sources/mdx-remote.ts +1 -0
  288. package/src/core/sources/normalize.ts +132 -30
  289. package/src/core/sources/notion.ts +26 -10
  290. package/src/core/sources/obsidian.ts +17 -3
  291. package/src/core/sources/payload.ts +1 -0
  292. package/src/core/sources/portable-text.ts +63 -44
  293. package/src/core/sources/remote.ts +18 -2
  294. package/src/core/sources/resolve.ts +52 -33
  295. package/src/core/sources/sanity.ts +1 -0
  296. package/src/core/sources/strapi-blocks.ts +9 -4
  297. package/src/core/sources/strapi.ts +1 -0
  298. package/src/core/sources/types.ts +31 -0
  299. package/src/core/types.ts +9 -0
  300. package/src/core/ui-packs/ar.ts +13 -0
  301. package/src/core/ui-packs/bg.ts +13 -0
  302. package/src/core/ui-packs/bn.ts +13 -0
  303. package/src/core/ui-packs/ca.ts +13 -0
  304. package/src/core/ui-packs/cs.ts +13 -0
  305. package/src/core/ui-packs/da.ts +13 -0
  306. package/src/core/ui-packs/de.ts +13 -0
  307. package/src/core/ui-packs/el.ts +13 -0
  308. package/src/core/ui-packs/es.ts +13 -0
  309. package/src/core/ui-packs/fa.ts +13 -0
  310. package/src/core/ui-packs/fi.ts +13 -0
  311. package/src/core/ui-packs/fr.ts +13 -0
  312. package/src/core/ui-packs/he.ts +13 -0
  313. package/src/core/ui-packs/hi.ts +13 -0
  314. package/src/core/ui-packs/hr.ts +13 -0
  315. package/src/core/ui-packs/hu.ts +13 -0
  316. package/src/core/ui-packs/id.ts +13 -0
  317. package/src/core/ui-packs/it.ts +13 -0
  318. package/src/core/ui-packs/ja.ts +13 -0
  319. package/src/core/ui-packs/ko.ts +13 -0
  320. package/src/core/ui-packs/nl.ts +13 -0
  321. package/src/core/ui-packs/no.ts +13 -0
  322. package/src/core/ui-packs/pl.ts +13 -0
  323. package/src/core/ui-packs/pt-br.ts +13 -0
  324. package/src/core/ui-packs/pt.ts +13 -0
  325. package/src/core/ui-packs/ro.ts +13 -0
  326. package/src/core/ui-packs/ru.ts +13 -0
  327. package/src/core/ui-packs/sk.ts +13 -0
  328. package/src/core/ui-packs/sr.ts +13 -0
  329. package/src/core/ui-packs/sv.ts +13 -0
  330. package/src/core/ui-packs/th.ts +13 -0
  331. package/src/core/ui-packs/tr.ts +13 -0
  332. package/src/core/ui-packs/uk.ts +13 -0
  333. package/src/core/ui-packs/vi.ts +13 -0
  334. package/src/core/ui-packs/zh-tw.ts +13 -0
  335. package/src/core/ui-packs/zh.ts +13 -0
  336. package/src/core/version-cut.ts +17 -2
  337. package/src/core/versions.ts +4 -1
  338. package/src/deploy/adapters/node.ts +5 -2
  339. package/src/deploy/adapters/registry.ts +2 -1
  340. package/src/deploy/adapters/types.ts +13 -1
  341. package/src/deploy/artifacts.ts +25 -5
  342. package/src/deploy/cloudflare-negotiation.ts +23 -4
  343. package/src/deploy/headers.ts +67 -51
  344. package/src/deploy/node-headers.ts +148 -27
  345. package/src/deploy/platforms/cloudflare.ts +1 -0
  346. package/src/deploy/platforms/netlify.ts +82 -5
  347. package/src/deploy/platforms/node.ts +7 -5
  348. package/src/deploy/platforms/static.ts +1 -0
  349. package/src/deploy/platforms/types.ts +7 -0
  350. package/src/deploy/platforms/vercel.ts +9 -3
  351. package/src/deploy/redirects.ts +14 -3
  352. package/src/deploy/vercel-negotiation.ts +35 -3
  353. package/src/eval/agents.ts +10 -2
  354. package/src/eval/run.ts +25 -0
  355. package/src/markdown/base-links.ts +55 -8
  356. package/src/markdown/index.ts +6 -3
  357. package/src/markdown/relative-links.ts +3 -23
  358. package/src/markdown/route-snapshot.ts +37 -0
  359. package/src/og/card.ts +20 -4
  360. package/src/og/derive.ts +145 -4
  361. package/src/openapi/graphql-build.ts +28 -2
  362. package/src/openapi/model.ts +77 -13
  363. package/src/openapi/render-mdx.ts +10 -1
  364. package/src/registry/eject.ts +119 -22
  365. package/src/search/documents.ts +22 -1
  366. package/src/search/sync/algolia.ts +36 -2
  367. package/src/sources/registry.ts +5 -0
  368. package/src/theme/entry.ts +11 -4
  369. package/src/translate/agents.ts +6 -1
  370. package/src/translate/ledger.ts +26 -3
  371. package/src/translate/meta.ts +11 -3
  372. package/src/translate/run.ts +11 -5
  373. package/src/translate/validate.ts +10 -1
  374. package/src/translate/work-list.ts +36 -3
  375. package/src/upgrade/upgrade.ts +36 -4
  376. package/dist/cli/chunk-2hn4b8z7.js.map +0 -12
  377. package/dist/cli/chunk-6crbhc3x.js.map +0 -14
  378. package/dist/cli/chunk-6hsn950k.js.map +0 -10
  379. package/dist/cli/chunk-79jhk4py.js.map +0 -35
  380. package/dist/cli/chunk-82bbrxdn.js +0 -51
  381. package/dist/cli/chunk-82bbrxdn.js.map +0 -10
  382. package/dist/cli/chunk-abh8yjkn.js +0 -31
  383. package/dist/cli/chunk-abh8yjkn.js.map +0 -10
  384. package/dist/cli/chunk-dh8cwk36.js.map +0 -10
  385. package/dist/cli/chunk-epjnccmv.js.map +0 -10
  386. package/dist/cli/chunk-fa25z98p.js.map +0 -11
  387. package/dist/cli/chunk-fs23ddbb.js.map +0 -35
  388. package/dist/cli/chunk-fz5wtpmh.js.map +0 -10
  389. package/dist/cli/chunk-kpf8rrjc.js.map +0 -19
  390. package/dist/cli/chunk-m3vmjgmq.js.map +0 -10
  391. package/dist/cli/chunk-mb2919y2.js.map +0 -10
  392. package/dist/cli/chunk-q5163e60.js.map +0 -11
  393. package/dist/cli/chunk-qkqwkpte.js.map +0 -182
  394. package/dist/cli/chunk-qs4q5p4e.js.map +0 -10
  395. package/dist/cli/chunk-s1p84fyh.js.map +0 -10
  396. package/dist/cli/chunk-wgm7m9qk.js.map +0 -36
  397. package/dist/cli/chunk-yt5n7ppj.js.map +0 -10
  398. /package/dist/cli/{chunk-vtk4a6dg.js.map → chunk-00gs3wqs.js.map} +0 -0
  399. /package/dist/cli/{chunk-6vm74dry.js.map → chunk-6dtt0zfn.js.map} +0 -0
  400. /package/dist/cli/{chunk-fxypxtvm.js.map → chunk-bbnwccaz.js.map} +0 -0
  401. /package/dist/cli/{chunk-ce574jw2.js.map → chunk-esh98wmb.js.map} +0 -0
  402. /package/dist/cli/{chunk-zxcczpyx.js.map → chunk-fsmrqk8a.js.map} +0 -0
  403. /package/dist/cli/{chunk-hdpx1tax.js.map → chunk-g698a744.js.map} +0 -0
  404. /package/dist/cli/{chunk-zxh4d9vy.js.map → chunk-h2ez8dzb.js.map} +0 -0
  405. /package/dist/cli/{chunk-5shv93fd.js.map → chunk-h7k3nq3v.js.map} +0 -0
  406. /package/dist/cli/{chunk-ah61y8py.js.map → chunk-mqc662a6.js.map} +0 -0
  407. /package/dist/cli/{chunk-ch6g3ar0.js.map → chunk-n1yg3tj3.js.map} +0 -0
  408. /package/dist/cli/{chunk-wm7js3j9.js.map → chunk-sqw4ekg1.js.map} +0 -0
  409. /package/dist/cli/{chunk-27g6wdth.js.map → chunk-wdrt2k2v.js.map} +0 -0
  410. /package/dist/cli/{chunk-s6jhgk0q.js.map → chunk-zp79m0ts.js.map} +0 -0
@@ -23,6 +23,10 @@ import {
23
23
  detectNeedsReact,
24
24
  detectUsesMath,
25
25
  languageIconCssFor,
26
+ PLAYGROUND_PROXY_ENTRY,
27
+ playgroundProxyPattern,
28
+ proxyAllowlistWarnings,
29
+ specOrigins,
26
30
  } from "../astro/generate.ts";
27
31
  import { discoverIslands } from "../astro/islands.ts";
28
32
  import { customOgRoutes, discoverPages, routeIsTaken } from "../astro/pages.ts";
@@ -32,6 +36,7 @@ import {
32
36
  astroConfigTemplate,
33
37
  catchAllPageTemplate,
34
38
  changelogIndexTemplate,
39
+ contentAssetsEndpointTemplate,
35
40
  contentConfigTemplate,
36
41
  exampleMapTemplate,
37
42
  exampleSlug,
@@ -43,6 +48,7 @@ import {
43
48
  mixedbreadSearchEndpointTemplate,
44
49
  notFoundPageTemplate,
45
50
  ogEndpointTemplate,
51
+ playgroundProxyTemplate,
46
52
  rawMarkdownEndpointTemplate,
47
53
  rssEndpointTemplate,
48
54
  runtimeDependencies,
@@ -51,6 +57,7 @@ import {
51
57
  searchEndpointTemplate,
52
58
  staticJsonEndpointTemplate,
53
59
  } from "../astro/templates.ts";
60
+ import { collectContentAssets } from "../core/content-assets.ts";
54
61
  import { buildIncludeGraph } from "../core/includes.ts";
55
62
  import { packageRoot } from "../core/package-root.ts";
56
63
  import { scanProject } from "../core/project-graph.ts";
@@ -62,7 +69,10 @@ import {
62
69
  import type { ProjectContext } from "../core/types.ts";
63
70
  import { buildRssFeeds, renderRssFeed } from "../deploy/rss.ts";
64
71
  import type { OpenApiData } from "../openapi/model.ts";
65
- import { hasScalarReferences } from "../openapi/references.ts";
72
+ import {
73
+ hasScalarReferences,
74
+ needsPlaygroundProxy,
75
+ } from "../openapi/references.ts";
66
76
  import { buildReferenceFiles } from "../openapi/scalar.ts";
67
77
  import { isOpenApiSource } from "../openapi/source.ts";
68
78
  import { buildSearchDocuments } from "../search/documents.ts";
@@ -190,6 +200,43 @@ const mcpDiscoveryPages = (
190
200
  ]
191
201
  : [];
192
202
 
203
+ /**
204
+ * The playground's built-in CORS proxy route (`playground: { proxy: true }`),
205
+ * mirroring `planPlaygroundProxy` in generate.ts: injected like the MCP
206
+ * discovery routes, because Astro treats a `_`-prefixed page file as private,
207
+ * and mounted under `basePath` at the URL the playground sends to. Empty when
208
+ * no reference routes its playground through it.
209
+ */
210
+ const playgroundProxyPages = (
211
+ config: BlumeProject["config"]
212
+ ): { entrypoint: string; pattern: string }[] =>
213
+ needsPlaygroundProxy(config)
214
+ ? [
215
+ {
216
+ entrypoint: toPosix(join("src", PLAYGROUND_PROXY_ENTRY)),
217
+ pattern: playgroundProxyPattern(config),
218
+ },
219
+ ]
220
+ : [];
221
+
222
+ /**
223
+ * The proxy endpoint behind {@link playgroundProxyPages}, with the origin
224
+ * allowlist baked in from the same parsed specs the reference pages render.
225
+ */
226
+ const playgroundProxyFiles = (
227
+ config: BlumeProject["config"],
228
+ openApiData: OpenApiData,
229
+ srcDir: string
230
+ ): { content: string; path: string }[] =>
231
+ needsPlaygroundProxy(config)
232
+ ? [
233
+ {
234
+ content: playgroundProxyTemplate(specOrigins(openApiData)),
235
+ path: join(srcDir, PLAYGROUND_PROXY_ENTRY),
236
+ },
237
+ ]
238
+ : [];
239
+
193
240
  /**
194
241
  * The MCP data snapshot, server endpoint, and `.well-known` discovery
195
242
  * documents, mirroring `writeMcpFiles` in generate.ts. Empty when the server
@@ -265,6 +312,40 @@ const changelogFiles = (
265
312
  ];
266
313
  };
267
314
 
315
+ /**
316
+ * The colocated-image map and the `/blume-assets/[...asset]` endpoint that
317
+ * serves it, mirroring generate.ts: the ejected raw Markdown points
318
+ * `![x](./diagram.png)` at `/blume-assets/content/…`, and the config aliases
319
+ * `blume:content-assets` to the map. Each original's path is written relative
320
+ * to the project root (the directory `astro dev`/`astro build` run from), so
321
+ * the ejected app builds from any checkout. Remote-source images need no
322
+ * endpoint: eject copies them into the project's own `public/blume-assets`.
323
+ */
324
+ const contentAssetFiles = async (
325
+ project: BlumeProject,
326
+ root: string,
327
+ srcDir: string,
328
+ genDir: string
329
+ ): Promise<{ content: string; path: string }[]> => {
330
+ const assets = await collectContentAssets(project);
331
+ const portable = Object.fromEntries(
332
+ Object.entries(assets).map(([param, abs]) => [
333
+ param,
334
+ toPosix(relative(root, abs)),
335
+ ])
336
+ );
337
+ return [
338
+ {
339
+ content: `${JSON.stringify(portable)}\n`,
340
+ path: join(genDir, "content-assets.json"),
341
+ },
342
+ {
343
+ content: contentAssetsEndpointTemplate(null),
344
+ path: join(srcDir, "pages", "blume-assets", "[...asset].ts"),
345
+ },
346
+ ];
347
+ };
348
+
268
349
  /** Contents of the configured `examples.css`, or `""` when unset/absent. */
269
350
  /**
270
351
  * Read a user stylesheet that eject inlines into a generated entry under
@@ -309,16 +390,18 @@ const examplesPreviewFiles = (
309
390
 
310
391
  /**
311
392
  * The packages the ejected app imports by bare name: Astro, Tailwind's Vite
312
- * plugin, `blume` itself, the integrations the config wires in, whatever the
313
- * configured adapters declare, and React when islands, examples, or the assistant
314
- * render with it (Blume ships React, so projects rarely list it). They have
315
- * to be the project's own dependencies after eject — under a strict linker
316
- * such as pnpm nothing else makes them resolvable, and `astro build` fails.
317
- * The hidden runtime reaches the same packages through its `node_modules`
318
- * junction into Blume's own, so only eject declares the ones its generated
319
- * files import directly: the AI SDK the assistant route streams through (unless
320
- * `ai.assistant.endpoint` points elsewhere, when no route is written) and the EPUB
321
- * generator's browser bundle `features.ts` loads.
393
+ * plugin, Tailwind and its typography plugin (which the generated `app.css`
394
+ * and `examples.css` import by name), `blume` itself, the integrations the
395
+ * config wires in, whatever the configured adapters declare, and React when
396
+ * islands, examples, or the assistant render with it (Blume ships React, so
397
+ * projects rarely list it). They have to be the project's own dependencies
398
+ * after eject — under a strict linker such as pnpm nothing else makes them
399
+ * resolvable, and `astro build` fails. The hidden runtime reaches the same
400
+ * packages through its `node_modules` junction into Blume's own, so only
401
+ * eject declares the ones its generated files import directly: the AI SDK the
402
+ * assistant route streams through (unless `ai.assistant.endpoint` points
403
+ * elsewhere, when no route is written) and the EPUB generator's browser bundle
404
+ * `features.ts` loads (only named there when `export.epub` is on).
322
405
  */
323
406
  const ejectDependencies = (
324
407
  options: Parameters<typeof runtimeDependencies>[0]
@@ -328,6 +411,8 @@ const ejectDependencies = (
328
411
  ...new Set([
329
412
  "astro",
330
413
  "@tailwindcss/vite",
414
+ "tailwindcss",
415
+ "@tailwindcss/typography",
331
416
  "blume",
332
417
  ...runtimeDependencies(options),
333
418
  ...(options.needsReact ? ["react", "react-dom"] : []),
@@ -439,7 +524,9 @@ export const eject = async (
439
524
  pattern: page.pattern,
440
525
  })),
441
526
  ...mcpDiscoveryPages(project, pages),
527
+ ...playgroundProxyPages(config),
442
528
  ];
529
+ const openApiData = ejectOpenApiData(project);
443
530
 
444
531
  // Non-filesystem sources eject their materialized MDX into `<root>/blume-staged`
445
532
  // (a dedicated dir so it never clashes with a content root literally named
@@ -555,7 +642,7 @@ export const eject = async (
555
642
  path: join(genDir, "Ask.astro"),
556
643
  },
557
644
  {
558
- content: `${JSON.stringify(ejectOpenApiData(project))}\n`,
645
+ content: `${JSON.stringify(openApiData)}\n`,
559
646
  path: join(genDir, "openapi.json"),
560
647
  },
561
648
  {
@@ -581,30 +668,38 @@ export const eject = async (
581
668
  },
582
669
  ];
583
670
 
671
+ files.push(...(await contentAssetFiles(project, root, srcDir, genDir)));
672
+
584
673
  if (assistantEnabled) {
585
674
  files.push(...(await askFiles(project, srcDir, genDir)));
586
675
  }
587
676
 
677
+ // The `/changelog` index, mirrored from the generated runtime (`[]` when
678
+ // the project has no changelog). The OG endpoint below adds its card.
679
+ const changelog = changelogFiles(project, pages, srcDir, {
680
+ exportEpub,
681
+ exportPdf,
682
+ needsReact,
683
+ staged: hasStaged,
684
+ });
685
+
588
686
  if (config.seo.og.enabled) {
589
687
  files.push({
590
688
  content: ogEndpointTemplate(
591
689
  customOgRoutes(pages, config.title, config.seo.og.titles),
592
- { pageDescriptions: config.seo.og.description !== false }
690
+ { pageDescriptions: config.seo.og.description !== false },
691
+ changelog.length > 0
593
692
  ),
594
693
  path: join(srcDir, "pages", "og", "[...slug].png.ts"),
595
694
  });
596
695
  }
597
696
 
598
- // The hosted MCP server and the `/changelog` index, mirrored from the
599
- // generated runtime (each helper returns `[]` when its feature is off).
697
+ // The hosted MCP server and the playground's built-in proxy, mirrored from
698
+ // the generated runtime (`[]` when each is off).
600
699
  files.push(
601
700
  ...(await mcpFiles(project, pages, srcDir, genDir)),
602
- ...changelogFiles(project, pages, srcDir, {
603
- exportEpub,
604
- exportPdf,
605
- needsReact,
606
- staged: hasStaged,
607
- })
701
+ ...changelog,
702
+ ...playgroundProxyFiles(config, openApiData, srcDir)
608
703
  );
609
704
 
610
705
  // Default 404 page, unless the project already owns `/404` (a custom
@@ -673,7 +768,9 @@ export const eject = async (
673
768
  // the ejected app keeps its reference routes — including the warnings (a
674
769
  // missing spec file, a route collision), which the caller surfaces exactly
675
770
  // like the generated-runtime path does.
676
- const warnings: string[] = [];
771
+ // A proxied spec with no origin to allow gets the generated runtime's
772
+ // diagnostic too.
773
+ const warnings: string[] = proxyAllowlistWarnings(config, openApiData);
677
774
  if (hasScalarReferences(config)) {
678
775
  const references = await buildReferenceFiles({
679
776
  config,
@@ -16,6 +16,7 @@ import { applyAudienceVisibility } from "../ai/visibility.ts";
16
16
  import type { VisibilityAudience } from "../ai/visibility.ts";
17
17
  import matter from "../core/frontmatter.ts";
18
18
  import { parseHeadingMarkers } from "../core/heading-markers.ts";
19
+ import { EN_UI, resolveUIStrings } from "../core/i18n-ui.ts";
19
20
  import { contentIndexable } from "../core/manifest.ts";
20
21
  import type { BlumeProject } from "../core/project-graph.ts";
21
22
  import { HTML_COMMENT } from "../core/sources/normalize.ts";
@@ -447,6 +448,26 @@ export const buildSearchDocuments = async (
447
448
  // otherwise.
448
449
  const components = projectComponentSerializers(project);
449
450
 
451
+ // A page in no sidebar group falls under the "Docs" section, in its own
452
+ // language: the label is a UI string, resolved once per locale.
453
+ const { i18n } = project.config;
454
+ const docsSections = new Map<string, string>();
455
+ const docsSection = (locale: string): string => {
456
+ let label = docsSections.get(locale);
457
+ if (label === undefined) {
458
+ label = (
459
+ i18n
460
+ ? resolveUIStrings(locale, {
461
+ defaultLocale: i18n.defaultLocale,
462
+ overrides: i18n.ui,
463
+ })
464
+ : EN_UI
465
+ ).search.docs;
466
+ docsSections.set(locale, label);
467
+ }
468
+ return label;
469
+ };
470
+
450
471
  return await Promise.all(
451
472
  indexable.map(async (route) => {
452
473
  const page = pageById.get(route.id);
@@ -461,7 +482,7 @@ export const buildSearchDocuments = async (
461
482
  description: page?.description ?? "",
462
483
  locale: route.locale,
463
484
  route: route.path,
464
- section: crumb?.section || "Docs",
485
+ section: crumb?.section || docsSection(route.locale),
465
486
  tags: tags && tags.length > 0 ? tags : undefined,
466
487
  title: route.title,
467
488
  version: route.version,
@@ -7,6 +7,19 @@ import type { SearchRecord } from "../documents.ts";
7
7
  /** The adapter options the sync reads (the public `apiKey` is never used). */
8
8
  export type AlgoliaSyncConfig = Pick<AlgoliaOptions, "appId" | "indexName">;
9
9
 
10
+ /**
11
+ * The record attributes the search dialog filters on (`facetFilters` on
12
+ * `locale:<code>` and `version:<id>`). In Algolia a facet filter on an
13
+ * attribute the index doesn't declare for faceting matches nothing — an i18n
14
+ * or versioned site would get no hits — so the sync declares them,
15
+ * filter-only.
16
+ */
17
+ const FILTER_ATTRIBUTES = ["locale", "version"] as const;
18
+
19
+ /** A faceting declaration's attribute: `filterOnly(locale)` → `locale`. */
20
+ const facetAttribute = (declaration: string): string =>
21
+ declaration.replace(/^\w+\((?<name>.*)\)$/u, "$<name>");
22
+
10
23
  /**
11
24
  * Upload the search records to Algolia. Uses the admin key from
12
25
  * `ALGOLIA_ADMIN_API_KEY` (never the adapter options, which hold only the
@@ -14,7 +27,9 @@ export type AlgoliaSyncConfig = Pick<AlgoliaOptions, "appId" | "indexName">;
14
27
  *
15
28
  * Uses `replaceAllObjects`, which atomically replaces the index contents, so
16
29
  * pages deleted or renamed since the last sync don't linger as stale search
17
- * hits that 404 when clicked.
30
+ * hits that 404 when clicked. Then adds `filterOnly(locale)` and
31
+ * `filterOnly(version)` to the index's `attributesForFaceting`, keeping any
32
+ * the site declared itself, so the dialog's locale and version filters match.
18
33
  */
19
34
  export const syncAlgolia = async (
20
35
  records: SearchRecord[],
@@ -28,8 +43,27 @@ export const syncAlgolia = async (
28
43
  // `core/node-require.ts`).
29
44
  const { algoliasearch }: typeof AlgoliaSdk = nodeRequire("algoliasearch");
30
45
  const client = algoliasearch(config.appId, adminKey);
46
+ const { indexName } = config;
31
47
  await client.replaceAllObjects({
32
- indexName: config.indexName,
48
+ indexName,
33
49
  objects: records.map((record) => ({ ...record, objectID: record._id })),
34
50
  });
51
+ const { attributesForFaceting = [] } = await client.getSettings({
52
+ indexName,
53
+ });
54
+ const declared = new Set(attributesForFaceting.map(facetAttribute));
55
+ const missing = FILTER_ATTRIBUTES.filter((name) => !declared.has(name));
56
+ if (missing.length === 0) {
57
+ return;
58
+ }
59
+ const { taskID } = await client.setSettings({
60
+ indexName,
61
+ indexSettings: {
62
+ attributesForFaceting: [
63
+ ...attributesForFaceting,
64
+ ...missing.map((name) => `filterOnly(${name})`),
65
+ ],
66
+ },
67
+ });
68
+ await client.waitForTask({ indexName, taskID });
35
69
  };
@@ -165,6 +165,11 @@ const sourceEntrySchema = z
165
165
  .custom<SourceAdapterInput>(
166
166
  (value) => isObjectLike(value) && "kind" in value,
167
167
  {
168
+ // `z.custom` marks its failure as aborting by default, which makes Zod
169
+ // skip even the `content` refinement that opts in to running after a
170
+ // failed field (the shorthand beside `sources`). The pipe below stops
171
+ // at this entry either way.
172
+ abort: false,
168
173
  error: (issue) => {
169
174
  const legacy = legacyEntrySchema.safeParse(issue.input);
170
175
  return entryHint(legacy.success ? legacy.data.type : undefined);
@@ -268,7 +268,7 @@ ${THEME_MAPPING}
268
268
  @apply block rounded-[0.65rem] px-2.5 py-1.5 text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground aria-[current=page]:bg-muted aria-[current=page]:font-medium aria-[current=page]:text-foreground;
269
269
  }
270
270
  @utility blume-nav-drill {
271
- @apply flex w-full items-center gap-2 rounded-[0.65rem] px-2.5 py-1.5 text-left font-medium text-foreground text-sm transition-colors hover:bg-muted;
271
+ @apply flex w-full items-center gap-2 rounded-[0.65rem] px-2.5 py-1.5 text-start font-medium text-foreground text-sm transition-colors hover:bg-muted;
272
272
  }
273
273
  @utility blume-nav-summary {
274
274
  @apply flex cursor-pointer list-none items-center gap-1.5 rounded-[0.65rem] px-2.5 py-1.5 text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground [&::-webkit-details-marker]:hidden;
@@ -884,11 +884,18 @@ pre:has(.line.focused):hover .line:not(.focused) {
884
884
  }
885
885
 
886
886
  /* Print / "Save as PDF" (the page-actions Export → PDF runs window.print()).
887
- Strip the surrounding chrome so the printout is just the article. */
887
+ Strip the surrounding chrome so the printout is just the article. Scoped to
888
+ the chrome itself, never bare \`header\`/\`aside\`: content renders those too
889
+ (every Callout and Panel is an <aside>, every changelog Update has a
890
+ <header>), and they belong in the printout. \`body > header\` keeps a
891
+ \`layout.Header\` override out as well. */
888
892
  @media print {
889
893
  [data-blume-banner],
890
- header,
891
- aside,
894
+ [data-blume-header],
895
+ body > header,
896
+ [data-blume-nav-drawer],
897
+ [data-blume-toc],
898
+ [data-blume-assistant-panel],
892
899
  [data-blume-page-actions],
893
900
  #blume-content > nav,
894
901
  #blume-content > details {
@@ -1,5 +1,5 @@
1
1
  import type { AgentKind } from "../audit/agent.ts";
2
- import { DISALLOWED_TOOLS } from "../eval/agents.ts";
2
+ import { CODEX_LOCKDOWN, DISALLOWED_TOOLS } from "../eval/agents.ts";
3
3
 
4
4
  /**
5
5
  * argv builders for the translator role. The subprocess machinery
@@ -28,6 +28,10 @@ const claudeArgs = (): string[] => [
28
28
  "1",
29
29
  ];
30
30
 
31
+ // `--sandbox read-only` alone still lets codex's shell read any file the user
32
+ // can, and the source page being translated is untrusted input (a remote
33
+ // contributor's prose), so the translator gets the same lockdown as an eval
34
+ // run: no shell, exec, or local-image tool, and no inherited environment.
31
35
  const codexArgs = (lastMessagePath: string): string[] => [
32
36
  "exec",
33
37
  "--skip-git-repo-check",
@@ -35,6 +39,7 @@ const codexArgs = (lastMessagePath: string): string[] => [
35
39
  "--ephemeral",
36
40
  "--sandbox",
37
41
  "read-only",
42
+ ...CODEX_LOCKDOWN,
38
43
  "--output-last-message",
39
44
  lastMessagePath,
40
45
  "-",
@@ -4,6 +4,7 @@ import { readFile } from "node:fs/promises";
4
4
  import { join } from "pathe";
5
5
  import { z } from "zod";
6
6
 
7
+ import { BlumeError } from "../core/diagnostics.ts";
7
8
  import { writeTextAtomic } from "../core/fs-atomic.ts";
8
9
 
9
10
  /**
@@ -39,21 +40,34 @@ export const emptyLedger = (): TranslationLedger => ({
39
40
  * every locale's stamp. sha256-16 like the audit snapshot's content hash.
40
41
  * Kept as its own function rather than sharing core's `hashText`: ledger
41
42
  * stamps persist in user repos, so this hash must never change shape when an
42
- * ephemeral cache hash does.
43
+ * ephemeral cache hash does. CRLF line endings are normalized to LF first, so
44
+ * a Windows checkout (`core.autocrlf`) hashes the same as everyone else's —
45
+ * LF text hashes exactly as it always has.
43
46
  */
44
47
  export const hashSource = (text: string): string =>
45
- createHash("sha256").update(text).digest("hex").slice(0, 16);
48
+ createHash("sha256")
49
+ .update(text.replaceAll("\r\n", "\n"))
50
+ .digest("hex")
51
+ .slice(0, 16);
52
+
53
+ /** A git merge-conflict marker line: `<<<<<<< ours`, `=======`, `>>>>>>> theirs`. */
54
+ const CONFLICT_MARKER = /^(?:<{7}|={7}|>{7})(?:[ \r]|$)/mu;
46
55
 
47
56
  /**
48
57
  * Read the ledger at `root`, tolerantly: a missing file, unparseable JSON, or
49
58
  * an unknown shape/version all resolve to an empty ledger rather than an error
50
59
  * (same posture as the dev lock's `parseLock`) — the worst outcome of a
51
60
  * corrupt ledger is retranslating files that were already up to date.
61
+ *
62
+ * Unresolved merge-conflict markers are the exception, and an error: read as
63
+ * empty, every existing translation would be adopted as current, so
64
+ * `--check` would pass over translations that are out of date.
52
65
  */
53
66
  export const readLedger = async (root: string): Promise<TranslationLedger> => {
67
+ const path = join(root, LEDGER_FILE);
54
68
  let raw: string;
55
69
  try {
56
- raw = await readFile(join(root, LEDGER_FILE), "utf-8");
70
+ raw = await readFile(path, "utf-8");
57
71
  } catch {
58
72
  return emptyLedger();
59
73
  }
@@ -61,6 +75,15 @@ export const readLedger = async (root: string): Promise<TranslationLedger> => {
61
75
  try {
62
76
  data = JSON.parse(raw);
63
77
  } catch {
78
+ if (CONFLICT_MARKER.test(raw)) {
79
+ throw new BlumeError({
80
+ code: "BLUME_TRANSLATE_LEDGER_CONFLICT",
81
+ file: path,
82
+ message: `${LEDGER_FILE} has unresolved merge-conflict markers, so it can't say which translations are current.`,
83
+ severity: "error",
84
+ suggestion: `Resolve the merge conflict in ${LEDGER_FILE}, then rerun.`,
85
+ });
86
+ }
64
87
  return emptyLedger();
65
88
  }
66
89
  const parsed = ledgerSchema.safeParse(data);
@@ -57,7 +57,9 @@ export const metaTargetPath = (
57
57
  /**
58
58
  * Discover the default-locale `meta.{ts,js,mjs}` files whose titles a
59
59
  * translation run covers. Skips (in order): non-`dir` i18n projects entirely,
60
- * files inside a configured locale directory (those ARE translations),
60
+ * files inside a configured locale directory (those ARE translations), files
61
+ * inside an archived-version snapshot (frozen, with their own translations —
62
+ * the same folders the navigation's meta lookup hoists as versions),
61
63
  * factory-form modules (a warning — the generator can't re-emit a function),
62
64
  * modules that fail meta validation (the scan already errors on those), and
63
65
  * modules with no `title` (nothing to translate).
@@ -74,6 +76,9 @@ export const discoverTranslatableMeta = async (
74
76
  locale.code === i18n.defaultLocale ? [] : [locale.code.toLowerCase()]
75
77
  )
76
78
  );
79
+ const versionDirs = new Set(
80
+ project.config.versions?.archived.map((version) => version.id)
81
+ );
77
82
 
78
83
  const load = createModuleLoader();
79
84
  const metas: TranslatableMeta[] = [];
@@ -92,8 +97,11 @@ export const discoverTranslatableMeta = async (
92
97
  });
93
98
  for (const file of files.toSorted()) {
94
99
  const dir = relative(contentRoot, dirname(file));
95
- const first = dir.split("/")[0]?.toLowerCase();
96
- if (first && localeDirs.has(first)) {
100
+ const [head] = dir.split("/");
101
+ if (
102
+ head &&
103
+ (localeDirs.has(head.toLowerCase()) || versionDirs.has(head))
104
+ ) {
97
105
  continue;
98
106
  }
99
107
  // oxlint-disable-next-line no-await-in-loop -- sequential, ordered discovery
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
4
4
 
5
5
  import pLimit from "p-limit";
6
6
  import pMap from "p-map";
7
- import { join } from "pathe";
7
+ import { dirname, join } from "pathe";
8
8
 
9
9
  import { AGENTS } from "../audit/agent.ts";
10
10
  import type { AgentKind } from "../audit/agent.ts";
@@ -18,6 +18,7 @@ import { DEFAULT_TRANSLATE_TIMEOUT_MS, translateAgentArgs } from "./agents.ts";
18
18
  import { hashSource, stampLedger } from "./ledger.ts";
19
19
  import type { TranslationLedger } from "./ledger.ts";
20
20
  import { generateMetaModule } from "./meta.ts";
21
+ import type { TranslatableMeta } from "./meta.ts";
21
22
  import { metaPrompt, pagePrompt } from "./prompts.ts";
22
23
  import { parseMetaTitles, validateTranslation } from "./validate.ts";
23
24
  import type {
@@ -88,8 +89,13 @@ interface RunContext {
88
89
  timeoutMs: number;
89
90
  }
90
91
 
91
- /** The root directory's key in a meta-titles prompt (an empty key is opaque). */
92
- const metaDirKey = (dir: string): string => (dir === "" ? "." : dir);
92
+ /**
93
+ * A meta file's key in a meta-titles prompt: its directory relative to the
94
+ * project root. The content-root-relative `dir` isn't unique — two
95
+ * filesystem sources can both have a `guides/` folder — and colliding keys
96
+ * would hand both folders one translation.
97
+ */
98
+ const metaKey = (meta: TranslatableMeta): string => dirname(meta.sourceRel);
93
99
 
94
100
  /**
95
101
  * One headless agent call. A missing executable rejects with ENOENT on every
@@ -183,7 +189,7 @@ const runMetaItem = async (
183
189
  });
184
190
 
185
191
  const titles = Object.fromEntries(
186
- item.entries.map((entry) => [metaDirKey(entry.meta.dir), entry.meta.title])
192
+ item.entries.map((entry) => [metaKey(entry.meta), entry.meta.title])
187
193
  );
188
194
  // SAFETY: `targets` maps every configured locale, and work items only carry
189
195
  // configured locale codes.
@@ -199,7 +205,7 @@ const runMetaItem = async (
199
205
 
200
206
  const parsed = parseMetaTitles(output.text, Object.keys(titles));
201
207
  for (const entry of item.entries) {
202
- const translated = parsed.titles[metaDirKey(entry.meta.dir)];
208
+ const translated = parsed.titles[metaKey(entry.meta)];
203
209
  if (translated === undefined) {
204
210
  continue;
205
211
  }
@@ -102,7 +102,16 @@ export const validateTranslation = (
102
102
  sourceText: string,
103
103
  agentText: string
104
104
  ): ValidationResult => {
105
- const candidate = stripOuterFence(agentText);
105
+ // Only a fence the agent added is stripped. A source that is itself one
106
+ // code block (a frontmatter-less partial holding a single bash block)
107
+ // starts and ends with its own fence, which the translation must keep — so
108
+ // for such a source the reply loses its outer fence only when it has more
109
+ // fence lines than the source.
110
+ const sourceIsFenced = stripOuterFence(sourceText) !== sourceText.trim();
111
+ const candidate =
112
+ sourceIsFenced && countFenceLines(agentText) <= countFenceLines(sourceText)
113
+ ? agentText.trim()
114
+ : stripOuterFence(agentText);
106
115
  if (candidate === "") {
107
116
  return { ok: false, reason: "agent returned empty output" };
108
117
  }
@@ -1,9 +1,10 @@
1
- import { existsSync } from "node:fs";
1
+ import { existsSync, readdirSync } from "node:fs";
2
2
  import { readFile } from "node:fs/promises";
3
3
 
4
4
  import { dirname, extname, join, relative } from "pathe";
5
5
 
6
6
  import { localeCodes, localeTargetPath } from "../core/i18n.ts";
7
+ import { scanProject } from "../core/project-graph.ts";
7
8
  import type { BlumeProject } from "../core/project-graph.ts";
8
9
  import type { Diagnostic, PageRecord } from "../core/types.ts";
9
10
  import { hashSource } from "./ledger.ts";
@@ -84,6 +85,36 @@ export interface TranslateWorkList {
84
85
 
85
86
  const PAGE_EXTENSIONS = new Set([".md", ".mdx"]);
86
87
 
88
+ /**
89
+ * The names in each content root, read once per root: a locale folder authored
90
+ * in another casing (`pt-br/` for `pt-BR`) is where `localeTargetPath` puts
91
+ * that locale's translations.
92
+ */
93
+ const rootFolders = (): ((contentRoot: string) => string[]) => {
94
+ const read = new Map<string, string[]>();
95
+ return (contentRoot) => {
96
+ const cached = read.get(contentRoot);
97
+ if (cached) {
98
+ return cached;
99
+ }
100
+ const names = readdirSync(contentRoot);
101
+ read.set(contentRoot, names);
102
+ return names;
103
+ };
104
+ };
105
+
106
+ /**
107
+ * Scan the project for a translation run: drafts included. A production
108
+ * (`build`) scan drops them, so a hand-written translation marked
109
+ * `draft: true` would look missing and be overwritten, and a source page
110
+ * drafted for a while would lose its ledger stamps — its outdated
111
+ * translations then reading as current once it's published again. A dev scan
112
+ * keeps drafts and differs only in reusing cached remote content, which
113
+ * translation never reads.
114
+ */
115
+ export const scanForTranslation = (root: string): Promise<BlumeProject> =>
116
+ scanProject(root, { mode: "dev" });
117
+
87
118
  /**
88
119
  * The translatable page universe: filesystem-backed default-locale pages.
89
120
  * Remote/staged sources have no writable path; fallback records are padding;
@@ -201,6 +232,7 @@ const partialWorkItems = async (
201
232
  page.sourcePath ? [page.sourcePath] : []
202
233
  )
203
234
  );
235
+ const folders = rootFolders();
204
236
  const partials = new Map<string, string>();
205
237
  for (const { page, contentRoot } of universe) {
206
238
  for (const partial of page.includes ?? []) {
@@ -220,7 +252,7 @@ const partialWorkItems = async (
220
252
  for (const locale of targetLocales) {
221
253
  const targetPath = join(
222
254
  contentRoot,
223
- localeTargetPath(contentRel, ext, locale, i18n)
255
+ localeTargetPath(contentRel, ext, locale, i18n, folders(contentRoot))
224
256
  );
225
257
  const item = (status: WorkStatus): PageWorkItem => {
226
258
  const partialItem: PageWorkItem = {
@@ -291,6 +323,7 @@ export const computeWorkList = async (
291
323
  let upToDate = 0;
292
324
 
293
325
  const universe = translatablePages(project, i18n);
326
+ const folders = rootFolders();
294
327
 
295
328
  for (const { page, contentRoot, ext, sourcePath } of universe) {
296
329
  const sourceRel = relative(root, sourcePath);
@@ -302,7 +335,7 @@ export const computeWorkList = async (
302
335
  for (const locale of targetLocales) {
303
336
  const targetPath = join(
304
337
  contentRoot,
305
- localeTargetPath(contentRel, ext, locale, i18n)
338
+ localeTargetPath(contentRel, ext, locale, i18n, folders(contentRoot))
306
339
  );
307
340
  const item = (status: WorkStatus): PageWorkItem => ({
308
341
  kind: "page",