@schilling.mark.a/software-methodology 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/.github/copilot-instructions.md +106 -0
  2. package/LICENSE +21 -0
  3. package/README.md +174 -0
  4. package/atdd-workflow/SKILL.md +117 -0
  5. package/atdd-workflow/references/green-phase.md +38 -0
  6. package/atdd-workflow/references/red-phase.md +62 -0
  7. package/atdd-workflow/references/refactor-phase.md +75 -0
  8. package/bdd-specification/SKILL.md +88 -0
  9. package/bdd-specification/references/example-mapping.md +105 -0
  10. package/bdd-specification/references/gherkin-patterns.md +214 -0
  11. package/cicd-pipeline/SKILL.md +64 -0
  12. package/cicd-pipeline/references/deployment-rollback.md +176 -0
  13. package/cicd-pipeline/references/environment-promotion.md +159 -0
  14. package/cicd-pipeline/references/pipeline-stages.md +198 -0
  15. package/clean-code/SKILL.md +77 -0
  16. package/clean-code/references/behavioral-patterns.md +329 -0
  17. package/clean-code/references/creational-patterns.md +197 -0
  18. package/clean-code/references/enterprise-patterns.md +334 -0
  19. package/clean-code/references/solid.md +230 -0
  20. package/clean-code/references/structural-patterns.md +238 -0
  21. package/continuous-improvement/SKILL.md +69 -0
  22. package/continuous-improvement/references/measurement.md +133 -0
  23. package/continuous-improvement/references/process-update.md +118 -0
  24. package/continuous-improvement/references/root-cause-analysis.md +144 -0
  25. package/dist/atdd-workflow.skill +0 -0
  26. package/dist/bdd-specification.skill +0 -0
  27. package/dist/cicd-pipeline.skill +0 -0
  28. package/dist/clean-code.skill +0 -0
  29. package/dist/continuous-improvement.skill +0 -0
  30. package/dist/green-implementation.skill +0 -0
  31. package/dist/product-strategy.skill +0 -0
  32. package/dist/story-mapping.skill +0 -0
  33. package/dist/ui-design-system.skill +0 -0
  34. package/dist/ui-design-workflow.skill +0 -0
  35. package/dist/ux-design.skill +0 -0
  36. package/dist/ux-research.skill +0 -0
  37. package/docs/INTEGRATION.md +229 -0
  38. package/docs/QUICKSTART.md +126 -0
  39. package/docs/SHARING.md +828 -0
  40. package/docs/SKILLS.md +296 -0
  41. package/green-implementation/SKILL.md +155 -0
  42. package/green-implementation/references/angular-patterns.md +239 -0
  43. package/green-implementation/references/common-rejections.md +180 -0
  44. package/green-implementation/references/playwright-patterns.md +321 -0
  45. package/green-implementation/references/rxjs-patterns.md +161 -0
  46. package/package.json +57 -0
  47. package/product-strategy/SKILL.md +71 -0
  48. package/product-strategy/references/business-model-canvas.md +199 -0
  49. package/product-strategy/references/canvas-alignment.md +108 -0
  50. package/product-strategy/references/value-proposition-canvas.md +159 -0
  51. package/project-templates/context.md.template +56 -0
  52. package/project-templates/test-strategy.md.template +87 -0
  53. package/story-mapping/SKILL.md +104 -0
  54. package/story-mapping/references/backbone.md +66 -0
  55. package/story-mapping/references/release-planning.md +92 -0
  56. package/story-mapping/references/task-template.md +78 -0
  57. package/story-mapping/references/walking-skeleton.md +63 -0
  58. package/ui-design-system/SKILL.md +48 -0
  59. package/ui-design-system/references/accessibility.md +134 -0
  60. package/ui-design-system/references/components.md +257 -0
  61. package/ui-design-system/references/design-tokens.md +209 -0
  62. package/ui-design-system/references/layout.md +136 -0
  63. package/ui-design-system/references/typography.md +114 -0
  64. package/ui-design-workflow/SKILL.md +90 -0
  65. package/ui-design-workflow/references/acceptance-targets.md +144 -0
  66. package/ui-design-workflow/references/component-selection.md +108 -0
  67. package/ui-design-workflow/references/scenario-to-ui.md +151 -0
  68. package/ui-design-workflow/references/screen-flows.md +116 -0
  69. package/ux-design/SKILL.md +75 -0
  70. package/ux-design/references/information-architecture.md +144 -0
  71. package/ux-design/references/interaction-patterns.md +141 -0
  72. package/ux-design/references/onboarding.md +159 -0
  73. package/ux-design/references/usability-evaluation.md +132 -0
  74. package/ux-research/SKILL.md +75 -0
  75. package/ux-research/references/journey-mapping.md +168 -0
  76. package/ux-research/references/mental-models.md +106 -0
  77. package/ux-research/references/personas.md +102 -0
