From 9be159de3d8202a69df48ce594b13d003cd1ebe3 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Thu, 14 May 2026 20:51:59 +0300 Subject: [PATCH] chore: harden architecture linting --- AGENTS.md | 4 ++-- scripts/steiger-rules.ts | 40 ++++++++++++++++++++++++++++++++++++++++ steiger.config.ts | 5 +++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ef430e0ce..bbe1392c3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -238,8 +238,8 @@ 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, app service/domain code (`src/app/**`) must not import app component/view layers, components must not import views, shared UI (`src/components/ui/**`) must not import app services/stores, property-panel internals must stay inside the property panel, and canvas/editor overlay code must not import property-panel internals. -- Test placement is strict and enforced by Steiger: app E2E tests live under `tests/e2e/**` and use `*.spec.ts`; Figma automation tests live under `tests/figma/**` and use `*.spec.ts`; engine/unit tests live under `tests/engine/**` and use `*.test.ts` (with `helpers.ts`, `*.bench.ts`, and `visual-*` support scripts allowed); shared test utilities live under `tests/helpers/**`. 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. +- 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, app service/domain code (`src/app/**`) must not import app component/view layers, components must not import views, shared UI (`src/components/ui/**`) must not import app services/stores, property-panel internals must stay inside the property panel, canvas/editor overlay code must not import property-panel internals, committed code must not import scratch/generated/vendor internals, and durable docs belong under `packages/docs/**` unless the root Markdown allowlist is deliberately updated. +- Test placement is strict and enforced by Steiger: app E2E tests live under `tests/e2e/**` and use `*.spec.ts`; Figma automation tests live under `tests/figma/**` and use `*.spec.ts`; engine/unit tests live under `tests/engine/**` and use `*.test.ts` (with `helpers.ts`, `*.bench.ts`, and `visual-*` support scripts allowed); shared test utilities live under `tests/helpers/**`. Do not commit temporary/profile specs (`*.tmp.*`, `*.profile.*`). 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 01c3cd508..ca3ab320e 100644 --- a/scripts/steiger-rules.ts +++ b/scripts/steiger-rules.ts @@ -24,6 +24,13 @@ type ImportRef = { } const TEXT_EXTENSIONS = new Set(['.ts', '.tsx', '.vue', '.js', '.jsx', '.mjs', '.mts']) +const ROOT_MARKDOWN_ALLOWLIST = new Set([ + 'AGENTS.md', + 'CHANGELOG.md', + 'CONTRIBUTING.md', + 'README.md', + 'SECURITY.md' +]) const PACKAGE_ALIASES: Record = { '#core/': 'packages/core/src/', '#vue/': 'packages/vue/src/', @@ -135,6 +142,10 @@ function createImportRule( const strictTestFilePlacement = createFileRule('open-pencil/strict-test-file-placement', (sourceRel) => { if (!sourceRel.startsWith('tests/')) return null if (!TEXT_EXTENSIONS.has(path.extname(sourceRel))) return null + const name = path.basename(sourceRel) + if (name.includes('.tmp.') || name.includes('.profile.')) { + return 'Temporary/profile test files must not be committed. Move exploratory specs to scratch/ or delete them.' + } if (sourceRel.startsWith('tests/e2e/')) { return sourceRel.endsWith('.spec.ts') ? null : 'E2E tests must live under tests/e2e/** and use *.spec.ts.' } @@ -173,6 +184,33 @@ const noE2EImportsInEngineTests = createImportRule( } ) +const noRootMarkdownClutter = createFileRule('open-pencil/no-root-markdown-clutter', (sourceRel) => { + if (sourceRel.includes('/')) return null + if (!sourceRel.endsWith('.md')) return null + if (ROOT_MARKDOWN_ALLOWLIST.has(sourceRel)) return null + return 'Do not add ad hoc root Markdown files. Put durable docs under packages/docs/** or update the root allowlist deliberately.' +}) + +const noPrototypeOrGeneratedImports = createImportRule( + 'open-pencil/no-prototype-or-generated-imports', + (sourceRel, _specifier, resolved) => { + if (!resolved) return null + if (resolved.startsWith('scratch/')) { + return 'Committed code must not import scratch prototypes.' + } + if (resolved.startsWith('desktop/generated/')) { + return 'Do not import generated desktop artifacts from TypeScript/app code.' + } + if ( + resolved.startsWith('packages/core/src/kiwi/kiwi-schema/') && + !sourceRel.startsWith('packages/core/src/kiwi/kiwi-schema/') + ) { + return 'Do not import vendored Kiwi schema internals directly; use the supported Kiwi APIs.' + } + return null + } +) + const noPropertyPanelImportsInCanvas = createImportRule( 'open-pencil/no-property-panel-imports-in-canvas', (sourceRel, _specifier, resolved) => { @@ -315,6 +353,8 @@ export const openPencilArchitecturePlugin = { strictTestFilePlacement, noEngineOnlyAssertionsInE2E, noE2EImportsInEngineTests, + noRootMarkdownClutter, + noPrototypeOrGeneratedImports, noPropertyPanelImportsInCanvas, noAppImportsInWorkspacePackages, noPackageInternalsInApp, diff --git a/steiger.config.ts b/steiger.config.ts index c46be47cc..0726119f0 100644 --- a/steiger.config.ts +++ b/steiger.config.ts @@ -3,6 +3,9 @@ import { defineConfig } from 'steiger' import { openPencilArchitecturePlugin } from './scripts/steiger-rules.ts' +// OpenPencil is not laid out as canonical Feature-Sliced Design layers. +// Keep Steiger focused on project-specific architecture boundaries instead of +// enabling fsd.configs.recommended, which treats src/ and packages/ as FSD layer typos. export default defineConfig([ fsd.plugin, openPencilArchitecturePlugin, @@ -21,6 +24,8 @@ export default defineConfig([ 'open-pencil/strict-test-file-placement': 'error', 'open-pencil/no-engine-only-assertions-in-e2e': 'error', 'open-pencil/no-e2e-imports-in-engine-tests': 'error', + 'open-pencil/no-root-markdown-clutter': 'error', + 'open-pencil/no-prototype-or-generated-imports': 'error', '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',