super-productivity/eslint.config.js
Johannes Millan 42e6626b76 docs(sync): consolidate sync docs + enforce the contributor model
Collapse the sprawling, partly-stale docs/sync-and-op-log/ tree into a
small authoritative set and make the sync-correctness invariant
partly lint-enforced instead of convention-only.

Docs:
- Delete superseded/duplicate/provably-stale design, plan, and
  background-research docs (quick-reference, the architecture-diagrams
  monolith, the "Hybrid Manifest" docs describing code that does not
  exist, completed long-term plans, LLM-synthesis analyses).
- Salvage load-bearing decision history into the surviving docs before
  deletion: rejected-alternatives rationale -> operation-log-architecture
  ("Why this architecture"); vector-clock pruning incident history ->
  vector-clocks.md; archive-payload optimization -> architecture E.7.
- Add contributor-sync-model.md as the single-invariant entry point
  (one user intent = one op; replayed/remote ops must not re-trigger
  effects), with a decision table mapping to the enforcing linters.
- Repoint external/internal cross-refs; add CONTRIBUTING.md + CLAUDE.md
  pointers; record the migration in a dated docs/plans/ design doc.

Enforcement (new eslint-local-rules):
- no-actions-in-effects (error): effects must inject LOCAL_ACTIONS /
  ALL_ACTIONS, never the raw @ngrx/effects Actions stream.
- no-multi-entity-effect (warn, heuristic): flags a literal returned
  array of >=2 action-creator calls; docstring + valid-case specs pin
  exactly which shapes are and are not detected.
- run-specs.js runner wired into `npm run lint` via test:lint-rules;
  refuses to run under test-framework globals and counts RuleTester.run
  invocations so a spec that asserts nothing fails instead of passing.
- Correct the ALL_ACTIONS JSDoc in local-actions.token.ts to match
  reality (archive-operation-handler uses LOCAL_ACTIONS).

Reviewed via parallel multi-agent review; findings W1/W2/W4 and a
dangling doc anchor addressed.
2026-05-15 16:51:50 +02:00

236 lines
7.2 KiB
JavaScript

