failproofai 0.0.14-beta.1 → 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.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- 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.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 +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- 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.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 +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +9 -9
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- 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.nft.json +1 -1
- 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.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
- 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.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 +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +9 -9
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- 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 +8 -8
- 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.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.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.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0--lkk6._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-219ec._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__04_h-a5._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__06cuf1y._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08w4wmd._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g6bbuw._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jnjf2t._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q-v9z2._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q0qzx1._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q2nbsl._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rv7m0k._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__16le-kd._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ffkmds._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1legmza._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mrihkj._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qb590j._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qxztj-._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1shcqgr._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1xuedea._.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +4 -4
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__015_i4t._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__030k0c6._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03lv-pe._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__05r_17v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0wp9w6i._.js → [root-of-the-server]__09v9cwb._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e2l3c1._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hci7t3._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hdsupo._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p84xee._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qeuy9c._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s89hoe._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0brjczq._.js → [root-of-the-server]__0twi4fj._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ynf7tx._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__170799-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17ai7sy._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1nq7ivq._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tkjqka._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_05whahf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1kje4fm._.js +1 -1
- 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 +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_0xkhw_v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_12rjfea.js → node_modules_html-to-image_es_index_06xsmte.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1ezd2jf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1tnuifj._.js +1 -1
- 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 +10 -10
- package/.next/standalone/.next/static/chunks/{0o9wpnze1ey8y.js → 0h-6ymo9v86b0.js} +2 -2
- package/.next/standalone/.next/static/chunks/0pndjhcxfu-0t.js +1 -0
- package/.next/standalone/.next/static/chunks/1-33f82_l32or.js +1 -0
- package/.next/standalone/.next/static/chunks/{2vav_x0_336ds.js → 1bg_g3g359c_d.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1359o5785rmve.js → 1gg-8e9q_m__9.js} +2 -2
- package/.next/standalone/.next/static/chunks/{0emg30_6svycu.js → 29zkom4s9de2d.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0d5lypu1zs21r.js → 2llkdafnaejw_.js} +1 -1
- package/.next/standalone/.next/static/chunks/{20tuctw38jbgw.js → 388hgfqye67xa.js} +1 -1
- package/.next/standalone/.next/static/chunks/396764jgd7-9j.js +1 -0
- package/.next/standalone/.next/static/chunks/3w7g0n0iwwnbs.js +1 -0
- package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +11 -2
- package/.next/standalone/app/components/copy-button.tsx +22 -6
- package/.next/standalone/{ci/cli-integration → integration-suite}/README.md +25 -7
- package/.next/standalone/integration-suite/ci-entrypoint.sh +164 -0
- package/.next/standalone/{ci/cli-integration → integration-suite}/run.sh +4 -4
- package/.next/standalone/lib/format-duration.ts +23 -9
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/server.js +1 -1
- package/dist/cli.mjs +342 -106
- package/lib/format-duration.ts +23 -9
- package/package.json +1 -1
- package/scripts/translate-docs/cli.ts +6 -0
- package/scripts/translate-docs/mdx-translator.ts +30 -19
- package/scripts/translate-docs/readme-translator.ts +31 -21
- package/scripts/translate-docs/translator.ts +142 -7
- package/scripts/translate-docs/types.ts +6 -0
- package/scripts/translate-docs/validate-translation.ts +132 -0
- package/scripts/validate-mdx.ts +80 -11
- package/src/audit/cli.ts +12 -6
- package/src/auth/cli.ts +14 -5
- package/src/hooks/builtin-policies.ts +168 -50
- package/src/hooks/configure-wizard.ts +238 -22
- package/src/hooks/custom-hooks-loader.ts +70 -4
- package/src/hooks/handler.ts +4 -1
- package/src/hooks/policy-types.ts +8 -0
- package/src/hooks/tui.ts +95 -28
- package/.next/standalone/.next/static/chunks/2ldhnaweu9ew3.js +0 -1
- package/.next/standalone/.next/static/chunks/3cg7x0joi2b98.js +0 -1
- package/.next/standalone/.next/static/chunks/3k87p-tijud-4.js +0 -1
- package/.next/standalone/.next/static/chunks/3kmaqcp7rldad.js +0 -1
- /package/.next/standalone/.next/static/{LkN-JjmhKGmL-WklH7WVi → YKxI22hC-F_nHp7MEKfb1}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{LkN-JjmhKGmL-WklH7WVi → YKxI22hC-F_nHp7MEKfb1}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{LkN-JjmhKGmL-WklH7WVi → YKxI22hC-F_nHp7MEKfb1}/_ssgManifest.js +0 -0
- /package/.next/standalone/{ci/cli-integration → integration-suite}/Dockerfile +0 -0
- /package/.next/standalone/{ci/cli-integration → integration-suite}/canary-policies.mjs +0 -0
- /package/.next/standalone/{ci/cli-integration → integration-suite}/capture-tokens.sh +0 -0
- /package/.next/standalone/{ci/cli-integration → integration-suite}/inject-tokens.sh +0 -0
- /package/.next/standalone/{ci/cli-integration → integration-suite}/install-clis.sh +0 -0
- /package/.next/standalone/{ci/cli-integration → integration-suite}/probe-cli.sh +0 -0
- /package/.next/standalone/{ci/cli-integration → integration-suite}/report.js +0 -0
package/lib/format-duration.ts
CHANGED
|
@@ -16,14 +16,28 @@ export function formatRelativeTime(ts: number): string {
|
|
|
16
16
|
|
|
17
17
|
export function formatDuration(ms: number): string {
|
|
18
18
|
if (ms < 1000) return `${ms}ms`;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return `${
|
|
19
|
+
|
|
20
|
+
// Round to the precision the output will actually use, then bucket.
|
|
21
|
+
// Seconds are shown with one decimal place, so round to the nearest 0.1 s
|
|
22
|
+
// before deciding whether we have crossed into the minute range.
|
|
23
|
+
const deciseconds = Math.round(ms / 100);
|
|
24
|
+
if (deciseconds < 600) {
|
|
25
|
+
return `${(deciseconds / 10).toFixed(1)}s`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Minutes are shown with whole seconds, so round to the nearest second
|
|
29
|
+
// before splitting into minutes and seconds.
|
|
30
|
+
const totalSeconds = Math.round(ms / 1000);
|
|
31
|
+
if (totalSeconds < 3600) {
|
|
32
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
33
|
+
const seconds = totalSeconds % 60;
|
|
34
|
+
return `${minutes}m ${seconds}s`;
|
|
26
35
|
}
|
|
27
|
-
|
|
28
|
-
|
|
36
|
+
|
|
37
|
+
// Hours are shown with whole minutes, so round to the nearest minute
|
|
38
|
+
// before splitting into hours and minutes.
|
|
39
|
+
const totalMinutes = Math.round(ms / 60000);
|
|
40
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
41
|
+
const minutes = totalMinutes % 60;
|
|
42
|
+
return `${hours}h ${minutes}m`;
|
|
29
43
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "failproofai",
|
|
3
|
-
"version": "0.0.14-beta.
|
|
3
|
+
"version": "0.0.14-beta.2",
|
|
4
4
|
"description": "The easiest way to manage policies that keep your AI agents reliable, on-task, and running autonomously — for Claude Code & the Agents SDK",
|
|
5
5
|
"bin": {
|
|
6
6
|
"failproofai": "./dist/cli.mjs"
|
|
@@ -419,6 +419,12 @@ async function main() {
|
|
|
419
419
|
if (totalInput > 0) {
|
|
420
420
|
console.log(`Total tokens: ${totalInput} input + ${totalOutput} output`);
|
|
421
421
|
}
|
|
422
|
+
// Surface pages that needed a re-translation to pass validation, so a run
|
|
423
|
+
// that quietly retried does not read as a clean one in the job log.
|
|
424
|
+
const retried = translated.filter((r) => (r.attempts ?? 1) > 1);
|
|
425
|
+
if (retried.length > 0) {
|
|
426
|
+
console.log(`Retried: ${retried.length}`);
|
|
427
|
+
}
|
|
422
428
|
|
|
423
429
|
if (errors.length > 0) {
|
|
424
430
|
process.exit(1);
|
|
@@ -10,7 +10,8 @@ import {
|
|
|
10
10
|
import { dirname, join, relative } from "node:path";
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
12
|
import { getLanguageByCode } from "./config";
|
|
13
|
-
import {
|
|
13
|
+
import { translateValidated } from "./translator";
|
|
14
|
+
import { findTranslationError } from "./validate-translation";
|
|
14
15
|
import {
|
|
15
16
|
readCache,
|
|
16
17
|
writeCache,
|
|
@@ -193,10 +194,13 @@ export async function translateMdxPage(
|
|
|
193
194
|
dryRun?: boolean;
|
|
194
195
|
model?: string;
|
|
195
196
|
cache?: TranslationCache;
|
|
197
|
+
/** Override the docs root. Tests point this at a fixture tree. */
|
|
198
|
+
docsDir?: string;
|
|
196
199
|
} = {},
|
|
197
200
|
): Promise<TranslationResult> {
|
|
198
|
-
const
|
|
199
|
-
const
|
|
201
|
+
const docsDir = options.docsDir ?? DOCS_DIR;
|
|
202
|
+
const relPath = relative(docsDir, sourcePath);
|
|
203
|
+
const outputPath = join(docsDir, lang, relPath);
|
|
200
204
|
const sourceContent = readFileSync(sourcePath, "utf-8");
|
|
201
205
|
|
|
202
206
|
const langConfig = getLanguageByCode(lang);
|
|
@@ -228,25 +232,31 @@ export async function translateMdxPage(
|
|
|
228
232
|
};
|
|
229
233
|
}
|
|
230
234
|
|
|
231
|
-
// Translate
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
235
|
+
// Translate and validate the exact bytes we will write. The render callback
|
|
236
|
+
// reproduces the historical sanitize + link-rewrite chain byte-for-byte
|
|
237
|
+
// (strip stray JSX-attribute quotes, drop an unmatched trailing fence,
|
|
238
|
+
// convert HTML comments to MDX, then add the language prefix to links), so
|
|
239
|
+
// the validated bytes ARE the written bytes and a pass here equals a pass in
|
|
240
|
+
// the deploy. On exhaustion translateValidated throws before we reach the
|
|
241
|
+
// write, so an invalid page is never written or cached.
|
|
242
|
+
const { rendered, inputTokens, outputTokens, attempts } =
|
|
243
|
+
await translateValidated({
|
|
244
|
+
source: sourceContent,
|
|
245
|
+
lang,
|
|
246
|
+
langName: langConfig.name,
|
|
247
|
+
model: options.model,
|
|
248
|
+
label: `${relPath} [${lang}]`,
|
|
249
|
+
render: (raw) =>
|
|
250
|
+
rewriteInternalLinks(
|
|
251
|
+
convertHtmlComments(stripStrayTrailingFence(sanitizeJsxAttributes(raw))),
|
|
252
|
+
lang,
|
|
253
|
+
),
|
|
254
|
+
validate: (bytes) => findTranslationError(bytes, sourceContent),
|
|
255
|
+
});
|
|
246
256
|
|
|
247
257
|
// Write output
|
|
248
258
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
249
|
-
writeFileSync(outputPath,
|
|
259
|
+
writeFileSync(outputPath, rendered);
|
|
250
260
|
|
|
251
261
|
// Update cache — skip if caller manages the cache (batch write)
|
|
252
262
|
if (!options.cache) {
|
|
@@ -269,6 +279,7 @@ export async function translateMdxPage(
|
|
|
269
279
|
inputTokens,
|
|
270
280
|
outputTokens,
|
|
271
281
|
cached: false,
|
|
282
|
+
attempts,
|
|
272
283
|
};
|
|
273
284
|
}
|
|
274
285
|
|
|
@@ -2,12 +2,13 @@ import { readFileSync, writeFileSync, mkdirSync } 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";
|
|
5
|
-
import {
|
|
5
|
+
import { translateValidated } from "./translator";
|
|
6
6
|
import {
|
|
7
7
|
stripStrayTrailingFence,
|
|
8
8
|
convertHtmlComments,
|
|
9
9
|
sanitizeJsxAttributes,
|
|
10
10
|
} from "./mdx-translator";
|
|
11
|
+
import { findTranslationError } from "./validate-translation";
|
|
11
12
|
import { readCache, writeCache, isCached, setCacheEntry } from "./cache";
|
|
12
13
|
import type { TranslationResult, TranslationCache } from "./types";
|
|
13
14
|
|
|
@@ -90,15 +91,12 @@ export async function translateReadme(
|
|
|
90
91
|
};
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
// Build the final output with header
|
|
94
|
+
// Compute the wrapper (disclaimer, language selector, RTL <div>) up front so
|
|
95
|
+
// the render callback can assemble the FINAL bytes on every attempt. The
|
|
96
|
+
// assembled bytes — not the raw model output — are what gets validated: the
|
|
97
|
+
// swallowed-`</div>` class is introduced by the wrapper AFTER the model
|
|
98
|
+
// returns, and `mintlify validate` never sees the README at all, so this gate
|
|
99
|
+
// is the only thing standing between a broken README and the deploy.
|
|
102
100
|
const disclaimer = langConfig.rtl
|
|
103
101
|
? `> **\u26a0\ufe0f** \u0647\u0630\u0647 \u062a\u0631\u062c\u0645\u0629 \u0622\u0644\u064a\u0629. \u0644\u0644\u0627\u0637\u0644\u0627\u0639 \u0639\u0644\u0649 \u0623\u062d\u062f\u062b \u0625\u0635\u062f\u0627\u0631\u060c \u0631\u0627\u062c\u0639 [English README](../../README.md).`
|
|
104
102
|
: `> **\u26a0\ufe0f** This is an auto-generated translation. For the latest version, see the [English README](../../README.md). Community corrections welcome!`;
|
|
@@ -107,20 +105,31 @@ export async function translateReadme(
|
|
|
107
105
|
const rtlOpen = langConfig.rtl ? `<div dir="rtl">\n\n` : "";
|
|
108
106
|
const rtlClose = langConfig.rtl ? `\n\n</div>` : "";
|
|
109
107
|
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
108
|
+
// Translate and validate the assembled bytes, re-translating on failure.
|
|
109
|
+
const { rendered, inputTokens, outputTokens, attempts } =
|
|
110
|
+
await translateValidated({
|
|
111
|
+
source: sourceContent,
|
|
112
|
+
lang,
|
|
113
|
+
langName: langConfig.name,
|
|
114
|
+
model: options.model,
|
|
115
|
+
label: `README.${lang}.md`,
|
|
116
|
+
render: (raw) => {
|
|
117
|
+
// Same MDX sanitizers as translateMdxPage — the README emits JSX (the
|
|
118
|
+
// logo table), so strip stray attribute quotes, drop any unmatched
|
|
119
|
+
// trailing code fence (which would swallow the RTL `</div>`), and
|
|
120
|
+
// convert HTML comments to MDX — then wrap in disclaimer + selector +
|
|
121
|
+
// RTL div.
|
|
122
|
+
const cleaned = convertHtmlComments(
|
|
123
|
+
stripStrayTrailingFence(sanitizeJsxAttributes(raw)),
|
|
124
|
+
);
|
|
125
|
+
return `${disclaimer}\n\n${langSelector}\n\n---\n${rtlOpen}\n${cleaned}\n${rtlClose}`;
|
|
126
|
+
},
|
|
127
|
+
validate: (bytes) => findTranslationError(bytes, sourceContent),
|
|
128
|
+
});
|
|
120
129
|
|
|
121
130
|
// Write output
|
|
122
131
|
mkdirSync(I18N_DIR, { recursive: true });
|
|
123
|
-
writeFileSync(outputPath,
|
|
132
|
+
writeFileSync(outputPath, rendered);
|
|
124
133
|
|
|
125
134
|
// Update cache — skip if caller manages the cache (batch write)
|
|
126
135
|
if (!options.cache) {
|
|
@@ -136,6 +145,7 @@ export async function translateReadme(
|
|
|
136
145
|
inputTokens,
|
|
137
146
|
outputTokens,
|
|
138
147
|
cached: false,
|
|
148
|
+
attempts,
|
|
139
149
|
};
|
|
140
150
|
}
|
|
141
151
|
|
|
@@ -25,6 +25,25 @@ const MAX_TOKENS =
|
|
|
25
25
|
? parsedMaxTokens
|
|
26
26
|
: 64000;
|
|
27
27
|
|
|
28
|
+
// Maximum TOTAL validation attempts per page: 1 initial translation plus up to
|
|
29
|
+
// MAX_ATTEMPTS-1 re-translations when the rendered output fails validation
|
|
30
|
+
// (see translateValidated below). Distinct from the SDK transport budget
|
|
31
|
+
// TRANSLATE_MAX_RETRIES (default 5, in getClient): that retries a single HTTP
|
|
32
|
+
// request on a connection error; this re-translates a page whose *content*
|
|
33
|
+
// failed the docs-build checks. They multiply — at most
|
|
34
|
+
// MAX_ATTEMPTS x (1 + maxRetries) HTTP requests for one page in the worst case.
|
|
35
|
+
// 3 collapses the observed per-page failure rate to negligible while costing
|
|
36
|
+
// wall-clock (a serial retry inside one worker slot), not peak concurrency.
|
|
37
|
+
// Override via TRANSLATE_MAX_ATTEMPTS (integer >= 1; 1 disables retries).
|
|
38
|
+
const parsedMaxAttempts = Number.parseInt(
|
|
39
|
+
process.env.TRANSLATE_MAX_ATTEMPTS ?? "",
|
|
40
|
+
10,
|
|
41
|
+
);
|
|
42
|
+
const MAX_ATTEMPTS =
|
|
43
|
+
Number.isInteger(parsedMaxAttempts) && parsedMaxAttempts > 0
|
|
44
|
+
? parsedMaxAttempts
|
|
45
|
+
: 3;
|
|
46
|
+
|
|
28
47
|
function getClient(): Anthropic {
|
|
29
48
|
if (!client) {
|
|
30
49
|
// Default 5 retries (up from SDK default of 2) so transient
|
|
@@ -44,7 +63,7 @@ const SYSTEM_PROMPT = `You are a professional technical documentation translator
|
|
|
44
63
|
|
|
45
64
|
1. **Preserve all code blocks exactly as-is** — never translate content inside backtick-fenced code blocks (\`\`\`...\`\`\`) or inline code (\`...\`).
|
|
46
65
|
2. **Preserve MDX component syntax** — tags like <Card>, <CardGroup>, <CodeGroup>, <Steps>, <Step>, <Note>, <Tip>, <Tabs>, <Tab>, <Warning> must remain unchanged. Their attribute names (title, icon, href, cols) must remain in English. Only translate the text content of the \`title\` attribute and the text body between tags. **Never put an ASCII straight \`"\` inside a \`title="…"\` (or any JSX attribute value)** — it terminates the attribute and breaks MDX parsing. If the target language would normally wrap a word in quotation marks (e.g. German „…", Japanese 「…」), drop the inner quotes inside attribute values and rely on the surrounding tag for emphasis.
|
|
47
|
-
3. **Preserve YAML frontmatter keys** — only translate the string values of \`title\` and \`description\`. Keep the \`icon\` value unchanged.
|
|
66
|
+
3. **Preserve YAML frontmatter keys** — only translate the string values of \`title\` and \`description\`. Keep the \`icon\` value unchanged. Never rename, add, or drop a frontmatter key. The \`title\` and \`description\` values are wrapped in double quotes: **never put an unescaped ASCII \`"\` inside them** — it terminates the YAML string and breaks the frontmatter parse (exactly as an ASCII \`"\` breaks a JSX attribute in rule 2). If the source value contains an escaped quote (\`\\"\`), keep it escaped in the same form; if the target language would quote a phrase, use typographic quotes (e.g. „…", «…», 「…」) or rephrase to avoid the inner quote.
|
|
48
67
|
4. **Preserve all URLs and paths** — never modify href values, image paths, or links.
|
|
49
68
|
5. **Preserve Markdown structure** — headers (#, ##), lists (-, *), tables (|), bold (**), italic (*), links ([text](url)) must keep their Markdown formatting.
|
|
50
69
|
6. **Preserve badge/shield URLs** — any [](url) pattern must remain completely unchanged.
|
|
@@ -65,11 +84,40 @@ ${DO_NOT_TRANSLATE.map((t) => `- ${t}`).join("\n")}
|
|
|
65
84
|
|
|
66
85
|
Return ONLY the translated content. Do not add explanations, notes, or commentary.`;
|
|
67
86
|
|
|
87
|
+
export interface RetryFeedback {
|
|
88
|
+
attempt: number;
|
|
89
|
+
maxAttempts: number;
|
|
90
|
+
/** The previous attempt's validation error — model-actionable text. */
|
|
91
|
+
error: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The repair note appended to the user turn on a retry. The failed attempt is
|
|
96
|
+
* NOT fed back as an assistant turn: a fresh re-translate keeps the input flat
|
|
97
|
+
* across attempts. Conversational repair would grow the input every attempt and
|
|
98
|
+
* push a large README toward the max_tokens ceiling on exactly the retry where
|
|
99
|
+
* a truncation would be worst — and the error is self-locating (it carries the
|
|
100
|
+
* caret excerpt / body snippet), so the model needs the note, not its own prior
|
|
101
|
+
* output, to fix the defect.
|
|
102
|
+
*/
|
|
103
|
+
function buildRetryFeedback(f: RetryFeedback): string {
|
|
104
|
+
return (
|
|
105
|
+
`[Retry ${f.attempt} of ${f.maxAttempts}. The source document above is unchanged.]\n\n` +
|
|
106
|
+
"Your previous translation of this document was REJECTED by the docs build " +
|
|
107
|
+
"and discarded. It failed validation with:\n\n" +
|
|
108
|
+
`${f.error}\n\n` +
|
|
109
|
+
"Translate the source again from the beginning and return ONLY the " +
|
|
110
|
+
"corrected translation — do not comment on this note. Do not reproduce that " +
|
|
111
|
+
"defect. Every rule above still applies."
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
68
115
|
export async function translateContent(
|
|
69
116
|
content: string,
|
|
70
117
|
targetLang: string,
|
|
71
118
|
targetLangName: string,
|
|
72
119
|
model: string = "claude-sonnet-4-6",
|
|
120
|
+
feedback?: RetryFeedback,
|
|
73
121
|
): Promise<{ translated: string; inputTokens: number; outputTokens: number }> {
|
|
74
122
|
const anthropic = getClient();
|
|
75
123
|
|
|
@@ -81,16 +129,18 @@ export async function translateContent(
|
|
|
81
129
|
// surfaces to the SDK as `APIConnectionError ("Connection error.")`.
|
|
82
130
|
// `messages.stream(...).finalMessage()` returns the same Message shape
|
|
83
131
|
// as `messages.create(...)`, so the rest of the pipeline is unchanged.
|
|
132
|
+
const base = `Translate the following documentation content into ${targetLangName} (${targetLang}).\n\n---\n\n${content}`;
|
|
133
|
+
// On a retry, append the repair note after the source in the SAME single user
|
|
134
|
+
// turn — no assistant turn is fed back, so the request stays flat-input.
|
|
135
|
+
const userContent = feedback
|
|
136
|
+
? `${base}\n\n---\n\n${buildRetryFeedback(feedback)}`
|
|
137
|
+
: base;
|
|
138
|
+
|
|
84
139
|
const response = await anthropic.messages.stream({
|
|
85
140
|
model,
|
|
86
141
|
max_tokens: MAX_TOKENS,
|
|
87
142
|
system: [{ type: "text", text: SYSTEM_PROMPT, cache_control: { type: "ephemeral" } }],
|
|
88
|
-
messages: [
|
|
89
|
-
{
|
|
90
|
-
role: "user",
|
|
91
|
-
content: `Translate the following documentation content into ${targetLangName} (${targetLang}).\n\n---\n\n${content}`,
|
|
92
|
-
},
|
|
93
|
-
],
|
|
143
|
+
messages: [{ role: "user", content: userContent }],
|
|
94
144
|
}).finalMessage();
|
|
95
145
|
|
|
96
146
|
// A truncated translation is worse than a failed one: the model stops
|
|
@@ -116,3 +166,88 @@ export async function translateContent(
|
|
|
116
166
|
outputTokens: response.usage.output_tokens,
|
|
117
167
|
};
|
|
118
168
|
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Translate `source`, render it to the exact bytes that will be written, and
|
|
172
|
+
* validate those bytes — re-translating with the validation error fed back
|
|
173
|
+
* until it passes or MAX_ATTEMPTS is reached.
|
|
174
|
+
*
|
|
175
|
+
* The loop lives here, not in translateContent, because each caller renders
|
|
176
|
+
* different final bytes (the MDX pages add rewriteInternalLinks; the README
|
|
177
|
+
* wraps the body in a disclaimer + RTL `<div>`). Validating the RENDERED bytes —
|
|
178
|
+
* not the raw model output — is what makes a pass here equal to a pass in
|
|
179
|
+
* `mintlify validate` and the deploy.
|
|
180
|
+
*
|
|
181
|
+
* On exhaustion it THROWS (never returns a partial), so the caller's write is
|
|
182
|
+
* unreachable and no invalid page is ever written or cached — the same
|
|
183
|
+
* fail-loud contract translateContent already enforces for max_tokens
|
|
184
|
+
* truncation. Transport/auth/max_tokens errors from translateContent propagate
|
|
185
|
+
* unchanged and consume no attempt: only *validity* failures retry.
|
|
186
|
+
*/
|
|
187
|
+
export async function translateValidated(opts: {
|
|
188
|
+
source: string;
|
|
189
|
+
lang: string;
|
|
190
|
+
langName: string;
|
|
191
|
+
model?: string;
|
|
192
|
+
/** Label for the per-attempt warning line, e.g. `agenteye/cli.mdx [de]`. */
|
|
193
|
+
label: string;
|
|
194
|
+
/** Turn raw model output into the exact bytes that will be written. */
|
|
195
|
+
render: (raw: string) => string;
|
|
196
|
+
/** Validate the rendered bytes; return an error message, or null if valid. */
|
|
197
|
+
validate: (rendered: string) => Promise<string | null>;
|
|
198
|
+
}): Promise<{
|
|
199
|
+
rendered: string;
|
|
200
|
+
inputTokens: number;
|
|
201
|
+
outputTokens: number;
|
|
202
|
+
attempts: number;
|
|
203
|
+
}> {
|
|
204
|
+
let inputTokens = 0;
|
|
205
|
+
let outputTokens = 0;
|
|
206
|
+
let lastError = "";
|
|
207
|
+
|
|
208
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
209
|
+
const feedback: RetryFeedback | undefined =
|
|
210
|
+
attempt > 1
|
|
211
|
+
? { attempt, maxAttempts: MAX_ATTEMPTS, error: lastError }
|
|
212
|
+
: undefined;
|
|
213
|
+
|
|
214
|
+
// No try/catch: a transport/auth/max_tokens throw is not a validity
|
|
215
|
+
// failure — let it propagate so it is never silently retried as one.
|
|
216
|
+
const result = await translateContent(
|
|
217
|
+
opts.source,
|
|
218
|
+
opts.lang,
|
|
219
|
+
opts.langName,
|
|
220
|
+
opts.model,
|
|
221
|
+
feedback,
|
|
222
|
+
);
|
|
223
|
+
inputTokens += result.inputTokens;
|
|
224
|
+
outputTokens += result.outputTokens;
|
|
225
|
+
|
|
226
|
+
if (result.translated.trim() === "") {
|
|
227
|
+
// An empty response is a validity failure, not a usable page — resample
|
|
228
|
+
// rather than write a blank file.
|
|
229
|
+
lastError =
|
|
230
|
+
"The translation was empty. Return the full translated document.";
|
|
231
|
+
console.warn(
|
|
232
|
+
` ${opts.label} -> attempt ${attempt}/${MAX_ATTEMPTS} produced empty output; retrying`,
|
|
233
|
+
);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const rendered = opts.render(result.translated);
|
|
238
|
+
const error = await opts.validate(rendered);
|
|
239
|
+
if (error === null) {
|
|
240
|
+
return { rendered, inputTokens, outputTokens, attempts: attempt };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
lastError = error;
|
|
244
|
+
console.warn(
|
|
245
|
+
` ${opts.label} -> attempt ${attempt}/${MAX_ATTEMPTS} failed validation: ${error.split("\n")[0]}`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
throw new Error(
|
|
250
|
+
`translation into ${opts.langName} (${opts.lang}) still fails validation ` +
|
|
251
|
+
`after ${MAX_ATTEMPTS} attempt(s): ${lastError.split("\n")[0]}`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
@@ -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
|
+
}
|