generated-files
Principle: every list or type that exists on both the Rust side and the
TypeScript side has exactly one hand-written source. The other copies are
produced by a generator. If you type the second copy by hand, it will drift —
and drift here has already shipped real bugs (a permission gate that allowed
everything, a publish command that rejected a valid manifest).
The two rules
- Never edit a generated file. Edit its Rust/JSON source, then run the generator.
- Never create a new hand-synced mirror. If you need Rust data in TS, write a generator — do not paste the list and add a "keep in sync" comment.
Rule 1 — the registry
Files below are outputs. Editing them directly is always wrong; your edit is erased on the next regen and CI fails on the diff.
SOURCE (hand-written) GENERATOR OUTPUT (never edit)
┌──────────────────────────────┐ ┌───────────────────────────┐ ┌────────────────────────────────┐
│ src-tauri/src/permissions.rs │──▶│ generate-permission- │──▶│ src/services/extension/ │
│ get_required_permission │ │ catalog.mjs │ ├▶│ gatedPermissions.ts │
└──────────────────────────────┘ │ (writes BOTH copies) │ │ │ asyar-sdk/cli/lib/ │
└───────────────────────────┘ └▶│ gatedPermissions.ts │
┌──────────────────────────────┐ ┌───────────────────────────┐ ├────────────────────────────────┤
│ src-tauri/src/error.rs │──▶│ generate-diagnostic- │──▶│ src/services/diagnostics/ │
│ src-tauri/src/search_engine/ │ │ kinds.mjs │ │ kinds.ts │
│ mod.rs (error variants) │ └───────────────────────────┘ ├────────────────────────────────┤
├──────────────────────────────┤ ┌───────────────────────────┐ │ │
│ src-tauri/src/runtimes/ │──▶│ generate-known- │──▶│ asyar-sdk/cli/lib/ │
│ catalog.fallback.json │ │ runtimes.mjs (in sdk) │ │ knownRuntimes.ts │
├──────────────────────────────┤ ├───────────────────────────┤ ├────────────────────────────────┤
│ any #[derive(specta::Type)] │──▶│ cargo test export_bindings│──▶│ src/bindings.ts (+ the 3 │
│ registered in models.rs:: │ │ -- --ignored │ │ re-export shims under │
│ export_bindings │ │ │ │ services/search/types/) │
├──────────────────────────────┤ ├───────────────────────────┤ ├────────────────────────────────┤
│ upstream emoji dataset │──▶│ extensions/emoji/scripts/ │──▶│ extensions/emoji/src/data/ │
│ │ │ generate-data.mjs │ │ emojis.ts │
└──────────────────────────────┘ └───────────────────────────┘ └────────────────────────────────┘
Concrete commands:
| You changed | Run (from this directory) |
|---|---|
permissions.rs::get_required_permission |
pnpm gen:permission-catalog — in asyar-launcher/ |
AppError / SearchError variants |
pnpm gen:diagnostic-kinds — in asyar-launcher/ |
runtimes/catalog.fallback.json |
npm run gen:known-runtimes — in asyar-sdk/ |
| any of the above (blanket) | pnpm gen:all — in that package |
a specta::Type struct, or the export list |
cargo test export_bindings -- --ignored — in src-tauri/ |
| emoji dataset | pnpm --filter org.asyar.emoji run gen:data — anywhere |
Note the launcher's package name is asyar, not asyar-launcher — so a
--filter asyar-launcher will not resolve. cd into the package instead.
You usually don't need to run the first four by hand. pretest,
pretest:run, and prebuild all chain gen:all in both packages, so
pnpm test self-heals them. bindings.ts is the exception — it needs a full
cargo compile, so it is deliberately not in any pretest hook. Changing a
specta type means you regenerate it yourself or CI fails.
How to recognise a generated file before you edit it
Line 1 says so. Check it:
// AUTO-GENERATED by scripts/generate-permission-catalog.mjs — do not edit.
// This file has been generated by Specta. Do not edit this file manually.
They are also all listed in .prettierignore — reformatting them just fights
the generator's own output style, and diagnostics.rs's kinds_contract test
byte-matches kinds.ts's exact "kind" quoting.
The safety nets that will catch you
.github/workflows/test-and-lint.yml→ "Verify generated Rust-derived files are up to date": the test step already rangen:all, so agit diff --exit-codeon the four generated TS files fails the build if the committed copy was stale.- A second step regenerates
bindings.tsand diffs it the same way. src-tauri/src/diagnostics.rs::kinds_contractasserts every Rust kind is present inkinds.ts, with the error message telling you which script to run.asyar-launcher/scripts/permission-coverage.test.mjs(picked up by vitest via thescripts/**/*.test.mjsglob) asserts every SDK.invoke()call type is classified gated-or-public in the Rust gate.
A red CI on any of these means "you edited a source and skipped the generator," not "the generator is broken."
Rule 2 — don't hand-write a new mirror
The failure shape to watch for, in your own output:
// ❌ Kept in sync by hand with src-tauri/src/permissions.rs
const GATED = ['clipboard:read', 'network' /* ... */];
That comment is the bug. A comment cannot keep anything in sync. Every instance of this pattern found in this repo had already drifted by the time it was found:
permissionCatalog.test.ts's 39-item array was missingfeedback:announce, so its "covers every permission" test had a silent hole.manifest.test.tscarried a second, 7-item permission mirror in a different package.VALID_RUNTIMESwas hand-typed againstknown_names().screen:pick-colorreached the Rust gate and the JS catalog but not the CLI validator →asyar publishrejected a permission the launcher enforced.
If you are about to write a TS constant whose values come from a Rust file: stop and write a generator instead.
Recipe for a new generated file
Copy asyar-launcher/scripts/generate-permission-catalog.mjs — it is the
reference implementation, including the two-output case.
- Parse the real source. Read the Rust/JSON file and regex the values out,
anchored to a specific function or block (that generator anchors on
fn get_required_permission(...) { ... }and throws if the anchor is gone — so a rename fails loudly instead of emitting an empty list). - Dedupe and sort, then
JSON.stringify(sorted, null, 2)for the literal. - Write a banner naming the generator, the regen command, and the source. Say what the list is and what it is not — the existing banners spell out that gated ≠ manifest-declarable, which stops the next reader conflating them.
- One generator writes every copy. Two consumers that can't share an import
(launcher app vs. separately-published SDK CLI) get two
targetsentries in one script — never two scripts. - Wire it up: add
gen:<thing>to that package'spackage.json, chain it intogen:all(whichpretest/pretest:run/prebuildalready run). - Add the output to
.prettierignore. - Add the output to the CI
git diff --exit-codelist in.github/workflows/test-and-lint.yml. - Prove it, don't assume it: delete the output and confirm
pnpm test:runrecreates it byte-identical; corrupt it by hand and confirmgit diff --exit-codereturns non-zero; run the generator twice and confirm zero drift (idempotent).
A generator without steps 5–7 is a convenience script, not a fix — the staleness gap stays open.
Deliberate exceptions — still hand-maintained
Do not "fix" these by generating them. They are broader or different-purpose lists, not mirrors:
| File | Why it stays hand-written |
|---|---|
asyar-sdk/cli/lib/manifest.ts VALID_PERMISSIONS |
Deliberately broader than the Rust gate — includes declarative-only permissions (store:read, tools:register, runs:track) that get_required_permission never sees. No single Rust source exists. |
asyar-launcher/src/services/extension/permissionCatalog.ts |
Human-written display copy (title + description) for the consent dialog. Prose, not data. |
docs/reference/permissions.md, docs/reference/sdk/README.md |
Hand-written developer docs that mirror the lists in prose. Known to drift; grep them when adding a permission. |
Adding a new permission — the full checklist
One new <namespace>:<action> string still touches five surfaces. Only the
generated one is automatic:
1. src-tauri/src/permissions.rs ← THE source. Enforcement lives here.
2. run gen:permission-catalog ← writes both gatedPermissions.ts copies
3. asyar-sdk/cli/lib/manifest.ts ← VALID_PERMISSIONS, by hand
(then `npm run build:cli` — the linked
`asyar` binary runs dist/, not src/)
4. permissionCatalog.ts ← title + description, by hand
5. docs/reference/permissions.md ← the table, by hand
Enforcement is Rust only — permissions.rs is the single gate. Steps 3–5
are catalog, consent copy, and docs; none of them enforce anything. The gate
fails closed on any call type that get_required_permission and
is_public_call both miss.
Red flags — you are violating this skill
- Editing a file whose first line says
AUTO-GENERATEDorgenerated by Specta. - Writing
// keep in sync withabove a TS array. - Running
prettier --writeon a file listed in.prettierignore. - Adding a
#[derive(specta::Type)]struct and hand-writing the matching TStypeinstead of.register()-ing it inexport_bindingsand regenerating. (This exact miss madecommands.tshand-roll a parallelSystemActionIdunion while the real type sat unregistered.) - Changing
permissions.rs/error.rs/catalog.fallback.jsonand calling the task done without running a generator or a test. - Reporting a generated-file CI diff as "unrelated" or "pre-existing."
If you catch yourself doing any of these: STOP. Find the source, edit that, regenerate.