// @ts-check
const tseslint = require('typescript-eslint');
const angular = require('angular-eslint');
const prettierRecommended = require('eslint-plugin-prettier/recommended');
const preferArrow = require('eslint-plugin-prefer-arrow');
const localRules = require('eslint-plugin-local-rules');
module.exports = tseslint.config(
// Global ignores
{
ignores: [
'app-builds/**/*',
'dist/**',
'node_modules/**/*',
'src/app/t.const.ts',
'src/assets/bundled-plugins/**/*',
'src/app/config/env.generated.ts',
'.tmp/**/*',
'packages/plugin-api/**/*',
'packages/plugin-dev/**/*',
'packages/shared-schema/**/*',
'packages/super-sync-server/**/*',
'packages/vite-plugin/**/*',
'packages/*/dist/**/*',
],
},
// TypeScript files
{
files: ['**/*.ts'],
extends: [
...tseslint.configs.recommended,
...angular.configs.tsRecommended,
prettierRecommended,
],
processor: angular.processInlineTemplates,
plugins: {
'prefer-arrow': preferArrow,
},
languageOptions: {
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
},
},
rules: {
// Disabled rules
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@angular-eslint/component-selector': 'off',
'@angular-eslint/no-input-rename': 'off',
'@typescript-eslint/no-inferrable-types': 'off',
'no-underscore-dangle': 'off',
'arrow-body-style': 'off',
'@typescript-eslint/member-ordering': 'off',
'import/order': 'off',
'arrow-parens': 'off',
'@typescript-eslint/explicit-member-accessibility': 'off',
// Enabled rules
'prettier/prettier': 'error',
'@typescript-eslint/no-unused-vars': [
'error',
{ args: 'none', caughtErrors: 'none' },
],
'@typescript-eslint/explicit-function-return-type': [
'error',
{
allowExpressions: true,
allowTypedFunctionExpressions: true,
allowHigherOrderFunctions: true,
allowDirectConstAssertionInArrowFunctions: true,
allowConciseArrowFunctionExpressionsStartingWithVoid: true,
},
],
'@typescript-eslint/naming-convention': [
'error',
{
selector: 'default',
format: ['camelCase', 'snake_case', 'UPPER_CASE', 'PascalCase'],
leadingUnderscore: 'allowSingleOrDouble',
trailingUnderscore: 'allow',
filter: { regex: '(should)|@tags', match: false },
},
{
selector: 'variable',
format: ['camelCase', 'snake_case', 'UPPER_CASE', 'PascalCase'],
leadingUnderscore: 'allowSingleOrDouble',
trailingUnderscore: 'allow',
},
{ selector: 'enum', format: ['PascalCase', 'UPPER_CASE'] },
{ selector: 'typeLike', format: ['PascalCase'] },
],
'prefer-const': 'error',
'@typescript-eslint/no-unused-expressions': 'error',
'@typescript-eslint/no-empty-object-type': 'error',
'max-len': [
'error',
{
ignorePattern: '^import \\{.+;$',
ignoreRegExpLiterals: true,
ignoreStrings: true,
ignoreUrls: true,
code: 150,
},
],
'id-blacklist': 'error',
// @typescript-eslint/member-delimiter-style removed in v8 - Prettier handles this
'no-shadow': 'off',
'@typescript-eslint/no-shadow': 'error',
'comma-dangle': ['error', 'always-multiline'],
'no-mixed-operators': 'error',
'prefer-arrow/prefer-arrow-functions': 'error',
'@angular-eslint/directive-selector': [
'error',
{ type: 'attribute', prefix: '', style: 'camelCase' },
],
// @typescript-eslint/ban-types replaced by specific rules in v8
'@typescript-eslint/no-unsafe-function-type': 'error',
'@typescript-eslint/no-wrapper-object-types': 'error',
},
},
{
files: ['packages/sync-core/**/*.ts'],
rules: {
'no-restricted-imports': [
'error',
{
paths: [
{
name: '@sp/shared-schema',
message:
'@sp/sync-core must stay domain-agnostic; shared-schema is SP-specific.',
},
],
patterns: [
{
group: [
'@angular/*',
'@ngrx/*',
'@sp/shared-schema/*',
'../shared-schema/*',
'../shared-schema/**',
'../../shared-schema/*',
'../../shared-schema/**',
'**/shared-schema/*',
'**/shared-schema/**',
'src/app/*',
'src/app/**',
'**/src/app/*',
'**/src/app/**',
],
message:
'@sp/sync-core must not import Angular, NgRx, app code, or SP-specific schema packages.',
},
],
},
],
'no-restricted-syntax': [
'error',
{
selector: 'ImportExpression',
message:
'@sp/sync-core must not use dynamic imports; they bypass package-boundary checks.',
},
],
},
},
{
files: ['packages/sync-providers/**/*.ts'],
rules: {
'no-restricted-imports': [
'error',
{
paths: [
{
name: '@sp/shared-schema',
message: '@sp/sync-providers must not import SP-specific schema packages.',
},
],
patterns: [
{
group: [
'@angular/*',
'@ngrx/*',
'@sp/shared-schema/*',
'@sp/sync-core/*',
'**/shared-schema/*',
'**/shared-schema/**',
'**/sync-core/*',
'**/sync-core/**',
'src/app/*',
'src/app/**',
'**/src/app/*',
'**/src/app/**',
],
message:
'@sp/sync-providers must use only public @sp/sync-core exports and must not import Angular, NgRx, app code, or SP-specific schema packages.',
},
],
},
],
'no-restricted-syntax': [
'error',
{
selector: 'ImportExpression',
message:
'@sp/sync-providers must not use dynamic imports; they bypass package-boundary checks.',
},
],
},
},
// NgRx effects files - require hydration guards on selector-based effects
{
files: ['**/*.effects.ts'],
plugins: {
'local-rules': localRules,
},
rules: {
'local-rules/require-hydration-guard': 'error',
'local-rules/require-entity-registry': 'warn',
'local-rules/no-actions-in-effects': 'error',
'local-rules/no-multi-entity-effect': 'warn',
},
},
// HTML files
{
files: ['**/*.html'],
extends: [...angular.configs.templateRecommended, prettierRecommended],
rules: {
'@angular-eslint/template/no-negated-async': 'off',
'prettier/prettier': 'error',
},
},
);