Skip to content

Commit 044bcfe

Browse files
TheLarkInnCopilot
andcommitted
Align shadow emission with the realigned reporter contracts
- Scoped reporters emit human messages on the messageEmitted channel with a fail-safe local-sensitive privacy default (was: activityChanged with a producer-set required flag, which was coalescible under pressure) - Producers no longer set 'required'; the manager derives it from event type - Register RUSH_PLUGIN_API_INCOMPATIBLE in the diagnostic registry with its template Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent b601ef5 commit 044bcfe

8 files changed

Lines changed: 293 additions & 20 deletions

File tree

common/reviews/api/rush-reporter.api.md

Lines changed: 259 additions & 0 deletions
Large diffs are not rendered by default.

libraries/reporter/src/diagnostics/RushDiagnosticCodeRegistry.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,13 @@ export const RUSH_DIAGNOSTIC_CODE_DEFINITIONS = [
130130
defaultSeverity: 'error',
131131
summaryKey: 'diagnostic.RUSH_INTERNAL_UNEXPECTED.summary',
132132
detailKey: 'diagnostic.RUSH_INTERNAL_UNEXPECTED.detail'
133+
},
134+
{
135+
code: 'RUSH_PLUGIN_API_INCOMPATIBLE',
136+
category: 'configuration',
137+
defaultSeverity: 'error',
138+
summaryKey: 'diagnostic.RUSH_PLUGIN_API_INCOMPATIBLE.summary',
139+
detailKey: undefined
133140
}
134141
] as const satisfies readonly IRushDiagnosticCodeDefinition[];
135142

libraries/reporter/src/diagnostics/templates/configuration.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,7 @@
1010
*/
1111
// eslint-disable-next-line @typescript-eslint/typedef -- literal keys are required for the Record<RushDiagnosticTemplateKey, string> aggregate check
1212
export const CONFIGURATION_DIAGNOSTIC_TEMPLATES = {
13-
'diagnostic.RUSH_CONFIG_INVALID_JSON.summary': 'The configuration file {file} contains invalid JSON.'
13+
'diagnostic.RUSH_CONFIG_INVALID_JSON.summary': 'The configuration file {file} contains invalid JSON.',
14+
'diagnostic.RUSH_PLUGIN_API_INCOMPATIBLE.summary':
15+
'The plugin {pluginName} declares plugin API version {declaredApiVersion}, which is incompatible with this Rush (supported: {supportedApiVersion}).'
1416
} as const;

libraries/reporter/src/lifecycle/LifecycleEmitter.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,6 @@ export class LifecycleEmitter {
149149
source: this._source,
150150
scope,
151151
privacy,
152-
required: true,
153152
type,
154153
payload
155154
});

libraries/reporter/src/session/ScopedReporterFactory.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,15 @@ export function createScopedReporter(options: ICreateScopedReporterOptions): ISc
6666

6767
return {
6868
emitMessage(messageOptions: IScopedMessageOptions): string {
69-
const required: boolean = messageOptions.severity === 'warning' || messageOptions.severity === 'error';
7069
return sink.emit({
7170
protocolVersion,
7271
sessionId,
7372
source,
7473
scope,
75-
privacy: messageOptions.privacy ?? 'public',
76-
required,
77-
type: 'activityChanged',
78-
payload: { kind: 'message', severity: messageOptions.severity, text: messageOptions.text }
74+
// Message text fails safe at local-sensitive by default.
75+
privacy: messageOptions.privacy ?? 'local-sensitive',
76+
type: 'messageEmitted',
77+
payload: { severity: messageOptions.severity, text: messageOptions.text }
7978
});
8079
},
8180

@@ -89,7 +88,6 @@ export function createScopedReporter(options: ICreateScopedReporterOptions): ISc
8988
source,
9089
scope,
9190
privacy: computeEnvelopePrivacyFloor(classifications),
92-
required: diagnostic.severity === 'error',
9391
type: 'diagnosticEmitted',
9492
payload: diagnostic
9593
});
@@ -105,7 +103,6 @@ export function createScopedReporter(options: ICreateScopedReporterOptions): ISc
105103
source,
106104
scope,
107105
privacy: 'public',
108-
required: false,
109106
type: 'extension',
110107
payload: { name, payload }
111108
});

