Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@rushstack/reporter",
"comment": "Add Heft integration over a negotiated inherited descriptor: HeftChildEmitter and HeftDescriptorHost with parent/child event correlation, a raw-stream fallback for older Heft, and descriptor allocation helpers",
"type": "minor"
}
],
"packageName": "@rushstack/reporter",
"email": "TheLarkInn@users.noreply.github.com"
}
81 changes: 81 additions & 0 deletions common/reviews/api/reporter.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export class AiReporter implements IReporter {
report(event: IReporterEventEnvelope<unknown>): void;
}

// @beta
export function allocateChildDescriptor(fdNumber?: number): IChildDescriptorPlan;

// @beta
export const ALREADY_REPORTED_ERROR_NAME: 'AlreadyReportedError';

Expand Down Expand Up @@ -167,6 +170,25 @@ export function getPrivacyClassificationRank(classification: ReporterPrivacyClas
// @beta
export function getSignalExitCode(signal: NodeJS.Signals): number;

// @beta
export class HeftChildEmitter {
constructor(options: IHeftChildEmitterOptions);
emitEvent(input: IHeftChildEventInput): string | undefined;
readonly mode: HeftChildReporterMode;
sendHello(): boolean;
writeRaw(stream: 'stdout' | 'stderr', text: string): void;
}

// @beta
export type HeftChildReporterMode = 'structured' | 'raw-fallback';

// @beta
export class HeftDescriptorHost {
constructor(options: IHeftDescriptorHostOptions);
processChildNdjson(ndjson: string): IHeftChildResult;
processChildRecords(records: readonly unknown[]): IHeftChildResult;
}

// @beta
export interface IAiDiagnostic {
// (undocumented)
Expand Down Expand Up @@ -284,6 +306,13 @@ export interface IBootstrapTruncation {
readonly truncated: boolean;
}

// @beta
export interface IChildDescriptorPlan {
readonly env: Record<string, string>;
readonly fdNumber: number;
readonly stdio: (string | number)[];
}

// @beta
export interface IClassifiedDiagnosticValue {
readonly privacy: ReporterPrivacyClassification;
Expand Down Expand Up @@ -404,6 +433,52 @@ export interface IGetMatchersOptions {
readonly version?: string;
}

// @beta
export interface IHeftChildEmitterOptions {
readonly capabilities?: readonly string[];
readonly childSessionId: string;
readonly env: Record<string, string | undefined>;
readonly now?: () => string;
readonly producerVersion: string;
readonly protocolVersion?: IReporterProtocolVersion;
readonly requiredFeatures?: readonly string[];
readonly source: IReporterEventSource;
readonly writeDescriptor?: (text: string) => void;
readonly writeStderr?: (text: string) => void;
readonly writeStdout?: (text: string) => void;
}

// @beta
export interface IHeftChildEventInput {
// (undocumented)
readonly payload?: unknown;
// (undocumented)
readonly privacy?: 'public' | 'local-sensitive' | 'secret';
// (undocumented)
readonly required: boolean;
// (undocumented)
readonly scope?: IReporterEventScope;
// (undocumented)
readonly type: string;
}

// @beta
export interface IHeftChildResult {
readonly accepted: boolean;
readonly ack?: IReporterHelloAck;
readonly diagnostic?: IRushDiagnostic;
readonly eventCount: number;
}

// @beta
export interface IHeftDescriptorHostOptions {
readonly forwardEnvelope: (envelope: IReporterEventEnvelope<unknown>) => void;
readonly parentOperationId?: string;
readonly parentSessionId: string;
readonly supportedCapabilities?: readonly string[];
readonly supportedProtocolVersion: IReporterProtocolVersion;
}

// @beta
export interface IInteractiveTerminal {
readonly columns: number;
Expand Down Expand Up @@ -1067,6 +1142,9 @@ export class ProblemMatcherRegistry {
// @beta
export function readBootstrapHandoffFileAsync(filePath: string): Promise<unknown[]>;

// @beta
export function readChildDescriptorFd(env: Record<string, string | undefined>): number | undefined;

// @beta
export function regroupOperationOutput(events: readonly IReporterEventEnvelope<unknown>[]): Map<string, string>;

Expand Down Expand Up @@ -1193,6 +1271,9 @@ export const RUSH_PLUGIN_API_VERSION: '1.0.0';
// @beta
export const RUSH_REPORTER_BOOTSTRAP_HANDOFF_ENV_VAR: '_RUSH_REPORTER_BOOTSTRAP_HANDOFF';

// @beta
export const RUSH_REPORTER_CHILD_FD_ENV_VAR: '_RUSH_REPORTER_CHILD_FD';

// @beta
export const RUSH_REPORTER_ENV_VAR: 'RUSH_REPORTER';

Expand Down
195 changes: 195 additions & 0 deletions libraries/reporter/src/heft/HeftChildEmitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import type { IReporterProtocolVersion } from '../events/ReporterProtocolVersion';
import type { IReporterEventScope, IReporterEventSource } from '../events/IReporterEventEnvelope';
import { encodeNdjsonRecord } from '../protocol/Ndjson';
import { REPORTER_PROTOCOL_VERSION } from '../protocol/ReporterProtocol';
import type { IReporterHello } from '../protocol/ReporterHandshake';
import { readChildDescriptorFd } from './HeftDescriptor';

/**
* The mode a Heft child reporter operates in.
*
* @beta
*/
export type HeftChildReporterMode = 'structured' | 'raw-fallback';

/**
* An event a Heft child emits.
*
* @beta
*/
export interface IHeftChildEventInput {
readonly type: string;
readonly required: boolean;
readonly privacy?: 'public' | 'local-sensitive' | 'secret';
readonly scope?: IReporterEventScope;
readonly payload?: unknown;
}

/**
* Options for {@link HeftChildEmitter}.
*
* @beta
*/
export interface IHeftChildEmitterOptions {
/**
* The environment variables, consulted for the inherited descriptor.
*/
readonly env: Record<string, string | undefined>;

/**
* The child session id stamped onto emitted events.
*/
readonly childSessionId: string;

/**
* The producer identity stamped onto emitted events.
*/
readonly source: IReporterEventSource;

/**
* The producer version advertised in the hello.
*/
readonly producerVersion: string;

/**
* The protocol version. Defaults to {@link REPORTER_PROTOCOL_VERSION}.
*/
readonly protocolVersion?: IReporterProtocolVersion;

/**
* The capabilities advertised in the hello.
*/
readonly capabilities?: readonly string[];

/**
* The required features advertised in the hello.
*/
readonly requiredFeatures?: readonly string[];

/**
* Writes NDJSON to the inherited descriptor. Required for structured mode.
*/
readonly writeDescriptor?: (text: string) => void;

/**
* Writes raw text to stdout, used in fallback mode.
*/
readonly writeStdout?: (text: string) => void;

/**
* Writes raw text to stderr, used in fallback mode.
*/
readonly writeStderr?: (text: string) => void;

/**
* Returns the current timestamp. Injectable for testing.
*/
readonly now?: () => string;
}

/**
* The child side of the Heft reporter descriptor negotiation.
*
* @remarks
* When the inherited descriptor is present, the child emits structured NDJSON
* events over it, stamping its child session id. When the descriptor is
* unavailable, it falls back to normal stdout and stderr, which Rush preserves
* and runs through problem matchers.
*
* @beta
*/
export class HeftChildEmitter {
/**
* Whether the child emits structured events or falls back to raw streams.
*/
public readonly mode: HeftChildReporterMode;

private readonly _writeDescriptor: ((text: string) => void) | undefined;
private readonly _writeStdout: ((text: string) => void) | undefined;
private readonly _writeStderr: ((text: string) => void) | undefined;
private readonly _childSessionId: string;
private readonly _source: IReporterEventSource;
private readonly _producerVersion: string;
private readonly _protocolVersion: IReporterProtocolVersion;
private readonly _capabilities: readonly string[];
private readonly _requiredFeatures: readonly string[];
private readonly _now: () => string;
private _sequence: number;
private _nextEventId: number;

public constructor(options: IHeftChildEmitterOptions) {
const fd: number | undefined = readChildDescriptorFd(options.env);
this.mode = fd !== undefined && options.writeDescriptor !== undefined ? 'structured' : 'raw-fallback';

this._writeDescriptor = options.writeDescriptor;
this._writeStdout = options.writeStdout;
this._writeStderr = options.writeStderr;
this._childSessionId = options.childSessionId;
this._source = options.source;
this._producerVersion = options.producerVersion;
this._protocolVersion = options.protocolVersion ?? REPORTER_PROTOCOL_VERSION;
this._capabilities = options.capabilities ?? [];
this._requiredFeatures = options.requiredFeatures ?? [];
this._now = options.now ?? (() => new Date().toISOString());
this._sequence = 1;
this._nextEventId = 1;
}

/**
* Sends the hello handshake over the descriptor. Returns `false` in fallback mode.
*/
public sendHello(): boolean {
if (this.mode !== 'structured' || this._writeDescriptor === undefined) {
return false;
}
const hello: IReporterHello = {
kind: 'hello',
protocolVersion: this._protocolVersion,
producerVersion: this._producerVersion,
capabilities: [...this._capabilities],
requiredFeatures: [...this._requiredFeatures]
};
this._writeDescriptor(encodeNdjsonRecord(hello));
return true;
}

/**
* Emits a structured event over the descriptor. Returns the event id, or
* `undefined` in fallback mode.
*/
public emitEvent(input: IHeftChildEventInput): string | undefined {
if (this.mode !== 'structured' || this._writeDescriptor === undefined) {
return undefined;
}
const eventId: string = `child_${this._nextEventId++}`;
const envelope: Record<string, unknown> = {
protocolVersion: this._protocolVersion,
eventId,
sessionId: this._childSessionId,
sequence: this._sequence++,
timestamp: this._now(),
source: this._source,
scope: input.scope,
privacy: input.privacy ?? 'public',
required: input.required,
type: input.type,
payload: input.payload ?? {}
};
this._writeDescriptor(encodeNdjsonRecord(envelope));
return eventId;
}

/**
* Writes raw output to stdout or stderr, preserved for problem matchers.
*/
public writeRaw(stream: 'stdout' | 'stderr', text: string): void {
if (stream === 'stderr') {
this._writeStderr?.(text);
} else {
this._writeStdout?.(text);
}
}
}
Loading