mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
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.
70 lines
2.6 KiB
JavaScript
70 lines
2.6 KiB
JavaScript
/**
|
|
* ESLint rule: no-actions-in-effects
|
|
*
|
|
* Effects must not use the raw `@ngrx/effects` `Actions` stream. They must
|
|
* inject `LOCAL_ACTIONS` (the default — remote/replayed ops filtered out) or,
|
|
* only for the op-log/archive effects that handle `isRemote` themselves,
|
|
* `ALL_ACTIONS`.
|
|
*
|
|
* This enforces Boundary 1 of the single sync invariant: replayed and remote
|
|
* operations must never re-trigger effects (duplicate side effects + phantom
|
|
* operations). See docs/sync-and-op-log/contributor-sync-model.md.
|
|
*
|
|
* Scoped to every `.effects.ts` file via eslint.config.js. The codebase is
|
|
* already 100% compliant — this is a regression guard, not a migration.
|
|
*
|
|
* Flags:
|
|
* - `import { Actions } from '@ngrx/effects'` (including aliased imports)
|
|
* - `inject(Actions)` (including the aliased local binding)
|
|
*/
|
|
module.exports = {
|
|
meta: {
|
|
type: 'problem',
|
|
docs: {
|
|
description:
|
|
'Effects must inject LOCAL_ACTIONS/ALL_ACTIONS, never the raw @ngrx/effects Actions stream',
|
|
category: 'Possible Errors',
|
|
recommended: true,
|
|
},
|
|
messages: {
|
|
noActionsImport:
|
|
"Do not import `Actions` from '@ngrx/effects' in an effect. Inject `LOCAL_ACTIONS` (default) — or `ALL_ACTIONS` only for op-log/archive effects. See docs/sync-and-op-log/contributor-sync-model.md.",
|
|
noActionsInject:
|
|
'Do not `inject(Actions)` in an effect. Use `inject(LOCAL_ACTIONS)` (default) — or `inject(ALL_ACTIONS)` only for op-log/archive effects. See docs/sync-and-op-log/contributor-sync-model.md.',
|
|
},
|
|
schema: [],
|
|
},
|
|
|
|
create(context) {
|
|
// Local binding name(s) that `Actions` from '@ngrx/effects' was imported as.
|
|
const actionsLocalNames = new Set();
|
|
|
|
return {
|
|
ImportDeclaration(node) {
|
|
if (node.source.value !== '@ngrx/effects') return;
|
|
for (const spec of node.specifiers) {
|
|
if (
|
|
spec.type === 'ImportSpecifier' &&
|
|
spec.imported &&
|
|
spec.imported.name === 'Actions'
|
|
) {
|
|
actionsLocalNames.add(spec.local.name);
|
|
context.report({ node: spec, messageId: 'noActionsImport' });
|
|
}
|
|
}
|
|
},
|
|
|
|
CallExpression(node) {
|
|
if (node.callee.type !== 'Identifier' || node.callee.name !== 'inject') {
|
|
return;
|
|
}
|
|
const arg = node.arguments[0];
|
|
if (!arg || arg.type !== 'Identifier') return;
|
|
// Catch the literal name and any aliased binding of @ngrx/effects Actions.
|
|
if (arg.name === 'Actions' || actionsLocalNames.has(arg.name)) {
|
|
context.report({ node, messageId: 'noActionsInject' });
|
|
}
|
|
},
|
|
};
|
|
},
|
|
};
|