failproofai 0.0.14-beta.0 → 0.0.14-beta.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 (219) hide show
  1. package/.next/standalone/.codex/hooks.json +6 -6
  2. package/.next/standalone/.cursor/hooks.json +6 -6
  3. package/.next/standalone/.factory/hooks.json +9 -9
  4. package/.next/standalone/.next/BUILD_ID +1 -1
  5. package/.next/standalone/.next/build-manifest.json +5 -5
  6. package/.next/standalone/.next/prerender-manifest.json +3 -3
  7. package/.next/standalone/.next/required-server-files.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
  9. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  10. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  11. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  13. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  14. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  15. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  16. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  17. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  18. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
  20. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  21. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  23. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  24. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  25. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  26. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  27. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
  28. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  29. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  30. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  31. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  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.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
  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.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  43. package/.next/standalone/.next/server/app/index.html +1 -1
  44. package/.next/standalone/.next/server/app/index.rsc +15 -15
  45. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  46. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  47. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  48. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
  49. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  50. package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
  51. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  52. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
  55. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  56. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  58. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
  59. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  60. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  62. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
  63. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  64. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  65. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  67. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
  68. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  69. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  71. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0--lkk6._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-219ec._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/[root-of-the-server]__04_h-a5._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06cuf1y._.js +1 -1
  75. package/.next/standalone/.next/server/chunks/[root-of-the-server]__08w4wmd._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g6bbuw._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jnjf2t._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q-v9z2._.js +1 -1
  79. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q0qzx1._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q2nbsl._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rv7m0k._.js +1 -1
  82. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xuaoik._.js +3 -3
  83. package/.next/standalone/.next/server/chunks/[root-of-the-server]__16le-kd._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ffkmds._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1legmza._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mrihkj._.js +1 -1
  89. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qb590j._.js +1 -1
  91. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qxztj-._.js +1 -1
  92. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1shcqgr._.js +1 -1
  93. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1xuedea._.js +1 -1
  94. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +4 -4
  95. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +1 -1
  96. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +1 -1
  97. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  98. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00cz9as._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__015_i4t._.js +2 -2
  100. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__030k0c6._.js +1 -1
  101. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03lv-pe._.js +2 -2
  102. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__042d3cd._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__05r_17v._.js +1 -1
  104. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09q1168._.js +1 -1
  105. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__07qfsfa._.js → [root-of-the-server]__09v9cwb._.js} +2 -2
  106. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e2l3c1._.js +1 -1
  107. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h12me5._.js +1 -1
  108. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hci7t3._.js +1 -1
  109. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hdsupo._.js +2 -2
  110. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p84xee._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qeuy9c._.js +2 -2
  112. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s89hoe._.js +2 -2
  113. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0twi4fj._.js +3 -0
  114. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ynf7tx._.js +2 -2
  115. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11rtg6s._.js +1 -1
  116. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__14dd6h8._.js +1 -1
  117. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__170799-._.js +1 -1
  118. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17ai7sy._.js +2 -2
  119. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dky4g0._.js +1 -1
  120. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1nq7ivq._.js +1 -1
  121. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tkjqka._.js +2 -2
  122. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__212nf49._.js +1 -1
  123. package/.next/standalone/.next/server/chunks/ssr/_05whahf._.js +1 -1
  124. package/.next/standalone/.next/server/chunks/ssr/_1kje4fm._.js +1 -1
  125. package/.next/standalone/.next/server/chunks/ssr/app_087bt9w._.js +1 -1
  126. package/.next/standalone/.next/server/chunks/ssr/app_1fvisnp._.js +1 -1
  127. package/.next/standalone/.next/server/chunks/ssr/app_209u41o._.js +1 -1
  128. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +2 -2
  129. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  130. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  131. package/.next/standalone/.next/server/chunks/ssr/lib_0xkhw_v._.js +1 -1
  132. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_0g5r64e.js → node_modules_html-to-image_es_index_06xsmte.js} +1 -1
  133. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0-uvagv.js +1 -1
  134. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_03c7gi5.js +1 -1
  135. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_09p-8om.js +1 -1
  136. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0v-kfiu.js +1 -1
  137. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0xrgzyz.js +1 -1
  138. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_1806lsc.js +1 -1
  139. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_1j6dd-e.js +1 -1
  140. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_1sa65r-.js +1 -1
  141. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +1 -1
  142. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1ezd2jf._.js +1 -1
  143. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1tnuifj._.js +1 -1
  144. package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
  145. package/.next/standalone/.next/server/pages/404.html +1 -1
  146. package/.next/standalone/.next/server/pages/500.html +1 -1
  147. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  148. package/.next/standalone/.next/server/server-reference-manifest.json +10 -10
  149. package/.next/standalone/.next/static/chunks/{13f1kmjea-0md.js → 02jcotghcxrbu.js} +1 -1
  150. package/.next/standalone/.next/static/chunks/{1374x9bv_s_vg.js → 0h-6ymo9v86b0.js} +2 -2
  151. package/.next/standalone/.next/static/chunks/0pndjhcxfu-0t.js +1 -0
  152. package/.next/standalone/.next/static/chunks/1-33f82_l32or.js +1 -0
  153. package/.next/standalone/.next/static/chunks/{003uz088wbmz7.js → 1bg_g3g359c_d.js} +1 -1
  154. package/.next/standalone/.next/static/chunks/{454dyizqik6t9.js → 1gg-8e9q_m__9.js} +2 -2
  155. package/.next/standalone/.next/static/chunks/{2ufuwhv03yvtb.js → 29zkom4s9de2d.js} +1 -1
  156. package/.next/standalone/.next/static/chunks/{17sdztgbcc0fi.js → 2llkdafnaejw_.js} +1 -1
  157. package/.next/standalone/.next/static/chunks/{0iinhn4417nk1.js → 3-ogr79xqbw0h.js} +1 -1
  158. package/.next/standalone/.next/static/chunks/36j05kb2x323i.css +2 -0
  159. package/.next/standalone/.next/static/chunks/{43weu8f59tlix.js → 388hgfqye67xa.js} +1 -1
  160. package/.next/standalone/.next/static/chunks/396764jgd7-9j.js +1 -0
  161. package/.next/standalone/.next/static/chunks/3w7g0n0iwwnbs.js +1 -0
  162. package/.next/standalone/.next/static/chunks/{turbopack-0vavqo2d-tlt3.js → turbopack-10hj8t5pf33tu.js} +1 -1
  163. package/.next/standalone/.opencode/plugins/failproofai.mjs +30 -11
  164. package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +11 -2
  165. package/.next/standalone/app/components/copy-button.tsx +22 -6
  166. package/.next/standalone/app/components/global-error-listeners.tsx +27 -0
  167. package/.next/standalone/components/reach-developers.tsx +1 -1
  168. package/.next/standalone/docker-hook-sync/Dockerfile +8 -4
  169. package/.next/standalone/integration-suite/Dockerfile +47 -0
  170. package/.next/standalone/integration-suite/README.md +87 -0
  171. package/.next/standalone/integration-suite/canary-policies.mjs +45 -0
  172. package/.next/standalone/integration-suite/capture-tokens.sh +48 -0
  173. package/.next/standalone/integration-suite/ci-entrypoint.sh +164 -0
  174. package/.next/standalone/integration-suite/inject-tokens.sh +44 -0
  175. package/.next/standalone/integration-suite/install-clis.sh +91 -0
  176. package/.next/standalone/integration-suite/probe-cli.sh +208 -0
  177. package/.next/standalone/integration-suite/report.js +74 -0
  178. package/.next/standalone/integration-suite/run.sh +107 -0
  179. package/.next/standalone/lib/error-origin.ts +67 -0
  180. package/.next/standalone/lib/format-duration.ts +23 -9
  181. package/.next/standalone/lib/install-check.ts +231 -0
  182. package/.next/standalone/package.json +1 -3
  183. package/.next/standalone/server.js +1 -1
  184. package/README.md +1 -17
  185. package/bin/failproofai.mjs +11 -1
  186. package/dist/cli.mjs +818 -320
  187. package/lib/error-origin.ts +67 -0
  188. package/lib/format-duration.ts +23 -9
  189. package/lib/install-check.ts +231 -0
  190. package/package.json +1 -3
  191. package/scripts/dev-hook.mjs +292 -0
  192. package/scripts/install-diagnosis.mjs +5 -8
  193. package/scripts/install-telemetry.mjs +12 -4
  194. package/scripts/translate-docs/cli.ts +53 -1
  195. package/scripts/translate-docs/mdx-translator.ts +158 -18
  196. package/scripts/translate-docs/readme-translator.ts +36 -18
  197. package/scripts/translate-docs/translator.ts +180 -9
  198. package/scripts/translate-docs/types.ts +6 -0
  199. package/scripts/translate-docs/validate-translation.ts +132 -0
  200. package/scripts/validate-mdx.ts +92 -15
  201. package/src/audit/cli.ts +53 -15
  202. package/src/auth/cli.ts +14 -5
  203. package/src/hooks/builtin-policies.ts +168 -50
  204. package/src/hooks/configure-wizard.ts +238 -22
  205. package/src/hooks/custom-hooks-loader.ts +70 -4
  206. package/src/hooks/handler.ts +4 -1
  207. package/src/hooks/policy-types.ts +8 -0
  208. package/src/hooks/tui.ts +96 -29
  209. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ge8e7w._.js +0 -3
  210. package/.next/standalone/.next/static/chunks/0405htbxay9vd.js +0 -1
  211. package/.next/standalone/.next/static/chunks/2m_tbl39fcedn.js +0 -1
  212. package/.next/standalone/.next/static/chunks/2o3ql0n5owqsw.js +0 -1
  213. package/.next/standalone/.next/static/chunks/35-90m9py64d4.js +0 -1
  214. package/.next/standalone/.next/static/chunks/3r--yyjkp9_7b.css +0 -2
  215. package/scripts/postinstall.mjs +0 -242
  216. package/scripts/preuninstall.mjs +0 -131
  217. /package/.next/standalone/.next/static/{MNJWFb2jLRrxRKGFNK6Hy → YKxI22hC-F_nHp7MEKfb1}/_buildManifest.js +0 -0
  218. /package/.next/standalone/.next/static/{MNJWFb2jLRrxRKGFNK6Hy → YKxI22hC-F_nHp7MEKfb1}/_clientMiddlewareManifest.js +0 -0
  219. /package/.next/standalone/.next/static/{MNJWFb2jLRrxRKGFNK6Hy → YKxI22hC-F_nHp7MEKfb1}/_ssgManifest.js +0 -0
