/** * Play Runtime Log Provenance. * * A play run emits a stream of log / step / progress lines from very different * places: the customer's own play code, the runtime's step lifecycle, replays * that re-run already-completed work, scheduler/sandbox internals, warnings, * and receipt/billing plumbing. On the wire they are all plain strings in one * ordered log buffer (`ctx.getLogs()` → `play.run.log`), so historically each * consumer re-derived "is this line noise?" with its own ad-hoc substring * match (`isInternalLogLine` in the dashboard, the `[worker] ...` regex ladder * in the SDK watch renderer). Those drifted apart and leaked internal churn to * users. * * This module is the single source of truth. It defines: * 1. `LogProvenance` — a CLOSED discriminated set naming WHO/WHY emitted a * line. Adding a class is a compile error everywhere that must route it. * 2. `LOG_PROVENANCE_POLICY` — the one table mapping each class to the * surfaces it reaches (`watch`, `ui`, `debug`). Exhaustive over the union. * 3. A structural tag carrier (`PROVENANCE_PREFIX`) so NEW emissions carry * their class inline in the log string without breaking persisted-log * readers, plus a legacy classifier (`classifyLegacyLogLine`) for old * untagged lines. * * Do NOT re-implement provenance decisions with ad-hoc string matching * elsewhere. Import `classifyLogLine` + `logProvenanceReaches`. */ /** * Closed set of provenance classes. Each names WHO emitted the line and WHY. * Fewer, sharper classes on purpose — a consumer only needs to know which * surface a line belongs on, not its exact emitter. */ export type LogProvenance = /** Play code output: `ctx.log(...)` and captured `[console.*]` from the * play body. The customer wrote this; it is always in-scope everywhere. */ | 'user' /** Runtime step/node lifecycle and map progress narration the runtime emits * to explain forward progress. The useful backbone of a `--watch` run. */ | 'lifecycle' /** Deterministic re-execution echoes: a resumed/replayed attempt re-runs * already-durable work and reprints the same lines ("recovered from * checkpoint", re-narrated step transitions). Real signal only once; the * replay copy is churn. */ | 'replay' /** Scheduler / sandbox / worker internals: run-file prep, sandbox * lifecycle, `[perf]` timings, batch-drain bookkeeping. Operator-facing. */ | 'infra' /** Warnings and diagnostics: `[warn]`, `[error]`, `[runtime.*_failure]`, * and forward-looking hints. Surfaced because a human may need to act. */ | 'diagnostic' /** Billing / receipt / ledger plumbing: receipt reads, completion sinks. * Never customer-facing. */ | 'receipt'; /** Every provenance class, for exhaustiveness checks and tests. */ export const LOG_PROVENANCE_CLASSES: readonly LogProvenance[] = [ 'user', 'lifecycle', 'replay', 'infra', 'diagnostic', 'receipt', ] as const; /** Rendering surfaces a log line can reach. */ export type LogSurface = /** The CLI `deepline plays run --watch` progress stream. */ | 'watch' /** The dashboard run-detail log tail. */ | 'ui' /** Verbose / `--debug` / internal inspection only. */ | 'debug'; /** * THE policy. Maps each provenance class to the surfaces it reaches. Exhaustive * over `LogProvenance` by construction (a `Record`), so adding a class forces a * routing decision here at compile time. * * Design intent: * - `watch` and `ui` carry what a person running/inspecting the play needs: * their own output, real step transitions, and warnings that matter. * - `replay`, `infra`, and `receipt` are internal churn — `debug` only. This * is what silences the repeated `step docflow:*` replay passes and the * scary-looking scaffolding on healthy runs. * - `debug` is a superset: everything is inspectable in verbose mode. */ export const LOG_PROVENANCE_POLICY: Record< LogProvenance, Readonly> > = { user: { watch: true, ui: true, debug: true }, lifecycle: { watch: true, ui: true, debug: true }, replay: { watch: false, ui: false, debug: true }, infra: { watch: false, ui: false, debug: true }, diagnostic: { watch: true, ui: true, debug: true }, receipt: { watch: false, ui: false, debug: true }, }; /** True when a line of the given provenance should render on the surface. */ export function logProvenanceReaches( provenance: LogProvenance, surface: LogSurface, ): boolean { return LOG_PROVENANCE_POLICY[provenance][surface]; } /** * Structural carrier for NEW emissions. A tagged line looks like: * `prov:replay` * The sentinel is a control char that never appears in human log text, so old * readers that don't know about it see a harmless invisible prefix rather than * a mangled message, and `stripProvenanceTag` recovers the exact original. * * We deliberately do NOT put the tag inside the `[...]` bracket space that * `formatPlayLogLine` / the dashboard timestamp parser rely on. */ const PROVENANCE_SENTINEL = ''; const PROVENANCE_PREFIX = `${PROVENANCE_SENTINEL}prov:`; /** Stamp a provenance class onto a log line for the wire. */ export function tagLogProvenance( provenance: LogProvenance, line: string, ): string { return `${PROVENANCE_PREFIX}${provenance}${PROVENANCE_SENTINEL}${line}`; } /** * Read a structural provenance tag off a line, if present, and return the tag * plus the original untagged line. Returns `null` provenance when untagged. */ export function readProvenanceTag(line: string): { provenance: LogProvenance | null; line: string; } { if (!line.startsWith(PROVENANCE_PREFIX)) { return { provenance: null, line }; } const end = line.indexOf(PROVENANCE_SENTINEL, PROVENANCE_PREFIX.length); if (end === -1) { return { provenance: null, line }; } const candidate = line.slice(PROVENANCE_PREFIX.length, end); const provenance = LOG_PROVENANCE_CLASSES.includes( candidate as LogProvenance, ) ? (candidate as LogProvenance) : null; return { provenance, line: line.slice(end + 1) }; } /** Remove any provenance tag, yielding the original human-readable line. */ export function stripProvenanceTag(line: string): string { return readProvenanceTag(line).line; } /** * Legacy classifier for old persisted log strings that carry NO structural * tag. Mirrors — and supersedes — the two ad-hoc filters that used to live in * `PlayRunDetailPanel.isInternalLogLine` and `formatPlayLogLine`. The patterns * are the historical `[worker] ...` / `[event] ...` / `[perf] ...` shapes plus * the checkpoint-replay marker. Order matters: most specific first. * * A legacy line the classifier can't place defaults to `user` — old runs * predate structural tagging, and the safe default for an unrecognized line is * to show it (never silently drop a line we can't prove is internal). */ export function classifyLegacyLogLine(line: string): LogProvenance { const message = stripLeadingTimestamp(line); // Replay / recovery echoes. if (/recovered (?:from checkpoint|response from checkpoint)/i.test(message)) { return 'replay'; } // Receipt / ledger plumbing. if (/^\[perf\] runtime receipt\b/i.test(message)) { return 'receipt'; } // Scheduler / sandbox / worker internals. if ( /^\[perf\] runtime (?:map|state)\b/i.test(message) || /\[worker\] picked up run\b/.test(message) || /\[worker\] heartbeat\b/.test(message) || /\[worker\] progress completedRows=\d+ totalRows=\d+ rowUpdates=\d+/.test( message, ) || /\[worker\] step started\b/.test(message) || /\[worker\] Preparing run files\b/.test(message) || /\[worker\] Run files ready\b/.test(message) || /\[worker\] Runtime ready\b/.test(message) || /\[worker\] Sandbox (?:starting|create start|create done|workspace ready|upload start|runner uploaded)\b/.test( message, ) || /^\[event\] play\.step\.progress\b/.test(message) || /^\[event\] play\.run\.snapshot\b/.test(message) || /^\[event\] play\.sheet\.summary\b/.test(message) ) { return 'infra'; } // Warnings / diagnostics. if ( /^\[warn\]/i.test(message) || /^\[error\]/i.test(message) || /^\[runtime\.[a-z_]*(?:failure|error)\]/i.test(message) ) { return 'diagnostic'; } return 'user'; } /** * Classify any log line — structurally tagged (new) or legacy (old) — into its * provenance class, returning the class and the original human-readable line. * This is the entry point every surface should use. */ export function classifyLogLine(line: string): { provenance: LogProvenance; line: string; } { const tagged = readProvenanceTag(line); if (tagged.provenance !== null) { return { provenance: tagged.provenance, line: tagged.line }; } return { provenance: classifyLegacyLogLine(tagged.line), line: tagged.line }; } /** True when a raw (possibly tagged) log line should render on the surface. */ export function logLineReaches(line: string, surface: LogSurface): boolean { return logProvenanceReaches(classifyLogLine(line).provenance, surface); } /** * Strip ONLY a leading timestamp bracket, if present. `ctx.log` stamps * `[] ` and the dashboard timeline stringifier prepends `[] ` * followed by a `[]` bracket. We must keep the `[]` bracket * (`[worker]`, `[event]`) because the legacy classifier's patterns key on it — * so we only remove a leading bracket whose contents parse as a date. */ function stripLeadingTimestamp(line: string): string { const match = line.match(/^\[([^\]]+)\]\s*([\s\S]*)$/); if (!match) { return line; } const inner = match[1] ?? ''; const isTimestamp = !Number.isNaN(new Date(inner).getTime()); return isTimestamp ? (match[2] ?? line) : line; }