blume 1.4.2 → 1.5.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/CHANGELOG.md +71 -0
- package/dist/cli/index.js +2260 -1100
- package/dist/cli/index.js.map +123 -117
- package/dist/types/ai/component-markdown.d.ts +14 -4
- package/dist/types/core/base-path.d.ts +8 -0
- package/dist/types/core/config-input.d.ts +87 -27
- package/dist/types/core/config.d.ts +2 -1
- package/dist/types/core/data.d.ts +16 -1
- package/dist/types/core/diagnostics.d.ts +5 -1
- package/dist/types/core/i18n-ui.d.ts +12 -0
- package/dist/types/core/schema.d.ts +116 -15
- package/dist/types/core/sources/types.d.ts +11 -1
- package/dist/types/core/standard-schema.d.ts +7 -3
- package/dist/types/core/types.d.ts +43 -2
- package/dist/types/core/ui-packs/index.d.ts +9 -1
- package/dist/types/openapi/references.d.ts +14 -7
- package/dist/types/seo/x-handle.d.ts +3 -2
- package/docs/advanced/api-reference.mdx +8 -6
- package/docs/configuration/ai.mdx +26 -8
- package/docs/configuration/search.mdx +2 -0
- package/docs/configuration/seo.mdx +1 -1
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/meta.mdx +2 -1
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +35 -1
- package/docs/content/sources.mdx +1 -1
- package/docs/content/versioning.mdx +106 -0
- package/docs/reference/cli.mdx +1 -0
- package/docs/reference/frontmatter.mdx +3 -0
- package/package.json +13 -1
- package/skills/blume-migrate/SKILL.md +2 -2
- package/skills/blume-migrate/references/docusaurus.md +1 -1
- package/skills/blume-migrate/references/fumadocs.md +1 -1
- package/skills/blume-migrate/references/mintlify.md +1 -1
- package/src/ai/agent-readability.ts +40 -12
- package/src/ai/api-catalog.ts +2 -2
- package/src/ai/ask-context.ts +49 -12
- package/src/ai/ask.ts +10 -1
- package/src/ai/component-markdown.ts +80 -43
- package/src/ai/llms.ts +42 -17
- package/src/ai/mcp/data.ts +48 -12
- package/src/ai/mcp/discovery.ts +52 -16
- package/src/ai/mcp/server.ts +280 -125
- package/src/ai/mcp/tools.ts +3 -3
- package/src/ai/skills.ts +32 -9
- package/src/ai/tar.ts +29 -70
- package/src/ai/visibility.ts +2 -2
- package/src/astro/component-slots.ts +2 -0
- package/src/astro/examples.ts +13 -5
- package/src/astro/generate.ts +113 -63
- package/src/astro/integration.ts +13 -2
- package/src/astro/islands.ts +23 -12
- package/src/astro/templates.ts +185 -41
- package/src/audit/agent.ts +16 -31
- package/src/audit/checks/content.ts +26 -11
- package/src/audit/checks/dns-aid.ts +3 -0
- package/src/audit/checks/indexability.ts +24 -6
- package/src/audit/checks/llms.ts +9 -4
- package/src/audit/checks/network.ts +2 -0
- package/src/audit/checks/social.ts +18 -10
- package/src/audit/crawl.ts +78 -25
- package/src/audit/report.ts +20 -19
- package/src/audit/run.ts +15 -5
- package/src/audit/snapshot.ts +29 -6
- package/src/audit/types.ts +25 -3
- package/src/blume-modules.d.ts +5 -1
- package/src/cli/commands/audit.ts +21 -21
- package/src/cli/commands/build.ts +30 -16
- package/src/cli/commands/dev.ts +15 -15
- package/src/cli/commands/doctor.ts +2 -0
- package/src/cli/commands/eject.ts +4 -4
- package/src/cli/commands/eval.ts +24 -30
- package/src/cli/commands/init.ts +9 -9
- package/src/cli/commands/mcp-stdio.ts +3 -0
- package/src/cli/commands/translate.ts +14 -3
- package/src/cli/commands/version.ts +85 -0
- package/src/cli/dev-lock.ts +31 -10
- package/src/cli/eject-scripts.ts +17 -2
- package/src/cli/env.ts +13 -30
- package/src/cli/index.ts +2 -0
- package/src/cli/init/questions.ts +1 -1
- package/src/cli/init/scaffold.ts +41 -13
- package/src/cli/internal-error.ts +1 -0
- package/src/cli/report-format.ts +22 -0
- package/src/components/content/AccordionItem.astro +2 -9
- package/src/components/content/ColorItem.astro +5 -13
- package/src/components/content/Component.astro +12 -8
- package/src/components/content/Frame.astro +2 -12
- package/src/components/content/Prompt.astro +12 -31
- package/src/components/content/Tab.astro +2 -9
- package/src/components/content/Tooltip.astro +1 -9
- package/src/components/content/Update.astro +2 -9
- package/src/components/content/auto-type-table.ts +3 -0
- package/src/components/content/diff.ts +9 -5
- package/src/components/content/github-info.ts +2 -0
- package/src/components/content/inline-markdown.ts +28 -0
- package/src/components/copy-feedback.ts +96 -0
- package/src/components/islands/ask-ai.tsx +111 -34
- package/src/components/islands/hooks.ts +5 -1
- package/src/components/islands/webmcp.ts +49 -12
- package/src/components/layout/Header.astro +25 -1
- package/src/components/layout/NavSelector.astro +11 -2
- package/src/components/layout/NavTree.astro +4 -2
- package/src/components/layout/PageActions.astro +20 -32
- package/src/components/layout/PageLayout.astro +8 -28
- package/src/components/layout/RootLayout.astro +24 -48
- package/src/components/layout/Search.astro +133 -22
- package/src/components/layout/VersionBanner.astro +39 -0
- package/src/components/layout/analytics-client.ts +8 -5
- package/src/components/layout/drawer-inert.ts +31 -0
- package/src/components/layout/hydration-hint.ts +1 -1
- package/src/components/layout/nav-utils.ts +1 -4
- package/src/components/layout/overrides.ts +25 -12
- package/src/components/layout/search/algolia.ts +18 -5
- package/src/components/layout/search/endpoint.ts +3 -0
- package/src/components/layout/search/flexsearch.ts +23 -7
- package/src/components/layout/search/orama-cloud.ts +1 -1
- package/src/components/layout/search/orama.ts +4 -1
- package/src/components/layout/search/pagefind.ts +8 -5
- package/src/components/layout/search/types.ts +45 -1
- package/src/components/layout/search/typesense.ts +19 -3
- package/src/components/openapi/ApiOverview.astro +32 -6
- package/src/components/openapi/AsyncApiOperation.astro +237 -0
- package/src/components/openapi/Bindings.astro +89 -0
- package/src/components/openapi/MethodBadge.astro +3 -0
- package/src/components/openapi/Operation.astro +7 -2
- package/src/components/openapi/PanelTabs.astro +131 -0
- package/src/components/openapi/ParametersTable.astro +2 -0
- package/src/components/openapi/RequestPanel.astro +12 -119
- package/src/components/openapi/async-snippets.ts +174 -0
- package/src/components/openapi/async.ts +348 -0
- package/src/components/openapi/helpers.ts +52 -20
- package/src/components/openapi/panel.ts +11 -8
- package/src/components/openapi/security.ts +102 -29
- package/src/components/openapi/snippets.ts +11 -11
- package/src/components/raf-throttle.ts +21 -0
- package/src/components/slug.ts +14 -0
- package/src/core/base-path.ts +18 -1
- package/src/core/component-overrides.ts +28 -23
- package/src/core/config-input.ts +96 -27
- package/src/core/config.ts +20 -7
- package/src/core/content.ts +3 -1
- package/src/core/data.ts +16 -1
- package/src/core/define-components.ts +5 -0
- package/src/core/diagnostics.ts +46 -38
- package/src/core/frontmatter.ts +74 -4
- package/src/core/graph.ts +137 -53
- package/src/core/i18n-ui.ts +15 -0
- package/src/core/i18n.ts +16 -8
- package/src/core/load-module.ts +1 -0
- package/src/core/manifest.ts +92 -3
- package/src/core/meta.ts +44 -14
- package/src/core/nav-diagnostics.ts +3 -3
- package/src/core/navigation.ts +247 -67
- package/src/core/probe.ts +7 -19
- package/src/core/project-graph.ts +27 -4
- package/src/core/schema.ts +219 -67
- package/src/core/site-url.ts +27 -0
- package/src/core/sources/assets.ts +2 -0
- package/src/core/sources/cache.ts +16 -8
- package/src/core/sources/github-releases.ts +39 -11
- package/src/core/sources/mdx-remote.ts +4 -0
- package/src/core/sources/normalize.ts +93 -22
- package/src/core/sources/notion.ts +76 -22
- package/src/core/sources/portable-text.ts +48 -12
- package/src/core/sources/resolve.ts +1 -0
- package/src/core/sources/sanity.ts +68 -14
- package/src/core/sources/types.ts +17 -1
- package/src/core/sources/watch.ts +1 -1
- package/src/core/standard-schema.ts +9 -3
- package/src/core/text-width.ts +26 -0
- package/src/core/tsconfig-aliases.ts +9 -5
- package/src/core/types.ts +45 -2
- package/src/core/ui-packs/index.ts +9 -1
- package/src/core/version-cut.ts +301 -0
- package/src/core/version.ts +2 -0
- package/src/core/versions.ts +170 -0
- package/src/deploy/adapter-output.ts +5 -2
- package/src/deploy/cloudflare-negotiation.ts +40 -11
- package/src/deploy/robots.ts +2 -1
- package/src/deploy/rss.ts +2 -1
- package/src/deploy/sitemap.ts +89 -8
- package/src/deploy/vercel-negotiation.ts +11 -4
- package/src/eval/agents.ts +13 -10
- package/src/eval/report.ts +5 -18
- package/src/eval/run.ts +2 -2
- package/src/eval/schema.ts +1 -1
- package/src/markdown/base-links.ts +6 -6
- package/src/markdown/directives.ts +7 -1
- package/src/markdown/heading-anchors.ts +17 -6
- package/src/markdown/index.ts +73 -24
- package/src/markdown/inline-code.ts +14 -2
- package/src/markdown/language-icon.ts +6 -2
- package/src/markdown/mdast.ts +18 -4
- package/src/markdown/package-commands.ts +63 -58
- package/src/markdown/table-wrap.ts +4 -1
- package/src/markdown/twoslash.ts +2 -0
- package/src/og/card.ts +50 -33
- package/src/og/derive.ts +43 -27
- package/src/openapi/asyncapi.ts +366 -0
- package/src/openapi/model.ts +135 -66
- package/src/openapi/parse.ts +166 -33
- package/src/openapi/references.ts +47 -22
- package/src/openapi/render-mdx.ts +137 -59
- package/src/openapi/scalar.ts +8 -10
- package/src/openapi/source.ts +126 -29
- package/src/registry/eject.ts +7 -2
- package/src/search/documents.ts +103 -39
- package/src/search/facets.ts +7 -5
- package/src/search/orama-index.ts +117 -32
- package/src/search/popular.ts +10 -5
- package/src/search/providers.ts +2 -2
- package/src/search/sync/index.ts +2 -0
- package/src/search/sync/typesense.ts +4 -2
- package/src/seo/jsonld.ts +24 -6
- package/src/seo/x-handle.ts +8 -3
- package/src/theme/chrome-icons.ts +7 -2
- package/src/theme/fonts.ts +8 -4
- package/src/theme/icons.ts +4 -2
- package/src/theme/palette.ts +27 -15
- package/src/translate/ledger.ts +4 -2
- package/src/translate/meta.ts +15 -6
- package/src/translate/report.ts +10 -19
- package/src/translate/run.ts +29 -38
- package/src/translate/validate.ts +52 -17
- package/src/translate/work-list.ts +0 -0
- package/src/cli/coalesce.ts +0 -43
package/src/translate/report.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { ColorFunction } from "consola/utils";
|
|
|
3
3
|
|
|
4
4
|
import { AGENTS } from "../audit/agent.ts";
|
|
5
5
|
import type { AgentKind } from "../audit/agent.ts";
|
|
6
|
+
import { duration, money, seconds } from "../cli/report-format.ts";
|
|
6
7
|
import { countBySeverity } from "../core/diagnostics.ts";
|
|
7
8
|
import type { Diagnostic } from "../core/types.ts";
|
|
8
9
|
import type {
|
|
@@ -28,17 +29,17 @@ import type {
|
|
|
28
29
|
|
|
29
30
|
const ESC = String.fromCodePoint(27);
|
|
30
31
|
|
|
31
|
-
const GLYPH
|
|
32
|
+
const GLYPH = {
|
|
32
33
|
failed: "✖",
|
|
33
34
|
partial: "!",
|
|
34
35
|
translated: "✔",
|
|
35
|
-
}
|
|
36
|
+
} satisfies Record<TranslateItemStatus, string>;
|
|
36
37
|
|
|
37
|
-
const STATUS_COLOR
|
|
38
|
+
const STATUS_COLOR = {
|
|
38
39
|
failed: colors.red,
|
|
39
40
|
partial: colors.yellow,
|
|
40
41
|
translated: colors.green,
|
|
41
|
-
}
|
|
42
|
+
} satisfies Record<TranslateItemStatus, ColorFunction>;
|
|
42
43
|
|
|
43
44
|
export const SPINNER_FRAMES = [
|
|
44
45
|
"⠋",
|
|
@@ -59,20 +60,6 @@ export const SPINNER_INTERVAL_MS = 80;
|
|
|
59
60
|
/** The clear-to-start-of-line prefix every TTY rewrite uses. */
|
|
60
61
|
const REWRITE = `\r${ESC}[K`;
|
|
61
62
|
|
|
62
|
-
const seconds = (ms: number): string => `${(ms / 1000).toFixed(1)}s`;
|
|
63
|
-
|
|
64
|
-
const money = (cost: number | undefined): string =>
|
|
65
|
-
cost === undefined ? "" : `$${cost.toFixed(2)}`;
|
|
66
|
-
|
|
67
|
-
const duration = (ms: number): string => {
|
|
68
|
-
if (ms < 60_000) {
|
|
69
|
-
return seconds(ms);
|
|
70
|
-
}
|
|
71
|
-
const minutes = Math.floor(ms / 60_000);
|
|
72
|
-
const rest = Math.round((ms % 60_000) / 1000);
|
|
73
|
-
return `${minutes}m ${rest}s`;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
63
|
/** `docs/guides/install.mdx → fr`, or the batched meta call's label. */
|
|
77
64
|
export const itemLabel = (item: WorkItem): string =>
|
|
78
65
|
item.kind === "page"
|
|
@@ -89,8 +76,12 @@ export const spinnerLine = (
|
|
|
89
76
|
total: number,
|
|
90
77
|
frame: number
|
|
91
78
|
): string => {
|
|
79
|
+
// SAFETY: the renderer only paints while at least one item is active, so the
|
|
80
|
+
// oldest active entry exists.
|
|
92
81
|
const first = active[0] as WorkItem;
|
|
93
82
|
const more = active.length > 1 ? ` (+${active.length - 1} more)` : "";
|
|
83
|
+
// SAFETY: `frame % SPINNER_FRAMES.length` is always an index into the
|
|
84
|
+
// non-empty frames array.
|
|
94
85
|
return ` ${colors.cyan(SPINNER_FRAMES[frame % SPINNER_FRAMES.length] as string)} ${itemLabel(first)}${more} ${colors.dim(`${done}/${total}`)}`;
|
|
95
86
|
};
|
|
96
87
|
|
|
@@ -229,7 +220,7 @@ export const checkReportJson = (workList: TranslateWorkList): string => {
|
|
|
229
220
|
};
|
|
230
221
|
|
|
231
222
|
/** One run result lowered to JSON-friendly, root-relative fields. */
|
|
232
|
-
const resultJson = (result: TranslateItemResult)
|
|
223
|
+
const resultJson = (result: TranslateItemResult) => ({
|
|
233
224
|
costUsd: result.costUsd,
|
|
234
225
|
detail: result.detail,
|
|
235
226
|
durationMs: result.durationMs,
|
package/src/translate/run.ts
CHANGED
|
@@ -2,9 +2,11 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { mkdtemp, readFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
|
|
5
|
+
import pLimit from "p-limit";
|
|
6
|
+
import pMap from "p-map";
|
|
5
7
|
import { join } from "pathe";
|
|
6
8
|
|
|
7
|
-
import { AGENTS
|
|
9
|
+
import { AGENTS } from "../audit/agent.ts";
|
|
8
10
|
import type { AgentKind } from "../audit/agent.ts";
|
|
9
11
|
import { writeTextAtomic } from "../core/fs-atomic.ts";
|
|
10
12
|
import type { BlumeProject } from "../core/project-graph.ts";
|
|
@@ -56,8 +58,9 @@ export interface TranslateRunOptions {
|
|
|
56
58
|
* Called after each finished item to flush the ledger to disk, so an
|
|
57
59
|
* interrupted run keeps everything already translated. Calls are serialized
|
|
58
60
|
* here — concurrent workers finishing together never race the same file.
|
|
61
|
+
* The flush's result (`writeLedger`'s wrote-or-not boolean) is ignored.
|
|
59
62
|
*/
|
|
60
|
-
persistLedger?: () => Promise<
|
|
63
|
+
persistLedger?: () => Promise<boolean | undefined>;
|
|
61
64
|
project: BlumeProject;
|
|
62
65
|
/** The spawn function — injectable so tests never launch a real agent. */
|
|
63
66
|
run?: HeadlessRunner;
|
|
@@ -89,9 +92,9 @@ interface RunContext {
|
|
|
89
92
|
const metaDirKey = (dir: string): string => (dir === "" ? "." : dir);
|
|
90
93
|
|
|
91
94
|
/**
|
|
92
|
-
* One headless agent call. A
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
+
* One headless agent call. A missing executable rejects with ENOENT on every
|
|
96
|
+
* platform (the runner spawns without a shell), which the command layer turns
|
|
97
|
+
* into an install hint.
|
|
95
98
|
*/
|
|
96
99
|
const invokeAgent = async (
|
|
97
100
|
context: RunContext,
|
|
@@ -104,13 +107,6 @@ const invokeAgent = async (
|
|
|
104
107
|
translateAgentArgs(context.kind, messagePath),
|
|
105
108
|
{ cwd: context.dir, prompt, timeoutMs: context.timeoutMs }
|
|
106
109
|
);
|
|
107
|
-
if (!result.timedOut && result.code === WINDOWS_COMMAND_NOT_FOUND) {
|
|
108
|
-
const missing = new Error(
|
|
109
|
-
`${context.bin} was not found on PATH`
|
|
110
|
-
) as NodeJS.ErrnoException;
|
|
111
|
-
missing.code = "ENOENT";
|
|
112
|
-
throw missing;
|
|
113
|
-
}
|
|
114
110
|
return await readAgentOutput(context.kind, result, messagePath);
|
|
115
111
|
};
|
|
116
112
|
|
|
@@ -134,6 +130,8 @@ const runPageItem = async (
|
|
|
134
130
|
});
|
|
135
131
|
|
|
136
132
|
const sourceText = await readFile(item.sourcePath, "utf-8");
|
|
133
|
+
// SAFETY: `targets` maps every configured locale, and work items only carry
|
|
134
|
+
// configured locale codes.
|
|
137
135
|
const target = context.targets.get(item.locale) as LocaleConfig;
|
|
138
136
|
// A hand-authored translation can live at a non-canonical name (see
|
|
139
137
|
// WorkStatus); the disk probe finds only canonical targets, and a miss just
|
|
@@ -180,6 +178,8 @@ const runMetaItem = async (
|
|
|
180
178
|
const titles = Object.fromEntries(
|
|
181
179
|
item.entries.map((entry) => [metaDirKey(entry.meta.dir), entry.meta.title])
|
|
182
180
|
);
|
|
181
|
+
// SAFETY: `targets` maps every configured locale, and work items only carry
|
|
182
|
+
// configured locale codes.
|
|
183
183
|
const target = context.targets.get(item.locale) as LocaleConfig;
|
|
184
184
|
const output = await invokeAgent(
|
|
185
185
|
context,
|
|
@@ -262,6 +262,7 @@ export const runTranslate = async (
|
|
|
262
262
|
if (!i18n) {
|
|
263
263
|
throw new Error("blume translate requires i18n to be configured");
|
|
264
264
|
}
|
|
265
|
+
// SAFETY: the config schema requires `defaultLocale` to be one of `locales`.
|
|
265
266
|
const source = i18n.locales.find(
|
|
266
267
|
(locale) => locale.code === i18n.defaultLocale
|
|
267
268
|
) as LocaleConfig;
|
|
@@ -277,42 +278,31 @@ export const runTranslate = async (
|
|
|
277
278
|
};
|
|
278
279
|
|
|
279
280
|
const { items } = options.workList;
|
|
280
|
-
const results: TranslateItemResult[] = Array.from({ length: items.length });
|
|
281
281
|
const concurrency = Math.max(
|
|
282
282
|
1,
|
|
283
283
|
Math.min(options.concurrency ?? 1, items.length || 1)
|
|
284
284
|
);
|
|
285
285
|
|
|
286
|
-
// The persist
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
const persist = (): Promise<
|
|
290
|
-
|
|
291
|
-
// oxlint-disable-next-line promise/prefer-await-to-then
|
|
292
|
-
persisting = persisting.then(() => options.persistLedger?.());
|
|
293
|
-
return persisting;
|
|
294
|
-
};
|
|
286
|
+
// The persist mutex: ledger flushes from concurrent lanes are serialized so
|
|
287
|
+
// two lanes never write the ledger file at the same time.
|
|
288
|
+
const persistLimit = pLimit(1);
|
|
289
|
+
const persist = (): Promise<boolean | undefined> =>
|
|
290
|
+
persistLimit(() => options.persistLedger?.());
|
|
295
291
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const index = nextIndex;
|
|
300
|
-
nextIndex += 1;
|
|
301
|
-
const item = items[index] as WorkItem;
|
|
292
|
+
const results = await pMap(
|
|
293
|
+
items,
|
|
294
|
+
async (item: WorkItem, index): Promise<TranslateItemResult> => {
|
|
302
295
|
options.onProgress?.({
|
|
303
296
|
index,
|
|
304
297
|
item,
|
|
305
298
|
kind: "item-start",
|
|
306
299
|
total: items.length,
|
|
307
300
|
});
|
|
308
|
-
// oxlint-disable-next-line no-await-in-loop -- each worker is a serial lane
|
|
309
301
|
const result = await (item.kind === "page"
|
|
310
302
|
? runPageItem(item, index, context, options.ledger)
|
|
311
303
|
: runMetaItem(item, index, context, options.ledger));
|
|
312
|
-
|
|
313
|
-
//
|
|
314
|
-
// loses at most the in-flight items.
|
|
315
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
304
|
+
// Flush this item's stamps before the slot frees for the next item, so
|
|
305
|
+
// a kill loses at most the in-flight items.
|
|
316
306
|
await persist();
|
|
317
307
|
options.onProgress?.({
|
|
318
308
|
index,
|
|
@@ -320,9 +310,10 @@ export const runTranslate = async (
|
|
|
320
310
|
result,
|
|
321
311
|
total: items.length,
|
|
322
312
|
});
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
313
|
+
return result;
|
|
314
|
+
},
|
|
315
|
+
{ concurrency }
|
|
316
|
+
);
|
|
326
317
|
|
|
327
318
|
const diagnostics: Diagnostic[] = [];
|
|
328
319
|
for (const result of results) {
|
|
@@ -331,11 +322,11 @@ export const runTranslate = async (
|
|
|
331
322
|
}
|
|
332
323
|
}
|
|
333
324
|
|
|
334
|
-
const counts
|
|
325
|
+
const counts = {
|
|
335
326
|
failed: 0,
|
|
336
327
|
partial: 0,
|
|
337
328
|
translated: 0,
|
|
338
|
-
}
|
|
329
|
+
} satisfies Record<TranslateItemStatus, number>;
|
|
339
330
|
for (const result of results) {
|
|
340
331
|
counts[result.status] += 1;
|
|
341
332
|
}
|
|
@@ -13,6 +13,24 @@ export type ValidationResult =
|
|
|
13
13
|
| { ok: true; text: string }
|
|
14
14
|
| { ok: false; reason: string };
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* A parsed YAML frontmatter value (agent meta replies parse from JSON into the
|
|
18
|
+
* same shape). js-yaml can also mint Dates and other rich scalars; the
|
|
19
|
+
* traversal below only ever distinguishes "keyed object" from "string", so
|
|
20
|
+
* they ride along as the object arm.
|
|
21
|
+
*/
|
|
22
|
+
type FrontmatterValue =
|
|
23
|
+
| string
|
|
24
|
+
| number
|
|
25
|
+
| boolean
|
|
26
|
+
| null
|
|
27
|
+
| FrontmatterValue[]
|
|
28
|
+
| FrontmatterData;
|
|
29
|
+
|
|
30
|
+
interface FrontmatterData {
|
|
31
|
+
[key: string]: FrontmatterValue;
|
|
32
|
+
}
|
|
33
|
+
|
|
16
34
|
const FRONTMATTER_OPEN = /^---\r?\n/u;
|
|
17
35
|
const FENCE_LINE = /^\s*(?:```|~~~)/u;
|
|
18
36
|
|
|
@@ -33,27 +51,41 @@ export const stripOuterFence = (text: string): string => {
|
|
|
33
51
|
const countFenceLines = (text: string): number =>
|
|
34
52
|
text.split("\n").filter((line) => FENCE_LINE.test(line)).length;
|
|
35
53
|
|
|
36
|
-
const
|
|
37
|
-
|
|
54
|
+
const isKeyedObject = (
|
|
55
|
+
value: FrontmatterValue | undefined
|
|
56
|
+
): value is FrontmatterData => typeof value === "object" && value !== null;
|
|
57
|
+
|
|
58
|
+
const isString = (value: FrontmatterValue | undefined): value is string =>
|
|
59
|
+
typeof value === "string";
|
|
60
|
+
|
|
61
|
+
const getPath = (
|
|
62
|
+
data: FrontmatterValue,
|
|
63
|
+
path: readonly string[]
|
|
64
|
+
): FrontmatterValue | undefined => {
|
|
65
|
+
let value: FrontmatterValue | undefined = data;
|
|
38
66
|
for (const key of path) {
|
|
39
|
-
if (
|
|
67
|
+
if (!isKeyedObject(value)) {
|
|
40
68
|
return;
|
|
41
69
|
}
|
|
42
|
-
value =
|
|
70
|
+
value = value[key];
|
|
43
71
|
}
|
|
44
72
|
return value;
|
|
45
73
|
};
|
|
46
74
|
|
|
47
75
|
/** Set `path` on `data`; only called for paths whose parents exist in `data`. */
|
|
48
76
|
const setPath = (
|
|
49
|
-
data:
|
|
77
|
+
data: FrontmatterData,
|
|
50
78
|
path: readonly string[],
|
|
51
79
|
value: string
|
|
52
80
|
): void => {
|
|
53
81
|
let parent = data;
|
|
54
82
|
for (const key of path.slice(0, -1)) {
|
|
55
|
-
|
|
83
|
+
// SAFETY: callers only set paths that getPath already resolved to a string
|
|
84
|
+
// on this same (cloned) data, so every intermediate step is a keyed object.
|
|
85
|
+
parent = parent[key] as FrontmatterData;
|
|
56
86
|
}
|
|
87
|
+
// SAFETY: every TRANSLATABLE_KEY_PATHS entry is a non-empty tuple, so the
|
|
88
|
+
// path always has a final key.
|
|
57
89
|
parent[path.at(-1) as string] = value;
|
|
58
90
|
};
|
|
59
91
|
|
|
@@ -84,7 +116,7 @@ export const validateTranslation = (
|
|
|
84
116
|
};
|
|
85
117
|
}
|
|
86
118
|
|
|
87
|
-
let parsed: { content: string; data:
|
|
119
|
+
let parsed: { content: string; data: FrontmatterData };
|
|
88
120
|
try {
|
|
89
121
|
parsed = matter(candidate);
|
|
90
122
|
} catch {
|
|
@@ -114,13 +146,13 @@ export const validateTranslation = (
|
|
|
114
146
|
// Reconciliation by reconstruction: start from the SOURCE data and overlay
|
|
115
147
|
// only the translatable key paths where both sides hold a string and the
|
|
116
148
|
// translation is non-empty.
|
|
117
|
-
const data = structuredClone(source.data)
|
|
149
|
+
const data: FrontmatterData = structuredClone(source.data);
|
|
118
150
|
for (const path of TRANSLATABLE_KEY_PATHS) {
|
|
119
151
|
const original = getPath(source.data, path);
|
|
120
152
|
const translated = getPath(parsed.data, path);
|
|
121
153
|
if (
|
|
122
|
-
|
|
123
|
-
|
|
154
|
+
isString(original) &&
|
|
155
|
+
isString(translated) &&
|
|
124
156
|
translated.trim() !== ""
|
|
125
157
|
) {
|
|
126
158
|
setPath(data, path, translated);
|
|
@@ -133,6 +165,12 @@ export const validateTranslation = (
|
|
|
133
165
|
};
|
|
134
166
|
};
|
|
135
167
|
|
|
168
|
+
/** A meta-batch parse: recovered titles by key, plus the keys still missing. */
|
|
169
|
+
export interface MetaTitlesResult {
|
|
170
|
+
titles: Record<string, string>;
|
|
171
|
+
missing: string[];
|
|
172
|
+
}
|
|
173
|
+
|
|
136
174
|
/**
|
|
137
175
|
* Extract translated sidebar titles from a meta reply: tolerant first-`{`
|
|
138
176
|
* to-last-`}` extraction (the eval `parseVerdict` idiom). Keys missing or
|
|
@@ -141,10 +179,10 @@ export const validateTranslation = (
|
|
|
141
179
|
export const parseMetaTitles = (
|
|
142
180
|
agentText: string,
|
|
143
181
|
expectedKeys: readonly string[]
|
|
144
|
-
):
|
|
182
|
+
): MetaTitlesResult => {
|
|
145
183
|
const start = agentText.indexOf("{");
|
|
146
184
|
const end = agentText.lastIndexOf("}");
|
|
147
|
-
let parsed:
|
|
185
|
+
let parsed: FrontmatterValue | undefined;
|
|
148
186
|
if (start !== -1 && end > start) {
|
|
149
187
|
try {
|
|
150
188
|
parsed = JSON.parse(agentText.slice(start, end + 1));
|
|
@@ -152,16 +190,13 @@ export const parseMetaTitles = (
|
|
|
152
190
|
parsed = undefined;
|
|
153
191
|
}
|
|
154
192
|
}
|
|
155
|
-
const record =
|
|
156
|
-
typeof parsed === "object" && parsed !== null
|
|
157
|
-
? (parsed as Record<string, unknown>)
|
|
158
|
-
: {};
|
|
193
|
+
const record: FrontmatterData = isKeyedObject(parsed) ? parsed : {};
|
|
159
194
|
|
|
160
195
|
const titles: Record<string, string> = {};
|
|
161
196
|
const missing: string[] = [];
|
|
162
197
|
for (const key of expectedKeys) {
|
|
163
198
|
const value = record[key];
|
|
164
|
-
if (
|
|
199
|
+
if (isString(value) && value.trim() !== "") {
|
|
165
200
|
titles[key] = value;
|
|
166
201
|
} else {
|
|
167
202
|
missing.push(key);
|
|
Binary file
|
package/src/cli/coalesce.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Wrap an async task so it never runs concurrently with itself. Triggering the
|
|
3
|
-
* returned function while a run is in flight coalesces into a single trailing
|
|
4
|
-
* run after the current one settles.
|
|
5
|
-
*
|
|
6
|
-
* Dev regeneration (`scanProject` + `generateRuntime`) is expensive on a large
|
|
7
|
-
* project — a full content re-scan that allocates big strings. A plain debounce
|
|
8
|
-
* still lets a fast burst of watch events (or, before it was fixed, a `.blume/`
|
|
9
|
-
* watch storm) start a new scan before the previous finished, piling up
|
|
10
|
-
* overlapping scans until the heap is exhausted (observed as an OOM after
|
|
11
|
-
* minutes of looping). Single-flighting bounds it to one scan at a time while
|
|
12
|
-
* still guaranteeing a final run reflects the latest change.
|
|
13
|
-
*
|
|
14
|
-
* The task must not reject: a rejection would surface as an unhandled promise
|
|
15
|
-
* rejection, so callers handle their own errors and always resolve.
|
|
16
|
-
*/
|
|
17
|
-
export const coalescedRunner = (task: () => Promise<void>): (() => void) => {
|
|
18
|
-
let inFlight: Promise<void> | null = null;
|
|
19
|
-
let pending = false;
|
|
20
|
-
|
|
21
|
-
// Drain any run requested during the current run, then release the lock. The
|
|
22
|
-
// `inFlight` promise is assigned synchronously by the caller below, so a
|
|
23
|
-
// re-entrant trigger sees the lock immediately and only sets `pending`.
|
|
24
|
-
const cycle = async (): Promise<void> => {
|
|
25
|
-
try {
|
|
26
|
-
do {
|
|
27
|
-
pending = false;
|
|
28
|
-
// oxlint-disable-next-line no-await-in-loop -- serialized by design
|
|
29
|
-
await task();
|
|
30
|
-
} while (pending);
|
|
31
|
-
} finally {
|
|
32
|
-
inFlight = null;
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
return () => {
|
|
37
|
-
if (inFlight) {
|
|
38
|
-
pending = true;
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
inFlight = cycle();
|
|
42
|
-
};
|
|
43
|
-
};
|