docs: fail the event matrix on a zero-dispatcher row; attribute provider-removed
The chained-fused-dispatch spelling fix (0dc2fe90) was whack-a-mole: each
unrecognized dispatch spelling silently drops a producer edge from the
generated matrix, and only a human reading the table catches it. Convert the
class to a build failure: the generator now hard-errors when any DECLARED
event ends with zero dispatchers — dead vocabulary or a missed spelling,
both actionable ('teach the scan or add a DYNAMIC_EVENT_DISPATCHERS
override'). Zero LISTENERS stays legal: seven current rows (agent/request,
system-prompt/assemble, tools/change, ...) are ordinary extension points
dispatched for out-of-repo plugins.
The guard caught a real one on its first run: subagent/provider-removed
routes through the same contained events.dispatch as subagent/start|end
(it fires inside the provider registration's disposer), but the
DYNAMIC_EVENT_DISPATCHERS override list never got an entry when that
containment routing was introduced — the committed matrix (on master too)
claimed the event has NO dispatcher while tool-subagent listens for it.
Override added; matrix regenerated with the producer edge restored.
This commit is contained in:
@@ -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<string, Set<string>>(), listeners: new Set<string>() }
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user