PolyStella — an Astro integration that translates content into additional locales at build time using AI, caches translations in Cloudflare R2, and injects locale-prefixed routes.
This file is the entry point for coding agents working on the PolyStella package itself. Four companion docs:
-
PACKAGE_ARCHITECTURE.md— post-migration package ownership, dependency direction, key files, and boundary checks. -
ARCHITECTURE.md— system design, invariants, glossary, per-subsystem reference. The "why" answers. -
skills/polystella-contributor/SKILL.md— step-by-step recipes for common contributor tasks (add an adapter, add a CLI subcommand, debug a translation, etc.). -
skills/polystella-consumer/SKILL.md— for agents working in a downstream Astro project that depends on this package.
All cross-references use stable slug anchors (#cache-key), not
section numbers. Inserting new sections never breaks links.
| Command | What it does |
|---|---|
pnpm test |
Run all package, Astro, workerd, and boundary tests. |
pnpm test:watch |
Run the Astro package tests in watch mode. |
pnpm build |
Build all five public packages. Astro emits its standalone CLI and library entries under packages/astro/dist/. |
pnpm typecheck |
Build all five packages, then typecheck every public package against its package-local tsconfig.json. |
pnpm check:packages |
Pack all five public packages and exercise every export from a clean temporary consumer. |
pnpm changeset |
Add a Changesets entry for package-affecting work. Use pnpm changeset add --empty only for changes that intentionally do not need a package release. |
No lint step yet.
Test counts age. The authoritative count is
pnpm test's output; the number here is a snapshot pinned bypackages/astro/tests/docs.test.ts.
Task → entry-point file(s) → key contract → deep-dive link.
| Task | Entry point | Contract | See |
|---|---|---|---|
| Add a file-format adapter | packages/adapters/src/adapters/<name>.ts; wrap/register in packages/astro/src/parsing/ |
FileAdapter in packages/adapters/src/adapter.ts |
#adapter-contract; recipe in contributor SKILL |
| Add a CLI subcommand | Handler in packages/astro/src/cli/<name>.ts; register in packages/astro/src/cli.ts |
Argv parser + run<Name>(args, deps) |
Recipe in contributor SKILL |
| Add a translation provider | Factory in packages/providers/src/; map config in packages/astro/src/translation/provider.ts |
Translator in packages/core/src/translator.ts |
#translator-contract; recipe in contributor SKILL |
| Change cache key formula | packages/astro/src/storage/hash.ts |
Invariant 1 — cache-wide invalidation | #cache-key |
| Edit translation batching | packages/core/src/{batch,translate-segments}.ts |
Invariant 2 — flat(groups) === segments |
#translation-batching |
| Modify cache/storage behaviour | packages/astro/src/storage/{cache,r2,prune,local-cache,report}.ts |
Apply-before-PUT (Invariant 3); index isolation (Invariant 4) | #cache-write-order, #local-staging-index |
| Modify runtime APIs (entry/collection/href/middleware) | packages/astro/src/runtime/* |
Bridge timing (Invariant 5); per-locale closures | #runtime-bridge |
| Modify routing shims | packages/astro/src/routing/{shim,expand-routes,walk-pages}.ts |
Stale shims nuked per build; CSS via routesImports |
#routing-shims |
| Edit UI-string handling | packages/astro/src/i18n/*, packages/astro/src/cli/{check,sync,translate}-ui.ts |
Three drift modes; layout-aware writer; {{token}} preservation |
#ui-strings |
| Edit catalog-only adoption | packages/astro/src/catalog/*, packages/astro/src/i18n/{translate,drift,sync}.ts |
Pure imports; middleware binds only t + lhref |
CATALOG_ONLY_PLAN.md; consumer SKILL |
| Edit content-collection wiring | packages/astro/src/content/* |
Sibling collections; custom-loader wrapper; bridge timing | #runtime-bridge |
| Debug a translation that's wrong | Start: pnpm translate --dry-run to inspect planned R2 keys; LOG_LEVEL=debug for batch detail |
— | Recipe in contributor SKILL |
| Tune cold-cache build performance | r2.bulkListOnStart, concurrency, batchInputTokenBudget knobs |
— | #bulk-prelist, #translation-batching |
If your task isn't on this list, the answer is in packages/astro/src/<area>/
matching one of the subsystem sections in
ARCHITECTURE.md.
The root is private. Reusable package code uses standard Web APIs and must
work without nodejs_compat, although consumers may enable it. Import
low-level contracts from core, formats from adapters, and transports from
providers; do not add compatibility shims to the Astro package.
Hard contracts. Don't violate without thinking carefully — link back to the explanatory section when adding code that touches one.
- Cache key formula —
sha256(body + selectedFrontmatterValues + glossaryHash + modelId + optionalExtractionPolicyHash). Changing any input is a cache-wide invalidation. → #cache-key - Group flattening —
flat(adapter.groupSegments(...)) === segments(reference-equal, order-preserved). Asserted at runtime. → #translation-batching - Apply before PUT —
applyTranslationsproduces the exact bytes PUT to R2; markers woven insideapply, never after. → #cache-write-order - Local cache index write isolation — workers write to
nextLocalCacheIndexonly; read only fromlocalCacheIndex. → #local-staging-index - Bridge timing — translation runs in
astro:config:setup, NOTbuild:start. → #hook-timing - URL-rewrite idempotence — both layers safe to apply twice. → #url-rewriting
- Path separators — R2 keys use
/, local paths usepath.sep. - Permanent vs retriable provider errors — only
PermanentProviderErrorshort-circuits the retry loop. → #translator-contract - Strict tsconfig — no
any, no!outside tests; useunknown+ type guards; declare callable-undefinedoptionals asfoo?: T | undefined.
- Run
pnpm testbefore pushing. Tests must stay green. - Add a Changesets entry for every change that affects the published
package, its documented behaviour, or release-facing contributor
guidance. Use
pnpm changesetfor release notes; usepnpm changeset add --emptyonly when the change deliberately does not require a package release (for example, docs-site-only or CI-only maintenance). - Let Changesets version core, adapters, and providers independently while
keeping Astro and its compatibility package in one fixed group; do not
manually bump individual manifests.
POLYSTELLA_VERSIONinpackages/astro/src/version.tsreads the Astro manifest at module-load time and flows topackages/astro/dist/version.jsafterpnpm build. → #version-constant - Mirror filesystem path semantics across OS: forward slashes for R2
keys,
path.sepfor local I/O. - Forward
signal: AbortSignalwhen adding a new async function on the hot path;signal?.throwIfAborted()at await boundaries that could otherwise run indefinitely. → #abortsignal
- Adding a new runtime dependency.
- Adding a new adapter (introduces a new file extension).
- Widening the permanent-provider-error set (HTTP status →
PermanentProviderError).
- Re-introduce long-form design comments that were removed; they
live in
ARCHITECTURE.mdnow. In-code comments stay tight ("why" only). - Commit R2 credentials or live API keys to fixtures.
- Widen the AI-translation marker contract without bumping the cache key formula. Cache hits return the marker verbatim, so an existing cache must remain interpretable.
- Use
any. Useunknownand narrow with type guards. - Use
!non-null assertions outside test code.
Tiered so you can scan the ones that matter for your change.
- Cache-key formula stability (#cache-key). Any change is a cache-wide invalidation; coordinate via changelog.
- Apply-before-PUT (#cache-write-order). The marker MUST be woven inside
apply. Doing it after the PUT lets cache hits return marker-less bytes. - Bridge timing (#hook-timing). Moving translation to
build:startmakes sibling collections see empty staging dirs at content-sync time. - Workers AI default
maxTokensis too low (#translator-contract). Default in our schema is8192. Lowering it truncates multi-segment translations into invalid JSON. {{token}}validation runs outsidetranslateBatch(#ui-strings). The orchestrator's retry wrapper setsmaxRetries: 0ontranslateBatchso the retry loop is single-layer. Don't add a second retry layer.
- Override files at
i18n/overrides/{locale}/<path>win over AI output verbatim. They run through the URL rewriter (idempotent) but are NOT written to R2. Cache key changes don't affect overrides. - MDX vs MD —
.mdxuses MDX syntax rules (imports/exports, JSX, expressions);.mdstays plain Markdown. Route by extension; never apply MDX rules to.md. - Drift check fails on empty placeholders (#ui-strings).
pnpm i18n:syncalone leaves the tree non-shippable untilpnpm i18n:translate(or a hand-edit) fills the placeholders. Intentionally-blank labels are supported via matching""in the source dict. - UI-string sync writer is layout-aware (#ui-strings). Running
prettier --writeon a synced file collapses the blank-line section breaks. The pre-commit hook only runsprettier --check, so it doesn't trip — but a manualpnpm formatwill churn diffs. translate-uipre-scans, then runs queued locales in parallel (#ui-strings). Complete locale JSONs are skipped before provider setup. Queued locales are capped at 3 concurrent workers; each locale is internally split into small sequential request batches. Workers MUST catch every error internally and record it on the per-locale outcome — never re-throw. Re-throwing kills the rest of the run.adapter.parseis called once per source per live run (#dry-run-vs-live). Adapterparseshould still be idempotent — calling it twice during debugging or in tests must not produce different output.
- R2 bulk pre-list (#bulk-prelist). Disable via
r2.bulkListOnStart: falsefor caches with >10k keys per locale. - Vitest
singleThread: trueis faster than multi-worker at current scale (~1.5s vs ~1.6s; per-worker startup dominates). Revisit when the suite outgrows the overhead. - Heartbeat
setIntervalisunref()'d (#heartbeat) so a stalled pool doesn't block process exit. Don't remove the unref.
Before pushing:
pnpm testmust pass.pnpm typecheckmust pass (strict mode).pnpm check:packagesmust pass for packaging or export changes.- For changes to the translation pipeline, run end-to-end against a
real consumer's fixtures:
polystella translate --dry-runwalks the full pipeline without hitting AI/R2. - For changes to UI-string sync / translate:
polystella check-ui(read-only),polystella sync-ui --check(read-only),polystella translate-ui --sync-only(offline, mutates JSONs).
ARCHITECTURE.md— system design and rationale.README.md— user-facing introduction.skills/polystella-contributor/SKILL.md— recipes for contributor tasks.skills/polystella-consumer/SKILL.md— for agents in downstream consumer repos.llms.txt— index for retrieval-time discovery.