/** * ScopeFacade — Base class that library consumers extend to create custom scope classes * * Wraps StageContext (from memory/) to provide a consumer-friendly API for * state access, debug logging, metrics, and recorder hooks. * * Consumers extend this class to add domain-specific properties: * * ```typescript * class MyScope extends ScopeFacade { * get userName(): string { return this.getValue('name') as string; } * set userName(value: string) { this.setValue('name', value); } * } * ``` */ import type { ExecutionEnv } from '../engine/types.js'; import { StageContext } from '../memory/StageContext.js'; import type { CommitEvent, RedactionPolicy, RedactionReport, ScopeRecorder } from './types.js'; export declare class ScopeFacade { static readonly BRAND: unique symbol; /** * Shared sentinel returned by `_getSubflowPath()` for root-level stages * (no subflow nesting). Avoids per-call allocation of a fresh * `Object.freeze([])` on every `emitEvent` in the common no-subflow case. */ private static readonly _EMPTY_SUBFLOW_PATH; protected _stageContext: StageContext; protected _stageName: string; protected readonly _readOnlyValues?: unknown; /** Cached deeply-frozen copy of readOnlyValues for getArgs(). Created once. */ private readonly _frozenArgs; /** Execution environment — read-only, inherited from parent executor. */ private readonly _executionEnv; /** RFC-003 D2: true when `getArgs()` can return actual data — an empty * `{}` read carries no information, so it is never flagged. */ private readonly _hasArgs; /** RFC-003 D2: true when `getEnv()` can return actual data. */ private readonly _hasEnv; /** * RFC-003 D2: keys this stage has TRACKED-read (via `getValue(key)`). * A silent read of a key in this set is SHADOWED — its read→write edge is * already captured, so it is not flagged as an untracked source. This is * what keeps TypedScope array-proxy internals (which always follow a * tracked property read) and `$batchArray` honest-but-quiet. */ private readonly _trackedReadKeys; private _recorders; private _redactedKeys; private _redactionPolicy; private _redactedFieldsByKey; constructor(context: StageContext, stageName: string, readOnlyValues?: unknown, executionEnv?: ExecutionEnv); /** * Share a redacted-keys set across multiple ScopeFacade instances. * Call this to make redaction persist across stages in the same pipeline. * @internal */ useSharedRedactedKeys(sharedSet: Set): void; /** * Returns the current redacted-keys set (for sharing with other scopes). * @internal */ getRedactedKeys(): Set; /** * Apply a declarative redaction policy. The policy is additive — * it works alongside manual `setValue(..., true)` calls. * @internal */ useRedactionPolicy(policy: RedactionPolicy): void; /** @internal */ getRedactionPolicy(): RedactionPolicy | undefined; /** * Returns a compliance-friendly report of all redaction activity. * Never includes actual values — only key names, field names, and patterns. */ getRedactionReport(): RedactionReport; attachScopeRecorder(recorder: ScopeRecorder): void; detachScopeRecorder(recorderId: string): void; getScopeRecorders(): ScopeRecorder[]; /** @internal */ notifyStageStart(): void; /** @internal */ notifyStageEnd(duration?: number): void; /** @internal */ notifyPause(pauseData?: unknown): void; /** @internal */ notifyResume(hasInput: boolean): void; /** @internal */ notifyCommit(mutations: CommitEvent['mutations']): void; /** Called by StageContext.commit() observer. Converts tracked writes to CommitEvent format. * Errors are caught to prevent recorder issues from aborting the traversal. */ private _onCommitFired; addDebugInfo(key: string, value: unknown): void; addDebugMessage(value: unknown): void; addErrorInfo(key: string, value: unknown): void; addMetric(metricName: string, value: unknown): void; addEval(metricName: string, value: unknown): void; /** * Fire a structured event to every attached recorder implementing * `onEmit`. Synchronous, in-order, pass-through — no buffering. * * - **Fast-path**: zero allocation + zero cost when no recorders are * attached (early return on empty list). * - **Enrichment**: library auto-adds `stageName`, `runtimeStageId`, * `subflowPath`, `pipelineId`, `timestamp` to the event. * - **Redaction**: `RedactionPolicy.emitPatterns` regexes are matched * against `name` — matched events have their payload replaced with * `'[REDACTED]'` before dispatch. * - **Error isolation**: a throwing `onEmit` does not propagate — it is * caught and routed to `onError` on remaining recorders, matching the * pattern used by other scope events. * * Consumers call this via the `scope.$emit(name, payload)` scope method; * the method routes here via `createTypedScope`. */ emitEvent(name: string, payload: unknown): void; /** See `ScopeMethods.$detachAndJoinLater`. */ detachAndJoinLater(driver: import('../detach/types.js').DetachDriver, child: import('../builder/types.js').FlowChart, input?: unknown): import('../detach/types.js').DetachHandle; /** See `ScopeMethods.$detachAndForget`. */ detachAndForget(driver: import('../detach/types.js').DetachDriver, child: import('../builder/types.js').FlowChart, input?: unknown): void; /** * Build the subflowPath (outer → inner) for event enrichment. * * Parses from `runtimeStageId` which has the format * `[subflowPath/]stageId#executionIndex` (see `lib/engine/runtimeStageId.ts`). * Subflow isolation prevents walking the parent-chain across boundaries, * so the runtimeStageId — globally unique, includes full path — is the * canonical source of truth for the subflow hierarchy at emit time. * * Examples: * 'seed#0' → [] (root) * 'sf-inner/inner#5' → ['sf-inner'] * 'sf-a/sf-b/stage#3' → ['sf-a', 'sf-b'] (nested) */ private _getSubflowPath; /** Returns all state keys without firing onRead. Used by TypedScope ownKeys/has traps. */ getStateKeys(): string[]; /** Check key existence without firing onRead. Used by TypedScope has trap. * Contract: returns false for keys never set OR keys set to undefined. * This matches deleteValue() semantics (sets to undefined = deleted). */ hasKey(key: string): boolean; /** Read state without firing onRead. Used by array proxy getCurrent() to avoid * phantom reads on internal array operations (.length, .has, iteration, etc.). * The initial property access fires one tracked onRead via getValue(); subsequent * internal array operations use this method to stay silent. * NOTE: Like getValue(), returns the raw value to the caller. Redaction applies * only to recorder dispatch — it does not filter the returned value. This matches * the existing getValue() contract where user code always receives raw data. * * RFC-003 D2: a silent read of a key this stage never TRACKED-read marks * the stage's commit with `untrackedSources: ['silent']` — a causal slice * built from onRead events would miss this dependency, and consumers must * be told. Silent reads shadowed by a tracked read of the same key (the * array-proxy pattern above) are not flagged: their edge is captured. A * whole-state silent read (no key) is always flagged. */ getValueSilent(key?: string): unknown; getInitialValueFor(key: string): any; /** * Tracked read of shared state. * * **Read values are BORROWED — do not mutate them.** Since the lazy buffer * (#13), reads before the stage's first write return references INTO * COMMITTED SHARED STATE, and reads after a write return references into * the stage's private transaction-buffer working copy (the eager engine * returned references into that working copy for ALL reads). Mutating a * returned value in place would corrupt state without a commit record — * write changes back via `setValue`/`updateValue` instead. TypedScope * consumers are safe automatically: the proxy routes every mutation * through `setValue`/`updateValue`/copy-on-write array ops. * * There is deliberately NO dev-mode freeze guard here: deep-freezing a * buffer-served read would freeze the stage's own working copy and make a * legitimate read-then-deep-write throw, and freezing a committed-state * read mutates an object shared with every other consumer of the live * state. See `src/lib/memory/README.md` ("Read values are borrowed"). * * Recorder note: the `onRead` event below passes the SAME live reference * (no clone) unless field-level redaction scrubs a copy — recorders must * treat event values as read-only too. */ getValue(key?: string): unknown; setValue(key: string, value: unknown, shouldRedact?: boolean, description?: string): void; updateValue(key: string, value: unknown, description?: string): void; deleteValue(key: string, description?: string): void; /** @internal */ setGlobal(key: string, value: unknown, description?: string): void; /** @internal */ getGlobal(key: string): any; /** @internal */ setObjectInRoot(key: string, value: unknown): void; /** * Returns the readonly input values passed to this pipeline, cast to `T`. * The returned object is deeply frozen — any attempt to mutate it throws. * Cached at construction time for zero-allocation repeated access. * * ```typescript * const { applicantName, income } = scope.getArgs<{ applicantName: string; income: number }>(); * ``` * * RFC-003 D2: args are untracked BY DESIGN, so calling this (with actual * input present) marks the stage's commit with `untrackedSources: ['args']` * — telling causal-slice consumers the backward slice may be incomplete * here. An empty-args read carries no information and is not flagged. */ getArgs>(): T; /** * Returns the execution environment — read-only infrastructure values * that propagate through nested executors (like `process.env` for flowcharts). * * Contains: signal (abort), timeoutMs, traceId. * Frozen at construction time. Inherited by subflows automatically. * * ```typescript * const { signal, traceId } = scope.getEnv(); * ``` * * RFC-003 D2: env is untracked BY DESIGN, so calling this (with a * non-empty environment) marks the stage's commit with * `untrackedSources: ['env']` — see {@link getArgs}. */ getEnv(): Readonly; /** @internal */ getPipelineId(): string; /** Checks if a key is redacted (explicit _redactedKeys set). */ private _isKeyRedacted; /** * Checks if a key should be auto-redacted by the policy (exact keys + patterns). * * ReDoS guard: pattern testing is capped at MAX_PATTERN_KEY_LEN characters. * Scope state keys are always short identifiers; any key exceeding the cap * is almost certainly not a legitimate scope key, so skipping pattern matching * for it does not risk leaking PII. Exact-key matching (Array.includes) is * still applied regardless of length and is not vulnerable to ReDoS. */ private _isPolicyRedacted; /** * Maximum key length (characters) that will be tested against regex redaction * patterns. Keys longer than this are skipped for pattern matching to prevent * ReDoS: a pathological regex tested against an unboundedly long key string * can cause catastrophic backtracking. * * 256 characters comfortably exceeds any realistic scope-state key name. */ private static readonly _MAX_PATTERN_KEY_LEN; /** * Returns a deep-cloned copy with specified fields replaced by '[REDACTED]'. * Supports dot-notation paths (e.g. 'address.zip') for nested objects. */ private _scrubFields; private _invokeHook; }