@@ -43,4 +43,10 @@ export interface TranslationResult {
43
43
  inputTokens: number;
44
44
  outputTokens: number;
45
45
  cached: boolean;
46
+ /**
47
+ * Total translation attempts spent (1 = valid on first try). Optional so the
48
+ * cached / dry-run result literals need no change; absent means "not
49
+ * translated this run" (cached) or "1".
50
+ */
51
+ attempts?: number;
46
52
  }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Generation-time validation for a freshly translated page.
3
+ *
4
+ * `findTranslationError` runs the exact checks the Mintlify deploy performs —
5
+ * frontmatter YAML then MDX body — plus one the deploy cannot: frontmatter
6
+ * KEY-PARITY against the English source. It is called on the exact bytes about
7
+ * to be written to disk (post-sanitize, post-link-rewrite), so a page that
8
+ * passes here is a page `mintlify validate` and the deploy accept.
9
+ *
10
+ * Why a translation-specific wrapper instead of `findPageError` alone:
11
+ * - Frontmatter YAML (class A): the model re-emits an inner `"` unescaped into
12
+ * a double-quoted `title:`/`description:` value, breaking the YAML. This is
13
+ * the class that failed run 29575781632; `findFrontmatterError` catches it.
14
+ * - Key parity (class B): the model drops the frontmatter block entirely, or
15
+ * renames a key. A dropped block is still *valid YAML* (mintlify tolerates
16
+ * it, deriving the title from the slug), so only comparing against the
17
+ * source's keys catches it. This is deliberately stricter than mintlify —
18
+ * the keys are prompt-forbidden to change, so it fails ~never but converts
19
+ * a silent content regression into a retry.
20
+ * - MDX body (classes C+): stray `<slug>`, `{#anchor}`, unmatched fence,
21
+ * swallowed `</div>` — `findMdxParseError` catches these.
22
+ *
23
+ * The message returned is written for a MODEL to act on: it names the failing
24
+ * construct and, for a body error, quotes the offending line, so the retry has
25
+ * a concrete defect to fix rather than a bare line number the RTL/README
26
+ * wrappers would have offset anyway.
27
+ */
28
+ import { findFrontmatterError, findMdxParseError } from "../validate-mdx";
29
+ import YAML from "yaml";
30
+
31
+ // Same matcher as validate-mdx.ts; duplicated locally so this module owns its
32
+ // frontmatter extraction and does not depend on an internal export.
33
+ const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n?/;
34
+
35
+ /**
36
+ * The sorted top-level keys of a page's frontmatter, or `null` when the page
37
+ * has no frontmatter block or its block is not a key/value mapping (an
38
+ * unparseable or scalar block). Callers distinguish "no block" from a YAML
39
+ * error separately via `findFrontmatterError`.
40
+ */
41
+ function frontmatterKeys(page: string): string[] | null {
42
+ const match = FRONTMATTER_RE.exec(page);
43
+ if (!match) return null;
44
+ try {
45
+ const doc = YAML.parse(match[1]);
46
+ if (doc && typeof doc === "object" && !Array.isArray(doc)) {
47
+ return Object.keys(doc).sort();
48
+ }
49
+ return null;
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * A ±2-line window around `line` (1-based), the failing line prefixed `> ` and
57
+ * its neighbours ` `. Empty string when `line` is undefined.
58
+ */
59
+ function excerpt(page: string, line?: number): string {
60
+ if (!line) return "";
61
+ const lines = page.split("\n");
62
+ const start = Math.max(1, line - 2);
63
+ const end = Math.min(lines.length, line + 2);
64
+ const out: string[] = [];
65
+ for (let n = start; n <= end; n++) {
66
+ out.push(`${n === line ? "> " : " "}${lines[n - 1]}`);
67
+ }
68
+ return out.join("\n");
69
+ }
70
+
71
+ const backticked = (keys: string[]): string =>
72
+ keys.map((k) => `\`${k}\``).join(", ");
73
+
74
+ /**
75
+ * Validate a rendered translation against its English `source`. Returns a
76
+ * model-actionable error message, or `null` when the page is publishable.
77
+ */
78
+ export async function findTranslationError(
79
+ rendered: string,
80
+ source: string,
81
+ ): Promise<string | null> {
82
+ // Validate the rendered frontmatter block FIRST, for every source shape —
83
+ // even when the source had none. findMdxParseError (below) blanks a leading
84
+ // `---` block before compiling, so a malformed block the model *added* to a
85
+ // frontmatter-less page would otherwise be invisible here yet still break the
86
+ // Mintlify deploy. Closing exactly that blind spot is this module's job, so
87
+ // it must hold regardless of whether the source has frontmatter.
88
+ const fm = findFrontmatterError(rendered);
89
+ if (fm) {
90
+ return (
91
+ "The YAML frontmatter (the `---` block at the top of the file) does " +
92
+ `not parse:\n\n${fm.message}`
93
+ );
94
+ }
95
+
96
+ const sourceKeys = frontmatterKeys(source);
97
+ if (sourceKeys) {
98
+ // The source has frontmatter, so the translation must carry the same keys.
99
+ // (Its block, if present, already parsed cleanly above.)
100
+ const keys = frontmatterKeys(rendered);
101
+ if (keys === null) {
102
+ return (
103
+ "The YAML frontmatter is missing. The English source starts with a " +
104
+ `\`---\` block containing ${backticked(sourceKeys)}; the translation ` +
105
+ "must start with the same block — translate the values, keep the keys."
106
+ );
107
+ }
108
+ // Subset check BY DESIGN: a missing or renamed key is a content regression
109
+ // (the page loses its title/description), so reject it. An *extra* key is
110
+ // harmless — Mintlify ignores unknown frontmatter keys — so tolerate it
111
+ // rather than burn a retry (and risk exhausting the whole batch) on a page
112
+ // that would deploy fine. Reject content loss; allow harmless additions.
113
+ const missing = sourceKeys.filter((k) => !keys.includes(k));
114
+ if (missing.length > 0) {
115
+ return (
116
+ `The YAML frontmatter keys changed. Expected ${backticked(sourceKeys)} ` +
117
+ `but got ${backticked(keys)}. Translate the values, never rename or ` +
118
+ "drop a key."
119
+ );
120
+ }
121
+ }
122
+
123
+ const body = await findMdxParseError(rendered);
124
+ if (body) {
125
+ const snippet = excerpt(rendered, body.line);
126
+ return `The MDX body does not parse: ${body.message}${
127
+ snippet ? `\n\n${snippet}` : ""
128
+ }`;
129
+ }
130
+
131
+ return null;
132
+ }
@@ -1,13 +1,13 @@
1
1
  /**
2
- * Validate that every docs MDX page parses with the same MDX engine Mintlify
3
- * runs at deploy time.
2
+ * Validate that every docs page (`.mdx` and `.md` Mintlify parses both as
3
+ * MDX) parses with the same MDX engine Mintlify runs at deploy time.
4
4
  *
5
- * Why this exists: `mintlify validate` (the existing `docs` CI job) only checks
6
- * `docs.json` structure and nav-link resolution it does NOT parse page
7
- * content. A page that is structurally referenced but contains an MDX syntax
8
- * error (e.g. a `<slug>` that escaped its surrounding backticks because a
9
- * translation dropped a closing `` ` ``) passes `mintlify validate` but fails
10
- * the Mintlify deploy with:
5
+ * Why this exists: `mintlify validate` (the existing `docs` CI job) checks
6
+ * `docs.json` structure, nav-link resolution, and frontmatter YAML but it
7
+ * does NOT report MDX *body* syntax errors. A page whose frontmatter is valid
8
+ * but whose body contains an MDX syntax error (e.g. a `<slug>` that escaped its
9
+ * surrounding backticks because a translation dropped a closing `` ` ``) passes
10
+ * `mintlify validate` but fails the Mintlify deploy with:
11
11
  *
12
12
  * Failed to parse page content at path tr/cli/audit.mdx:
13
13
  * Expected a closing tag for `<slug>` (61:127-61:133) before the end of `paragraph`
@@ -21,12 +21,16 @@
21
21
  *
22
22
  * The error string above is emitted by `@mdx-js/mdx`'s micromark MDX layer,
23
23
  * which is the same engine Mintlify uses, so compiling here reproduces the
24
- * deploy-time parse faithfully.
24
+ * deploy-time parse faithfully. `main()` validates the frontmatter too (via
25
+ * `findPageError`), so this net is a strict superset of `mintlify validate`:
26
+ * it catches both the frontmatter YAML class that fails `mintlify validate`
27
+ * and the body-MDX class that `mintlify validate` lets through to deploy.
25
28
  */
26
29
  import { readdirSync, statSync, readFileSync } from "node:fs";
27
30
  import { dirname, join, relative } from "node:path";
28
31
  import { fileURLToPath } from "node:url";
29
32
  import { compile } from "@mdx-js/mdx";
33
+ import YAML from "yaml";
30
34
 
31
35
  const __dirname = dirname(fileURLToPath(import.meta.url));
32
36
  const DOCS_DIR = join(__dirname, "..", "docs");
@@ -37,21 +41,86 @@ export interface MdxParseError {
37
41
  column?: number;
38
42
  }
39
43
 
44
+ /**
45
+ * Shared frontmatter matcher. The capture group is the YAML block *body* (the
46
+ * text between the fences); `match[0]` is still the whole block including the
47
+ * fences and trailing newline, which is what `stripFrontmatter` blanks.
48
+ */
49
+ const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n?/;
50
+
40
51
  /**
41
52
  * Replace a leading YAML frontmatter block (`--- … ---`) with blank lines.
42
53
  *
43
54
  * Mintlify parses frontmatter as YAML, not MDX, so it never causes an MDX parse
44
- * error. We blank it rather than delete it so the remaining content keeps its
45
- * original line numbers error positions then match the real file.
55
+ * error but it CAN contain a YAML syntax error, which blanking here hides
56
+ * from `findMdxParseError`. That gap is covered separately by
57
+ * `findFrontmatterError`; `findPageError` runs both. We blank rather than delete
58
+ * so the remaining body keeps its original line numbers — body error positions
59
+ * then match the real file.
46
60
  */
47
61
  export function stripFrontmatter(source: string): string {
48
- const match = /^---\r?\n[\s\S]*?\r?\n---[ \t]*\r?\n?/.exec(source);
62
+ const match = FRONTMATTER_RE.exec(source);
49
63
  if (!match) return source;
50
64
  // Keep newlines, drop every other character, so line numbers stay aligned.
51
65
  const blanked = match[0].replace(/[^\n]/g, "");
52
66
  return blanked + source.slice(match[0].length);
53
67
  }
54
68
 
69
+ /**
70
+ * Parse the leading YAML frontmatter block and return its parse error, or
71
+ * `null` when the block is absent (legal — some pages and every i18n README
72
+ * have none) or valid.
73
+ *
74
+ * This is the half of page validation that `findMdxParseError` structurally
75
+ * cannot do: it compiles only the *body* (frontmatter blanked), so a YAML
76
+ * syntax error in `title:`/`description:` — the exact class that failed the
77
+ * `consolidate` job's `mintlify validate` step, and that this repo's own
78
+ * `validate:mdx` net could not see — sailed straight through. `mintlify`
79
+ * parses the frontmatter as YAML, so parsing it here reproduces that check.
80
+ *
81
+ * The reported `line` is FILE-relative (the opening `---` is file line 1), so
82
+ * it matches the convention `findMdxParseError` already uses for body errors.
83
+ * That is deliberately one greater than the block-relative number `mintlify`
84
+ * prints — do NOT "fix" it to match mintlify; file-relative is what points a
85
+ * reader (or a model asked to repair the page) at the right line.
86
+ */
87
+ export function findFrontmatterError(source: string): MdxParseError | null {
88
+ const match = FRONTMATTER_RE.exec(source);
89
+ if (!match) return null;
90
+ try {
91
+ YAML.parse(match[1]);
92
+ return null;
93
+ } catch (err) {
94
+ const e = err as {
95
+ message?: string;
96
+ linePos?: Array<{ line: number; col: number }>;
97
+ };
98
+ const raw = e.message ?? String(err);
99
+ return {
100
+ // Drop yaml's block-relative "at line N, column N:" phrase (it would
101
+ // contradict the file-relative line we report) but keep the caret-
102
+ // underlined excerpt after it — the single most useful signal for a
103
+ // model asked to repair the offending line.
104
+ message: raw.replace(/ at line \d+, column \d+:/, ":"),
105
+ line: e.linePos?.[0] ? e.linePos[0].line + 1 : undefined,
106
+ column: e.linePos?.[0]?.col,
107
+ };
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Validate a full page the way the Mintlify deploy does: frontmatter YAML
113
+ * first, then the MDX body. Returns the first error found, or `null`. This is
114
+ * the single entry point shared by `validate:mdx` (`main()` below) and the
115
+ * translation pipeline's generation-time gate, so the two can never disagree
116
+ * about what is publishable.
117
+ */
118
+ export async function findPageError(
119
+ source: string,
120
+ ): Promise<MdxParseError | null> {
121
+ return findFrontmatterError(source) ?? (await findMdxParseError(source));
122
+ }
123
+
55
124
  /**
56
125
  * Compile one MDX source string with the deploy-time parser. Returns `null`
57
126
  * when it parses cleanly, or the parse error (with position) otherwise.
@@ -91,12 +160,20 @@ export function encodeAnnotation(value: string): string {
91
160
  .replace(/\n/g, "%0A");
92
161
  }
93
162
 
94
- function collectMdxFiles(dir: string): string[] {
163
+ /**
164
+ * Collect every Mintlify content page under `dir`. Mintlify runs BOTH `.mdx`
165
+ * and `.md` files through the same MDX pipeline at deploy time, so a `.md` page
166
+ * with an MDX syntax error (e.g. an HTML `<!-- -->` comment, which MDX rejects)
167
+ * fails the deploy exactly like a broken `.mdx` would. The docs/i18n README
168
+ * translations are `.md`, so restricting this walk to `.mdx` let their breakage
169
+ * sail past this safety net and reach the post-merge deploy — collect both.
170
+ */
171
+ export function collectMdxFiles(dir: string): string[] {
95
172
  const out: string[] = [];
96
173
  for (const entry of readdirSync(dir)) {
97
174
  const full = join(dir, entry);
98
175
  if (statSync(full).isDirectory()) out.push(...collectMdxFiles(full));
99
- else if (entry.endsWith(".mdx")) out.push(full);
176
+ else if (entry.endsWith(".mdx") || entry.endsWith(".md")) out.push(full);
100
177
  }
101
178
  return out;
102
179
  }
@@ -106,7 +183,7 @@ async function main(): Promise<void> {
106
183
  const failures: Array<{ file: string; error: MdxParseError }> = [];
107
184
 
108
185
  for (const file of files) {
109
- const error = await findMdxParseError(readFileSync(file, "utf-8"));
186
+ const error = await findPageError(readFileSync(file, "utf-8"));
110
187
  if (error) failures.push({ file: relative(process.cwd(), file), error });
111
188
  }
112
189
 
package/src/audit/cli.ts CHANGED
@@ -22,6 +22,7 @@ import { trackHookEvent } from "../hooks/hook-telemetry";
22
22
  import { getInstanceId } from "../../lib/telemetry-id";
23
23
  import { sanitizeErrorMessage } from "../../lib/telemetry-sanitize";
24
24
  import { openWhenReady } from "./open-browser";
25
+ import { brandAnsi, ANSI_RESET, ANSI_BOLD, ANSI_DIM } from "../hooks/tui";
25
26
 
26
27
  /** Port the bundled dashboard binds to. Matches `scripts/launch.ts`'s default
27
28
  * for `start` mode, which `failproofai` (bare) already uses. */
@@ -59,12 +60,17 @@ WHAT IT DOES
59
60
  `.trimStart();
60
61
 
61
62
  // ── ANSI helpers ────────────────────────────────────────────────────────────
62
- const RESET = "\x1b[0m";
63
- const DIM = "\x1b[2m";
64
- const BOLD = "\x1b[1m";
65
- const PINK = "\x1b[38;5;204m";
66
- const GREEN = "\x1b[38;5;120m";
67
- const CYAN = "\x1b[38;5;81m";
63
+ // Colours come from the shared brand palette in hooks/tui.ts, so `audit` reads
64
+ // as the same product as `config`. This file used to define its own 256-colour
65
+ // set (a green and a blue that appear nowhere in the brand). The palette has
66
+ // two accents rather than three, so success and command/URL text share the
67
+ // teal the glyph (✓) and the surrounding copy already distinguish them.
68
+ const RESET = ANSI_RESET;
69
+ const DIM = ANSI_DIM;
70
+ const BOLD = ANSI_BOLD;
71
+ const PINK = brandAnsi("pink");
72
+ const GREEN = brandAnsi("guide"); // success — brand teal
73
+ const CYAN = brandAnsi("guide"); // commands & URLs — brand teal
68
74
  const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
69
75
 
70
76
  function colorOn(): boolean {
@@ -237,6 +243,26 @@ function printSummary(result: AuditResult): void {
237
243
  for (const line of buildSummary(result)) process.stdout.write(` ${line}\n`);
238
244
  }
239
245
 
246
+ // ── Audit telemetry ──────────────────────────────────────────────────────────
247
+
248
+ /**
249
+ * Which entry point ran the audit. `onboarding` is the automatic post-setup run;
250
+ * `cli` is an explicit `failproofai audit`. Carried on every cli_audit_* event so
251
+ * the first audit a user ever runs is distinguishable from later deliberate ones.
252
+ */
253
+ type AuditSource = "cli" | "onboarding";
254
+
255
+ /** Shared so both entry points report cli_audit_completed identically. */
256
+ function auditCompletedProps(source: AuditSource, result: AuditResult) {
257
+ return {
258
+ source,
259
+ events_scanned: result.eventsScanned,
260
+ sessions_scanned: result.transcripts.scanned,
261
+ total_hits: result.totals.hits,
262
+ findings: result.results.length,
263
+ };
264
+ }
265
+
240
266
  // ── Post-setup background audit ────────────────────────────────────────────────
241
267
 
242
268
  /**
@@ -252,7 +278,13 @@ function printSummary(result: AuditResult): void {
252
278
  */
253
279
  export async function runPostSetupAudit(): Promise<void> {
254
280
  if (process.env.FAILPROOFAI_NO_AUTO_AUDIT === "1") return;
255
-
281
+
282
+ const instanceId = getInstanceId();
283
+ // Fire-and-forget, as in runAuditCli: the multi-second scan below keeps the
284
+ // process alive long enough for this to land, and the completed/failed event
285
+ // that follows is awaited.
286
+ void trackHookEvent(instanceId, "cli_audit_started", { source: "onboarding" });
287
+
256
288
  process.stdout.write(
257
289
  `\n ${c(PINK, "✦")} ${c(BOLD, "failproofai audit now running")} ${c(DIM, "· ctrl+c to stop")}\n\n`,
258
290
  );
@@ -260,13 +292,25 @@ export async function runPostSetupAudit(): Promise<void> {
260
292
  let result: AuditResult;
261
293
  try {
262
294
  result = await runWithProgress({});
263
- } catch {
295
+ } catch (err) {
296
+ // Awaited: this function returns straight into the dashboard boot, and a
297
+ // fire-and-forget fetch would race it.
298
+ await trackHookEvent(instanceId, "cli_audit_failed", {
299
+ source: "onboarding",
300
+ error_type: err instanceof Error ? err.name : "unknown",
301
+ error_message: sanitizeErrorMessage(err),
302
+ });
264
303
  process.stdout.write(
265
304
  ` ${c(PINK, "!")} ${c(DIM, "audit couldn't finish — run")} ${c(CYAN, "failproofai audit")} ${c(DIM, "later.")}\n\n`,
266
305
  );
267
306
  return;
268
307
  }
269
308
 
309
+ // Reported before the empty-history return below, so an onboarding audit that
310
+ // finds nothing is still counted — matching runAuditCli, which reports
311
+ // completed regardless of what the scan turned up.
312
+ await trackHookEvent(instanceId, "cli_audit_completed", auditCompletedProps("onboarding", result));
313
+
270
314
  if (result.eventsScanned === 0) {
271
315
  process.stdout.write(
272
316
  `\n ${c(DIM, "no agent sessions to audit yet — come back after using your agent.")}\n\n`,
@@ -328,13 +372,7 @@ export async function runAuditCli(args: string[]): Promise<void> {
328
372
  // would otherwise drop this event. On the dashboard path launch() keeps the
329
373
  // process alive, but awaiting makes delivery reliable on every exit path.
330
374
  // Bounded (5s) and never throws.
331
- await trackHookEvent(instanceId, "cli_audit_completed", {
332
- source: "cli",
333
- events_scanned: result.eventsScanned,
334
- sessions_scanned: result.transcripts.scanned,
335
- total_hits: result.totals.hits,
336
- findings: result.results.length,
337
- });
375
+ await trackHookEvent(instanceId, "cli_audit_completed", auditCompletedProps("cli", result));
338
376
 
339
377
  // No sessions on disk — guide the user instead of opening an empty dashboard.
340
378
  if (result.eventsScanned === 0) {
package/src/auth/cli.ts CHANGED
@@ -28,6 +28,7 @@ import {
28
28
  writeAuth,
29
29
  } from "../../lib/auth/auth-store";
30
30
  import { CliError } from "../cli-error";
31
+ import { brandAnsi, colorsEnabled, ANSI_RESET, ANSI_DIM } from "../hooks/tui";
31
32
  import { trackHookEvent } from "../hooks/hook-telemetry";
32
33
  import { getInstanceId } from "../../lib/telemetry-id";
33
34
 
@@ -123,11 +124,19 @@ function prompt(question: string, opts: { hidden?: boolean } = {}): Promise<stri
123
124
  });
124
125
  }
125
126
 
126
- const DIM = "\x1b[2m";
127
- const RESET = "\x1b[0m";
128
- const PINK = "\x1b[38;5;204m";
129
- const GREEN = "\x1b[38;5;120m";
130
- const RED = "\x1b[38;5;197m";
127
+ // Brand palette (see hooks/tui.ts) rather than this file's former private
128
+ // 256-colour set, so `auth` matches `config` and `audit`. Resolved once here
129
+ // and blanked when colour is off — these are interpolated directly into
130
+ // template literals at ~15 call sites, which previously meant `auth` emitted
131
+ // escape codes even under NO_COLOR. Blanking the constants fixes every call
132
+ // site at once. The palette has no red, and both uses are recoverable input
133
+ // errors ("try again") rather than failures, so they take the amber warn hue.
134
+ const COLOR = colorsEnabled(process.stdout);
135
+ const DIM = COLOR ? ANSI_DIM : "";
136
+ const RESET = COLOR ? ANSI_RESET : "";
137
+ const PINK = COLOR ? brandAnsi("pink") : "";
138
+ const GREEN = COLOR ? brandAnsi("guide") : "";
139
+ const RED = COLOR ? brandAnsi("warn") : "";
131
140
 
132
141
  function emailLooksValid(email: string): boolean {
133
142
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);