chore(member-work-sync): clarify lifecycle dispatch boundary
This commit is contained in:
parent
d27c1bcc51
commit
90401b41a1
5 changed files with 18 additions and 16 deletions
|
|
@ -3045,15 +3045,15 @@ Current implementation:
|
||||||
|
|
||||||
## 22. Runtime Defaults And No Feature Flags
|
## 22. Runtime Defaults And No Feature Flags
|
||||||
|
|
||||||
Phase 1 should ship without feature flags.
|
Phase 1 shipped without feature flags.
|
||||||
|
|
||||||
Reason:
|
Reason:
|
||||||
|
|
||||||
- Phase 1 has no nudges, no inbox writes, no task mutation, and no runtime restart behavior.
|
- 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.
|
- 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 |
|
| Behavior | Default | Why |
|
||||||
|---|---:|---|
|
|---|---:|---|
|
||||||
|
|
@ -3061,7 +3061,9 @@ Phase 1 defaults:
|
||||||
| `member_work_sync_status` | on | read-only diagnostics |
|
| `member_work_sync_status` | on | read-only diagnostics |
|
||||||
| `member_work_sync_report` | on | server-validated, no board mutation |
|
| `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 |
|
| 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:
|
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 policy:
|
||||||
|
|
||||||
- Phase 2 is a separate implementation, not a disabled code path hidden behind a flag in Phase 1.
|
- Phase 2 is implemented as a separate outbox/dispatcher/scheduler path, not as hidden branching inside passive diagnostics.
|
||||||
- If Phase 2 adds nudges, it must add dispatcher/outbox code in its own cut after metrics review.
|
- Phase 2 does not bypass shadow readiness. If metrics are noisy, the planner returns `phase2_not_ready`.
|
||||||
- Phase 2 may use constants/configuration for rate limits and timing, but not a broad "new vs legacy" branch.
|
- 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:
|
Phase 2 runtime constants can be normal typed defaults, not feature gates:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ export function createMemberWorkSyncFeature(deps: {
|
||||||
kanbanManager: TeamKanbanManager;
|
kanbanManager: TeamKanbanManager;
|
||||||
membersMetaStore: TeamMembersMetaStore;
|
membersMetaStore: TeamMembersMetaStore;
|
||||||
isTeamActive?: (teamName: string) => Promise<boolean> | boolean;
|
isTeamActive?: (teamName: string) => Promise<boolean> | boolean;
|
||||||
listActiveTeamNames?: () => Promise<string[]>;
|
listLifecycleActiveTeamNames?: () => Promise<string[]>;
|
||||||
logger?: MemberWorkSyncLoggerPort;
|
logger?: MemberWorkSyncLoggerPort;
|
||||||
}): MemberWorkSyncFeatureFacade {
|
}): MemberWorkSyncFeatureFacade {
|
||||||
const clock = new SystemClockAdapter();
|
const clock = new SystemClockAdapter();
|
||||||
|
|
@ -110,9 +110,9 @@ export function createMemberWorkSyncFeature(deps: {
|
||||||
logger: deps.logger,
|
logger: deps.logger,
|
||||||
});
|
});
|
||||||
const router = new MemberWorkSyncTeamChangeRouter(agendaSource, queue);
|
const router = new MemberWorkSyncTeamChangeRouter(agendaSource, queue);
|
||||||
const nudgeDispatchScheduler = deps.listActiveTeamNames
|
const nudgeDispatchScheduler = deps.listLifecycleActiveTeamNames
|
||||||
? new MemberWorkSyncNudgeDispatchScheduler({
|
? new MemberWorkSyncNudgeDispatchScheduler({
|
||||||
listActiveTeamNames: deps.listActiveTeamNames,
|
listLifecycleActiveTeamNames: deps.listLifecycleActiveTeamNames,
|
||||||
dispatchDue: (teamNames) =>
|
dispatchDue: (teamNames) =>
|
||||||
nudgeDispatcher.dispatchDue({
|
nudgeDispatcher.dispatchDue({
|
||||||
teamNames,
|
teamNames,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ function unrefTimer(timer: ReturnType<typeof setTimeout>): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MemberWorkSyncNudgeDispatchSchedulerDeps {
|
export interface MemberWorkSyncNudgeDispatchSchedulerDeps {
|
||||||
listActiveTeamNames(): Promise<string[]>;
|
listLifecycleActiveTeamNames(): Promise<string[]>;
|
||||||
dispatchDue(teamNames: string[]): Promise<MemberWorkSyncNudgeDispatchSummary>;
|
dispatchDue(teamNames: string[]): Promise<MemberWorkSyncNudgeDispatchSummary>;
|
||||||
intervalMs?: number;
|
intervalMs?: number;
|
||||||
logger?: MemberWorkSyncLoggerPort;
|
logger?: MemberWorkSyncLoggerPort;
|
||||||
|
|
@ -84,7 +84,7 @@ export class MemberWorkSyncNudgeDispatchScheduler {
|
||||||
|
|
||||||
private async dispatchOnce(): Promise<void> {
|
private async dispatchOnce(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const teamNames = uniqueNonEmpty(await this.deps.listActiveTeamNames());
|
const teamNames = uniqueNonEmpty(await this.deps.listLifecycleActiveTeamNames());
|
||||||
if (teamNames.length === 0) {
|
if (teamNames.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1236,7 +1236,7 @@ async function initializeServices(): Promise<void> {
|
||||||
isTeamActive: (teamName) =>
|
isTeamActive: (teamName) =>
|
||||||
teamProvisioningService.isTeamAlive(teamName) ||
|
teamProvisioningService.isTeamAlive(teamName) ||
|
||||||
teamProvisioningService.hasProvisioningRun(teamName),
|
teamProvisioningService.hasProvisioningRun(teamName),
|
||||||
listActiveTeamNames: async () => {
|
listLifecycleActiveTeamNames: async () => {
|
||||||
const teams = await teamDataService.listTeams();
|
const teams = await teamDataService.listTeams();
|
||||||
return teams
|
return teams
|
||||||
.filter(
|
.filter(
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ describe('MemberWorkSyncNudgeDispatchScheduler', () => {
|
||||||
return { claimed: 1, delivered: 1, superseded: 0, retryable: 0, terminal: 0 };
|
return { claimed: 1, delivered: 1, superseded: 0, retryable: 0, terminal: 0 };
|
||||||
});
|
});
|
||||||
const scheduler = new MemberWorkSyncNudgeDispatchScheduler({
|
const scheduler = new MemberWorkSyncNudgeDispatchScheduler({
|
||||||
listActiveTeamNames: async () => ['team-a', 'team-a', ' ', 'team-b'],
|
listLifecycleActiveTeamNames: async () => ['team-a', 'team-a', ' ', 'team-b'],
|
||||||
dispatchDue,
|
dispatchDue,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -31,7 +31,7 @@ describe('MemberWorkSyncNudgeDispatchScheduler', () => {
|
||||||
it('skips dispatch when there are no active teams', async () => {
|
it('skips dispatch when there are no active teams', async () => {
|
||||||
const dispatchDue = vi.fn();
|
const dispatchDue = vi.fn();
|
||||||
const scheduler = new MemberWorkSyncNudgeDispatchScheduler({
|
const scheduler = new MemberWorkSyncNudgeDispatchScheduler({
|
||||||
listActiveTeamNames: async () => [],
|
listLifecycleActiveTeamNames: async () => [],
|
||||||
dispatchDue,
|
dispatchDue,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -43,7 +43,7 @@ describe('MemberWorkSyncNudgeDispatchScheduler', () => {
|
||||||
it('logs and survives list failures without throwing', async () => {
|
it('logs and survives list failures without throwing', async () => {
|
||||||
const warn = vi.fn();
|
const warn = vi.fn();
|
||||||
const scheduler = new MemberWorkSyncNudgeDispatchScheduler({
|
const scheduler = new MemberWorkSyncNudgeDispatchScheduler({
|
||||||
listActiveTeamNames: async () => {
|
listLifecycleActiveTeamNames: async () => {
|
||||||
throw new Error('list failed');
|
throw new Error('list failed');
|
||||||
},
|
},
|
||||||
dispatchDue: vi.fn(),
|
dispatchDue: vi.fn(),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue