2026-07-09 02:38:54 +08:00
/**
2026-07-12 03:36:43 +08:00
* Scoped-dispatch drift gate: the set of scope-filtered events is declared in TWO places that
* must never diverge — the dev-invariants runtime table (the `scopedSubject` map in
* `packages/support/invariants/src/index.ts`, which enforces carriers at dispatch time) and
* the event declarations' JSDoc (the "Scope-filtered dispatch" sentence rendered into the
* events catalog, which tells plugin authors what a scoped listener will and won't hear).
2026-07-13 23:27:00 +08:00
* Registry-subject notifications are intentionally unfiltered and belong in neither set.
2026-07-09 02:38:54 +08:00
*/
import { globSync , readFileSync } from 'node:fs'
import { resolve } from 'node:path'
const root = resolve ( import . meta . dirname , '..' )
/** The marker sentence every scope-filtered event's JSDoc carries. */
const MARKER = 'Scope-filtered dispatch'
/** Events that are deliberately UNFILTERED registry-subject notifications. */
const REGISTRY_SUBJECT = new Set ( [ 'tools/change' , 'system-prompt/change' , 'subagent/provider-added' , 'subagent/provider-removed' ] )
function invariantTable ( ) : Set < string > {
const source = readFileSync ( resolve ( root , 'packages/support/invariants/src/index.ts' ) , 'utf8' )
const start = source . indexOf ( 'const scopedSubject' )
if ( start < 0 ) throw new Error ( 'verify-scoped-dispatch: cannot find the scopedSubject table in dsh-invariants' )
const block = source . slice ( start , source . indexOf ( '}' , start ) )
return new Set ( [ . . . block . matchAll ( /'([a-z-]+\/[a-z-]+)':/g ) ] . flatMap ( match = > match [ 1 ] === undefined ? [ ] : [ match [ 1 ] ] ) )
}
function documentedSet ( ) : Set < string > {
const documented = new Set < string > ( )
for ( const rel of globSync ( 'packages/*/*/src/**/*.ts' , { cwd : root } ) ) {
const source = readFileSync ( resolve ( root , rel ) , 'utf8' )
if ( ! source . includes ( MARKER ) ) continue
// Each event declaration: a JSDoc block followed by the quoted event name.
// Tolerate `//` comment lines between the JSDoc and the declaration
// (e.g. an inline TODO under the doc block).
for ( const match of source . matchAll ( /\/\*\*([\s\S]*?)\*\/\s*\n(?:\s*\/\/[^\n]*\n)*\s*'([a-z-]+\/[a-z-]+)'\(/g ) ) {
const [ , doc , event ] = match
if ( doc === undefined || event === undefined ) continue
if ( doc . includes ( MARKER ) ) documented . add ( event )
}
}
return documented
}
const table = invariantTable ( )
const documented = documentedSet ( )
const problems : string [ ] = [ ]
for ( const event of table ) {
if ( ! documented . has ( event ) ) {
problems . push ( ` " ${ event } " is enforced by the dev-invariants carrier table but its declaration JSDoc carries no " ${ MARKER } " sentence — document the filtering plugin authors will observe. ` )
}
if ( REGISTRY_SUBJECT . has ( event ) ) {
problems . push ( ` " ${ event } " is a registry-subject notification (deliberately unfiltered) but appears in the dev-invariants carrier table. ` )
}
}
for ( const event of documented ) {
if ( ! table . has ( event ) ) {
problems . push ( ` " ${ event } " documents scope-filtered dispatch but is missing from the dev-invariants carrier table (packages/support/invariants) — a bare dispatch of it would silently revert to global delivery. ` )
}
}
if ( problems . length > 0 ) {
console . error ( ` verify-scoped-dispatch: ${ problems . length } drift(s) between the invariant table and the documented scoped-event set: ` )
for ( const problem of problems ) console . error ( ` - ${ problem } ` )
process . exit ( 1 )
}
console . log ( ` verify-scoped-dispatch: ${ table . size } scope-filtered event(s) consistent between the invariant table and the declaration docs. ` )