failproofai 1.0.0 → 1.0.1-beta.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 (238) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +0 -1
  3. package/.next/standalone/.next/build-manifest.json +3 -3
  4. package/.next/standalone/.next/prerender-manifest.json +5 -5
  5. package/.next/standalone/.next/required-server-files.json +1 -1
  6. package/.next/standalone/.next/routes-manifest.json +0 -6
  7. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page.js +4 -3
  9. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  10. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  14. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  15. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page.js +4 -3
  18. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  20. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  21. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  22. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  23. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  24. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  25. package/.next/standalone/.next/server/app/api/audit/invite/route.js +2 -2
  26. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/audit/run/route.js +3 -3
  28. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/audit/status/route.js +2 -2
  30. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/auth/login-request/route.js +4 -3
  32. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +2 -2
  34. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/auth/logout/route.js +3 -2
  36. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/auth/status/route.js +4 -3
  38. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  41. package/.next/standalone/.next/server/app/audit/page.js +5 -3
  42. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  44. package/.next/standalone/.next/server/app/index.html +1 -1
  45. package/.next/standalone/.next/server/app/index.rsc +15 -15
  46. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  47. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  48. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  49. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  50. package/.next/standalone/.next/server/app/page.js +4 -3
  51. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +10 -10
  54. package/.next/standalone/.next/server/app/policies/page.js +6 -5
  55. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  58. package/.next/standalone/.next/server/app/project/[name]/page.js +5 -4
  59. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  61. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  62. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  63. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +6 -4
  64. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  65. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  66. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  67. package/.next/standalone/.next/server/app/projects/page.js +5 -4
  68. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  69. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  70. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  71. package/.next/standalone/.next/server/app/settings/page.js +6 -4
  72. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  73. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  74. package/.next/standalone/.next/server/app-paths-manifest.json +0 -1
  75. package/.next/standalone/.next/server/chunks/[root-of-the-server]__05ja_05._.js +3 -0
  76. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0agdw2a._.js +3 -0
  77. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d1u_na._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0nne229._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ny9q5d._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0oy4kxb._.js +3 -0
  81. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zf034w._.js +3 -0
  82. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +3 -0
  83. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1hstmft._.js +3 -0
  84. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p8poxd._.js +3 -0
  85. package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +1 -1
  89. package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_0jx6cp6._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_0f-x2b0._.js} +1 -1
  90. package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_1f290jp._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_0hwvl7e._.js} +1 -1
  91. package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_1g2ji1t._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_1ctjdzl._.js} +1 -1
  92. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  93. package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +3 -0
  94. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03eykvd._.js +5 -0
  95. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__07175vb._.js → [root-of-the-server]__07oh8b5._.js} +1 -1
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cxe_2_._.js +1 -1
  97. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g2rg69._.js +3 -0
  98. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kgy9da._.js +4 -0
  99. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0m98kzy._.js +4 -0
  100. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +3 -0
  101. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0sjzdjg._.js +3 -0
  102. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0spkm68._.js +2 -2
  103. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0v5_91z._.js +4 -0
  104. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-a3135._.js +4 -0
  105. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1hvwiw8._.js → [root-of-the-server]__11z_299._.js} +1 -1
  106. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1apc90t._.js → [root-of-the-server]__15c_z97._.js} +1 -1
  107. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1wmno1q._.js → [root-of-the-server]__17d_ffl._.js} +1 -1
  108. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1_dfocn._.js +4 -0
  109. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1e5g_ur._.js +4 -0
  110. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1hmqefd._.js +4 -0
  111. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jiwfsj._.js +1 -1
  112. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1kzapg5._.js +6 -0
  113. package/.next/standalone/.next/server/chunks/ssr/_04j57fb._.js +3 -0
  114. package/.next/standalone/.next/server/chunks/ssr/_05enntk._.js +3 -0
  115. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +3 -0
  116. package/.next/standalone/.next/server/chunks/ssr/_0hom1s1._.js +3 -0
  117. package/.next/standalone/.next/server/chunks/ssr/_1nq-35m._.js +23 -0
  118. package/.next/standalone/.next/server/chunks/ssr/_1q7j05x._.js +3 -0
  119. package/.next/standalone/.next/server/chunks/ssr/_1ylae7o._.js +3 -0
  120. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +2 -2
  121. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  122. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  123. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +3 -0
  124. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_1n3w9lb._.js +1 -1
  125. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
  126. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +4 -0
  127. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +3 -0
  128. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  129. package/.next/standalone/.next/server/pages/404.html +1 -1
  130. package/.next/standalone/.next/server/pages/500.html +1 -1
  131. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  132. package/.next/standalone/.next/server/server-reference-manifest.json +15 -15
  133. package/.next/standalone/.next/static/chunks/00q6cfzuzpc85.js +1 -0
  134. package/.next/standalone/.next/static/chunks/{0zm3_8jpqitxy.js → 054eg7y_cjyf9.js} +22 -22
  135. package/.next/standalone/.next/static/chunks/{2ouq44jv_0x04.js → 0mjufa_0xxz75.js} +1 -1
  136. package/.next/standalone/.next/static/chunks/0u64yey446k4f.css +1 -0
  137. package/.next/standalone/.next/static/chunks/{0zi8zjav5a4rq.js → 1-oz4co31nvc5.js} +1 -1
  138. package/.next/standalone/.next/static/chunks/13--2yvqek_s1.js +1 -0
  139. package/.next/standalone/.next/static/chunks/136ldjmzxhush.js +1 -0
  140. package/.next/standalone/.next/static/chunks/18fqy3doo2ial.js +6 -0
  141. package/.next/standalone/.next/static/chunks/1eyl7fzkmnkf_.js +1 -0
  142. package/.next/standalone/.next/static/chunks/{31k3yxpy57_fy.js → 1o5sxp_mdaoci.js} +1 -1
  143. package/.next/standalone/.next/static/chunks/29-iqnp9v_axt.css +1 -0
  144. package/.next/standalone/.next/static/chunks/{2676v-lyzgo91.js → 2es4z1ei90vsv.js} +1 -1
  145. package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +1 -0
  146. package/.next/standalone/.next/static/chunks/3peix2vxzj7eh.js +1 -0
  147. package/.next/standalone/.next/static/chunks/3qfoeqhy5l10x.js +1 -0
  148. package/.next/standalone/.next/static/chunks/{0km-jn9o5xyqs.css → 3wq9x0c-7ld5d.css} +1 -1
  149. package/.next/standalone/app/actions/get-scheduled-audit.ts +65 -6
  150. package/.next/standalone/app/actions/update-scheduled-audit.ts +90 -17
  151. package/.next/standalone/app/api/auth/status/route.ts +7 -17
  152. package/.next/standalone/app/audit/_components/audit-dashboard.tsx +2 -6
  153. package/.next/standalone/app/audit/_components/come-back-better-section.tsx +101 -281
  154. package/.next/standalone/app/audit/_components/empty-state.tsx +2 -2
  155. package/.next/standalone/app/audit/_components/run-progress.tsx +1 -1
  156. package/.next/standalone/app/audit/audit-styles.css +150 -26
  157. package/.next/standalone/app/globals.css +39 -1
  158. package/.next/standalone/app/settings/page.tsx +38 -21
  159. package/.next/standalone/app/settings/settings-client.tsx +583 -392
  160. package/.next/standalone/app/settings/settings.css +354 -0
  161. package/.next/standalone/components/navbar.tsx +33 -4
  162. package/.next/standalone/lib/auth/api-server-client.ts +51 -29
  163. package/.next/standalone/lib/auth/auth-store.ts +44 -59
  164. package/.next/standalone/package.json +12 -10
  165. package/.next/standalone/server.js +1 -1
  166. package/bin/failproofai.mjs +6 -0
  167. package/bin/failproofaid-shim.mjs +0 -0
  168. package/dist/cli.mjs +1889 -426
  169. package/dist/worker.mjs +27 -5
  170. package/lib/auth/api-server-client.ts +51 -29
  171. package/lib/auth/auth-store.ts +44 -59
  172. package/package.json +12 -10
  173. package/scripts/docs-audit.ts +493 -0
  174. package/scripts/translate-docs/cli.ts +16 -2
  175. package/scripts/translate-docs/mdx-translator.ts +6 -1
  176. package/scripts/translate-docs/readme-translator.ts +4 -2
  177. package/src/audit/cli-login.ts +330 -0
  178. package/src/audit/cli.ts +124 -6
  179. package/src/audit/dashboard-cache.ts +44 -8
  180. package/src/audit/harm-report.ts +260 -0
  181. package/src/audit/machine-store.ts +120 -0
  182. package/src/audit/redact-example.ts +312 -0
  183. package/src/audit/report-harm.ts +167 -0
  184. package/src/audit/schedule-cli.ts +352 -0
  185. package/src/hooks/builtin-policies.ts +26 -0
  186. package/src/hooks/daemon-service.ts +76 -0
  187. package/src/hooks/fp-config.ts +96 -5
  188. package/src/hooks/fp-home.ts +118 -8
  189. package/src/hooks/fp-reset.ts +79 -9
  190. package/src/hooks/migrations.ts +241 -4
  191. package/src/hooks/tui.ts +61 -3
  192. package/.next/standalone/.next/server/app/api/auth/reminder/route/app-paths-manifest.json +0 -3
  193. package/.next/standalone/.next/server/app/api/auth/reminder/route/build-manifest.json +0 -12
  194. package/.next/standalone/.next/server/app/api/auth/reminder/route/server-reference-manifest.json +0 -4
  195. package/.next/standalone/.next/server/app/api/auth/reminder/route.js +0 -9
  196. package/.next/standalone/.next/server/app/api/auth/reminder/route.js.map +0 -5
  197. package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +0 -1
  198. package/.next/standalone/.next/server/app/api/auth/reminder/route_client-reference-manifest.js +0 -3
  199. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-xggru._.js +0 -3
  200. package/.next/standalone/.next/server/chunks/[root-of-the-server]__00ds05_._.js +0 -3
  201. package/.next/standalone/.next/server/chunks/[root-of-the-server]__094p1f1._.js +0 -3
  202. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1dxagv_._.js +0 -3
  203. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fgqj1_._.js +0 -3
  204. package/.next/standalone/.next/server/chunks/_084toou._.js +0 -3
  205. package/.next/standalone/.next/server/chunks/_0g4t8c9._.js +0 -3
  206. package/.next/standalone/.next/server/chunks/_0km521a._.js +0 -3
  207. package/.next/standalone/.next/server/chunks/_0t8kefc._.js +0 -3
  208. package/.next/standalone/.next/server/chunks/_16dwkfn._.js +0 -3
  209. package/.next/standalone/.next/server/chunks/_1phm9ui._.js +0 -3
  210. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_auth_reminder_route_actions_1kjgxf8.js +0 -3
  211. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0dk8r_l._.js +0 -3
  212. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kviq2m._.js +0 -3
  213. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ogyhu0._.js +0 -3
  214. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-rfu32._.js +0 -5
  215. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1mmefsr._.js +0 -5
  216. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1t62w37._.js +0 -3
  217. package/.next/standalone/.next/server/chunks/ssr/_032wo7w._.js +0 -3
  218. package/.next/standalone/.next/server/chunks/ssr/_08840em._.js +0 -3
  219. package/.next/standalone/.next/server/chunks/ssr/_0gl2ujp._.js +0 -3
  220. package/.next/standalone/.next/server/chunks/ssr/_0j0ggkc._.js +0 -3
  221. package/.next/standalone/.next/server/chunks/ssr/_0kbv5-q._.js +0 -3
  222. package/.next/standalone/.next/server/chunks/ssr/_0r1jgvp._.js +0 -3
  223. package/.next/standalone/.next/server/chunks/ssr/_0t5xq1b._.js +0 -3
  224. package/.next/standalone/.next/server/chunks/ssr/_0w_be0g._.js +0 -3
  225. package/.next/standalone/.next/server/chunks/ssr/_15gba46._.js +0 -5
  226. package/.next/standalone/.next/server/chunks/ssr/_1qyhb9r._.js +0 -23
  227. package/.next/standalone/.next/server/chunks/ssr/_1vel2ey._.js +0 -3
  228. package/.next/standalone/.next/static/chunks/0wd503tv801gy.js +0 -1
  229. package/.next/standalone/.next/static/chunks/1hfocb0sdl85c.js +0 -1
  230. package/.next/standalone/.next/static/chunks/1v1gqvpjk62jm.js +0 -1
  231. package/.next/standalone/.next/static/chunks/2kopjq_or743g.js +0 -6
  232. package/.next/standalone/.next/static/chunks/2uvhw57mf9lg_.css +0 -2
  233. package/.next/standalone/.next/static/chunks/2ynl0-6aqng_b.js +0 -1
  234. package/.next/standalone/.next/static/chunks/3xvtgz6arztp_.js +0 -1
  235. package/.next/standalone/app/api/auth/reminder/route.ts +0 -213
  236. /package/.next/standalone/.next/static/{Aulh3DRWwRfwh2POCvZR_ → jSKB7CQQSNTX-uSbY2bUs}/_buildManifest.js +0 -0
  237. /package/.next/standalone/.next/static/{Aulh3DRWwRfwh2POCvZR_ → jSKB7CQQSNTX-uSbY2bUs}/_clientMiddlewareManifest.js +0 -0
  238. /package/.next/standalone/.next/static/{Aulh3DRWwRfwh2POCvZR_ → jSKB7CQQSNTX-uSbY2bUs}/_ssgManifest.js +0 -0
