diff --git a/AGENTS.md b/AGENTS.md index 8a2800462..2d6c98ebc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -93,7 +93,8 @@ The app editor session (`src/app/editor/session/create.ts`) is a thin Vue wrappe ## Commands -- `bun run check` — type-aware lint + typecheck via oxlint + tsgo (run before committing) +- `bun run check` — type-aware lint + typecheck via oxlint + tsgo + architecture checks (run before committing) +- `bun run check:arch` — Steiger architecture lint for project-specific import boundaries - `bun run check:vue` — vue-tsc type-check for .vue files (has pre-existing errors, fix progressively) - `bun run test:dupes` — jscpd copy-paste detection across all TS sources - `bun run format` — oxfmt with import sorting @@ -237,6 +238,7 @@ Release commits are the exception: keep using `Release v0.x.y`. ## Code conventions - Do not place code or tests ad hoc. Before adding or moving files, inspect the existing folder structure and nearby patterns, then put changes in the established domain-specific location. If no proper location exists, create one deliberately and update docs/conventions as needed. +- Architecture boundaries are enforced by Steiger (`bun run check:arch`). App code must use public workspace package exports, workspace packages must not import app `src/` code, package-local aliases (`#core`, `#vue`, `#cli`, `#mcp`) are only for their owning package, core must stay framework-agnostic, and canvas/editor overlay code must not import property-panel internals. - Test placement is strict: E2E tests live under `tests/e2e/**` and use `*.spec.ts`; engine/unit tests live under `tests/engine/**` and use `*.test.ts`. Do not put store-only/internal-state assertions in E2E. If a test drives the UI like a user and verifies visible behavior, it can be E2E; if it creates nodes through internals and asserts graph state, it belongs in engine/unit coverage. ### File and folder naming diff --git a/scripts/steiger-rules.ts b/scripts/steiger-rules.ts index 2b841b190..ab9898144 100644 --- a/scripts/steiger-rules.ts +++ b/scripts/steiger-rules.ts @@ -29,6 +29,13 @@ const PACKAGE_ALIASES: Record = { '#mcp/': 'packages/mcp/src/' } +const PACKAGE_ALIAS_OWNERS: Record = { + '#core/': 'packages/core/src/', + '#vue/': 'packages/vue/src/', + '#cli/': 'packages/cli/src/', + '#mcp/': 'packages/mcp/src/' +} + function normalizePath(filePath: string) { return filePath.split(path.sep).join('/') } @@ -134,6 +141,33 @@ const noAppImportsInWorkspacePackages = createImportRule( } ) +const noPackageInternalsInApp = createImportRule( + 'open-pencil/no-package-internals-in-app', + (sourceRel, specifier, resolved) => { + if (!sourceRel.startsWith('src/')) return null + if (specifier in PACKAGE_ALIASES || Object.keys(PACKAGE_ALIASES).some((alias) => specifier.startsWith(alias))) { + return 'App code must use package public exports such as @open-pencil/core or @open-pencil/vue, not package-local aliases.' + } + if (resolved?.startsWith('packages/')) { + return 'App code must not import workspace package internals. Use package public exports instead.' + } + return null + } +) + +const noForeignPackageLocalAliases = createImportRule( + 'open-pencil/no-foreign-package-local-aliases', + (sourceRel, specifier) => { + if (sourceRel.startsWith('scripts/')) return null + for (const [alias, owner] of Object.entries(PACKAGE_ALIAS_OWNERS)) { + if (specifier.startsWith(alias) && !sourceRel.startsWith(owner)) { + return `Package-local alias ${alias} can only be used inside ${owner}. Use a public package export across package boundaries.` + } + } + return null + } +) + const noUiImportsInCore = createImportRule( 'open-pencil/no-ui-imports-in-core', (sourceRel, specifier) => { @@ -156,6 +190,8 @@ export const openPencilArchitecturePlugin = { ruleDefinitions: [ noPropertyPanelImportsInCanvas, noAppImportsInWorkspacePackages, + noPackageInternalsInApp, + noForeignPackageLocalAliases, noUiImportsInCore ] } diff --git a/steiger.config.ts b/steiger.config.ts index cd2e8a0dd..62eb8a6f2 100644 --- a/steiger.config.ts +++ b/steiger.config.ts @@ -21,6 +21,8 @@ export default defineConfig([ rules: { 'open-pencil/no-property-panel-imports-in-canvas': 'error', 'open-pencil/no-app-imports-in-workspace-packages': 'error', + 'open-pencil/no-package-internals-in-app': 'error', + 'open-pencil/no-foreign-package-local-aliases': 'error', 'open-pencil/no-ui-imports-in-core': 'error' } }