@@ -495,8 +495,9 @@ function objectUnit(file, src, obj, fileFunctions) {
495495 * - ANY of them registered from inside a handler → that handler's own origin,
496496 * because it can only run once the registering handler has.
497497 *
498- * A fired hook's name is a string literal, or a parameter the call site bound
499- * to one (`emitCatalogEvent(ctx, 'app:registered', sys)` reaches
498+ * A hook is fired by `.trigger` on the PLUGIN CONTEXT (or `trigger.call` with
499+ * the context as `this`). Its name is a string literal, or a parameter the call
500+ * site bound to one (`emitCatalogEvent(ctx, 'app:registered', sys)` reaches
500501 * `trigger.call(ctx, event, payload)`); anything else is recorded UNRESOLVED,
501502 * and `audit()` refuses on it rather than derive a population it cannot name.
502503 */
@@ -507,6 +508,42 @@ function preBindReads(unit, src, hookNames = new Set([READY_HOOK, ...Object.keys
507508 /** Local (block-scoped) function bindings seen anywhere along the walk. */
508509 const locals = new Map ( ) ;
509510
511+ // The PLUGIN CONTEXT, by the names it travels under: the lifecycle methods'
512+ // first parameter, every helper parameter a call site hands it to, and every
513+ // `this.X = <context>` field. Only a `.trigger` on the context fires a kernel
514+ // hook — a job or flow service's `.trigger(name)` is not one, and reading it
515+ // as one would refuse a correct plugin with a remedy that does not apply.
516+ const contextNames = new Set ( ) ;
517+ const contextFields = new Set ( ) ;
518+ for ( const phase of [ 'init' , 'start' ] ) {
519+ const p = unit . methods . get ( phase ) ?. parameters ?. [ 0 ] ;
520+ if ( p && ts . isIdentifier ( p . name ) ) contextNames . add ( p . name . text ) ;
521+ }
522+ const unwrap = ( node ) => {
523+ let n = node ;
524+ while ( n && ( ts . isParenthesizedExpression ( n ) || ts . isAsExpression ( n ) ||
525+ ts . isNonNullExpression ( n ) || ts . isTypeAssertionExpression ( n ) ) ) n = n . expression ;
526+ return n ;
527+ } ;
528+ const isContext = ( node ) => {
529+ const n = unwrap ( node ) ;
530+ if ( ! n ) return false ;
531+ if ( ts . isIdentifier ( n ) ) return contextNames . has ( n . text ) ;
532+ return ts . isPropertyAccessExpression ( n ) && n . expression . kind === ts . SyntaxKind . ThisKeyword &&
533+ contextFields . has ( n . name . text ) ;
534+ } ;
535+ for ( const method of unit . methods . values ( ) ) {
536+ const scanFields = ( node ) => {
537+ if ( ts . isBinaryExpression ( node ) && node . operatorToken . kind === ts . SyntaxKind . EqualsToken &&
538+ ts . isPropertyAccessExpression ( node . left ) && node . left . expression . kind === ts . SyntaxKind . ThisKeyword &&
539+ isContext ( node . right ) ) {
540+ contextFields . add ( node . left . name . text ) ;
541+ }
542+ ts . forEachChild ( node , scanFields ) ;
543+ } ;
544+ if ( method . body ) scanFields ( method . body ) ;
545+ }
546+
510547 const walk = ( fnNode , origin , visited , onHook , bindings = new Map ( ) ) => {
511548 if ( ! fnNode ) return ;
512549 // Keyed on the node AND its string bindings: one helper reached twice with
@@ -535,11 +572,13 @@ function preBindReads(unit, src, hookNames = new Set([READY_HOOK, ...Object.keys
535572 return undefined ;
536573 } ;
537574
538- /** The callee's parameters bound to whatever string arguments resolve. */
575+ /** The callee's parameters bound to whatever string arguments resolve —
576+ * and, unit-wide, the parameters it receives the context through. */
539577 const bindArgs = ( target , args ) => {
540578 const out = new Map ( ) ;
541579 ( target . parameters ?? [ ] ) . forEach ( ( p , i ) => {
542580 if ( ! ts . isIdentifier ( p . name ) ) return ;
581+ if ( isContext ( args [ i ] ) ) contextNames . add ( p . name . text ) ;
543582 const value = resolveString ( args [ i ] ) ;
544583 if ( value !== undefined ) out . set ( p . name . text , value ) ;
545584 } ) ;
@@ -600,12 +639,14 @@ function preBindReads(unit, src, hookNames = new Set([READY_HOOK, ...Object.keys
600639 if ( handler ) onHook ( nameArg . text , handler ) ;
601640 }
602641 }
603- // `<anything>.trigger(name, …)` — the hook is FIRED here.
604- if ( callee . name . text === EMIT_CALLEE ) recordEmit ( node . arguments [ 0 ] , node ) ;
605- // `trigger.call(thisArg, name, …)` — AppPlugin's spelling, which
642+ // `<context>.trigger(name, …)` — the hook is FIRED here.
643+ if ( callee . name . text === EMIT_CALLEE && isContext ( callee . expression ) ) {
644+ recordEmit ( node . arguments [ 0 ] , node ) ;
645+ }
646+ // `trigger.call(<context>, name, …)` — AppPlugin's spelling, which
606647 // reads the method off the context first so a kernel without one
607- // is a no-op rather than a throw.
608- if ( callee . name . text === 'call' &&
648+ // is a no-op rather than a throw. The context is the `this` argument.
649+ if ( callee . name . text === 'call' && isContext ( node . arguments [ 0 ] ) &&
609650 ( ( ts . isIdentifier ( callee . expression ) && callee . expression . text === EMIT_CALLEE ) ||
610651 ( ts . isPropertyAccessExpression ( callee . expression ) && callee . expression . name . text === EMIT_CALLEE ) ) ) {
611652 recordEmit ( node . arguments [ 1 ] , node ) ;
@@ -1448,6 +1489,7 @@ function selfTest() {
14481489 // emitCatalogEvent; the continuation is the over-budget seed path.
14491490 {
14501491 const { units } = auditSource ( PROVIDER + `
1492+ function fireFrom(pluginCtx: PluginContext) { pluginCtx.trigger('x:renamed'); }
14511493 export class EmitterPlugin implements Plugin {
14521494 name = 'plugin.emitter';
14531495 private emitCatalogEvent(ctx: PluginContext, event: string, sys: any): void {
@@ -1457,9 +1499,13 @@ function selfTest() {
14571499 private emitWhenSettled(ctx: PluginContext, event: string): void {
14581500 settled.then(() => ctx.trigger(event));
14591501 }
1460- async init(ctx: PluginContext) { await ctx.trigger('x:init', {}); }
1502+ async init(ctx: PluginContext) { this.ctx = ctx; await ctx.trigger('x:init', {}); }
14611503 async start(ctx: PluginContext) {
14621504 await ctx.trigger('x:direct', this.service);
1505+ this.ctx.trigger('x:stored');
1506+ fireFrom(ctx);
1507+ jobs.trigger('nightly-cleanup');
1508+ this.jobs.trigger(jobName);
14631509 const emitSettled = (overBudget: boolean) => {
14641510 const trigger = (ctx as any).trigger;
14651511 trigger.call(ctx, 'x:call', { overBudget });
@@ -1476,8 +1522,10 @@ function selfTest() {
14761522 ` ) ;
14771523 const derived = derivePreBindHooks ( units ) ;
14781524 const names = [ ...derived . fired . keys ( ) ] . sort ( ) ;
1479- assert ( JSON . stringify ( names ) === JSON . stringify ( [ 'x:call' , 'x:direct' , 'x:init' , 'x:param' , 'x:then' , 'x:then-param' ] ) ,
1480- `the derivation reads every shipped fire-site spelling and nothing deferred (got ${ names . join ( ', ' ) } )` ) ;
1525+ assert ( JSON . stringify ( names ) === JSON . stringify (
1526+ [ 'x:call' , 'x:direct' , 'x:init' , 'x:param' , 'x:renamed' , 'x:stored' , 'x:then' , 'x:then-param' ] ) ,
1527+ `the derivation reads every shipped fire-site spelling on the context, and nothing deferred or ` +
1528+ `off-context (got ${ names . join ( ', ' ) } )` ) ;
14811529 assert ( derived . fired . get ( 'x:init' ) [ 0 ] . origin === 'init-body' , 'an init() fire site is recorded as init-body' ) ;
14821530 assert ( derived . unresolved . length === 1 && derived . unresolved [ 0 ] . text === 'eventName' ,
14831531 'a hook name the walk cannot resolve is recorded, not dropped' ) ;
0 commit comments