Web lint boundaries¶
The web lint gate rejects new dependencies against the existing layer direction, cross-feature implementation imports and production imports of test code. It also checks three Angular reactivity and injection-context mistakes. This is development tooling; no feature flag is needed.
Delivery boundary¶
The minimal slice is executable policy in the current standalone app, with fixtures and CI enforcement. It preserves existing runtime code and the existing ESLint suppression file. Acceptance requires valid imports to pass, representative invalid imports to fail, legacy exceptions to be exact and necessary, and every PR/fork/release web test job to run lint before unit tests. Remediating old dependencies or migrating supported Angular APIs is follow-up work.
The critical path is: inspect the actual graph → configure a maintained resolver/rule → review existing edges → exercise valid/invalid fixtures → run the same lint command locally and in CI.
Evidence and sources¶
Audited on the PR's base a73ccd60af, rather than the older local main checkout:
| Tool | Installed version |
|---|---|
| Angular / TypeScript | 22.2.3 / 6.0.3 |
| angular-eslint | 22.6.0 |
| ESLint / typescript-eslint | 10.8.1 / 8.67.0 |
| NgRx / NgRx ESLint plugin | 22.0.1 |
| Added boundaries plugin / TypeScript resolver | 7.4.1 / 4.4.5 |
The actual policies below are SyRF-specific choices inferred from its source and documented direction.
They are not Angular requirements. Their implementation follows the installed plugin's
element descriptors and
dependency policies, with the
TypeScript resolver reading the existing
tsconfig.json aliases. Plugin peer ranges accept ESLint 10; executable fixtures verify compatibility.
The Angular rules are present in the installed angular-eslint 22.6.0 package, rather than assumed from
the latest upstream configuration.
What is enforced¶
src/services/web/eslint.config.boundaries.js uses boundaries/dependencies. A resolved target determines
the boundary: changing @app/ to @core/, a relative path or an equivalent path does not evade it.
Static imports, type imports, re-exports, literal dynamic imports, require and TypeScript import()
types are checked. Runtime-computed module paths and external packages are outside this rule's scope;
TypeScript/Angular still resolve and compile the application.
| Source | Permitted local targets, before legacy exceptions |
|---|---|
core/ |
Core, generated declarations, assets and environments |
shared/ and reusable subsystems |
Core, shared, reusable subsystems, generated declarations and platform files |
| A feature area | Its own area, the common layers, and another area's root *.routes.ts |
App shell (src/app/*.ts, bootstrap and other source outside the classified areas) |
Common layers, shell code, area root routes/components/guards |
| Generated/platform code | Core, generated/platform code; generated feature selectors legitimately depend on core |
*.spec.ts, src/app/testing/ |
Any application or test code, for integration fixtures |
| Production code, including an import inside the same area | Never a spec or test helper |
pdf-tools/, pdf-viewer/ and study-presence/ are explicitly recognized reusable subsystems. They
already serve multiple hosts; classifying them as isolated routed features would misrepresent the app.
Their internal APIs remain open to common-layer consumers in this slice. All other top-level app folders
automatically become feature areas, including new folders. Feature areas here are the current route
folders, not a claim that each is a fully isolated domain or bounded context.
Concrete examples:
project/project-statistics.store.tscan import core selectors. A new import of that store from a core service fails, because global infrastructure cannot depend on a feature's state.- A feature can lazy-import
admin/admin.routes.ts. Importingstage/stage-review/stage-review.component.tsfrom another feature fails unless that exact old edge is recorded. Area routes are the current public composition entry points; this does not require barrels. - The app shell can lazy-load
study-source-window/study-source-window.component.tsas it does today. That exception to route-file composition applies only to a component or guard at the area's root, not arbitrary nested feature code. The rule does not prove that an allowed import is lazy. core/actions/project-detail.actions.tsalready importsAddSearchDatafrom the create-search component. That exact file pair is temporarily allowed. Adding an admin-component import in the same action file fails. Another core file importing the create-search component also fails.- A component spec can import
@testing/mock-config. A production component cannot import it, nor can it import a sibling*.spec.tsto obtain a mock. Internal imports are checked explicitly.
Three additional maintained Angular rules run on hand-written production src/app/**/*.ts, excluding
specs, *.generated.ts and src/app/generated/. They do not rewrite constructor DI, inputs, outputs,
forms or change detection:
| Rule | Concrete invalid/valid pattern and codebase relevance |
|---|---|
@angular-eslint/no-uncalled-signals |
if (this.scanning) tests a signal function; if (this.scanning()) reads its value. shared/keyword-highlight/keyword-panel/keyword-panel.component.ts uses the valid form. |
@angular-eslint/computed-must-return |
computed(() => { this.active(); }) omits its value; computed(() => this.active()) returns it. project/project-statistics.store.ts has block computations returning coherent summaries. |
@angular-eslint/no-implicit-take-until-destroyed |
start() { stream.pipe(takeUntilDestroyed()) } lacks an injection context; pass an injected DestroyRef. Constructor use remains valid, as in notifications/notification-inbox-button.component.ts. |
These follow Angular's signal getter semantics and DestroyRef requirements, and the tagged angular-eslint rule docs. The computed rule detects an absent return, not every missing branch. These checks supplement compiler diagnostics and tests; they do not establish correctness of an entire reactive flow.
The existing recommended Angular/template rules, NgRx all configuration, naming conventions,
animation-frame-scheduler restriction and Handsontable hook selectors remain active. External HTML and
inline templates retain angular-eslint's recommended control-flow checks and template processor.
The existing NgRx all configuration is retained to avoid silently dropping checks during this change;
it is not a recommendation to enable every rule in other plugins.
Incremental enforcement and exceptions¶
The initial strict architecture scan reported 122 declarations / 120 distinct file pairs. Eight
pairs are legitimate app-shell route entries covered by the public-entry policy. It initially recorded
112 legacy pairs. Studies Activity (#4723) also landed on main before this gate; 12 additional
pairs remain after pruning a removed route-guard import: the project route's workspace guard and three
Activity files' processing/report contracts. The first-enforcement allowlist contains 124 pairs in
68 source files, recorded in eslint-boundary-exceptions.json with reasons. These additional pairs
preserve already-shipped behavior; they do not widen any layer or public-entry policy.
This includes core UI composition, feature-owned models used by global state, cross-area UI reuse and
the detached-window protocol. These are grandfathered paths, not proof that those dependencies are
good architecture.
The fixture suite removes all exception policies and checks those actual source files. Its findings
must exactly equal the recorded pairs, so a removed or newly legitimate dependency requires pruning its
exception. File-wide disabling and wildcard exceptions are not used. A pair allowance covers the
existing two files, not individual imported symbols; tightening symbol-level contracts is deferred.
Do not add an exception just to silence a new dependency. Move the shared contract downward or explicitly
review a new public API. Never regenerate eslint-suppressions.json to absorb new violations.
The three Angular checks found zero pre-existing violations, so they need no legacy exceptions or new suppressions. Existing generated-code lint behavior and the suppression file are preserved.
Commands and CI¶
From src/services/web:
pnpm run test:lint # Node tests of ESLint configuration and real typed fixtures
pnpm run lint # fixture suite, then ng lint with committed suppressions
pnpm exec ng lint still runs all the configured rules, but does not run configuration fixtures.
The Node fixture suite tests tooling; application specs still run only through Angular's test builder.
The fixtures cover aliases/relative paths, type/re-export/dynamic imports, same-feature imports, public
routes, new areas, subsystem consumers, test isolation, narrow exceptions, real Angular signal types
and CI wiring. They also assert the normal effective config enables the rules.
Previously neither PR web lane nor the release web job invoked lint. Both PR lanes (trusted and fork)
and the release web job now run pnpm run lint after install and before unit tests. Lint errors fail the
job; there is no conditional skip or continue-on-error. Existing web change detection includes the
configuration, fixtures and package files under src/services/web/. Runner routing is unchanged.
Ordered follow-ups¶
- Extract feature-owned DTOs/models used by core actions and selectors. Examples:
AddSearchData,LibraryFileType, membership-table and review-progress types. This removes dependency reversals without moving screens; retain behavioral tests and remove exact exceptions as each slice lands. - Move shell UI composition out of core and separate root-service dialog orchestration. Preserve current navigation, confirmation and error-recovery behavior before deleting its exception groups.
- Establish deliberate public contracts for detached-window protocols and shared study-management UI. Then retire cross-feature exceptions in small PRs. Audit cycles before proposing deeper domain isolation or an entry-point contract inside every subsystem. No circular-dependency rule was added here: the legacy graph is coupled and needs a separate measured migration.
- Resolve duplicate template attributes (46 reports in 18 files) and async lifecycle methods
(four hooks) with focused behavior tests, then enable their maintained rules. Duplicated classes
may need merging; merely deleting an attribute can alter styles. Angular does not await async hooks,
so those fixes need explicit loading/error behavior rather than mechanical
voidwrappers. - Incrementally restore
prefer-injectand adopt signal inputs/outputs/queries and host metadata where safe. Constructor injection and decorator inputs remain supported; see Angular's style guide and input guide. Avoid blanket bans, selector renames or automatic OnPush/zoneless changes in this tooling PR. - Review the existing 53 warning/unused-disable findings, stale disabled rule names, and broad NgRx
preset. Then consider a warning budget, targeted RxJS/lifetime rules and template accessibility rules
with measured migration scope. Neither
--max-warnings=0nor a large new recommended preset is needed to make this boundary gate useful.