


Work in a TypeScript Frontend Monorepo Without Crossing Package Boundaries
Use when an AI changes a modular TypeScript frontend codebase. It covers package boundaries and a framework-free core, routing state changes through owners, file and test placement, the checks to run before committing, the release steps, and UI layers.
To use this AI rule
## Cross Package Boundaries Only Through Public Exports
- In a multi-package workspace, give each package one responsibility and cross boundaries only through public package exports; never import another package's internals from application code, since internal imports couple callers to private structure and break when it changes. Enforce boundaries with automated lint and architecture checks rather than review, and where no correct location exists, create one deliberately and update the documented conventions.
- Keep the shared core framework-agnostic and free of browser DOM: put UI-framework bindings and DOM/CSS projection in dedicated layers, and load heavy runtime dependencies such as WASM modules or large SDKs from one central module that hands callers an injected instance instead of importing them ad hoc at each call site.
- Do not create re-export shim files whose only purpose is forwarding another package's API; import the owning package directly at call sites. A compatibility barrel may re-export an owner only to preserve an established public API.
- Prefer narrow, targeted subpath exports over one catch-all barrel when smaller imports make dependency intent clearer.
## Change Shared State Only Through Its Owning Action
- Assemble large subsystems from a shared context plus domain-specific action modules (viewport, selection, history, structure, and so on) instead of one growing file. Before adding behavior, inspect the existing folder and nearby patterns and place the change in the nearest matching domain module; do not pile unrelated logic into whichever file is already open.
- Route every state mutation through the owning setter or action so side effects — events, undo batching, derived updates — fire consistently. Never assign to shared state fields directly, or those effects silently do not run, and keep no module-level mutable state inside components; hold shared state in the store.
- Expose changes through a typed event system and have subscribers react to events rather than polling. Keep domain modules sharing state through the context object, not by importing application or framework code.
- Application wrappers may add reactive state, persistence, and platform I/O around the core, but the core stays framework-agnostic; guard any browser API in shared code with an explicit runtime check before use.
## Ban the `any` Type and Non-Null Assertions in TypeScript
- Ban `any`: use precise types, generics, and declaration merging. Ban non-null assertions: use guards, optional chaining, and nullish coalescing. Both keep the type checker able to catch real errors instead of masking them.
- Prefer precise union types over loose primitives — a small string-literal union instead of `number | string | null` — and do not inline a type definition when a named shared type already exists; import and reuse it.
- Name repeated or cross-feature constants in a shared constants module instead of duplicating literals across the codebase.
- Never use a non-cryptographic random source for identifiers or anything security-relevant; use a cryptographic RNG everywhere. Deep-copy nested objects with a structured clone, never a shallow spread, when mutating nested data.
## Name Folders by Domain and Put Each Test Type in Its Place
- Organize by domain namespace rather than deep structural ceremony, and keep application services, route or layout views, and UI in their own top-level areas.
- Use a subfolder for a multi-file domain instead of sibling files sharing a repeated prefix. When a domain gains its second file, create the folder immediately rather than prefixing names — prefer `selection/hit-test` over `selection-hit-test`.
- Keep conventional entrypoint filenames such as `index`, `types`, and `context` lowercase, and follow the ecosystem's casing for component files. Do not place code or tests ad hoc; if no proper location exists, create one deliberately and document it.
- Keep test placement strict and location-based: put each test type in its established directory (end-to-end, integration, unit, shared helpers, per-package). User-visible behavior belongs in end-to-end tests; internal-state and data assertions belong in unit tests. Never commit temporary or profiling test files.
## Use an Existing Library Instead of Hand-Writing the Same Code
- Do not hand-roll what a dependency already does. Check existing dependencies first; if none fits, adopt a quality library rather than inlining an implementation — for example use a diff library for unified diffs and a color library for color math, not a hand-written loop.
- Before building custom UI, controls, or composables, read the upstream documentation for the dependency instead of guessing from local usage, preferring machine-readable doc entrypoints when offered. If upstream docs contradict local patterns, follow current upstream APIs and update local wrappers deliberately.
- Put maintainable programs and helpers in a dedicated tools area organized by domain, each with focused tests, not as ad-hoc root scripts. Reserve a scripts area only for tiny compatibility entrypoint shims that import from the tools area, keeping implementation logic out of the shims.
- Include tooling tests plus lint and format coverage in the standard checks so the tools area is held to the same bar as product code.
## Run the Full Quality Checks Before Committing, and Write Conventional Commits
- Run the full quality gate before committing and again before opening a change for review: type-aware lint and typecheck with zero errors, the formatter with import sorting, copy-paste and duplication detection with zero clones, and the relevant unit and end-to-end suites.
- Self-review before submitting: if duplication rises, extract shared helpers or reuse existing types; remove inline type definitions that duplicate named types and any copy-pasted logic; keep files under a few hundred lines and split by domain when they grow.
- Write commit messages as Conventional Commits (`feat`, `fix`, `refactor`, `perf`, `docs`, `test`, `build`, `ci`, `chore`): a short, imperative, optionally scoped first line with a lowercase type, and rationale in the body with each body line starting on an uppercase word.
- Preserve product and acronym casing in subjects and bodies rather than flattening acronyms to lowercase prose, and use the narrowest honest scope — or omit the scope when a change spans unrelated areas.
## Release in a Fixed Order and Keep the Changelog Current
- Follow a fixed, ordered release procedure: bump the version in every file that carries it, move changelog entries from an "Unreleased" section under a new dated version heading, commit with a release message, then create and push the tag. Confirm any signing or release secrets exist before tagging.
- Treat the build and release workflow as the single source of truth for packaging and publishing and keep any duplicated package lists in sync with it. Gate production deploys on release tags or explicit manual dispatch, never on ordinary default-branch pushes, so shipping stays intentional.
- Keep documentation current: update the changelog's "Unreleased" section and user-facing docs whenever behavior changes, and update the contributor or agent reference when architecture or conventions change.
- Keep published docs free of speculative or internal implementation plans — put temporary plans in an ignored scratch area and distill durable direction into canonical docs — and do not create placeholder localized copies; link localized navigation to the canonical page until a real translation exists.
## Keep Shared UI Primitives From Importing App Code
- Separate a shared, reusable UI primitive layer from feature and shell layers. Shared primitives and low-level styled controls must not import application services, stores, or feature panels; feature components own their feature-specific controls.
- Resolve styling through static, typed theme variants defined in one place and merged per instance, rather than scattering conditional utility-class branches or one-off per-slot class props. Bind visual state through semantic data attributes and resolve the typed variants in script.
- For test locators, prefer user-facing role, label, and text; expose scoped semantic attributes for identity, and reserve dedicated test ids for rare integration boundaries rather than manufacturing globally unique ids inside shared components.
- Do not pass imperative setters or actions through slots unless a component is explicitly a renderless primitive; prefer model bindings, emitted events, and normal props. Use an icon library rather than raw inline SVG or Unicode symbols, and keep labels semantic by rendering key bindings from command metadata instead of baking shortcut text into labels.
