From 90401b41a19ed2fae35e474bae22e1e6ba7fe016 Mon Sep 17 00:00:00 2001 From: 777genius Date: Wed, 29 Apr 2026 16:09:46 +0300 Subject: [PATCH] chore(member-work-sync): clarify lifecycle dispatch boundary --- .../member-work-sync-control-plane-plan.md | 16 +++++++++------- .../composition/createMemberWorkSyncFeature.ts | 6 +++--- .../MemberWorkSyncNudgeDispatchScheduler.ts | 4 ++-- src/main/index.ts | 2 +- .../MemberWorkSyncNudgeDispatchScheduler.test.ts | 6 +++--- 5 files changed, 18 insertions(+), 16 deletions(-) diff --git a/docs/team-management/member-work-sync-control-plane-plan.md b/docs/team-management/member-work-sync-control-plane-plan.md index 1f845c83..46f0aa61 100644 --- a/docs/team-management/member-work-sync-control-plane-plan.md +++ b/docs/team-management/member-work-sync-control-plane-plan.md @@ -3045,15 +3045,15 @@ Current implementation: ## 22. Runtime Defaults And No Feature Flags -Phase 1 should ship without feature flags. +Phase 1 shipped without feature flags. Reason: - Phase 1 has no nudges, no inbox writes, no task mutation, and no runtime restart behavior. - Adding feature flags for passive status/report validation creates extra branches and makes failures harder to reason about. -- The safe boundary is architectural, not configurational: Phase 1 code simply does not contain the side-effect dispatcher. +- The safe boundary is architectural, not configurational: passive status/report validation stays independent from Phase 2 side effects. -Phase 1 defaults: +Runtime defaults: | Behavior | Default | Why | |---|---:|---| @@ -3061,7 +3061,9 @@ Phase 1 defaults: | `member_work_sync_status` | on | read-only diagnostics | | `member_work_sync_report` | on | server-validated, no board mutation | | pending report intent fallback | on only when identity is not terminally invalid | compatibility with old app/runtime boundaries | -| nudges/outbox/inbox writes | not implemented | avoids hidden flag branches | +| outbox planning | on only for queued reconciles and only when `phase2Readiness=shadow_ready` | prevents status reads from causing side effects | +| scheduled nudge dispatch | on only for lifecycle-active teams | stopped teams must not claim or supersede pending nudges | +| inbox nudge writes | guarded by dispatcher revalidation | lifecycle, current fingerprint, readiness, busy signal, rate limit, and watchdog cooldown are checked immediately before write | Do not add: @@ -3073,9 +3075,9 @@ If Phase 1 needs to be disabled during development, revert or patch the narrow c Phase 2 policy: -- Phase 2 is a separate implementation, not a disabled code path hidden behind a flag in Phase 1. -- If Phase 2 adds nudges, it must add dispatcher/outbox code in its own cut after metrics review. -- Phase 2 may use constants/configuration for rate limits and timing, but not a broad "new vs legacy" branch. +- Phase 2 is implemented as a separate outbox/dispatcher/scheduler path, not as hidden branching inside passive diagnostics. +- Phase 2 does not bypass shadow readiness. If metrics are noisy, the planner returns `phase2_not_ready`. +- Phase 2 uses constants/configuration for rate limits and timing, but not a broad "new vs legacy" branch. Phase 2 runtime constants can be normal typed defaults, not feature gates: diff --git a/src/features/member-work-sync/main/composition/createMemberWorkSyncFeature.ts b/src/features/member-work-sync/main/composition/createMemberWorkSyncFeature.ts index cbd5acbb..4bac2374 100644 --- a/src/features/member-work-sync/main/composition/createMemberWorkSyncFeature.ts +++ b/src/features/member-work-sync/main/composition/createMemberWorkSyncFeature.ts @@ -59,7 +59,7 @@ export function createMemberWorkSyncFeature(deps: { kanbanManager: TeamKanbanManager; membersMetaStore: TeamMembersMetaStore; isTeamActive?: (teamName: string) => Promise | boolean; - listActiveTeamNames?: () => Promise; + listLifecycleActiveTeamNames?: () => Promise; logger?: MemberWorkSyncLoggerPort; }): MemberWorkSyncFeatureFacade { const clock = new SystemClockAdapter(); @@ -110,9 +110,9 @@ export function createMemberWorkSyncFeature(deps: { logger: deps.logger, }); const router = new MemberWorkSyncTeamChangeRouter(agendaSource, queue); - const nudgeDispatchScheduler = deps.listActiveTeamNames + const nudgeDispatchScheduler = deps.listLifecycleActiveTeamNames ? new MemberWorkSyncNudgeDispatchScheduler({ - listActiveTeamNames: deps.listActiveTeamNames, + listLifecycleActiveTeamNames: deps.listLifecycleActiveTeamNames, dispatchDue: (teamNames) => nudgeDispatcher.dispatchDue({ teamNames, diff --git a/src/features/member-work-sync/main/infrastructure/MemberWorkSyncNudgeDispatchScheduler.ts b/src/features/member-work-sync/main/infrastructure/MemberWorkSyncNudgeDispatchScheduler.ts index f2d5a5c0..276b04a1 100644 --- a/src/features/member-work-sync/main/infrastructure/MemberWorkSyncNudgeDispatchScheduler.ts +++ b/src/features/member-work-sync/main/infrastructure/MemberWorkSyncNudgeDispatchScheduler.ts @@ -14,7 +14,7 @@ function unrefTimer(timer: ReturnType): void { } export interface MemberWorkSyncNudgeDispatchSchedulerDeps { - listActiveTeamNames(): Promise; + listLifecycleActiveTeamNames(): Promise; dispatchDue(teamNames: string[]): Promise; intervalMs?: number; logger?: MemberWorkSyncLoggerPort; @@ -84,7 +84,7 @@ export class MemberWorkSyncNudgeDispatchScheduler { private async dispatchOnce(): Promise { try { - const teamNames = uniqueNonEmpty(await this.deps.listActiveTeamNames()); + const teamNames = uniqueNonEmpty(await this.deps.listLifecycleActiveTeamNames()); if (teamNames.length === 0) { return; } diff --git a/src/main/index.ts b/src/main/index.ts index 86ff2de2..109aa9e2 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1236,7 +1236,7 @@ async function initializeServices(): Promise { isTeamActive: (teamName) => teamProvisioningService.isTeamAlive(teamName) || teamProvisioningService.hasProvisioningRun(teamName), - listActiveTeamNames: async () => { + listLifecycleActiveTeamNames: async () => { const teams = await teamDataService.listTeams(); return teams .filter( diff --git a/test/features/member-work-sync/main/MemberWorkSyncNudgeDispatchScheduler.test.ts b/test/features/member-work-sync/main/MemberWorkSyncNudgeDispatchScheduler.test.ts index ceb65ab5..90cc0a30 100644 --- a/test/features/member-work-sync/main/MemberWorkSyncNudgeDispatchScheduler.test.ts +++ b/test/features/member-work-sync/main/MemberWorkSyncNudgeDispatchScheduler.test.ts @@ -13,7 +13,7 @@ describe('MemberWorkSyncNudgeDispatchScheduler', () => { return { claimed: 1, delivered: 1, superseded: 0, retryable: 0, terminal: 0 }; }); const scheduler = new MemberWorkSyncNudgeDispatchScheduler({ - listActiveTeamNames: async () => ['team-a', 'team-a', ' ', 'team-b'], + listLifecycleActiveTeamNames: async () => ['team-a', 'team-a', ' ', 'team-b'], dispatchDue, }); @@ -31,7 +31,7 @@ describe('MemberWorkSyncNudgeDispatchScheduler', () => { it('skips dispatch when there are no active teams', async () => { const dispatchDue = vi.fn(); const scheduler = new MemberWorkSyncNudgeDispatchScheduler({ - listActiveTeamNames: async () => [], + listLifecycleActiveTeamNames: async () => [], dispatchDue, }); @@ -43,7 +43,7 @@ describe('MemberWorkSyncNudgeDispatchScheduler', () => { it('logs and survives list failures without throwing', async () => { const warn = vi.fn(); const scheduler = new MemberWorkSyncNudgeDispatchScheduler({ - listActiveTeamNames: async () => { + listLifecycleActiveTeamNames: async () => { throw new Error('list failed'); }, dispatchDue: vi.fn(),