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.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +0 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +5 -5
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/routes-manifest.json +0 -6
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js +4 -3
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +4 -3
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +3 -3
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js +2 -2
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js +4 -3
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +2 -2
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js +3 -2
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js +4 -3
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js +5 -3
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +4 -3
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +10 -10
- package/.next/standalone/.next/server/app/policies/page.js +6 -5
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +5 -4
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +6 -4
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +5 -4
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
- package/.next/standalone/.next/server/app/settings/page.js +6 -4
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app-paths-manifest.json +0 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__05ja_05._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0agdw2a._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d1u_na._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0nne229._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ny9q5d._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0oy4kxb._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zf034w._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1hstmft._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p8poxd._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +1 -1
- 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
- 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
- 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
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03eykvd._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__07175vb._.js → [root-of-the-server]__07oh8b5._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cxe_2_._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g2rg69._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kgy9da._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0m98kzy._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0sjzdjg._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0spkm68._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0v5_91z._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-a3135._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1hvwiw8._.js → [root-of-the-server]__11z_299._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1apc90t._.js → [root-of-the-server]__15c_z97._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1wmno1q._.js → [root-of-the-server]__17d_ffl._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1_dfocn._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1e5g_ur._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1hmqefd._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jiwfsj._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1kzapg5._.js +6 -0
- package/.next/standalone/.next/server/chunks/ssr/_04j57fb._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_05enntk._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0hom1s1._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1nq-35m._.js +23 -0
- package/.next/standalone/.next/server/chunks/ssr/_1q7j05x._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1ylae7o._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_1n3w9lb._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +3 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +15 -15
- package/.next/standalone/.next/static/chunks/00q6cfzuzpc85.js +1 -0
- package/.next/standalone/.next/static/chunks/{0zm3_8jpqitxy.js → 054eg7y_cjyf9.js} +22 -22
- package/.next/standalone/.next/static/chunks/{2ouq44jv_0x04.js → 0mjufa_0xxz75.js} +1 -1
- package/.next/standalone/.next/static/chunks/0u64yey446k4f.css +1 -0
- package/.next/standalone/.next/static/chunks/{0zi8zjav5a4rq.js → 1-oz4co31nvc5.js} +1 -1
- package/.next/standalone/.next/static/chunks/13--2yvqek_s1.js +1 -0
- package/.next/standalone/.next/static/chunks/136ldjmzxhush.js +1 -0
- package/.next/standalone/.next/static/chunks/18fqy3doo2ial.js +6 -0
- package/.next/standalone/.next/static/chunks/1eyl7fzkmnkf_.js +1 -0
- package/.next/standalone/.next/static/chunks/{31k3yxpy57_fy.js → 1o5sxp_mdaoci.js} +1 -1
- package/.next/standalone/.next/static/chunks/29-iqnp9v_axt.css +1 -0
- package/.next/standalone/.next/static/chunks/{2676v-lyzgo91.js → 2es4z1ei90vsv.js} +1 -1
- package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +1 -0
- package/.next/standalone/.next/static/chunks/3peix2vxzj7eh.js +1 -0
- package/.next/standalone/.next/static/chunks/3qfoeqhy5l10x.js +1 -0
- package/.next/standalone/.next/static/chunks/{0km-jn9o5xyqs.css → 3wq9x0c-7ld5d.css} +1 -1
- package/.next/standalone/app/actions/get-scheduled-audit.ts +65 -6
- package/.next/standalone/app/actions/update-scheduled-audit.ts +90 -17
- package/.next/standalone/app/api/auth/status/route.ts +7 -17
- package/.next/standalone/app/audit/_components/audit-dashboard.tsx +2 -6
- package/.next/standalone/app/audit/_components/come-back-better-section.tsx +101 -281
- package/.next/standalone/app/audit/_components/empty-state.tsx +2 -2
- package/.next/standalone/app/audit/_components/run-progress.tsx +1 -1
- package/.next/standalone/app/audit/audit-styles.css +150 -26
- package/.next/standalone/app/globals.css +39 -1
- package/.next/standalone/app/settings/page.tsx +38 -21
- package/.next/standalone/app/settings/settings-client.tsx +583 -392
- package/.next/standalone/app/settings/settings.css +354 -0
- package/.next/standalone/components/navbar.tsx +33 -4
- package/.next/standalone/lib/auth/api-server-client.ts +51 -29
- package/.next/standalone/lib/auth/auth-store.ts +44 -59
- package/.next/standalone/package.json +12 -10
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +6 -0
- package/bin/failproofaid-shim.mjs +0 -0
- package/dist/cli.mjs +1889 -426
- package/dist/worker.mjs +27 -5
- package/lib/auth/api-server-client.ts +51 -29
- package/lib/auth/auth-store.ts +44 -59
- package/package.json +12 -10
- package/scripts/docs-audit.ts +493 -0
- package/scripts/translate-docs/cli.ts +16 -2
- package/scripts/translate-docs/mdx-translator.ts +6 -1
- package/scripts/translate-docs/readme-translator.ts +4 -2
- package/src/audit/cli-login.ts +330 -0
- package/src/audit/cli.ts +124 -6
- package/src/audit/dashboard-cache.ts +44 -8
- package/src/audit/harm-report.ts +260 -0
- package/src/audit/machine-store.ts +120 -0
- package/src/audit/redact-example.ts +312 -0
- package/src/audit/report-harm.ts +167 -0
- package/src/audit/schedule-cli.ts +352 -0
- package/src/hooks/builtin-policies.ts +26 -0
- package/src/hooks/daemon-service.ts +76 -0
- package/src/hooks/fp-config.ts +96 -5
- package/src/hooks/fp-home.ts +118 -8
- package/src/hooks/fp-reset.ts +79 -9
- package/src/hooks/migrations.ts +241 -4
- package/src/hooks/tui.ts +61 -3
- package/.next/standalone/.next/server/app/api/auth/reminder/route/app-paths-manifest.json +0 -3
- package/.next/standalone/.next/server/app/api/auth/reminder/route/build-manifest.json +0 -12
- package/.next/standalone/.next/server/app/api/auth/reminder/route/server-reference-manifest.json +0 -4
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js +0 -9
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.map +0 -5
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +0 -1
- package/.next/standalone/.next/server/app/api/auth/reminder/route_client-reference-manifest.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-xggru._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__00ds05_._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__094p1f1._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1dxagv_._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fgqj1_._.js +0 -3
- package/.next/standalone/.next/server/chunks/_084toou._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0g4t8c9._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0km521a._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0t8kefc._.js +0 -3
- package/.next/standalone/.next/server/chunks/_16dwkfn._.js +0 -3
- package/.next/standalone/.next/server/chunks/_1phm9ui._.js +0 -3
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_auth_reminder_route_actions_1kjgxf8.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0dk8r_l._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kviq2m._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ogyhu0._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-rfu32._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1mmefsr._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1t62w37._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_032wo7w._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_08840em._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0gl2ujp._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0j0ggkc._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0kbv5-q._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0r1jgvp._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0t5xq1b._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0w_be0g._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_15gba46._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/_1qyhb9r._.js +0 -23
- package/.next/standalone/.next/server/chunks/ssr/_1vel2ey._.js +0 -3
- package/.next/standalone/.next/static/chunks/0wd503tv801gy.js +0 -1
- package/.next/standalone/.next/static/chunks/1hfocb0sdl85c.js +0 -1
- package/.next/standalone/.next/static/chunks/1v1gqvpjk62jm.js +0 -1
- package/.next/standalone/.next/static/chunks/2kopjq_or743g.js +0 -6
- package/.next/standalone/.next/static/chunks/2uvhw57mf9lg_.css +0 -2
- package/.next/standalone/.next/static/chunks/2ynl0-6aqng_b.js +0 -1
- package/.next/standalone/.next/static/chunks/3xvtgz6arztp_.js +0 -1
- package/.next/standalone/app/api/auth/reminder/route.ts +0 -213
- /package/.next/standalone/.next/static/{Aulh3DRWwRfwh2POCvZR_ → jSKB7CQQSNTX-uSbY2bUs}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{Aulh3DRWwRfwh2POCvZR_ → jSKB7CQQSNTX-uSbY2bUs}/_clientMiddlewareManifest.js +0 -0
- /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
|
-
|
|
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
|
-
|
|
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,
|