mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
* docs(sync): add simplification roadmap * docs(sync): strengthen simplification roadmap * docs(sync): simplify sync implementation plan Prioritize deletion, atomic single-tab ownership, and full-sync trigger consolidation while deferring compatibility removals and speculative abstractions. * docs(sync): make simplification roadmap compatibility-safe Narrow deletion work behind persisted-data, provider-eligibility, delivery, and request-cost gates. Keep schema readers and separate unrelated startup and maintenance correctness projects. * docs(sync): fold code-verified review findings into plan * docs(sync): align simplification plan with latest changes * docs(sync): correct trigger-branch claim and add footprint estimate * docs: cap services at 1000 LOC * chore(lint): warn when a service exceeds 1000 lines * chore(lint): raise service size cap to 1200 lines * docs(sync): re-align simplification plan to master6fefd741c5* docs(sync): fold #9054 footprint auth into plan contracts Master advanced 3 commits past the plan's6fefd741c5baseline. Exactly one touches a task surface: #9054 (authenticated LWW project-move footprint, GHSA-8pxh) landed in conflict-resolution.service.ts — the file Task 6 carves up. Contract 14 documented only #9045's decrypt-path check and emphasised the weaker half ("synthetic ops are intentionally exempt; that exemption must survive the disjoint-merge removal"). Read alone, that could bless an op.entityIds read while removing disjoint-merge, reopening the vector one merge removed. #9045's exemption sentence is still accurate at master, so 14 was incomplete rather than wrong. Name the invariant #9054 established: getTaskProjectMoveEntityIds re-derives footprints from the authenticated payload.projectMoveFootprint (LWW) / actionPayload.projectMoveSubTaskIds + actionPayload.task.id (raw TASK_SHARED_UPDATE), never the plaintext envelope, and that one choke point serves the disjoint-merge, local-win and superseded-op callers. Also fence #9054 in §4 and re-align the baseline to7ef7e69e96(#9052 orphan-task navigation and #9056 locale touch no task surface). * chore(lint): make the service size cap enforce on new services 'warn' could not enforce what AGENTS.md asserts. `ng lint` runs the @angular-eslint builder, whose maxWarnings defaults to -1 and is not overridden in angular.json, so warnings never fail a build: a new 1300-line service merged green. The documented flip-to-'error' gate ("once every *.service.ts is under the cap") needed conflict-resolution.service.ts to go 3934 -> 1200, i.e. the whole 7-task simplification plan, so it would realistically never flip. Ratchet instead: 'error' on **/*.service.ts, with the 8 pre-existing offenders grandfathered to non-failing warnings at their real size, so the debt stays visible and the list can only shrink. Verified: a new over-cap service errors (exit 1); all 8 grandfathered warn (exit 0); full `npm run lint:ts` is exit 0 with exactly 8 warnings and 0 errors, which proves the list is complete — a missed offender would fail the build. Every >1200-line @Injectable in the repo is named *.service.ts, so the glob has no coverage hole, and .service.spec.ts is exempt (checked against the 7797-line conflict-resolution spec). Also correct AGENTS.md "1200 LOC": max-lines defaults to skipComments/skipBlankLines false, so the cap counts physical lines.
291 lines
9.4 KiB
JavaScript
291 lines
9.4 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',
|
|
},
|
|
},
|
|
// Op-log persistence: inside an adapter.transaction() callback only the tx
|
|
// handle may be used — adapter methods enqueue behind the transaction's own
|
|
// FIFO queue slot on the SQLite backend and deadlock (see
|
|
// SqliteOpLogAdapter._serialize()).
|
|
{
|
|
files: ['src/app/op-log/**/*.ts'],
|
|
plugins: {
|
|
'local-rules': localRules,
|
|
},
|
|
rules: {
|
|
'local-rules/no-adapter-in-tx': 'error',
|
|
},
|
|
},
|
|
// App code must route logging through Log/SyncLog/OpLog/... helpers.
|
|
// Direct console.* calls bypass the exportable log history users attach
|
|
// to bug reports. The Log implementation itself, tests, and benchmarks
|
|
// (which intentionally dump timing numbers to stdout) are exempt.
|
|
{
|
|
files: ['src/app/**/*.ts'],
|
|
ignores: ['src/app/**/*.spec.ts', 'src/app/**/*.benchmark.ts', 'src/app/core/log.ts'],
|
|
rules: {
|
|
'no-console': 'error',
|
|
},
|
|
},
|
|
// Service size cap (AGENTS.md → Project rules): no service may exceed 1200
|
|
// lines. 'error' so a new service crossing the cap fails CI on the PR that
|
|
// introduces it — 'warn' would be inert, since `ng lint` defaults to
|
|
// maxWarnings: -1 and never fails a build on warnings. Spec files end in
|
|
// `.service.spec.ts`, so they are not matched by this glob and are exempt.
|
|
{
|
|
files: ['**/*.service.ts'],
|
|
rules: {
|
|
'max-lines': ['error', { max: 1200 }],
|
|
},
|
|
},
|
|
// Grandfathered offenders: services already over the cap, downgraded to a
|
|
// (non-failing) warning so they don't red-CI while they are split down. They
|
|
// still warn at their real size, so the debt stays visible in lint output.
|
|
// This list may only ever SHRINK — a new entry means the cap was bypassed.
|
|
// Delete the block once every file below is under 1200 lines.
|
|
{
|
|
files: [
|
|
'src/app/op-log/sync/conflict-resolution.service.ts',
|
|
'src/app/op-log/sync-providers/file-based/file-based-sync-adapter.service.ts',
|
|
'src/app/op-log/persistence/operation-log-store.service.ts',
|
|
'src/app/op-log/sync/operation-log-sync.service.ts',
|
|
'src/app/plugins/plugin-bridge.service.ts',
|
|
'src/app/plugins/plugin.service.ts',
|
|
'src/app/imex/sync/sync-wrapper.service.ts',
|
|
'src/app/features/tasks/task.service.ts',
|
|
],
|
|
rules: {
|
|
'max-lines': ['warn', { max: 1200 }],
|
|
},
|
|
},
|
|
// HTML files
|
|
{
|
|
files: ['**/*.html'],
|
|
extends: [...angular.configs.templateRecommended, prettierRecommended],
|
|
rules: {
|
|
'@angular-eslint/template/no-negated-async': 'off',
|
|
'prettier/prettier': 'error',
|
|
},
|
|
},
|
|
);
|