diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 5687ad15d8..3ea9c7cbbd 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -27,7 +27,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:79`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`parallel`) | [`session-persistence`](../packages/session-persistence/session-persistence) | | `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:107`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude) | | `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:73`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:84`](../packages/subagent/subagent/src/index.ts) | - | [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:84`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) | | `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:96`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude) | | `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:44`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | - | | `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:54`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - | diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index f7e2ce9f4b..a61bfca763 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -203,6 +203,10 @@ const DYNAMIC_EVENT_DISPATCHERS: Array<{ event: string; pkg: string; method: str // listeners or strand an already-started child run. { event: 'subagent/start', pkg: 'subagent', method: 'events.dispatch' }, { event: 'subagent/end', pkg: 'subagent', method: 'events.dispatch' }, + // provider-removed fires inside the provider registration's DISPOSER and + // routes through the same contained dispatch (see emitLifecycle in + // dsh-subagent), so the AST scan cannot attribute it either. + { event: 'subagent/provider-removed', pkg: 'subagent', method: 'events.dispatch' }, ] function generatedHeader(title: string): string[] { @@ -579,6 +583,24 @@ function renderEventRelations(pkgs: Pkg[]): string { const relation = relations.get(event.name) ?? { dispatchers: new Map>(), listeners: new Set() } lines.push(`| \`${event.name}\` | \`${event.mode}\` | ${sourceLink(event.source)} | ${relationPackages(relation.dispatchers, pkgsByShort)} | ${listenerPackages(relation.listeners, pkgsByShort)} |`) } + // Completeness guard: every DECLARED event must have at least one dispatcher + // edge — a zero-dispatcher row is either dead vocabulary or (the observed + // failure mode) a dispatch spelling the AST scan does not recognize, silently + // dropping the producer from the matrix. Fail the generation loud instead: + // teach the scan the new spelling, add a DYNAMIC_EVENT_DISPATCHERS override, + // or remove the dead event. Zero LISTENERS is deliberately legal — an event + // dispatched for out-of-repo plugins is an ordinary extension point. + const undispatched = [...events] + .filter(event => (relations.get(event.name)?.dispatchers.size ?? 0) === 0) + .map(event => event.name) + .sort() + if (undispatched.length > 0) { + throw new Error( + `event-producer-consumer matrix: no dispatcher found for declared event${undispatched.length > 1 ? 's' : ''} ` + + `${undispatched.map(name => `"${name}"`).join(', ')} — dead vocabulary, or a dispatch spelling the scan misses ` + + '(teach scripts/gen-doc-graphs.ts the spelling or add a DYNAMIC_EVENT_DISPATCHERS override)', + ) + } const declared = new Set(events.map(event => event.name)) const extra = [...relations.keys()].filter(event => !declared.has(event)).sort() if (extra.length > 0) {