libraries/reporter/src/test/Lifecycle.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
summarizeShadowResult,
88
createRushDiagnostic,
99
ReporterManager,
10+
isReporterEventRequired,
1011
type IReporter,
1112
type IReporterEventEnvelope,
1213
type IReporterEventSink,
@@ -63,7 +64,8 @@ describe('LifecycleEmitter', () => {
6364
emitter.emitOperationRegistered({ operationId: 'op1', projectName: 'p', phaseName: '_phase:build' });
6465

6566
expect(sink.inputs[0].type).toBe('operationRegistered');
66-
expect(sink.inputs[0].required).toBe(true);
67+
// Lifecycle events are protected; the manager derives `required` from the type.
68+
expect(isReporterEventRequired('operationRegistered')).toBe(true);
6769
expect(sink.inputs[0].scope).toEqual({
6870
commandName: 'build',
6971
operationId: 'op1',
@@ -82,7 +84,7 @@ describe('LifecycleEmitter', () => {
8284
);
8385
expect(sink.inputs[0].type).toBe('diagnosticEmitted');
8486
expect(sink.inputs[0].privacy).toBe('local-sensitive');
85-
expect(sink.inputs[0].required).toBe(true);
87+
expect(isReporterEventRequired('diagnosticEmitted')).toBe(true);
8688
});
8789

8890
it('writes nothing to stdout or stderr while events flow (shadow mode)', () => {

libraries/reporter/src/test/Session.test.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
RUSH_PLUGIN_API_VERSION,
1111
isPluginApiVersionSupported,
1212
createPluginApiIncompatibleDiagnostic,
13+
isReporterEventRequired,
1314
type IReporter,
1415
type IReporterEventEnvelope,
1516
type IReporterEventScope,
@@ -65,19 +66,24 @@ describe('createScopedReporter', () => {
6566
});
6667

6768
reporter.emitMessage({ severity: 'info', text: 'hello' });
68-
expect(sink.inputs[0].type).toBe('activityChanged');
69+
expect(sink.inputs[0].type).toBe('messageEmitted');
6970
expect(sink.inputs[0].scope).toEqual(scope);
70-
expect(sink.inputs[0].required).toBe(false);
71-
expect(sink.inputs[0].payload).toEqual({ kind: 'message', severity: 'info', text: 'hello' });
71+
// The manager derives `required`; messages are never coalescible.
72+
expect(isReporterEventRequired('messageEmitted')).toBe(true);
73+
// Message text fails safe at local-sensitive by default.
74+
expect(sink.inputs[0].privacy).toBe('local-sensitive');
75+
expect(sink.inputs[0].payload).toEqual({ severity: 'info', text: 'hello' });
7276
});
7377

74-
it('marks warning and error messages as required', () => {
78+
it('emits messages on the messageEmitted channel for every severity', () => {
7579
const sink: CapturingSink = new CapturingSink();
7680
const reporter: IScopedReporter = createScopedReporter({ sink, sessionId: 'sess', source: SOURCE });
7781
reporter.emitMessage({ severity: 'warning', text: 'careful' });
7882
reporter.emitMessage({ severity: 'error', text: 'boom' });
79-
expect(sink.inputs[0].required).toBe(true);
80-
expect(sink.inputs[1].required).toBe(true);
83+
expect(sink.inputs[0].type).toBe('messageEmitted');
84+
expect(sink.inputs[0].payload).toEqual({ severity: 'warning', text: 'careful' });
85+
expect(sink.inputs[1].type).toBe('messageEmitted');
86+
expect(sink.inputs[1].payload).toEqual({ severity: 'error', text: 'boom' });
8187
});
8288

8389
it('emits diagnostics with the envelope privacy floor', () => {
@@ -93,7 +99,7 @@ describe('createScopedReporter', () => {
9399
expect(sink.inputs[0].type).toBe('diagnosticEmitted');
94100
// Least sensitive field is the floor.
95101
expect(sink.inputs[0].privacy).toBe('public');
96-
expect(sink.inputs[0].required).toBe(true);
102+
expect(isReporterEventRequired('diagnosticEmitted')).toBe(true);
97103
expect(sink.inputs[0].payload).toBe(diagnostic);
98104
});
99105

@@ -103,7 +109,9 @@ describe('createScopedReporter', () => {
103109
reporter.emitExtension('acme.cache-warmed', { hits: 3 });
104110
expect(sink.inputs[0].type).toBe('extension');
105111
expect(sink.inputs[0].payload).toEqual({ name: 'acme.cache-warmed', payload: { hits: 3 } });
106-
expect(() => reporter.emitExtension('notnamespaced', {})).toThrow(/Invalid extension event name/);
112+
expect(() => reporter.emitExtension('notnamespaced' as Parameters<IScopedReporter['emitExtension']>[0], {})).toThrow(
113+
/Invalid extension event name/
114+
);
107115
});
108116

109117
it('exposes only emit methods, hiding modes, destinations, and thresholds', () => {

libraries/reporter/src/test/Telemetry.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ function rawInput(type: string, payload: unknown): IReporterEmitEventInput<unkno
4646
sessionId: 'sess',
4747
source: SOURCE,
4848
privacy: 'public',
49-
required: false,
5049
type: type as IReporterEmitEventInput<unknown>['type'],
5150
payload
5251
};

0 commit comments

Comments
 (0)