@@ -0,0 +1,180 @@
1
+ # Common PR Rejections
2
+
3
+ This file tracks patterns that repeatedly cause PR rejections. Each entry
4
+ comes from actual reviewer feedback captured via `record_pr_feedback` in the
5
+ MCP server. When a pattern appears 3+ times, it gets added here.
6
+
7
+ Update this file as part of the PDCA Act step. Every entry here should also
8
+ have a corresponding automated rule in `team-standards.json`.
9
+
10
+ ---
11
+
12
+ ## Architecture Violations
13
+
14
+ ### HttpClient in Components
15
+
16
+ **Frequency:** Very common
17
+ **Rule:** `arch-001`
18
+
19
+ Reviewers reject any component that imports or injects `HttpClient`.
20
+ All HTTP calls go through services.
21
+
22
+ ```typescript
23
+ // ❌ Always rejected
24
+ import { HttpClient } from '@angular/common/http';
25
+ @Component({ ... })
26
+ export class MyComponent {
27
+ constructor(private http: HttpClient) {}
28
+ }
29
+
30
+ // ✅ Fix
31
+ @Component({ ... })
32
+ export class MyComponent {
33
+ constructor(private myService: MyService) {}
34
+ }
35
+ ```
36
+
37
+ ### Business Logic in Component Templates
38
+
39
+ **Frequency:** Common
40
+
41
+ Complex expressions in templates are flagged. Move logic to
42
+ component properties or service methods.
43
+
44
+ ```typescript
45
+ // ❌ Rejected — logic in template
46
+ <div *ngIf="users.filter(u => u.role === 'admin' && u.active).length > 0">
47
+
48
+ // ✅ Fix — computed property
49
+ get hasActiveAdmins(): boolean {
50
+ return this.users.some(u => u.role === 'admin' && u.active);
51
+ }
52
+ // Template: *ngIf="hasActiveAdmins"
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Naming
58
+
59
+ ### Hungarian Notation
60
+
61
+ **Frequency:** Very common
62
+ **Rule:** `naming-001`
63
+
64
+ Team uses plain camelCase. Type prefixes (str, arr, int, bool, obj)
65
+ are always flagged.
66
+
67
+ ### Missing Observable Suffix
68
+
69
+ **Frequency:** Common
70
+ **Rule:** `naming-002`
71
+
72
+ Any property typed as `Observable<T>` must end with `$`.
73
+
74
+ ---
75
+
76
+ ## Error Handling
77
+
78
+ ### Empty Catch Blocks
79
+
80
+ **Frequency:** Very common
81
+ **Rule:** `err-001`
82
+
83
+ Swallowing errors silently is always rejected. Log the error and
84
+ either re-throw or handle it meaningfully.
85
+
86
+ ### switchMap Without catchError
87
+
88
+ **Frequency:** Common (added after PR feedback)
89
+ **Rule:** `rxjs-001` (if added)
90
+
91
+ Inner observables in switchMap that don't handle errors kill the
92
+ outer stream. Reviewers flag this because it causes silent failures
93
+ in production that are extremely hard to debug.
94
+
95
+ ---
96
+
97
+ ## Testing
98
+
99
+ ### Raw Selectors in Test Files
100
+
101
+ **Frequency:** Very common
102
+ **Rule:** `test-001`
103
+
104
+ Any `page.getByTestId()`, `page.locator()`, or `page.getByRole()` directly
105
+ in a `.spec.ts` file is rejected. All selectors must be in Page Object classes.
106
+
107
+ ### Hard-Coded Test Credentials
108
+
109
+ **Frequency:** Common
110
+ **Rule:** `test-002`
111
+
112
+ Hard-coded email addresses, passwords, or API keys in test files are
113
+ always flagged. Use fixtures or test data builders.
114
+
115
+ ### Manual Waits
116
+
117
+ **Frequency:** Common
118
+ **Rule:** `pw-001`
119
+
120
+ `page.waitForTimeout()` is always rejected. Use web-first assertions
121
+ that auto-wait.
122
+
123
+ ---
124
+
125
+ ## Security
126
+
127
+ ### console.log in Production Code
128
+
129
+ **Frequency:** Very common
130
+ **Rule:** `sec-001`
131
+
132
+ Any `console.log`, `console.warn`, or `console.error` in production
133
+ code (non-test files) is rejected. Use LoggerService.
134
+
135
+ ### innerHTML Assignment
136
+
137
+ **Frequency:** Occasional
138
+ **Rule:** `sec-002`
139
+
140
+ Direct `element.innerHTML = ...` creates XSS vulnerabilities.
141
+ Use Angular's `[innerHTML]` binding with DomSanitizer.
142
+
143
+ ---
144
+
145
+ ## Style
146
+
147
+ ### Magic Numbers
148
+
149
+ **Frequency:** Common
150
+ **Rule:** `style-001`
151
+
152
+ Numeric literals other than 0, 1, -1 are flagged. Extract to
153
+ named constants.
154
+
155
+ ```typescript
156
+ // ❌ Rejected
157
+ if (retries >= 3) { ... }
158
+ if (items.length > 50) { ... }
159
+
160
+ // ✅ Fix
161
+ const MAX_RETRIES = 3;
162
+ const MAX_DISPLAY_ITEMS = 50;
163
+ if (retries >= MAX_RETRIES) { ... }
164
+ if (items.length > MAX_DISPLAY_ITEMS) { ... }
165
+ ```
166
+
167
+ ---
168
+
169
+ ## Adding New Entries
170
+
171
+ When a PR is rejected for a pattern not listed here:
172
+
173
+ 1. Call `record_pr_feedback` in the MCP server
174
+ 2. Determine if the pattern can be caught by regex → `add_rule`
175
+ 3. Add an entry here with:
176
+ - **Frequency** — how often this has been flagged
177
+ - **Rule** — the corresponding team-standards.json rule ID
178
+ - **✅/❌ examples** — concrete code showing right vs wrong
179
+ 4. Update the corresponding section in SKILL.md if the summary
180
+ table doesn't already cover it
@@ -0,0 +1,321 @@
1
+ # Playwright & Page Object Model Patterns
2
+
3
+ ## Page Object Structure
4
+
5
+ ### Base Page Object
6
+
7
+ Every page object extends a base class that provides common utilities.
8
+
9
+ ```typescript
10
+ // tests/helpers/base.page.ts
11
+ import { type Page, type Locator, expect } from '@playwright/test';
12
+
13
+ export abstract class BasePage {
14
+ constructor(protected readonly page: Page) {}
15
+
16
+ /** Navigate to this page's URL */
17
+ abstract goto(): Promise<void>;
18
+
19
+ /** Assert the page loaded correctly */
20
+ abstract expectLoaded(): Promise<void>;
21
+
22
+ /** Common: wait for Angular stability */
23
+ protected async waitForAngular(): Promise<void> {
24
+ await this.page.waitForLoadState('networkidle');
25
+ }
26
+ }
27
+ ```
28
+
29
+ ### Feature Page Object
30
+
31
+ ```typescript
32
+ // tests/helpers/user-list.page.ts
33
+ import { type Page, type Locator, expect } from '@playwright/test';
34
+ import { BasePage } from './base.page';
35
+
36
+ export class UserListPage extends BasePage {
37
+ // ─── Locators (user-facing preferred) ──────────────
38
+ readonly heading = this.page.getByRole('heading', { name: 'Users' });
39
+ readonly searchInput = this.page.getByLabel('Search users');
40
+ readonly userRows = this.page.getByRole('row').filter({ hasNot: this.page.getByRole('columnheader') });
41
+ readonly addUserButton = this.page.getByRole('button', { name: 'Add user' });
42
+ readonly loadingSpinner = this.page.getByTestId('loading-spinner');
43
+ readonly emptyState = this.page.getByText('No users found');
44
+ readonly errorBanner = this.page.getByRole('alert');
45
+ readonly paginationNext = this.page.getByRole('button', { name: 'Next page' });
46
+
47
+ constructor(page: Page) {
48
+ super(page);
49
+ }
50
+
51
+ // ─── Navigation ────────────────────────────────────
52
+ async goto(): Promise<void> {
53
+ await this.page.goto('/users');
54
+ await this.waitForAngular();
55
+ }
56
+
57
+ async expectLoaded(): Promise<void> {
58
+ await expect(this.heading).toBeVisible();
59
+ }
60
+
61
+ // ─── Actions (named after user intent) ─────────────
62
+ async searchFor(term: string): Promise<void> {
63
+ await this.searchInput.fill(term);
64
+ // Wait for debounced search to complete
65
+ await this.page.waitForResponse(resp =>
66
+ resp.url().includes('/api/users') && resp.status() === 200
67
+ );
68
+ }
69
+
70
+ async selectUser(name: string): Promise<void> {
71
+ await this.page.getByRole('row').filter({ hasText: name }).click();
72
+ }
73
+
74
+ async goToNextPage(): Promise<void> {
75
+ await this.paginationNext.click();
76
+ await this.waitForAngular();
77
+ }
78
+
79
+ // ─── Assertions (high-level, readable) ─────────────
80
+ async expectUserCount(count: number): Promise<void> {
81
+ await expect(this.userRows).toHaveCount(count);
82
+ }
83
+
84
+ async expectUserVisible(name: string): Promise<void> {
85
+ await expect(this.page.getByRole('row').filter({ hasText: name })).toBeVisible();
86
+ }
87
+
88
+ async expectEmptyState(): Promise<void> {
89
+ await expect(this.emptyState).toBeVisible();
90
+ }
91
+
92
+ async expectError(message: string): Promise<void> {
93
+ await expect(this.errorBanner).toContainText(message);
94
+ }
95
+ }
96
+ ```
97
+
98
+ ### Rules for Page Objects
99
+
100
+ 1. **Locators are properties, not methods** — declare them in the class body
101
+ 2. **Actions are named after user intent** — `searchFor()`, `selectUser()`, not `fillSearchInput()`
102
+ 3. **Assertions belong in the PO** — `expectUserCount()`, not raw `expect()` in test files
103
+ 4. **No test logic in POs** — no conditionals, no test data creation
104
+ 5. **One PO per page/major section** — don't create POs for individual components unless reused across pages
105
+
106
+ ## Test File Structure
107
+
108
+ ### Tests Call Page Object Methods Only
109
+
110
+ ```typescript
111
+ // tests/acceptance/users/user-search.spec.ts
112
+ import { test } from '@playwright/test';
113
+ import { UserListPage } from '../../helpers/user-list.page';
114
+
115
+ test.describe('User Search', () => {
116
+ let userList: UserListPage;
117
+
118
+ test.beforeEach(async ({ page }) => {
119
+ userList = new UserListPage(page);
120
+ await userList.goto();
121
+ await userList.expectLoaded();
122
+ });
123
+
124
+ test('should filter users by name when searching', async () => {
125
+ // Given the user list is loaded (beforeEach)
126
+
127
+ // When I search for "Mark"
128
+ await userList.searchFor('Mark');
129
+
130
+ // Then I should see only matching users
131
+ await userList.expectUserVisible('Mark Schilling');
132
+ await userList.expectUserCount(1);
133
+ });
134
+
135
+ test('should show empty state when no results match', async () => {
136
+ await userList.searchFor('zzzznonexistent');
137
+ await userList.expectEmptyState();
138
+ });
139
+
140
+ test('should show error when search API fails', async ({ page }) => {
141
+ // Simulate API failure
142
+ await page.route('**/api/users**', route => route.abort());
143
+ await userList.searchFor('anything');
144
+ await userList.expectError('Search unavailable');
145
+ });
146
+ });
147
+ ```
148
+
149
+ ### What NEVER Appears in Test Files
150
+
151
+ ```typescript
152
+ // ❌ Raw selectors in test file — move to Page Object
153
+ await page.getByTestId('search-input').fill('Mark');
154
+ await page.locator('.user-row').first().click();
155
+
156
+ // ❌ Manual waits — use web-first assertions
157
+ await page.waitForTimeout(2000);
158
+
159
+ // ❌ Hard-coded credentials
160
+ const email = 'admin@example.com';
161
+ const password = 'Secret123!';
162
+
163
+ // ❌ CSS/XPath selectors
164
+ await page.locator('#user-list > div:nth-child(2)').click();
165
+ ```
166
+
167
+ ## Locator Priority
168
+
169
+ Use the most user-facing locator available:
170
+
171
+ | Priority | Locator | When |
172
+ |---|---|---|
173
+ | 1st | `getByRole()` | Buttons, links, headings, lists, rows |
174
+ | 2nd | `getByLabel()` | Form inputs with labels |
175
+ | 3rd | `getByText()` | Visible text content |
176
+ | 4th | `getByPlaceholder()` | Inputs without visible labels |
177
+ | 5th | `getByTestId()` | No user-facing way to identify the element |
178
+ | Avoid | `locator('.class')` | Fragile — breaks on CSS changes |
179
+ | Never | `locator('#id')` or XPath | Extremely fragile |
180
+
181
+ ## Assertions
182
+
183
+ ### Web-First Only (Auto-Waiting)
184
+
185
+ ```typescript
186
+ // ✅ These auto-wait for the condition to be true
187
+ await expect(locator).toBeVisible();
188
+ await expect(locator).toHaveText('Welcome');
189
+ await expect(locator).toHaveCount(5);
190
+ await expect(locator).toBeEnabled();
191
+ await expect(locator).toHaveAttribute('href', '/users');
192
+ await expect(page).toHaveURL(/\/dashboard/);
193
+
194
+ // ❌ NEVER use manual waits
195
+ await page.waitForTimeout(2000); // Arbitrary delay
196
+ await page.waitForSelector('.loaded'); // CSS selector
197
+ ```
198
+
199
+ ### Negative Assertions
200
+
201
+ ```typescript
202
+ // ✅ Assert something is NOT visible
203
+ await expect(locator).not.toBeVisible();
204
+ await expect(locator).toBeHidden();
205
+
206
+ // ✅ Assert count is zero
207
+ await expect(locator).toHaveCount(0);
208
+ ```
209
+
210
+ ## Test Data
211
+
212
+ ### Fixture Pattern
213
+
214
+ ```typescript
215
+ // tests/fixtures/user.fixtures.ts
216
+ import { type User } from '../../src/app/users/user.model';
217
+
218
+ export class UserFixtures {
219
+ static readonly VALID_USER: Readonly<User> = {
220
+ id: 'test-user-001',
221
+ name: 'Test User',
222
+ email: 'test@example.com',
223
+ role: 'viewer',
224
+ };
225
+
226
+ static readonly ADMIN_USER: Readonly<User> = {
227
+ id: 'test-admin-001',
228
+ name: 'Test Admin',
229
+ email: 'admin@example.com',
230
+ role: 'admin',
231
+ };
232
+
233
+ static createUser(overrides: Partial<User> = {}): User {
234
+ return {
235
+ ...UserFixtures.VALID_USER,
236
+ id: `test-${Date.now()}`,
237
+ ...overrides,
238
+ };
239
+ }
240
+ }
241
+ ```
242
+
243
+ ### API Mocking for Deterministic Tests
244
+
245
+ ```typescript
246
+ // ✅ Mock API responses for predictable test data
247
+ test.beforeEach(async ({ page }) => {
248
+ await page.route('**/api/users', route =>
249
+ route.fulfill({
250
+ status: 200,
251
+ contentType: 'application/json',
252
+ body: JSON.stringify([
253
+ UserFixtures.VALID_USER,
254
+ UserFixtures.ADMIN_USER,
255
+ ]),
256
+ })
257
+ );
258
+ });
259
+ ```
260
+
261
+ ## Feature Flags (LaunchDarkly)
262
+
263
+ ### Testing Feature Flag Variants
264
+
265
+ ```typescript
266
+ // tests/helpers/feature-flags.ts
267
+ export async function setFeatureFlag(
268
+ page: Page,
269
+ flagKey: string,
270
+ value: boolean | string | number
271
+ ): Promise<void> {
272
+ await page.route('**/sdk/evalx/**', async route => {
273
+ const response = await route.fetch();
274
+ const body = await response.json();
275
+ body[flagKey] = { ...body[flagKey], value };
276
+ await route.fulfill({ response, body: JSON.stringify(body) });
277
+ });
278
+ }
279
+
280
+ // In test
281
+ test('should show new dashboard when feature flag enabled', async ({ page }) => {
282
+ await setFeatureFlag(page, 'new-dashboard', true);
283
+ const dashboard = new DashboardPage(page);
284
+ await dashboard.goto();
285
+ await dashboard.expectNewLayoutVisible();
286
+ });
287
+
288
+ test('should show legacy dashboard when feature flag disabled', async ({ page }) => {
289
+ await setFeatureFlag(page, 'new-dashboard', false);
290
+ const dashboard = new DashboardPage(page);
291
+ await dashboard.goto();
292
+ await dashboard.expectLegacyLayoutVisible();
293
+ });
294
+ ```
295
+
296
+ ## File Organization
297
+
298
+ ```
299
+ tests/
300
+ ├── acceptance/ # Outer loop — one dir per domain
301
+ │ ├── users/
302
+ │ │ ├── user-list.spec.ts
303
+ │ │ ├── user-search.spec.ts
304
+ │ │ └── user-create.spec.ts
305
+ │ └── auth/
306
+ │ ├── login.spec.ts
307
+ │ └── logout.spec.ts
308
+ ├── unit/ # Inner loop — mirrors src/ structure
309
+ │ └── users/
310
+ │ ├── user.service.spec.ts
311
+ │ └── user.model.spec.ts
312
+ ├── fixtures/ # Shared test data
313
+ │ ├── user.fixtures.ts
314
+ │ └── auth.fixtures.ts
315
+ └── helpers/ # Page objects + utilities
316
+ ├── base.page.ts
317
+ ├── user-list.page.ts
318
+ ├── login.page.ts
319
+ ├── feature-flags.ts
320
+ └── test-data-builder.ts
321
+ ```
@@ -0,0 +1,161 @@
1
+ # RxJS Implementation Patterns
2
+
3
+ ## Operator Safety Rules
4
+
5
+ ### switchMap Must Have catchError
6
+
7
+ Every `switchMap` creates an inner observable. If that inner observable errors
8
+ without a `catchError`, the entire outer stream dies silently.
9
+
10
+ ```typescript
11
+ // ✅ CORRECT — error handled inside switchMap
12
+ this.searchTerm$.pipe(
13
+ debounceTime(300),
14
+ distinctUntilChanged(),
15
+ switchMap(term =>
16
+ this.searchService.search(term).pipe(
17
+ catchError(error => {
18
+ this.logger.error('Search failed', { term, error });
19
+ return of({ results: [], error: 'Search unavailable' });
20
+ })
21
+ )
22
+ )
23
+ );
24
+
25
+ // ❌ REJECTED — inner observable error kills the stream
26
+ this.searchTerm$.pipe(
27
+ debounceTime(300),
28
+ switchMap(term => this.searchService.search(term))
29
+ );
30
+ ```
31
+
32
+ This applies to all higher-order mapping operators:
33
+ `switchMap`, `mergeMap`, `concatMap`, `exhaustMap`.
34
+
35
+ ### Prefer async/await Over .then() Chains
36
+
37
+ ```typescript
38
+ // ✅ CORRECT
39
+ async onSubmit(): Promise<void> {
40
+ try {
41
+ const result = await firstValueFrom(this.userService.saveUser(this.form.value));
42
+ this.router.navigate(['/users', result.id]);
43
+ } catch (error: unknown) {
44
+ this.handleError(error);
45
+ }
46
+ }
47
+
48
+ // ❌ REJECTED — .then() chains
49
+ onSubmit(): void {
50
+ this.userService.saveUser(this.form.value)
51
+ .toPromise()
52
+ .then(result => this.router.navigate(['/users', result.id]))
53
+ .then(() => this.notification.show('Saved'))
54
+ .catch(error => this.handleError(error));
55
+ }
56
+ ```
57
+
58
+ Exception: when working inside a purely reactive pipeline (e.g., a store effect
59
+ or an interceptor), use operators like `tap` and `catchError` instead of
60
+ async/await. The rule is about component/service methods that call observables,
61
+ not about replacing operators inside pipes.
62
+
63
+ ## Common Reactive Patterns
64
+
65
+ ### Combining Multiple Data Sources
66
+
67
+ ```typescript
68
+ // ✅ combineLatest — emits when any source changes
69
+ readonly viewModel$ = combineLatest([
70
+ this.userService.getUsers(),
71
+ this.filterService.activeFilter$,
72
+ this.sortService.activeSort$,
73
+ ]).pipe(
74
+ map(([users, filter, sort]) => ({
75
+ users: this.applyFilter(users, filter),
76
+ filter,
77
+ sort,
78
+ count: users.length,
79
+ }))
80
+ );
81
+ ```
82
+
83
+ ### Loading State Pattern
84
+
85
+ ```typescript
86
+ // ✅ Track loading, error, and data in one stream
87
+ readonly state$ = this.loadTrigger$.pipe(
88
+ switchMap(() =>
89
+ this.userService.getUsers().pipe(
90
+ map(users => ({ loading: false, error: null, users })),
91
+ catchError(error => of({ loading: false, error: error.message, users: [] as User[] })),
92
+ startWith({ loading: true, error: null, users: [] as User[] })
93
+ )
94
+ )
95
+ );
96
+ ```
97
+
98
+ ### Form Value Changes
99
+
100
+ ```typescript
101
+ // ✅ Debounced reactive form search
102
+ this.searchControl.valueChanges.pipe(
103
+ debounceTime(300),
104
+ distinctUntilChanged(),
105
+ filter((term): term is string => term !== null && term.length >= 2),
106
+ switchMap(term =>
107
+ this.searchService.search(term).pipe(
108
+ catchError(() => of([]))
109
+ )
110
+ ),
111
+ takeUntilDestroyed(this.destroyRef)
112
+ ).subscribe(results => this.results = results);
113
+ ```
114
+
115
+ ## Operators to Prefer
116
+
117
+ | Situation | Use | Avoid |
118
+ |---|---|---|
119
+ | Cancel previous on new emission | `switchMap` | `mergeMap` (causes race conditions) |
120
+ | Queue sequential requests | `concatMap` | `mergeMap` (unordered) |
121
+ | Ignore while processing | `exhaustMap` | `switchMap` (cancels work) |
122
+ | Wait for first value then complete | `firstValueFrom()` | `.toPromise()` (deprecated) |
123
+ | Take one value | `first()` or `take(1)` | Subscribing without unsubscribe |
124
+ | Side effects in a pipe | `tap()` | Nested subscribes |
125
+
126
+ ## Anti-Patterns
127
+
128
+ ### Never Nest Subscribes
129
+
130
+ ```typescript
131
+ // ❌ Nested subscribe — callback hell, memory leaks
132
+ this.route.params.subscribe(params => {
133
+ this.userService.getUserById(params['id']).subscribe(user => {
134
+ this.roleService.getRoles(user.roleId).subscribe(roles => {
135
+ this.roles = roles;
136
+ });
137
+ });
138
+ });
139
+
140
+ // ✅ Flatten with operators
141
+ this.route.params.pipe(
142
+ switchMap(params => this.userService.getUserById(params['id'])),
143
+ switchMap(user => this.roleService.getRoles(user.roleId).pipe(
144
+ catchError(() => of([]))
145
+ )),
146
+ takeUntilDestroyed(this.destroyRef)
147
+ ).subscribe(roles => this.roles = roles);
148
+ ```
149
+
150
+ ### Never Subscribe Just to Assign
151
+
152
+ ```typescript
153
+ // ❌ Subscribe to assign — use async pipe instead
154
+ ngOnInit() {
155
+ this.userService.getUsers().subscribe(users => this.users = users);
156
+ }
157
+
158
+ // ✅ Let Angular manage the subscription
159
+ users$ = this.userService.getUsers();
160
+ // Template: *ngIf="users$ | async as users"
161
+ ```