chore: harden architecture linting

This commit is contained in:
Danila Poyarkov 2026-05-14 20:51:59 +03:00
parent f673d5bbc1
commit 9be159de3d
3 changed files with 47 additions and 2 deletions

View file

@ -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

View file

@ -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<string, string> = {
'#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,

View file

@ -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',