From 45f727fc5393fd762d5f62afd2ec7084444ca320 Mon Sep 17 00:00:00 2001 From: ghost <49853598+JSONbored@users.noreply.github.com> Date: Fri, 10 Jul 2026 23:40:24 -0700 Subject: [PATCH 1/2] fix(public-stats): redact sparse accuracy trend counts --- apps/gittensory-ui/public/openapi.json | 9 +- src/openapi/schemas.ts | 10 +-- src/services/public-accuracy-trend.ts | 21 ++--- test/integration/public-stats-route.test.ts | 2 +- test/unit/public-accuracy-trend.test.ts | 22 +++-- worker-configuration.d.ts | 91 +++++++++++++++++---- 6 files changed, 104 insertions(+), 51 deletions(-) diff --git a/apps/gittensory-ui/public/openapi.json b/apps/gittensory-ui/public/openapi.json index 5bd04f3df7..0267e9fc77 100644 --- a/apps/gittensory-ui/public/openapi.json +++ b/apps/gittensory-ui/public/openapi.json @@ -474,13 +474,16 @@ "type": "string" }, "merged": { - "type": "number" + "type": "number", + "nullable": true }, "closed": { - "type": "number" + "type": "number", + "nullable": true }, "reversed": { - "type": "number" + "type": "number", + "nullable": true }, "accuracyPct": { "type": "number", diff --git a/src/openapi/schemas.ts b/src/openapi/schemas.ts index fb384a00b8..e92af604c0 100644 --- a/src/openapi/schemas.ts +++ b/src/openapi/schemas.ts @@ -120,14 +120,14 @@ export const PublicStatsSchema = z accuracyPct: z.number().nullable(), }), ), - /** Trailing weekly history of totals.accuracyPct's SAME formula (#4447) -- null accuracyPct on a week means - * too few decided (merged+closed) PRs that week to publish a meaningful percentage, not zero accuracy. */ + /** Trailing weekly history of totals.accuracyPct's SAME formula (#4447) -- null counts/accuracyPct on a week means + * too few decided (merged+closed) PRs to publish meaningful or non-identifying details. */ accuracyTrend: z.array( z.object({ weekStart: z.string(), - merged: z.number(), - closed: z.number(), - reversed: z.number(), + merged: z.number().nullable(), + closed: z.number().nullable(), + reversed: z.number().nullable(), accuracyPct: z.number().nullable(), }), ), diff --git a/src/services/public-accuracy-trend.ts b/src/services/public-accuracy-trend.ts index 0ba7f5286b..dd7eed665c 100644 --- a/src/services/public-accuracy-trend.ts +++ b/src/services/public-accuracy-trend.ts @@ -18,9 +18,9 @@ export const MIN_ACCURACY_TREND_SAMPLE = 3; export type PublicAccuracyTrendWeek = { /** UTC Monday (YYYY-MM-DD) that starts the bucket. */ weekStart: string; - merged: number; - closed: number; - reversed: number; + merged: number | null; + closed: number | null; + reversed: number | null; accuracyPct: number | null; }; @@ -34,11 +34,11 @@ function roundPct(value: number): number { /** Same formula as public-stats.ts's accuracyPct, reused so the trend and the live number can never drift * apart into two competing definitions of "accuracy". */ -function accuracyPctOf(merged: number, closed: number, reversed: number): number | null { - const decided = merged + closed; - if (decided < MIN_ACCURACY_TREND_SAMPLE) return null; - const reversalRate = Math.min(1, reversed / decided); - return roundPct(1 - reversalRate); +function publicBucketOf(bucket: { merged: number; closed: number; reversed: number }): Omit { + const decided = bucket.merged + bucket.closed; + if (decided < MIN_ACCURACY_TREND_SAMPLE) return { merged: null, closed: null, reversed: null, accuracyPct: null }; + const reversalRate = Math.min(1, bucket.reversed / decided); + return { merged: bucket.merged, closed: bucket.closed, reversed: bucket.reversed, accuracyPct: roundPct(1 - reversalRate) }; } /** Fold day-granularity rows into `weeks` trailing UTC-Monday buckets ending in the week containing `nowMs`. @@ -61,10 +61,7 @@ export function buildPublicAccuracyTrend(dayRows: DayRow[], nowMs: number, weeks return buckets.map((bucket, offset) => ({ weekStart: isoWeekStart(oldestStartMs + offset * MS_PER_WEEK), - merged: bucket.merged, - closed: bucket.closed, - reversed: bucket.reversed, - accuracyPct: accuracyPctOf(bucket.merged, bucket.closed, bucket.reversed), + ...publicBucketOf(bucket), })); } diff --git a/test/integration/public-stats-route.test.ts b/test/integration/public-stats-route.test.ts index 13ccfb40c1..254f4b2597 100644 --- a/test/integration/public-stats-route.test.ts +++ b/test/integration/public-stats-route.test.ts @@ -63,7 +63,7 @@ describe("GET /v1/public/stats (#1059)", () => { totals: Record; weekly: { reviewed: number; merged: number }; byProject: Array<{ project: string; reviewed: number }>; - accuracyTrend: Array<{ weekStart: string; merged: number; closed: number; reversed: number; accuracyPct: number | null }>; + accuracyTrend: Array<{ weekStart: string; merged: number | null; closed: number | null; reversed: number | null; accuracyPct: number | null }>; reuseRateTrend: Array<{ weekStart: string; hits: number; misses: number; reuseRatePct: number | null }>; reviewVolumeTrend: Array<{ weekStart: string; reviewed: number; merged: number; filteredPct: number | null }>; }; diff --git a/test/unit/public-accuracy-trend.test.ts b/test/unit/public-accuracy-trend.test.ts index 7dc552c5ad..7763f4632a 100644 --- a/test/unit/public-accuracy-trend.test.ts +++ b/test/unit/public-accuracy-trend.test.ts @@ -45,22 +45,22 @@ describe("buildPublicAccuracyTrend", () => { it("REGRESSION: ignores day rows outside the trailing window instead of letting them corrupt the oldest bucket", () => { const currentMonday = isoWeekStart(NOW); const tooOld = isoWeekStart(NOW - 30 * 86_400_000); - const trend = buildPublicAccuracyTrend([{ day: tooOld, merged: 999, closed: 999, reversed: 999 }, { day: currentMonday, merged: 1, closed: 0, reversed: 0 }], NOW, 2); - expect(trend[0]).toMatchObject({ merged: 0, closed: 0, reversed: 0 }); - expect(trend[1]).toMatchObject({ merged: 1, closed: 0, reversed: 0 }); + const trend = buildPublicAccuracyTrend([{ day: tooOld, merged: 999, closed: 999, reversed: 999 }, { day: currentMonday, merged: MIN_ACCURACY_TREND_SAMPLE, closed: 0, reversed: 0 }], NOW, 2); + expect(trend[0]).toMatchObject({ merged: null, closed: null, reversed: null }); + expect(trend[1]).toMatchObject({ merged: MIN_ACCURACY_TREND_SAMPLE, closed: 0, reversed: 0 }); }); it("ignores an unparseable day string rather than throwing or corrupting a bucket", () => { const currentMonday = isoWeekStart(NOW); - const trend = buildPublicAccuracyTrend([{ day: "not-a-date", merged: 5, closed: 5, reversed: 5 }, { day: currentMonday, merged: 1, closed: 0, reversed: 0 }], NOW, 1); + const trend = buildPublicAccuracyTrend([{ day: "not-a-date", merged: 5, closed: 5, reversed: 5 }, { day: currentMonday, merged: MIN_ACCURACY_TREND_SAMPLE, closed: 0, reversed: 0 }], NOW, 1); expect(trend).toHaveLength(1); - expect(trend[0]).toMatchObject({ merged: 1, closed: 0, reversed: 0 }); + expect(trend[0]).toMatchObject({ merged: MIN_ACCURACY_TREND_SAMPLE, closed: 0, reversed: 0 }); }); - it("returns null accuracyPct (not a misleading 0% or 100%) below MIN_ACCURACY_TREND_SAMPLE decided PRs", () => { + it("REGRESSION: redacts counts and accuracyPct below MIN_ACCURACY_TREND_SAMPLE decided PRs", () => { const week = isoWeekStart(NOW); const trend = buildPublicAccuracyTrend([{ day: week, merged: MIN_ACCURACY_TREND_SAMPLE - 1, closed: 0, reversed: 0 }], NOW, 1); - expect(trend[0]?.accuracyPct).toBeNull(); + expect(trend[0]).toMatchObject({ merged: null, closed: null, reversed: null, accuracyPct: null }); }); it("returns a real percentage at exactly MIN_ACCURACY_TREND_SAMPLE decided PRs", () => { @@ -83,7 +83,7 @@ describe("buildPublicAccuracyTrend", () => { it("returns all-zero, null-accuracy buckets for an empty input (a brand-new / not-yet-enabled deployment)", () => { const trend = buildPublicAccuracyTrend([], NOW, 3); expect(trend).toHaveLength(3); - for (const week of trend) expect(week).toMatchObject({ merged: 0, closed: 0, reversed: 0, accuracyPct: null }); + for (const week of trend) expect(week).toMatchObject({ merged: null, closed: null, reversed: null, accuracyPct: null }); }); }); @@ -130,7 +130,7 @@ describe("loadPublicAccuracyTrend — end-to-end over the real live tables", () expect(currentWeek?.reversed).toBe(1); }); - it("still reports the Orb-fleet side when GITTENSORY_PUBLIC_STATS_REPOS is empty (no own-ledger allowlist)", async () => { + it("redacts a sparse Orb-fleet week when GITTENSORY_PUBLIC_STATS_REPOS is empty (no own-ledger allowlist)", async () => { const env = createTestEnv({ GITTENSORY_PUBLIC_STATS_REPOS: "" }); const thisMonday = isoWeekStart(NOW); const thisWeekIso = `${thisMonday}T09:00:00.000Z`; @@ -141,8 +141,6 @@ describe("loadPublicAccuracyTrend — end-to-end over the real live tables", () const trend = await loadPublicAccuracyTrend(env, NOW); const currentWeek = trend[trend.length - 1]; - expect(currentWeek?.closed).toBe(1); - expect(currentWeek?.merged).toBe(0); - expect(currentWeek?.reversed).toBe(0); + expect(currentWeek).toMatchObject({ merged: null, closed: null, reversed: null, accuracyPct: null }); }); }); diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index b3c449102c..8bba12f23e 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -621,7 +621,7 @@ declare abstract class DurableObjectNamespace; jurisdiction(jurisdiction: DurableObjectJurisdiction): DurableObjectNamespace; } -type DurableObjectJurisdiction = "eu" | "fedramp" | "fedramp-high"; +type DurableObjectJurisdiction = "eu" | "fedramp" | "fedramp-high" | "us"; interface DurableObjectNamespaceNewUniqueIdOptions { jurisdiction?: DurableObjectJurisdiction; } @@ -12316,6 +12316,13 @@ interface ForwardableEmailMessage extends EmailMessage { * @returns A promise that resolves when the email message is replied. */ reply(message: EmailMessage): Promise; + /** + * Reply to the sender of this email message with a message built from the given + * fields. Threading headers (In-Reply-To/References) are set automatically. + * @param builder The reply message contents. + * @returns A promise that resolves when the email message is replied. + */ + reply(builder: EmailReplyMessageBuilder): Promise; } /** A file attachment for an email message */ type EmailAttachment = { @@ -12336,23 +12343,46 @@ interface EmailAddress { name: string; email: string; } +/** + * Recipient fields for `SendEmail.send()`. At least one of `to`, `cc`, or + * `bcc` must be provided. + */ +type EmailDestinations = { + to?: string | EmailAddress | (string | EmailAddress)[]; + cc?: string | EmailAddress | (string | EmailAddress)[]; + bcc?: string | EmailAddress | (string | EmailAddress)[]; +} & ({ + to: string | EmailAddress | (string | EmailAddress)[]; +} | { + cc: string | EmailAddress | (string | EmailAddress)[]; +} | { + bcc: string | EmailAddress | (string | EmailAddress)[]; +}); +/** + * Fields shared by all composed emails (no recipients). Used directly by + * `ForwardableEmailMessage.reply()`, which always replies to the original + * sender, and extended by `EmailMessageBuilder` for `SendEmail.send()`. + */ +interface EmailReplyMessageBuilder { + from: string | EmailAddress; + subject: string; + replyTo?: string | EmailAddress; + headers?: Record; + text?: string; + html?: string; + attachments?: EmailAttachment[]; +} +/** + * Fields for composing an email without constructing raw MIME, for + * `SendEmail.send()`. Requires at least one of `to`, `cc`, or `bcc`. + */ +type EmailMessageBuilder = EmailReplyMessageBuilder & EmailDestinations; /** * A binding that allows a Worker to send email messages. */ interface SendEmail { send(message: EmailMessage): Promise; - send(builder: { - from: string | EmailAddress; - to: string | EmailAddress | (string | EmailAddress)[]; - subject: string; - replyTo?: string | EmailAddress; - cc?: string | EmailAddress | (string | EmailAddress)[]; - bcc?: string | EmailAddress | (string | EmailAddress)[]; - headers?: Record; - text?: string; - html?: string; - attachments?: EmailAttachment[]; - }): Promise; + send(builder: EmailMessageBuilder): Promise; } declare abstract class EmailEvent extends ExtendableEvent { readonly message: ForwardableEmailMessage; @@ -13174,6 +13204,11 @@ declare namespace CloudflareWorkersModule { export type WorkflowDurationLabel = 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year'; export type WorkflowSleepDuration = `${number} ${WorkflowDurationLabel}${'s' | ''}` | number; export type WorkflowDelayDuration = WorkflowSleepDuration; + export type WorkflowDynamicDelayContext = { + ctx: WorkflowStepContext; + error: Error; + }; + export type WorkflowDelayFunction = (input: WorkflowDynamicDelayContext) => WorkflowDelayDuration | Promise; export type WorkflowTimeoutDuration = WorkflowSleepDuration; export type WorkflowRetentionDuration = WorkflowSleepDuration; export type WorkflowBackoff = 'constant' | 'linear' | 'exponential'; @@ -13181,7 +13216,7 @@ declare namespace CloudflareWorkersModule { export type WorkflowStepConfig = { retries?: { limit: number; - delay: WorkflowDelayDuration | number; + delay: WorkflowDelayDuration | number | WorkflowDelayFunction; backoff?: WorkflowBackoff; }; timeout?: WorkflowTimeoutDuration | number; @@ -13207,13 +13242,22 @@ declare namespace CloudflareWorkersModule { type: string; sensitive?: WorkflowStepSensitivity; }; - export type WorkflowStepContext = { + export type WorkflowStepContext = { step: { name: string; count: number; }; attempt: number; - config: WorkflowStepConfig; + config: { + retries?: { + limit: number; + backoff?: WorkflowBackoff; + } & (Delay extends WorkflowDelayFunction ? {} : { + delay: WorkflowDelayDuration | number; + }); + timeout?: WorkflowTimeoutDuration | number; + sensitive?: WorkflowStepSensitivity; + }; }; export type WorkflowRollbackContext = { ctx: WorkflowStepContext; @@ -13229,7 +13273,9 @@ declare namespace CloudflareWorkersModule { }; export abstract class WorkflowStep { do>(name: string, callback: (ctx: WorkflowStepContext) => Promise, rollbackOptions?: WorkflowStepRollbackOptions): Promise; - do>(name: string, config: WorkflowStepConfig, callback: (ctx: WorkflowStepContext) => Promise, rollbackOptions?: WorkflowStepRollbackOptions): Promise; + do, const C extends WorkflowStepConfig>(name: string, config: C, callback: (ctx: WorkflowStepContext) => Promise, rollbackOptions?: WorkflowStepRollbackOptions): Promise; sleep: (name: string, duration: WorkflowSleepDuration) => Promise; sleepUntil: (name: string, timestamp: Date | number) => Promise; waitForEvent>(name: string, options: { @@ -14139,6 +14185,7 @@ declare namespace TailStream { readonly dispatchNamespace?: string; readonly entrypoint?: string; readonly executionModel: string; + readonly durableObjectId?: string; readonly scriptName?: string; readonly scriptTags?: string[]; readonly scriptVersion?: ScriptVersion; @@ -14693,6 +14740,13 @@ interface WorkflowError { code?: number; message: string; } +interface WorkflowInstanceTerminateOptions { + /** + * If true, run registered rollback handlers before terminating the instance. + * Only steps that registered rollback handlers are rolled back. + */ + rollback?: boolean; +} interface WorkflowInstanceRestartOptions { /** * Restart from a specific step. If omitted, the instance restarts from the beginning. @@ -14726,8 +14780,9 @@ declare abstract class WorkflowInstance { public resume(): Promise; /** * Terminate the instance. If it is errored, terminated or complete, an error will be thrown. + * @param options Options for termination, including whether registered rollback handlers should run. */ - public terminate(): Promise; + public terminate(options?: WorkflowInstanceTerminateOptions): Promise; /** * Restart the instance. Optionally restart from a specific step, preserving * cached results for all steps before it. From 2e013dc775e0e316d508890a193bb1f37457ec86 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Sat, 11 Jul 2026 06:26:29 -0700 Subject: [PATCH 2/2] fix(public-stats): use origin/main's exact worker-configuration.d.ts The rebase's own worker-configuration.d.ts regeneration (done to resolve a merge conflict) picked up 176 lines of unrelated drift from this local machine's wrangler/workerd cache being out of sync with whatever last regenerated main's copy correctly. This PR has no legitimate reason to touch this generated file at all -- copy main's exact, already-correct content instead of regenerating it locally. --- worker-configuration.d.ts | 91 ++++++++------------------------------- 1 file changed, 18 insertions(+), 73 deletions(-) diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index 8bba12f23e..b3c449102c 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -621,7 +621,7 @@ declare abstract class DurableObjectNamespace; jurisdiction(jurisdiction: DurableObjectJurisdiction): DurableObjectNamespace; } -type DurableObjectJurisdiction = "eu" | "fedramp" | "fedramp-high" | "us"; +type DurableObjectJurisdiction = "eu" | "fedramp" | "fedramp-high"; interface DurableObjectNamespaceNewUniqueIdOptions { jurisdiction?: DurableObjectJurisdiction; } @@ -12316,13 +12316,6 @@ interface ForwardableEmailMessage extends EmailMessage { * @returns A promise that resolves when the email message is replied. */ reply(message: EmailMessage): Promise; - /** - * Reply to the sender of this email message with a message built from the given - * fields. Threading headers (In-Reply-To/References) are set automatically. - * @param builder The reply message contents. - * @returns A promise that resolves when the email message is replied. - */ - reply(builder: EmailReplyMessageBuilder): Promise; } /** A file attachment for an email message */ type EmailAttachment = { @@ -12343,46 +12336,23 @@ interface EmailAddress { name: string; email: string; } -/** - * Recipient fields for `SendEmail.send()`. At least one of `to`, `cc`, or - * `bcc` must be provided. - */ -type EmailDestinations = { - to?: string | EmailAddress | (string | EmailAddress)[]; - cc?: string | EmailAddress | (string | EmailAddress)[]; - bcc?: string | EmailAddress | (string | EmailAddress)[]; -} & ({ - to: string | EmailAddress | (string | EmailAddress)[]; -} | { - cc: string | EmailAddress | (string | EmailAddress)[]; -} | { - bcc: string | EmailAddress | (string | EmailAddress)[]; -}); -/** - * Fields shared by all composed emails (no recipients). Used directly by - * `ForwardableEmailMessage.reply()`, which always replies to the original - * sender, and extended by `EmailMessageBuilder` for `SendEmail.send()`. - */ -interface EmailReplyMessageBuilder { - from: string | EmailAddress; - subject: string; - replyTo?: string | EmailAddress; - headers?: Record; - text?: string; - html?: string; - attachments?: EmailAttachment[]; -} -/** - * Fields for composing an email without constructing raw MIME, for - * `SendEmail.send()`. Requires at least one of `to`, `cc`, or `bcc`. - */ -type EmailMessageBuilder = EmailReplyMessageBuilder & EmailDestinations; /** * A binding that allows a Worker to send email messages. */ interface SendEmail { send(message: EmailMessage): Promise; - send(builder: EmailMessageBuilder): Promise; + send(builder: { + from: string | EmailAddress; + to: string | EmailAddress | (string | EmailAddress)[]; + subject: string; + replyTo?: string | EmailAddress; + cc?: string | EmailAddress | (string | EmailAddress)[]; + bcc?: string | EmailAddress | (string | EmailAddress)[]; + headers?: Record; + text?: string; + html?: string; + attachments?: EmailAttachment[]; + }): Promise; } declare abstract class EmailEvent extends ExtendableEvent { readonly message: ForwardableEmailMessage; @@ -13204,11 +13174,6 @@ declare namespace CloudflareWorkersModule { export type WorkflowDurationLabel = 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year'; export type WorkflowSleepDuration = `${number} ${WorkflowDurationLabel}${'s' | ''}` | number; export type WorkflowDelayDuration = WorkflowSleepDuration; - export type WorkflowDynamicDelayContext = { - ctx: WorkflowStepContext; - error: Error; - }; - export type WorkflowDelayFunction = (input: WorkflowDynamicDelayContext) => WorkflowDelayDuration | Promise; export type WorkflowTimeoutDuration = WorkflowSleepDuration; export type WorkflowRetentionDuration = WorkflowSleepDuration; export type WorkflowBackoff = 'constant' | 'linear' | 'exponential'; @@ -13216,7 +13181,7 @@ declare namespace CloudflareWorkersModule { export type WorkflowStepConfig = { retries?: { limit: number; - delay: WorkflowDelayDuration | number | WorkflowDelayFunction; + delay: WorkflowDelayDuration | number; backoff?: WorkflowBackoff; }; timeout?: WorkflowTimeoutDuration | number; @@ -13242,22 +13207,13 @@ declare namespace CloudflareWorkersModule { type: string; sensitive?: WorkflowStepSensitivity; }; - export type WorkflowStepContext = { + export type WorkflowStepContext = { step: { name: string; count: number; }; attempt: number; - config: { - retries?: { - limit: number; - backoff?: WorkflowBackoff; - } & (Delay extends WorkflowDelayFunction ? {} : { - delay: WorkflowDelayDuration | number; - }); - timeout?: WorkflowTimeoutDuration | number; - sensitive?: WorkflowStepSensitivity; - }; + config: WorkflowStepConfig; }; export type WorkflowRollbackContext = { ctx: WorkflowStepContext; @@ -13273,9 +13229,7 @@ declare namespace CloudflareWorkersModule { }; export abstract class WorkflowStep { do>(name: string, callback: (ctx: WorkflowStepContext) => Promise, rollbackOptions?: WorkflowStepRollbackOptions): Promise; - do, const C extends WorkflowStepConfig>(name: string, config: C, callback: (ctx: WorkflowStepContext) => Promise, rollbackOptions?: WorkflowStepRollbackOptions): Promise; + do>(name: string, config: WorkflowStepConfig, callback: (ctx: WorkflowStepContext) => Promise, rollbackOptions?: WorkflowStepRollbackOptions): Promise; sleep: (name: string, duration: WorkflowSleepDuration) => Promise; sleepUntil: (name: string, timestamp: Date | number) => Promise; waitForEvent>(name: string, options: { @@ -14185,7 +14139,6 @@ declare namespace TailStream { readonly dispatchNamespace?: string; readonly entrypoint?: string; readonly executionModel: string; - readonly durableObjectId?: string; readonly scriptName?: string; readonly scriptTags?: string[]; readonly scriptVersion?: ScriptVersion; @@ -14740,13 +14693,6 @@ interface WorkflowError { code?: number; message: string; } -interface WorkflowInstanceTerminateOptions { - /** - * If true, run registered rollback handlers before terminating the instance. - * Only steps that registered rollback handlers are rolled back. - */ - rollback?: boolean; -} interface WorkflowInstanceRestartOptions { /** * Restart from a specific step. If omitted, the instance restarts from the beginning. @@ -14780,9 +14726,8 @@ declare abstract class WorkflowInstance { public resume(): Promise; /** * Terminate the instance. If it is errored, terminated or complete, an error will be thrown. - * @param options Options for termination, including whether registered rollback handlers should run. */ - public terminate(options?: WorkflowInstanceTerminateOptions): Promise; + public terminate(): Promise; /** * Restart the instance. Optionally restart from a specific step, preserving * cached results for all steps before it.