@@ -0,0 +1,493 @@
1
+ /**
2
+ * A standing audit of the documentation, run weekly on the canary box
3
+ * (integration-suite/local/jobs/docs-audit.sh) and available by hand as
4
+ * `bun run docs:audit`.
5
+ *
6
+ * WHY THIS IS NOT `mintlify validate` OR `validate:mdx`. Those two answer "does
7
+ * this build" — a gate, run per PR, on the pages a PR touches. They pass
8
+ * happily on a corpus that builds perfectly and is quietly wrong: a page nobody
9
+ * has edited since the CLI it documents was rewritten, a page in the nav that
10
+ * no longer exists, a page that exists and is in no nav, a link to a page that
11
+ * was renamed, a translation still describing last quarter's behaviour. None of
12
+ * that fails a build, so nothing catches it — which is exactly the shape of
13
+ * problem a periodic sweep is for and a per-PR gate is not.
14
+ *
15
+ * So this is a REPORT, not a gate. It exits 0 with findings by design; the
16
+ * weekly Slack post is the product. `--fail-on-findings` is there for anyone
17
+ * who later wants it in CI, deliberately off by default: a docs audit that
18
+ * turns the build red on the day a page passes an age threshold would be
19
+ * turned off within a week, and then nobody would have either the gate or the
20
+ * report.
21
+ *
22
+ * Everything below `auditDocs()` is pure and takes its inputs as arguments —
23
+ * the git log, the file list, the cache — so the analysis is unit-testable
24
+ * without a repo, a docs tree, or a clock.
25
+ */
26
+ import { execFileSync } from "node:child_process";
27
+ import { existsSync, readFileSync } from "node:fs";
28
+ import { dirname, join, relative } from "node:path";
29
+ import { fileURLToPath } from "node:url";
30
+ import { getLanguageCodes } from "./translate-docs/config";
31
+ import { getEnglishMdxPages } from "./translate-docs/mdx-translator";
32
+ import {
33
+ getNavigationPageReferences,
34
+ readDocsConfig,
35
+ } from "./translate-docs/mintlify-nav";
36
+ import { contentHash, getCacheKey, readCache } from "./translate-docs/cache";
37
+ import { findBrokenAssetRefs } from "./validate-mdx";
38
+ import type { TranslationCache } from "./translate-docs/types";
39
+
40
+ const __dirname = dirname(fileURLToPath(import.meta.url));
41
+ const ROOT_DIR = join(__dirname, "..");
42
+ const DOCS_DIR = join(ROOT_DIR, "docs");
43
+
44
+ /** A page's age, measured from its last CONTENT commit. */
45
+ export interface PageAge {
46
+ relPath: string;
47
+ lastChanged: string; // ISO 8601
48
+ ageDays: number;
49
+ }
50
+
51
+ export interface BrokenLink {
52
+ relPath: string;
53
+ line: number;
54
+ target: string;
55
+ }
56
+
57
+ export interface TranslationDrift {
58
+ lang: string;
59
+ stale: string[]; // English source changed since this was translated
60
+ missing: string[]; // cache says translated, file is not on disk
61
+ untranslated: string[]; // never translated at all
62
+ }
63
+
64
+ export interface DocsAuditReport {
65
+ pages: number;
66
+ maxAgeDays: number;
67
+ aged: PageAge[];
68
+ navOrphans: string[]; // on disk, in no nav
69
+ navDangling: string[]; // in the nav, not on disk
70
+ brokenLinks: BrokenLink[];
71
+ brokenAssets: BrokenLink[];
72
+ drift: TranslationDrift[];
73
+ }
74
+
75
+ /**
76
+ * Pages whose last content change is older than the threshold, oldest first.
77
+ *
78
+ * "Content change" is the last commit that touched the file, which is a proxy
79
+ * and a deliberately generous one: a typo fix resets the clock. The alternative
80
+ * — trying to tell a substantive edit from a cosmetic one — would need a
81
+ * judgement this cannot make, and being generous errs toward silence rather
82
+ * than toward a weekly report of things that are fine.
83
+ */
84
+ export function findAgedPages(
85
+ ages: PageAge[],
86
+ maxAgeDays: number,
87
+ ): PageAge[] {
88
+ return ages
89
+ .filter((a) => a.ageDays > maxAgeDays)
90
+ .sort((a, b) => b.ageDays - a.ageDays);
91
+ }
92
+
93
+ /**
94
+ * The two directions a page and the navigation can disagree.
95
+ *
96
+ * `mintlify validate` catches DANGLING (a nav entry with no file) because that
97
+ * breaks the build. It cannot catch an ORPHAN — a page on disk that no nav
98
+ * references — because nothing is broken: the file simply cannot be reached by
99
+ * a reader, which is indistinguishable from deliberate until someone looks.
100
+ */
101
+ export function findNavMismatches(
102
+ navRefs: string[],
103
+ pageRelPaths: string[],
104
+ ): { orphans: string[]; dangling: string[] } {
105
+ const nav = new Set(navRefs.map(normalizeRef));
106
+ const pages = new Set(pageRelPaths.map(normalizeRef));
107
+ return {
108
+ orphans: [...pages].filter((p) => !nav.has(p)).sort(),
109
+ dangling: [...nav].filter((n) => !pages.has(n)).sort(),
110
+ };
111
+ }
112
+
113
+ /** `cli/audit.mdx`, `/cli/audit`, `cli/audit` all name the same page. */
114
+ export function normalizeRef(ref: string): string {
115
+ return ref.replace(/^\//, "").replace(/\.mdx?$/, "");
116
+ }
117
+
118
+ /**
119
+ * In-body links to pages that are not there.
120
+ *
121
+ * Only ROOT-RELATIVE links (`/cli/audit`) are checked, because that is the form
122
+ * Mintlify resolves and the only one whose target this can determine without
123
+ * guessing. External URLs, anchors and relative paths are skipped rather than
124
+ * guessed at — a false finding in a weekly report costs more than a missed one,
125
+ * since the first one nobody can reproduce is the one that gets the whole
126
+ * report ignored.
127
+ */
128
+ export function findBrokenInternalLinks(
129
+ relPath: string,
130
+ source: string,
131
+ knownPages: Set<string>,
132
+ ): BrokenLink[] {
133
+ const out: BrokenLink[] = [];
134
+ const seen = new Set<string>();
135
+ const lines = source.split("\n");
136
+ for (let i = 0; i < lines.length; i++) {
137
+ const targets = [
138
+ ...lines[i].matchAll(/\]\((\/[^)\s#?]*)/g),
139
+ ...lines[i].matchAll(/href=["'](\/[^"'#?]*)["']/g),
140
+ ].map((m) => m[1]);
141
+ for (const target of targets) {
142
+ if (!target || target === "/") continue;
143
+ // An asset reference, not a page link — findBrokenAssetRefs owns those.
144
+ if (/\.[a-z0-9]{2,5}$/i.test(target) && !/\.mdx?$/i.test(target)) continue;
145
+ const norm = normalizeRef(target);
146
+ if (knownPages.has(norm)) continue;
147
+ const key = `${i}:${target}`;
148
+ if (seen.has(key)) continue;
149
+ seen.add(key);
150
+ out.push({ relPath, line: i + 1, target });
151
+ }
152
+ }
153
+ return out;
154
+ }
155
+
156
+ /**
157
+ * Where each language stands against the current English source.
158
+ *
159
+ * Three distinct states, kept apart because they need different actions:
160
+ * STALE means the English changed and the translation is now describing older
161
+ * behaviour (the nightly run fixes it by itself); MISSING means the cache
162
+ * claims a translation that is not on disk — the non-convergent case the
163
+ * `existsSync` guard exists for, and worth seeing if it ever recurs;
164
+ * UNTRANSLATED means the page has never been translated into that language at
165
+ * all, which the nightly run also fixes but which shows up here as coverage.
166
+ */
167
+ export function findTranslationDrift(
168
+ cache: TranslationCache,
169
+ pages: { relPath: string; hash: string }[],
170
+ langs: string[],
171
+ outputExists: (lang: string, relPath: string) => boolean,
172
+ ): TranslationDrift[] {
173
+ return langs.map((lang) => {
174
+ const stale: string[] = [];
175
+ const missing: string[] = [];
176
+ const untranslated: string[] = [];
177
+ for (const { relPath, hash } of pages) {
178
+ const entry = cache.translations[getCacheKey(relPath, lang)];
179
+ if (!entry) {
180
+ untranslated.push(relPath);
181
+ } else if (entry.sourceHash !== hash) {
182
+ stale.push(relPath);
183
+ } else if (!outputExists(lang, relPath)) {
184
+ missing.push(relPath);
185
+ }
186
+ }
187
+ return { lang, stale, missing, untranslated };
188
+ });
189
+ }
190
+
191
+ /** Last commit date per file, as ISO strings. Absent from history → null. */
192
+ export function lastChangedISO(repoRoot: string, relFile: string): string | null {
193
+ try {
194
+ const out = execFileSync(
195
+ "git",
196
+ ["log", "-1", "--format=%cI", "--", relFile],
197
+ { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] },
198
+ ).trim();
199
+ return out || null;
200
+ } catch {
201
+ return null;
202
+ }
203
+ }
204
+
205
+ export function daysBetween(from: string, now: Date): number {
206
+ const then = new Date(from).getTime();
207
+ if (Number.isNaN(then)) return 0;
208
+ return Math.floor((now.getTime() - then) / 86_400_000);
209
+ }
210
+
211
+ export function auditDocs(opts: { maxAgeDays: number; now?: Date }): DocsAuditReport {
212
+ const now = opts.now ?? new Date();
213
+ const pageFiles = getEnglishMdxPages();
214
+ const relPaths = pageFiles.map((f) => relative(DOCS_DIR, f));
215
+ const knownPages = new Set(relPaths.map(normalizeRef));
216
+
217
+ const ages: PageAge[] = [];
218
+ for (const file of pageFiles) {
219
+ const iso = lastChangedISO(ROOT_DIR, relative(ROOT_DIR, file));
220
+ if (!iso) continue; // never committed — a new page in the working tree
221
+ ages.push({
222
+ relPath: relative(DOCS_DIR, file),
223
+ lastChanged: iso,
224
+ ageDays: daysBetween(iso, now),
225
+ });
226
+ }
227
+
228
+ const brokenLinks: BrokenLink[] = [];
229
+ const brokenAssets: BrokenLink[] = [];
230
+ const pages: { relPath: string; hash: string }[] = [];
231
+ for (const file of pageFiles) {
232
+ const rel = relative(DOCS_DIR, file);
233
+ const source = readFileSync(file, "utf8");
234
+ pages.push({ relPath: rel, hash: contentHash(source) });
235
+ brokenLinks.push(...findBrokenInternalLinks(rel, source, knownPages));
236
+ for (const ref of findBrokenAssetRefs(file, source)) {
237
+ brokenAssets.push({ relPath: rel, line: ref.line, target: ref.ref });
238
+ }
239
+ }
240
+
241
+ // English nav only. The localized trees are generated from it by
242
+ // `--update-nav`, so auditing them would report every English finding once
243
+ // per language and bury the one that matters.
244
+ const config = readDocsConfig();
245
+ const navRefs = getNavigationPageReferences(config.navigation)
246
+ .filter((r) => !r.language || r.language === "en")
247
+ .map((r) => r.page);
248
+ const { orphans, dangling } = findNavMismatches(navRefs, relPaths);
249
+
250
+ const langs = getLanguageCodes();
251
+ const drift = findTranslationDrift(readCache(), pages, langs, (lang, rel) =>
252
+ existsSync(join(DOCS_DIR, lang, rel)),
253
+ );
254
+
255
+ return {
256
+ pages: pageFiles.length,
257
+ maxAgeDays: opts.maxAgeDays,
258
+ aged: findAgedPages(ages, opts.maxAgeDays),
259
+ navOrphans: orphans,
260
+ navDangling: dangling,
261
+ brokenLinks,
262
+ brokenAssets,
263
+ drift,
264
+ };
265
+ }
266
+
267
+ /**
268
+ * The weekly Slack post.
269
+ *
270
+ * Ordered by what someone should act on, and CAPPED per section: a report that
271
+ * prints 300 stale translations is one nobody reads past, so each section shows
272
+ * a few and says how many more there are. A clean week says so in one line —
273
+ * the report has to be worth reading in the common case or it stops being read
274
+ * in the uncommon one.
275
+ */
276
+ export function formatSlackReport(
277
+ report: DocsAuditReport,
278
+ opts: { ref?: string; sha?: string } = {},
279
+ ): string {
280
+ const at = [opts.ref, opts.sha].filter(Boolean).join(" @ ");
281
+ const head = `📚 *Weekly docs audit* — ${report.pages} English pages${at ? ` (${at})` : ""}`;
282
+ const sections: string[] = [];
283
+ const cap = 5;
284
+ const more = (n: number) => (n > cap ? `\n …and ${n - cap} more` : "");
285
+
286
+ if (report.navDangling.length) {
287
+ sections.push(
288
+ `*In the nav, not on disk* (${report.navDangling.length})\n` +
289
+ report.navDangling.slice(0, cap).map((p) => ` • ${p}`).join("\n") +
290
+ more(report.navDangling.length),
291
+ );
292
+ }
293
+ if (report.navOrphans.length) {
294
+ sections.push(
295
+ `*On disk, in no nav* (${report.navOrphans.length}) — unreachable by a reader\n` +
296
+ report.navOrphans.slice(0, cap).map((p) => ` • ${p}`).join("\n") +
297
+ more(report.navOrphans.length),
298
+ );
299
+ }
300
+ if (report.brokenLinks.length) {
301
+ sections.push(
302
+ `*Links to pages that do not exist* (${report.brokenLinks.length})\n` +
303
+ report.brokenLinks
304
+ .slice(0, cap)
305
+ .map((l) => ` • ${l.relPath}:${l.line} → ${l.target}`)
306
+ .join("\n") +
307
+ more(report.brokenLinks.length),
308
+ );
309
+ }
310
+ if (report.brokenAssets.length) {
311
+ sections.push(
312
+ `*Images that do not resolve* (${report.brokenAssets.length})\n` +
313
+ report.brokenAssets
314
+ .slice(0, cap)
315
+ .map((l) => ` • ${l.relPath}:${l.line} → ${l.target}`)
316
+ .join("\n") +
317
+ more(report.brokenAssets.length),
318
+ );
319
+ }
320
+ if (report.aged.length) {
321
+ sections.push(
322
+ `*Not touched in over ${report.maxAgeDays} days* (${report.aged.length})\n` +
323
+ report.aged
324
+ .slice(0, cap)
325
+ .map((a) => ` • ${a.relPath} — ${a.ageDays} days`)
326
+ .join("\n") +
327
+ more(report.aged.length),
328
+ );
329
+ }
330
+
331
+ const staleTotal = report.drift.reduce((n, d) => n + d.stale.length, 0);
332
+ const missingTotal = report.drift.reduce((n, d) => n + d.missing.length, 0);
333
+ const untranslatedTotal = report.drift.reduce((n, d) => n + d.untranslated.length, 0);
334
+ if (staleTotal || missingTotal || untranslatedTotal) {
335
+ const parts = [
336
+ staleTotal ? `${staleTotal} stale` : "",
337
+ untranslatedTotal ? `${untranslatedTotal} never translated` : "",
338
+ // Worth its own mention: this is the state the existsSync guard exists
339
+ // for, so a non-zero count means either that guard regressed or the
340
+ // nightly job has not run since the pages landed.
341
+ missingTotal ? `*${missingTotal} claimed by the cache but absent from disk*` : "",
342
+ ].filter(Boolean);
343
+ sections.push(
344
+ `*Translations* across ${report.drift.length} languages — ${parts.join(", ")}\n` +
345
+ ` (the nightly translation job closes stale and never-translated by itself)`,
346
+ );
347
+ }
348
+
349
+ if (!sections.length) return `${head}\n\n✅ nothing to report.`;
350
+ return `${head}\n\n${sections.join("\n\n")}`;
351
+ }
352
+
353
+ /**
354
+ * Does this report warrant a tracking issue staying open?
355
+ *
356
+ * The five structural findings do. So does a translation the cache claims but
357
+ * disk lacks — that is the non-convergent state the `existsSync` guard exists
358
+ * for, and a non-zero count means either that guard regressed or the nightly
359
+ * job has not run since those pages landed.
360
+ *
361
+ * STALE and NEVER-TRANSLATED deliberately do NOT count. The nightly translation
362
+ * closes both by itself, so counting them would hold the issue open forever and
363
+ * teach everyone to ignore it — which is the only way a tracking issue can
364
+ * actually fail.
365
+ */
366
+ export function countActionable(report: DocsAuditReport): number {
367
+ return (
368
+ report.aged.length +
369
+ report.navOrphans.length +
370
+ report.navDangling.length +
371
+ report.brokenLinks.length +
372
+ report.brokenAssets.length +
373
+ report.drift.reduce((n, d) => n + d.missing.length, 0)
374
+ );
375
+ }
376
+
377
+ /** The tracking issue's body. Markdown, unlike the Slack post's mrkdwn. */
378
+ export function formatMarkdownReport(
379
+ report: DocsAuditReport,
380
+ opts: { ref?: string; sha?: string; at?: string } = {},
381
+ ): string {
382
+ const lines: string[] = [];
383
+ lines.push(`_${report.pages} English pages audited`
384
+ + `${opts.ref ? ` at \`${opts.ref}\`` : ""}`
385
+ + `${opts.sha ? ` @ \`${opts.sha}\`` : ""}`
386
+ + `${opts.at ? ` — ${opts.at}` : ""}._`);
387
+ lines.push("");
388
+
389
+ const section = (title: string, items: string[], render: (s: string) => string) => {
390
+ if (!items.length) return;
391
+ lines.push(`### ${title} (${items.length})`, "");
392
+ for (const i of items.slice(0, 25)) lines.push(`- ${render(i)}`);
393
+ if (items.length > 25) lines.push(`- _…and ${items.length - 25} more_`);
394
+ lines.push("");
395
+ };
396
+
397
+ section("In the nav, not on disk", report.navDangling, (p) => `\`${p}\``);
398
+ section("On disk, in no nav — unreachable by a reader", report.navOrphans, (p) => `\`${p}\``);
399
+ if (report.brokenLinks.length) {
400
+ lines.push(`### Links to pages that do not exist (${report.brokenLinks.length})`, "");
401
+ for (const l of report.brokenLinks.slice(0, 25)) {
402
+ lines.push(`- \`${l.relPath}:${l.line}\` → \`${l.target}\``);
403
+ }
404
+ lines.push("");
405
+ }
406
+ if (report.brokenAssets.length) {
407
+ lines.push(`### Images that do not resolve (${report.brokenAssets.length})`, "");
408
+ for (const l of report.brokenAssets.slice(0, 25)) {
409
+ lines.push(`- \`${l.relPath}:${l.line}\` → \`${l.target}\``);
410
+ }
411
+ lines.push("");
412
+ }
413
+ if (report.aged.length) {
414
+ lines.push(`### Not touched in over ${report.maxAgeDays} days (${report.aged.length})`, "");
415
+ for (const a of report.aged.slice(0, 25)) {
416
+ lines.push(`- \`${a.relPath}\` — ${a.ageDays} days (last changed ${a.lastChanged.slice(0, 10)})`);
417
+ }
418
+ lines.push("");
419
+ }
420
+
421
+ const missing = report.drift.flatMap((d) => d.missing.map((p) => `${d.lang}/${p}`));
422
+ if (missing.length) {
423
+ lines.push(`### Claimed by the translation cache but absent from disk (${missing.length})`, "");
424
+ lines.push(
425
+ "_Non-convergent if it persists: the cache says these are done, so they are never regenerated._",
426
+ "",
427
+ );
428
+ for (const p of missing.slice(0, 25)) lines.push(`- \`${p}\``);
429
+ lines.push("");
430
+ }
431
+
432
+ const stale = report.drift.reduce((n, d) => n + d.stale.length, 0);
433
+ const untranslated = report.drift.reduce((n, d) => n + d.untranslated.length, 0);
434
+ if (stale || untranslated) {
435
+ lines.push(
436
+ `<sub>Translations across ${report.drift.length} languages: ${stale} stale, `
437
+ + `${untranslated} never translated. Not listed above — the nightly translation `
438
+ + `closes both by itself.</sub>`,
439
+ "",
440
+ );
441
+ }
442
+
443
+ if (!countActionable(report)) {
444
+ lines.push("Nothing to report. :tada:", "");
445
+ }
446
+ lines.push("<sub>Opened and maintained by the weekly `docs-audit` job on the canary box.</sub>");
447
+ return lines.join("\n");
448
+ }
449
+
450
+ async function main(): Promise<void> {
451
+ const args = process.argv.slice(2);
452
+ const maxAgeDays = Number.parseInt(
453
+ args.find((a) => a.startsWith("--max-age="))?.split("=")[1] ??
454
+ process.env.DOCS_AUDIT_MAX_AGE_DAYS ??
455
+ "180",
456
+ 10,
457
+ );
458
+ const report = auditDocs({
459
+ maxAgeDays: Number.isInteger(maxAgeDays) && maxAgeDays > 0 ? maxAgeDays : 180,
460
+ });
461
+
462
+ if (args.includes("--json")) {
463
+ console.log(JSON.stringify(report, null, 2));
464
+ } else if (args.includes("--markdown")) {
465
+ console.log(
466
+ formatMarkdownReport(report, {
467
+ ref: process.env.DOCS_AUDIT_REF,
468
+ sha: process.env.DOCS_AUDIT_SHA,
469
+ at: process.env.DOCS_AUDIT_AT,
470
+ }),
471
+ );
472
+ } else if (args.includes("--count")) {
473
+ // For the box job's clean/not-clean decision, so it never has to re-derive
474
+ // "actionable" from prose and drift from what the report itself counts.
475
+ console.log(String(countActionable(report)));
476
+ } else {
477
+ console.log(
478
+ formatSlackReport(report, {
479
+ ref: process.env.DOCS_AUDIT_REF,
480
+ sha: process.env.DOCS_AUDIT_SHA,
481
+ }),
482
+ );
483
+ }
484
+
485
+ const findings = countActionable(report);
486
+ // Exit 0 with findings BY DESIGN — see the header. The flag is for a future
487
+ // caller that wants a gate, never for the weekly report.
488
+ process.exit(args.includes("--fail-on-findings") && findings > 0 ? 1 : 0);
489
+ }
490
+
491
+ if (import.meta.main) {
492
+ await main();
493
+ }
@@ -268,7 +268,18 @@ async function main() {
268
268
  if (
269
269
  !isForce &&
270
270
  !isDryRun &&
271
- isCached(cache, relPath, lang, pageContents.get(page)!)
271
+ isCached(cache, relPath, lang, pageContents.get(page)!) &&
272
+ // The cache records that a translation was PRODUCED, never that it
273
+ // EXISTS. Output lands on an unmerged auto-translate PR branch, so
274
+ // until that merges the checked-out tree lacks the file while the
275
+ // cache still says "done" — the page is never regenerated, and
276
+ // `--update-nav` (which reads the ENGLISH tree) emits a nav entry
277
+ // pointing at a file that is not there, so `mintlify validate` fails.
278
+ // That is non-convergent: a cache hit fails validation, and only a
279
+ // full cache MISS — 120 runner-minutes — produces a green run.
280
+ // Statting the output makes the cache self-healing against any
281
+ // "translated once, never landed" gap, whatever opened it.
282
+ existsSync(join(DOCS_DIR, lang, relPath))
272
283
  ) {
273
284
  cachedTasks.push(task);
274
285
  } else {
@@ -345,7 +356,10 @@ async function main() {
345
356
  if (
346
357
  !isForce &&
347
358
  !isDryRun &&
348
- isCached(cache, "README.md", lang, readmeSource)
359
+ isCached(cache, "README.md", lang, readmeSource) &&
360
+ // Same reason as the MDX branch above: cached means translated once,
361
+ // not present now.
362
+ existsSync(join(DOCS_DIR, "i18n", `README.${lang}.md`))
349
363
  ) {
350
364
  console.log(` README.${lang}.md -> cached`);
351
365
  results.push({
@@ -209,7 +209,12 @@ export async function translateMdxPage(
209
209
  // Check cache — use provided cache object or read from disk
210
210
  if (!options.force && !options.dryRun) {
211
211
  const cache = options.cache ?? readCache();
212
- if (isCached(cache, relPath, lang, sourceContent)) {
212
+ // `&& existsSync(outputPath)` for the same reason as the batch path in
213
+ // cli.ts: a cache entry says a translation was produced once, not that the
214
+ // file is on disk now. This branch is the single-page path — the batch run
215
+ // never reaches it for a cached page — so it is guarded separately or the
216
+ // two disagree about what "cached" means.
217
+ if (isCached(cache, relPath, lang, sourceContent) && existsSync(outputPath)) {
213
218
  return {
214
219
  lang,
215
220
  sourcePath,
@@ -1,4 +1,4 @@
1
- import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { LANGUAGES, getLanguageByCode } from "./config";
@@ -220,7 +220,9 @@ export async function translateReadme(
220
220
  // Check cache — use provided cache object or read from disk
221
221
  if (!options.force && !options.dryRun) {
222
222
  const cache = options.cache ?? readCache();
223
- if (isCached(cache, "README.md", lang, sourceContent)) {
223
+ // `&& existsSync(outputPath)` — see the MDX path. Cached records that a
224
+ // translation was produced, not that the file is there now.
225
+ if (isCached(cache, "README.md", lang, sourceContent) && existsSync(outputPath)) {
224
226
  return {
225
227
  lang,
226
228
  sourcePath: README_PATH,