chore: harden architecture linting
This commit is contained in:
parent
f673d5bbc1
commit
9be159de3d
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue