chore: tighten architecture boundaries

This commit is contained in:
Danila Poyarkov 2026-05-14 18:56:45 +03:00
parent d2c3b1bce8
commit 042271f519
3 changed files with 41 additions and 1 deletions

View file

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

View file

@ -29,6 +29,13 @@ const PACKAGE_ALIASES: Record<string, string> = {
'#mcp/': 'packages/mcp/src/'
}
const PACKAGE_ALIAS_OWNERS: Record<string, string> = {
'#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
]
}

View file

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