openpencil/AGENTS.md

128 lines
20 KiB
Markdown
Raw Normal View History

2026-02-28 20:18:55 +00:00
# OpenPencil
Vue 3 + CanvasKit (Skia WASM) + Yoga WASM design editor. Tauri v2 desktop, also runs in browser. Bun workspace monorepo.
This file holds the repository map and the rules that apply everywhere. Rules for one folder live in that folder's `AGENTS.md`. **Before changing files under a mapped path, read this file and that path's guide.** Paths in every guide are repository-relative. Process for humans (setup, PRs, commits) is in `CONTRIBUTING.md`; product direction and Figma gaps are in `packages/docs/development/roadmap.md`.
## Map
| Path | Owns | Guide |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
2026-10-05 14:06:07 +00:00
| `packages/scene-graph` | Framework-neutral graph, node types, geometry, copy/snap/undo, variables, instances, hit testing, plus the shared primitives formats need: color conversion and management, CSS value parsing, text/layout direction | `packages/scene-graph/AGENTS.md` |
| `packages/pen` | Pencil.dev `.pen` model, parser, SceneGraph adapter | — |
| `packages/kiwi` | SceneGraph-independent Kiwi schema/runtime, codecs, containers, parse helpers | `packages/fig/AGENTS.md` |
| `packages/fig` | `.fig` archives, SceneGraph conversion, metadata policy, component/instance interpretation, Figma clipboard | `packages/fig/AGENTS.md` |
| `packages/core` | Renderer, layout, editor, Figma API, tools, clipboard, vector conversion, document I/O; depends on scene-graph and the format packages (pen, kiwi, fig, dom-css, design-jsx); no browser DOM | `packages/core/AGENTS.md` |
feat(scene-graph)!: let a SceneGraph take its ID generator (#887) * feat(scene-graph): let a SceneGraph take its ID generator The constructor accepts an ID generator, used for the root node and for nodes, variables, and collections created later; generated IDs skip any node, variable, collection, or mode ID already in the graph. Refs #770 Signed-off-by: Marc Went <marc@went.io> * perf(scene-graph): check mode IDs without allocating per created entity Generating an ID spread every collection into a new array and scanned it for each candidate, which runs on every createNode. Check the node, variable, and collection maps first and walk the modes with an early exit. An index of mode IDs is not kept because history snapshots, transfer, and undo replace collection maps and edit modes in place, which would leave it stale. * test(scene-graph): cover injected ID generators and collision skipping * docs: note the SceneGraph ID generator in the changelog * test(core): keep reopened .fig GUIDs when a sibling is inserted before them Refs #770 * test(collab): cover concurrent additions, same-property writes, and delete versus edit Move the synced-store harness to tests/helpers/collab so the new cases live in tests/app/collab, and let it bind graph events as a collab session does. The cases edit both peers while disconnected and check the converged graphs. Refs #770 * fix(scene-graph): keep a collection's IDs apart and stop on an exhausted generator createCollection asks for the collection and default mode IDs before registering either, so a generator repeating a candidate gave both the same ID. A generator that only returned taken IDs looped forever, even in the constructor; it now throws after a bounded number of attempts. * test: require the inserted GUID and a real disconnect in new tests The GUID test passed even if export dropped the inserted layer, and the concurrent-edit helper would have tested sequential sync had its peers not been disconnectable. * feat(scene-graph): create variable modes through the graph's ID generator The editor minted added and duplicated modes as mode:<random hex>, so an injected generator governed every graph entity except modes added after a collection's first. SceneGraph.createMode mints the ID and adds the mode; undo and redo replay it with addMode. * test(core): type the reopened .fig buffer in the GUID test * refactor(scene-graph)!: share random helpers and one component property ID randomHex, randomInt, and randomIndex move from @open-pencil/core/random to @open-pencil/scene-graph/random, so format packages can use them: design-jsx and the MCP test server dropped private copies. Component property IDs, written as prop:<random hex> in six places across Core and design-jsx, come from createComponentPropertyId. * docs: point the randomness rule at the shared ID and random helpers --------- Signed-off-by: Marc Went <marc@went.io> Co-authored-by: Marc Went <marc@went.io>
2026-10-05 08:21:22 +00:00
| `packages/dom-css` | DOM/CSS/HTML/JSX/Tailwind projection and browser/headless adapters; depends only on scene-graph and emit, and takes engine services such as web-font resolution as injected options | `packages/dom-css/AGENTS.md` |
| `packages/emit` | Source emission for exporters: ESTree and JSX builders, template filling, and esrap printing, with the literal rules that keep exported strings from being reinterpreted | — |
| `packages/design-jsx` | OpenPencil design JSX: elements, paint/effect helpers, variables, schema and authoring reference, JSX export, and a renderer that takes icons, SVG, and layout as injected services | `packages/design-jsx/AGENTS.md` |
| `packages/vue` | Headless Vue 3 SDK primitives, composables, commands, i18n, menu model | `packages/vue/AGENTS.md` |
| `packages/cli` | Headless `.fig` inspection, export, linting, `eval` | `packages/cli/AGENTS.md` |
| `packages/mcp` | stdio and Hono HTTP MCP server reusing Core tools | `packages/mcp/AGENTS.md` |
| `packages/harness` | Optional Node companion for HarnessAgent sessions | `packages/harness/AGENTS.md` |
| `packages/docs` | Published VitePress site | `packages/docs/AGENTS.md` |
| `src` | Tauri/Vite app: services and state in `src/app/**`, views in `src/views/**`, UI in `src/components/**` | `src/AGENTS.md` |
| `desktop` | Tauri v2 shell, capabilities, native credentials, menus | `desktop/AGENTS.md` |
| `tests` | Central app, integration, E2E, native, and Figma acceptance tests | `tests/AGENTS.md` |
| `tools`, `.github` | Private repo tooling, CI classification, releases, brand generation | `tools/AGENTS.md` |
| `skills/open-pencil` | Installable agent skill | `packages/core/AGENTS.md` (Tools) |
| `assets/brand` | Canonical brand artwork | `assets/brand/README.md` |
2026-02-28 20:18:55 +00:00
## Commands
- `bun run dev:portless` — preferred browser server at `https://open-pencil.localhost`; worktrees get `https://<branch>.open-pencil.localhost` and a sibling `mcp.open-pencil` URL with isolated runtime discovery.
- `bun run dev` — fixed `http://localhost:1420`; use only for Playwright, Tauri, and Dev Containers.
- `bun run tauri dev` — desktop app with hot reload.
- A fresh worktree needs `bun install` and `bun run build:packages` before docs, Storybook, or any workflow that resolves workspace subpath exports; without the package builds those resolve to missing `packages/*/dist` targets.
2026-09-02 07:56:00 +00:00
- `bun run check` — complete build, lint, type, architecture, docs, package, dependency, security, tooling, and duplication gate.
- `bun run format` — format and sort imports.
- `bun run test:unit` / `bun run test` / `bun run test:storybook` — engine/unit, app Playwright, and Storybook Playwright suites. See `tests/AGENTS.md` for server selection and worktree ports.
2026-09-02 07:56:00 +00:00
- `bun open-pencil --help` — current CLI command list.
Before a PR run `bun run check`, `bun run format`, `bun run test:unit`, and `bun run test`.
## Package boundaries
- Across package/app boundaries import the owning package's public exports, never workspace internals or forwarding-only shims. `@open-pencil/scene-graph` owns graph types and primitives; `@open-pencil/kiwi` owns low-level Kiwi/FIG helpers; `@open-pencil/core` provides the compatibility barrel plus the subpaths listed in `packages/core/package.json`.
- `bun run check:arch` enforces: public workspace exports, framework-neutral Core, no app services in views or shared UI, property-panel internals scoped to that panel.
- Package aliases are `#core/*`, `#fig/*`, `#vue/*`, `#cli/*`, `#mcp/*`, `#dom-css/*`, `#design-jsx/*`, `#emit/*`; a package's own tests use `#core-tests/*` and `#fig-tests/*`; the app uses `@/`. Prefer clear relative imports nearby. Never escape an alias root with `../` (for example `#tests/../vite`); fix module ownership instead.
test(core): move the fig round-trip suite home, and stop test imports drilling * test(core): move the fig round-trip suite into the package test home The reader change added `verifier-contracts.test.ts` under `tests/engine` and grew the migration baseline to admit it, which the testing architecture forbids: new tests go to the canonical home and the baseline only shrinks. It also started `packages/core/tests/io/formats/fig/roundtrip/`, leaving two homes for one domain. These tests drive Core's exporter, so Core's home is the canonical one. The directory moves whole — eleven files, its helpers and raw verifiers with it — and picks up the local `assert`, `traversal`, `test-utils` and fixture helpers already there. The baseline loses nine entries. * refactor(core): address test helpers by alias instead of drilling `open-pencil/no-deep-parent-relative-imports` already forbids `../../` drilling, but `lint:structure` names the directories it checks and `packages/{core,scene-graph,vue}/tests` were never added — so the 72 drilled imports in Core's tests, including the ones the round-trip move just wrote, were never seen. Core gains `#core-tests/*` beside fig's `#fig-tests/*`, registered with the architecture rules, and its tests reach their helpers through it and their source through `#core/*`. The three test directories join `lint:structure`, which also surfaced six errors they had never been checked for: four duplicated imports, an empty `noop`, and a self-assignment the test meant, now written through a named binding. Both guides state the rule, since a lint nobody can find in prose is how this went unnoticed. * fix(tools): point the heavy unit patterns at the relocated round-trip tests `HEAVY_UNIT_TEST_PATTERNS` still listed the three heavy round-trip tests under `tests/engine`. `test:unit` discovers them by directory so the move looked clean, but `--heavy-only` selects by path and silently stopped choosing them: 8 files, 52 tests, where it now runs 11 and 80.
2026-10-01 10:11:53 +00:00
- Never drill with `../../`: one `../` to a sibling folder is fine, two or more means an alias. This holds in tests as much as in source — a test reaches its package's source through `#<pkg>/*` and its own helpers through `#<pkg>-tests/*`. `open-pencil/no-deep-parent-relative-imports` enforces it; a new test directory must be added to `lint:structure` so the rule reaches it.
- Reuse named types from `@open-pencil/scene-graph`; do not respell `Color`, `Vector`, `SceneNode`, `Effect`, `Fill`, or `Stroke`.
## Code conventions
2026-02-28 20:18:55 +00:00
- Put code and tests in the established owning domain; inspect nearby structure before adding files. Group multi-file domains in subfolders instead of repeated sibling prefixes (`selection/container.ts`, not `selection-container.ts`).
- Non-component folders and files use lowercase or kebab-case except standard entrypoints (`README.md`, `AGENTS.md`, `index.ts`). Component domains use kebab-case folders; Vue files stay PascalCase and component composables camelCase. Do not add new PascalCase app folders or root-level base controls; migrate old ones when touched.
feat(scene-graph)!: let a SceneGraph take its ID generator (#887) * feat(scene-graph): let a SceneGraph take its ID generator The constructor accepts an ID generator, used for the root node and for nodes, variables, and collections created later; generated IDs skip any node, variable, collection, or mode ID already in the graph. Refs #770 Signed-off-by: Marc Went <marc@went.io> * perf(scene-graph): check mode IDs without allocating per created entity Generating an ID spread every collection into a new array and scanned it for each candidate, which runs on every createNode. Check the node, variable, and collection maps first and walk the modes with an early exit. An index of mode IDs is not kept because history snapshots, transfer, and undo replace collection maps and edit modes in place, which would leave it stale. * test(scene-graph): cover injected ID generators and collision skipping * docs: note the SceneGraph ID generator in the changelog * test(core): keep reopened .fig GUIDs when a sibling is inserted before them Refs #770 * test(collab): cover concurrent additions, same-property writes, and delete versus edit Move the synced-store harness to tests/helpers/collab so the new cases live in tests/app/collab, and let it bind graph events as a collab session does. The cases edit both peers while disconnected and check the converged graphs. Refs #770 * fix(scene-graph): keep a collection's IDs apart and stop on an exhausted generator createCollection asks for the collection and default mode IDs before registering either, so a generator repeating a candidate gave both the same ID. A generator that only returned taken IDs looped forever, even in the constructor; it now throws after a bounded number of attempts. * test: require the inserted GUID and a real disconnect in new tests The GUID test passed even if export dropped the inserted layer, and the concurrent-edit helper would have tested sequential sync had its peers not been disconnectable. * feat(scene-graph): create variable modes through the graph's ID generator The editor minted added and duplicated modes as mode:<random hex>, so an injected generator governed every graph entity except modes added after a collection's first. SceneGraph.createMode mints the ID and adds the mode; undo and redo replay it with addMode. * test(core): type the reopened .fig buffer in the GUID test * refactor(scene-graph)!: share random helpers and one component property ID randomHex, randomInt, and randomIndex move from @open-pencil/core/random to @open-pencil/scene-graph/random, so format packages can use them: design-jsx and the MCP test server dropped private copies. Component property IDs, written as prop:<random hex> in six places across Core and design-jsx, come from createComponentPropertyId. * docs: point the randomness rule at the shared ID and random helpers --------- Signed-off-by: Marc Went <marc@went.io> Co-authored-by: Marc Went <marc@went.io>
2026-10-05 08:21:22 +00:00
- No `any`, non-null assertions, or `Math.random()`; use precise types, guards, and `crypto.getRandomValues()` — through `randomHex`, `randomInt`, and `randomIndex` from `@open-pencil/scene-graph/random` rather than new copies. New component property IDs come from `createComponentPropertyId()`, and graph entity IDs from the `SceneGraph`'s generator (`createNode`, `createCollection`, `createMode`).
- Valibot for first-party runtime validation. Keep Zod only where an upstream dependency requires it; never maintain parallel first-party schemas in both.
- Use existing dependencies before writing utilities: `culori` for color conversion, VueUse for browser, event, focus, clipboard, storage, and timer behavior (one-shot rAF or service-owned timers are fine when clearer), `dedent` for multiline prompt composition and embedded examples. Keep substantial prompt prose in the owning Markdown source and compose it.
- Reach for `es-toolkit` before hand-writing a collection or object helper: `uniq`/`uniqBy` over `Set` round trips, `compact` over `filter(Boolean)`, `groupBy`/`keyBy`/`partition`/`countBy` over accumulator loops, `pick`/`omit`/`mapValues`/`mapKeys` over rewriting `Object.entries`, `isEmptyObject`/`isEqual` over manual checks. Keep native code where it is a single clear call (`map`, `some`, `Object.fromEntries` of a direct mapping) or on a measured hot path such as per-frame rendering and layout. `open-pencil/prefer-es-toolkit` enforces the `uniq` and `compact` cases.
refactor!: move shared primitives below dom-css and core (#771) * refactor!: move shared primitives below dom-css and core dom-css depended on core for color conversion, base64 helpers, text direction, and web-font assets, so core could not use dom-css and every caller special-cased HTML and Tailwind output. Color conversion and management, base64 helpers, and text/layout direction now live in scene-graph under `color`, `bytes`, and `text-direction`. dom-css takes web-font resolution as an injected `fonts` option and owns the font face types, so it depends only on scene-graph and core can depend on it. BREAKING CHANGE: `@open-pencil/core/color` and `@open-pencil/core/bytes` are removed, and the direction helpers are no longer exported from `@open-pencil/core/text`; import them from `@open-pencil/scene-graph` subpaths. `exportHTMLBundle` takes a font resolver in `fonts` instead of `'assets'`. * fix(tools): import color parsing from scene-graph in visual bisect * fix(mcp): declare the scene-graph dependency MCP imports `@open-pencil/scene-graph/bytes` since base64 helpers moved there, but only reached scene-graph through core, so isolated installs and package checks depended on transitive resolution. * refactor!: use js-base64 directly instead of a base64 wrapper Base64 helpers had moved into scene-graph only to sit below dom-css, but they are a thin wrapper over js-base64 and unrelated to the graph; fig already called js-base64 directly. Callers use js-base64 and check `isValid` where input comes from outside (clipboard, imported HTML, tool arguments, the plugin API). A new `open-pencil/no-hand-rolled-base64` lint rule rejects atob, btoa, and Buffer Base64 conversions, and AGENTS.md records the convention. BREAKING CHANGE: `@open-pencil/core/bytes` is removed; use `js-base64`. * fix(dom-css): keep images with invalid Base64 inline in HTML export `exportHTMLBundle` accepts documents parsed from outside HTML, and js-base64 drops characters it cannot decode, so extracting an invalid image data URL wrote different bytes. Such images now stay inline.
2026-09-26 07:39:11 +00:00
- Use `js-base64` directly for Base64: `fromUint8Array`/`toUint8Array` for bytes, `encode`/`decode` for text, and `isValid` before decoding input from outside (clipboard, imported files, tool arguments). Do not wrap it or use `atob`, `btoa`, or `Buffer` Base64 conversions; `open-pencil/no-hand-rolled-base64` enforces this.
fix: validate parsed JSON at untrusted boundaries with Valibot (#855) * fix: validate parsed JSON at untrusted boundaries with Valibot Clipboard HTML, library revisions from shared storage, MCP and automation WebSocket messages, the MCP discovery file, sidecar output and AI/MCP tool arguments were JSON.parse'd and cast to their expected types, so a malformed payload reached the document or crashed paste. They now go through v.pipe(v.string(), v.parseJson(), Schema), which reports bad JSON and a wrong shape as the same validation failure. The path_set tool rejects an invalid VectorNetwork and shares its parser with create_vector. The CLI library catalog validates its files and runs revisions through the same size, identity and content-hash checks as the app; reading image bytes as index-keyed records also stops them coming back empty. Hand-rolled typeof readers for plugin data, document metadata, caches and preferences become schemas with their behaviour preserved, and readCacheJSON takes a schema for its payload. open-pencil/no-unvalidated-json-parse rejects type assertions on JSON.parse results other than `as unknown` in src and packages/*/src. * refactor: validate parsed JSON in tests and tooling Extend open-pencil/no-unvalidated-json-parse beyond source: tests, helpers and repo tooling now parse JSON through Valibot schemas instead of asserting a type. The shared fixture reader returns a validated object; its old array annotation never matched the fixtures. * fix: validate clipboard geometry bytes, library images and model catalogs Clipboard geometry blobs and library image bytes must be bytes at contiguous indexes, so out-of-range or gapped values are rejected instead of silently becoming different geometry or images; serialized library nodes must carry source metadata. The models.dev and OpenRouter responses are validated like their cached copies, and activate-tab rejects a CDP frame it cannot read instead of hanging. * refactor: extend the JSON validation lint to .json() results no-unvalidated-json-parse now also rejects type assertions on Response, Bun.file and shell .json() results, the same unchecked parse in another form. MCP server tests read /health through a validated readHealth helper and discovery files through parseDiscoveryInfo; the remaining tooling reads its JSON through schemas. * test: validate the RPC request body in the CLI app export test * test: validate CLI JSON output in the tool and app command tests * test: compare the malformed models.dev fallback with the curated list
2026-10-04 17:01:50 +00:00
- Parse JSON from outside the process (clipboard, files, network, storage, tool arguments) with `v.safeParse(v.pipe(v.string(), v.parseJson(), Schema), text)`, so malformed JSON and a wrong shape fail the same way. Never assert a type on a `JSON.parse` or `.json()` result other than `unknown`; `open-pencil/no-unvalidated-json-parse` enforces this in source, tests, and tooling.
- Browser-shipped code targets the supported browser baseline in `src/app/shell/support/baseline.ts`: TypeScript `lib` stays ES2023 and `compat/compat` rejects missing Web APIs; Node-only packages (`cli`, `mcp`, `harness`) are exempt. Details in `src/AGENTS.md` (Browser baseline).
- Components must not hold module-level mutable state. Name repeated or cross-feature constants; app-wide values belong in `src/constants.ts`.
- Window API augmentations belong to the owning compilation boundary: `src/global.d.ts` for the app, the package's `global.d.ts` for package DOM gaps, `tests/helpers/tauri/native-global.d.ts` for native tests. Never put `declare global` in specs or implementation modules; include canonical declarations through tsconfig. Keep app API contracts named and owned by their implementation domain; derive vendor API types from top-level type imports. Optional runtime globals stay optional and need a runtime guard.
- Use `structuredClone` or typed copy helpers for nested mutable data. Self-review for duplication, named shared types, precise unions, and files approaching ~600 lines.
- Detect desktop with `IS_TAURI`, never ad-hoc `__TAURI_INTERNALS__` checks.
### Dependency documentation
Before using an unfamiliar dependency API or writing a replacement, inspect existing project wrappers and read the official documentation. Start with these indexes, then fetch specific pages rather than entire `llms-full.txt` dumps. Match versions in the manifests and lockfile; verify signatures against installed types when versions differ. Do not guess APIs or invent primitives already supplied by dependencies. Read current Reka UI, VueUse, and Tailwind/tailwind-variants docs before inventing UI primitives or composables, and update local wrappers deliberately when upstream APIs changed.
| Dependency | Official documentation entrypoint |
| ---------------------------- | ---------------------------------------------------------------------------------------------- |
| Vue | https://vuejs.org/llms.txt |
| VueUse | https://vueuse.org/guide/ — follow individual composable documentation. |
| Reka UI | https://reka-ui.com/llms.txt |
| Tauri v2 | https://v2.tauri.app/llms.txt |
| Tailwind CSS | https://tailwindcss.com/docs |
| Tailwind Variants | https://www.tailwind-variants.org/llms.txt |
| Motion (use the Vue section) | https://motion.dev/llms.txt |
| Valibot | https://valibot.dev/llms.txt |
| es-toolkit | https://es-toolkit.dev/llms.txt |
| CanvasKit | https://skia.org/docs/user/modules/canvaskit/ and installed `canvaskit-wasm/types/index.d.ts`. |
VueUse serves HTML at its `llms.txt` URL; Tailwind CSS and Skia have no verified index. If an index disappears or returns HTML, fall back to official API documentation, not guessed methods or unofficial generated indexes.
## Tests
Follow `packages/docs/development/testing.md` and `tests/AGENTS.md`. Package-local tests mirror source domains; central app tests mirror `src/app/**`; central integration requires a genuinely cross-owner contract. Test contracts, not source text; never commit temporary or profile specs. Pixel-affecting renderer changes need committed canvas snapshots; simple CSS-only UI changes need visual verification, not new automated tests.
## Documentation and changelog
- `CHANGELOG.md` — curated user-facing changes by version; `Unreleased` stays first. `README.md` — concise features, setup, CLI, and overview. `AGENTS.md` files — contributor/agent rules. `packages/docs/` — public site (`packages/docs/AGENTS.md`). Keep internal plans in ignored `scratch/`.
- For user-facing work add one present-tense outcome under the single appropriate `Unreleased` category: `Breaking changes`, `Added`, `Changed`, `Fixed`, `Performance`, or `Security`. Treat it as release notes: omit tests, benchmarks, CI, internal refactors/tooling, and bugs both introduced and fixed since the last release. End sentences with periods and retain issue/PR references. Update `README.md` when appropriate and the owning `AGENTS.md` when architecture or conventions change.
- `CHANGELOG.md` merges with git's `union` driver (`.gitattributes`), so two branches adding entries combine instead of conflicting; the driver concatenates blindly, which is why the pass below is load-bearing rather than tidy-up.
- After merges, compare the whole section with changes since the latest release, preserve important outcomes, consolidate related work, and remove duplicate bullets and headings.
- Before finalizing `Unreleased`: compare released behavior at the latest tag with the final implementation, not commit subjects; verify questionable fixes existed at that tag and fold fixes to newly added features into the feature bullet; check public exports, model/config/data contracts, and peer requirements for `Breaking changes`; remove superseded intermediate behavior; state platform requirements and concrete supported behavior instead of unqualified claims; run `bun run check:changelog`. Keep historical sections unchanged.
## Commits, PRs, and issues
`CONTRIBUTING.md` is canonical for commit messages, PR titles and bodies, and attribution. Digest:
- Conventional Commits (`feat`, `fix`, `refactor`, `perf`, `docs`, `test`, `build`, `ci`, `chore`); short imperative subjects, rationale in the body; release commits are exactly `Release vX.Y.Z`. Preserve product casing: DOM/CSS, HTML, JSX, Tailwind, Kiwi, `.fig`, MCP, CLI, AI, ACP, i18n. Validate with `bun run check:commits --last` or `--from`/`--to`.
- PR titles are Conventional Commits because GitHub uses them as merge subjects; branch-update merges get explicit subjects such as `chore: merge master into <branch>`. Do not rewrite published history solely to normalize messages.
- Disclose AI assistance in the PR's AI assistance section, never as commit authorship, `Co-authored-by` trailers, promotional signatures, or session links. Preserve human co-author credits and third-party notices.
- Stacked pull requests: `gh stack`, rebase never merge, land from the top layer via the merge queue (`CONTRIBUTING.md#stacked-pull-requests`).
- Issues, PR descriptions, and public comments use concise concrete technical prose: lead with the problem and outcome, add a short example when needed, avoid filler, promotional claims, decorative emojis, unnecessary tables, and file-by-file inventories. Link long logs or design notes.
## Code review
- Review codebase fit, not just the diff. Inspect the owning folder, nearby analogous implementations, shared helpers and types, public exports, callers, and tests. Prefer an existing abstraction when it fits; do not invent a parallel pattern or demand unrelated cleanup.
- Verify findings against the current PR head and pinned dependency APIs. Give the concrete failing scenario and consequence; distinguish demonstrated bugs from hardening and preferences. State when runtime validation or dependency source was unavailable.
- On re-review, check later commits and the discussion before repeating a finding. Green checks and resolved threads are not substitutes for reviewing the current code.
- Request evidence appropriate to the change: engine tests for state contracts, Storybook for isolated component states, browser integration tests for workflows, canvas snapshots for rendering, native tests for platform delivery. One does not prove another.
- Preserve intentional behavior unless a concrete regression is demonstrated; for example, preferences and native credentials cannot transact together, so documented partial-save outcomes and retryable drafts are not bugs.
- Keep comments concise and actionable; cite the location and the repository rule or existing analogue. Independently assess automated suggestions; never bulk-apply or bulk-resolve them to make a bot green.
## Maintaining these guides
- One rule per bullet, at most three sentences, ending with its anchor: the enforcing check, the analogous file, or the test. A rule that can become a lint, `check:arch` boundary, or type-level test should become one, and the prose is then removed.
- A rule lives in exactly one guide: here if it applies across the repository, otherwise in the nearest `AGENTS.md` of the owning folder. Explanations belong in `packages/docs/development/`, not in guides.
- Every nested `AGENTS.md` is listed in the Map above; `bun run check:docs` fails on an unlisted or missing guide. Nested guides are classified as documentation by CI.
- When a section here passes about ten lines, move it to a domain guide and leave the map row.
2026-02-28 20:27:36 +00:00
## Reference
[`figma-use`](https://github.com/dannote/figma-use) is a historical code reference and a live-Figma oracle (`packages/core/AGENTS.md`, Figma API); verify current paths, types, and behavior before adapting anything from it.