diff --git a/docs/data-pipeline.md b/docs/data-pipeline.md
index ef8e420bb..f5c996318 100644
--- a/docs/data-pipeline.md
+++ b/docs/data-pipeline.md
@@ -442,3 +442,14 @@ All normalizer logic lives in `packages/db/src/etl/normalizers.ts`. The function
- **v2 (2025-12-19+)**: Separate `prefill_tp` / `decode_tp` / `prefill_ep` / `decode_ep` / `prefill_dp_attention` / `decode_dp_attention` / `prefill_num_workers` / `decode_num_workers` / `num_prefill_gpu` / `num_decode_gpu` fields are present. These map directly; `num_prefill_gpu` / `num_decode_gpu` fall back to `tp * ep` if absent.
Detection is a single `'prefill_tp' in row` check — no version field is required in the artifact.
+
+### Power Audit Provenance (`power_invalid_reasons`, `power_audit`)
+
+Producers (`aggregate_power.py`) annotate every aggregate result row with two optional provenance fields alongside the `power_valid` verdict:
+
+- **`power_invalid_reasons`** — array of snake_case reason-code strings explaining a withheld verdict (emitted when `power_valid == 0`), e.g. `sampling_gap_exceeded`, `expected_gpu_count_mismatch`.
+- **`power_audit`** — compact measurement-window audit object with a fixed 8-key shape: `window_start_unix`, `window_end_unix`, `expected_gpu_count`, `observed_gpu_count`, `sample_count`, `max_sample_gap_s`, `producer_sha`, `exporter_image_sha256`. Present on valid and invalid rows alike.
+
+`mapBenchmarkRow()` narrows them defensively (`extractPowerInvalidReasons` / `extractPowerAudit`): reason codes must match `/^[a-z][a-z0-9_]*$/` (≤ 64 chars, deduplicated, capped at 32), audit numerics must be finite (counts: non-negative safe integers), shas collapse to `null` unless a non-empty string ≤ 128 chars, and unknown audit keys are dropped. An empty result maps to `undefined`, so the dedicated `benchmark_results.power_invalid_reasons` / `power_audit` JSONB columns (migration 014, mirroring the `workers` precedent from migration 006) store SQL NULL — never `[]` or `{}`. Legacy artifacts without the fields flow through every layer as NULL/undefined.
+
+Reads are **permanently tolerant**: `queries/benchmarks.ts` selects the columns as `to_jsonb(br) -> 'power_invalid_reasons'` (and `lb` on the matview branch) rather than bare column references. A bare reference fails during query planning until the next ingest workflow applies the migration, because migrations run in the ingest workflows rather than at Vercel deploy. The key lookup degrades to NULL while the column is missing and is byte-identical once it exists, making deploy order irrelevant.
diff --git a/packages/app/src/app/api/unofficial-run/route.test.ts b/packages/app/src/app/api/unofficial-run/route.test.ts
index d4a8e5f4d..72f124925 100644
--- a/packages/app/src/app/api/unofficial-run/route.test.ts
+++ b/packages/app/src/app/api/unofficial-run/route.test.ts
@@ -193,6 +193,40 @@ describe('normalizeArtifactRows', () => {
},
);
+ it('carries power audit provenance on overlay rows', () => {
+ const audit = {
+ window_start_unix: 1756174800,
+ window_end_unix: 1756175400,
+ expected_gpu_count: 8,
+ observed_gpu_count: 8,
+ sample_count: 4800,
+ max_sample_gap_s: 1.013,
+ producer_sha: null,
+ exporter_image_sha256: null,
+ };
+ const [row] = normalizeArtifactRows(
+ [
+ rawRow({
+ power_valid: 0,
+ power_invalid_reasons: ['sampling_gap_exceeded'],
+ power_audit: audit,
+ }),
+ ],
+ '2026-03-01',
+ );
+
+ expect(row.power_invalid_reasons).toEqual(['sampling_gap_exceeded']);
+ expect(row.power_audit).toEqual(audit);
+ expect(row.metrics).not.toHaveProperty('power_invalid_reasons');
+ expect(row.metrics).not.toHaveProperty('power_audit');
+ });
+
+ it('leaves provenance keys undefined for rows without the contract fields', () => {
+ const [row] = normalizeArtifactRows([rawRow()], '2026-03-01');
+ expect(row.power_invalid_reasons).toBeUndefined();
+ expect(row.power_audit).toBeUndefined();
+ });
+
it('preserves recipe identity for unofficial overlays', () => {
const rows = normalizeArtifactRows(
[
diff --git a/packages/app/src/app/api/unofficial-run/route.ts b/packages/app/src/app/api/unofficial-run/route.ts
index b827b6d75..48a517a3f 100644
--- a/packages/app/src/app/api/unofficial-run/route.ts
+++ b/packages/app/src/app/api/unofficial-run/route.ts
@@ -73,6 +73,8 @@ export function normalizeArtifactRows(
// Surface the same per-worker payload the DB path emits so unofficial
// overlays carry the multinode measured-power breakdown too.
workers: params.workers,
+ power_invalid_reasons: params.powerInvalidReasons,
+ power_audit: params.powerAudit,
date,
run_url: runUrl,
});
diff --git a/packages/app/src/components/inference/types.ts b/packages/app/src/components/inference/types.ts
index da0cb54ad..433fb321e 100644
--- a/packages/app/src/components/inference/types.ts
+++ b/packages/app/src/components/inference/types.ts
@@ -115,6 +115,7 @@ export interface AggDataEntry {
// Measured GPU telemetry (emitted by runner's aggregate_power.py).
// Optional because historical runs predate the fields.
power_valid?: number;
+ power_invalid_reasons?: string[];
power_metric_schema_version?: number;
/**
* Certification tier for the measured power telemetry, derived by
diff --git a/packages/app/src/components/inference/utils/tooltip-utils.test.ts b/packages/app/src/components/inference/utils/tooltip-utils.test.ts
index 013f77e02..57018979c 100644
--- a/packages/app/src/components/inference/utils/tooltip-utils.test.ts
+++ b/packages/app/src/components/inference/utils/tooltip-utils.test.ts
@@ -798,6 +798,82 @@ describe('generateGPUGraphTooltipContent', () => {
});
});
+describe('measured-power withheld tooltip line', () => {
+ const reasons = ['sampling_gap_exceeded', 'expected_gpu_count_mismatch'];
+
+ it('renders the withheld line with humanized codes (en)', () => {
+ const html = generateTooltipContent(
+ tooltipConfig({ data: pt({ power_valid: 0, power_invalid_reasons: reasons }) }),
+ );
+ expect(html).toContain('Measured power withheld');
+ expect(html).toContain('sampling gap exceeded');
+ expect(html).toContain('expected gpu count mismatch');
+ });
+
+ it('renders the withheld line in Chinese on /zh surfaces', () => {
+ const html = generateTooltipContent(
+ tooltipConfig({
+ data: pt({ power_valid: 0, power_invalid_reasons: reasons }),
+ locale: 'zh',
+ }),
+ );
+ expect(html).toContain('实测功耗未采信');
+ expect(html).toContain('sampling gap exceeded');
+ });
+
+ it('filters malformed codes before HTML interpolation (defense in depth)', () => {
+ const html = generateTooltipContent(
+ tooltipConfig({
+ data: pt({
+ power_valid: 0,
+ power_invalid_reasons: ['
', 'sampling_gap_exceeded', 'UPPER'],
+ }),
+ }),
+ );
+ expect(html).not.toContain('
');
+ expect(html).not.toContain('UPPER');
+ expect(html).toContain('sampling gap exceeded');
+ });
+
+ it('omits the line entirely when every code is malformed', () => {
+ const html = generateTooltipContent(
+ tooltipConfig({ data: pt({ power_valid: 0, power_invalid_reasons: ['
'] }) }),
+ );
+ expect(html).not.toContain('Measured power withheld');
+ });
+
+ it.each([
+ ['absent reasons', pt({ power_valid: 0 })],
+ ['empty reasons', pt({ power_valid: 0, power_invalid_reasons: [] })],
+ ['valid row', pt({ power_valid: 1 })],
+ ])('omits the line for %s', (_name, data) => {
+ const html = generateTooltipContent(tooltipConfig({ data }));
+ expect(html).not.toContain('Measured power withheld');
+ });
+
+ it('gives unofficial overlay tooltips the same line', () => {
+ const html = generateOverlayTooltipContent({
+ ...tooltipConfig({ data: pt({ power_valid: 0, power_invalid_reasons: reasons }) }),
+ overlayData: {
+ label: 'feature-branch',
+ hardwareConfig: mockHardwareConfig,
+ data: [],
+ runUrl: 'https://example.com',
+ } as any,
+ } as OverlayTooltipConfig);
+ expect(html).toContain('Measured power withheld');
+ expect(html).toContain('sampling gap exceeded');
+ });
+
+ it('gives GPU comparison tooltips the same line', () => {
+ const html = generateGPUGraphTooltipContent(
+ tooltipConfig({ data: pt({ power_valid: 0, power_invalid_reasons: reasons }) }),
+ );
+ expect(html).toContain('Measured power withheld');
+ expect(html).toContain('sampling gap exceeded');
+ });
+});
+
describe('worker power drilldown', () => {
const workers = [
{ role: 'frontend', worker_idx: 0, hosts: ['fe0'], num_gpus: 0, avg_power_w: 120 },
diff --git a/packages/app/src/components/inference/utils/tooltipUtils.ts b/packages/app/src/components/inference/utils/tooltipUtils.ts
index cd7524a83..bae5550f1 100644
--- a/packages/app/src/components/inference/utils/tooltipUtils.ts
+++ b/packages/app/src/components/inference/utils/tooltipUtils.ts
@@ -147,6 +147,7 @@ const TOOLTIP_STRINGS = {
powerData: 'Power Measurement',
powerCertified: 'Validated (current PowerX method)',
powerLegacy: 'Historical (not validated under the current method)',
+ powerWithheld: 'Measured power withheld',
},
zh: {
dismiss: '点击其他区域关闭',
@@ -164,6 +165,7 @@ const TOOLTIP_STRINGS = {
powerData: '功耗测量',
powerCertified: '已验证(采用当前 PowerX 方法)',
powerLegacy: '历史测量(尚未按当前方法验证)',
+ powerWithheld: '实测功耗未采信',
},
} as const;
@@ -519,6 +521,20 @@ const generateParallelismHTML = (d: InferenceData, locale: Locale = 'en'): strin
${tooltipLine(t.dpAttention, d.dp_attention ? t.yes : t.no)}`;
};
+const POWER_REASON_CODE_RE = /^[a-z][a-z0-9_]*$/u;
+
+/** Revalidate producer codes before interpolating them into raw tooltip HTML. */
+const powerWithheldHTML = (d: InferenceData, locale: Locale): string => {
+ if (!Array.isArray(d.power_invalid_reasons) || d.power_invalid_reasons.length === 0) return '';
+ const codes = d.power_invalid_reasons
+ .filter(
+ (code) => typeof code === 'string' && code.length <= 64 && POWER_REASON_CODE_RE.test(code),
+ )
+ .map((code) => code.replaceAll('_', ' '));
+ if (codes.length === 0) return '';
+ return tooltipLine(TOOLTIP_STRINGS[locale].powerWithheld, codes.join(', '));
+};
+
/**
* Generates HTML content for official data point tooltips.
*
@@ -572,6 +588,7 @@ export const generateTooltipContent = (config: TooltipConfig): string => {
${tooltipLine(t.concurrency, `${d.conc}`)}
${tooltipLine(t.precision, `${d.precision.toUpperCase()}`)}
${generateCacheMetadataHTML(d, locale)}
+ ${powerWithheldHTML(d, locale)}
${generateAgenticHTML(d, locale)}
${generateWorkerPowerHTML(d, isPinned, locale)}
${runLinkHTML(runUrl, locale)}
@@ -614,6 +631,7 @@ export const generateOverlayTooltipContent = (config: OverlayTooltipConfig): str
${tooltipLine(t.concurrency, `${d.conc}`)}
${tooltipLine(t.precision, `${d.precision.toUpperCase()}`)}
${generateCacheMetadataHTML(d, locale)}
+ ${powerWithheldHTML(d, locale)}
${generateAgenticHTML(d, locale)}
${generateWorkerPowerHTML(d, isPinned, locale)}
@@ -673,6 +691,7 @@ export const generateGPUGraphTooltipContent = (config: TooltipConfig): string =>
${tooltipLine(t.concurrency, `${d.conc}`)}
${tooltipLine(t.precision, `${d.precision.toUpperCase()}`)}
${generateCacheMetadataHTML(d, locale)}
+ ${powerWithheldHTML(d, locale)}
${generateAgenticHTML(d, locale)}
${generateWorkerPowerHTML(d, isPinned, locale)}
${runLinkHTML(runUrl, locale)}
diff --git a/packages/app/src/lib/api-documentation.power.test.ts b/packages/app/src/lib/api-documentation.power.test.ts
index d5d80bdd0..ab0aceb3c 100644
--- a/packages/app/src/lib/api-documentation.power.test.ts
+++ b/packages/app/src/lib/api-documentation.power.test.ts
@@ -23,12 +23,14 @@ describe('measured-power API documentation', () => {
}
});
- it('reserves optional power_invalid_reasons and power_audit row fields', () => {
+ it('documents optional power_invalid_reasons and power_audit row fields', () => {
const reasons = benchmarkRowSchema?.properties?.power_invalid_reasons;
expect(reasons?.type).toBe('array');
expect(reasons?.items).toEqual({ type: 'string' });
+ expect(reasons?.description).not.toMatch(/reserved|forthcoming/iu);
const audit = benchmarkRowSchema?.properties?.power_audit;
+ expect(audit?.description).not.toMatch(/reserved|forthcoming/iu);
expect(Object.keys(audit?.properties ?? {}).toSorted()).toEqual(
[
'window_start_unix',
@@ -41,7 +43,6 @@ describe('measured-power API documentation', () => {
'exporter_image_sha256',
].toSorted(),
);
- // Producers may emit partial audits, so individual audit fields remain optional.
expect(audit?.required).toBeUndefined();
expect(benchmarkRowSchema?.required).not.toContain('power_invalid_reasons');
diff --git a/packages/app/src/lib/api-documentation.ts b/packages/app/src/lib/api-documentation.ts
index 21386dc70..b3b4ca6ad 100644
--- a/packages/app/src/lib/api-documentation.ts
+++ b/packages/app/src/lib/api-documentation.ts
@@ -248,7 +248,7 @@ const powerAuditSchema: ApiSchema = {
},
additionalProperties: true,
description:
- 'Optional power measurement-window audit. Individual fields may be absent; legacy rows omit the object.',
+ 'Compact power measurement-window audit emitted alongside the power_valid verdict: window bounds, expected vs. observed GPU counts, sample statistics, and producer identity (producer_sha / exporter_image_sha256 are null for single-node telemetry without an srt-slurm producer). Present on valid and invalid rows from provenance-aware producers; absent on legacy rows.',
};
const workerPowerSchema = objectSchemaWithOptional(
{
@@ -297,7 +297,7 @@ const benchmarkRowSchema = objectSchemaWithOptional(
power_invalid_reasons: {
...arraySchema(stringSchema),
description:
- 'Optional snake_case validation reason codes when metrics.power_valid == 0. Absent on legacy rows.',
+ 'Producer snake_case reason codes explaining a withheld measurement, present when metrics.power_valid == 0. Absent on legacy rows and validated rows.',
},
power_audit: powerAuditSchema,
date: { type: 'string', format: 'date' },
@@ -2678,8 +2678,8 @@ const overview = {
id: 'measured-power',
title: text('Measured power', '实测功率'),
description: text(
- 'Benchmark rows may carry measured power, energy, and GPU-telemetry metric keys (avg_power_w, joules_per_*, avg_temp_c, peak_temp_c, avg_util_pct, avg_mem_used_mb). power_valid is tri-state: 1 means the measurement window was validated; 0 means validation failed and measured values are withheld end-to-end (the producer strips them and ingest scrubs them — treat any that remain as unreliable); absent marks a legacy row predating validation. power_metric_schema_version == 2 defines every unprefixed joules_per_* field as whole-deployment energy — unversioned disaggregated joules are ambiguous because those fields previously carried role-local values. workers[] carries the per-worker power/telemetry breakdown on multinode and disaggregated runs. power_invalid_reasons and power_audit provide optional producer validation details. For measured-power requests, use powerValid=strictV2 to require power_valid == 1 and power_metric_schema_version == 2. It is the only supported power filter. Omit powerValid for general benchmark requests so results remain available even when they lack valid power measurements.',
- '基准测试数据行可能包含实测功率、能耗和 GPU 遥测指标(avg_power_w、joules_per_*、avg_temp_c、peak_temp_c、avg_util_pct、avg_mem_used_mb)。power_valid 有三种状态:1 表示测量窗口已通过验证;0 表示验证失败,生产端会移除实测值,摄取端也会再次清除,若仍有残留,应视为不可靠;缺失表示该数据行早于验证机制。power_metric_schema_version == 2 规定所有无前缀的 joules_per_* 字段均按整个部署统计能耗。未标注版本的分离式部署数据中,这些字段曾记录单个角色的能耗,因此其统计口径不明确。多节点和分离式运行中,各 worker 的功率和遥测明细位于 workers[]。power_invalid_reasons 与 power_audit 可包含生产端的验证详情。查询实测功率时,使用 powerValid=strictV2,仅保留 power_valid == 1 且 power_metric_schema_version == 2 的行。这是唯一支持的功率筛选值。常规基准测试请求应省略 powerValid,以保留缺少有效功率测量的结果。',
+ 'Benchmark rows may carry measured power, energy, and GPU-telemetry metric keys (avg_power_w, joules_per_*, avg_temp_c, peak_temp_c, avg_util_pct, avg_mem_used_mb). power_valid is tri-state: 1 means the measurement window was validated; 0 means validation failed and measured values are withheld end-to-end (the producer strips them and ingest scrubs them — treat any that remain as unreliable); absent marks a legacy row predating validation. power_metric_schema_version == 2 defines every unprefixed joules_per_* field as whole-deployment energy — unversioned disaggregated joules are ambiguous because those fields previously carried role-local values. workers[] carries the per-worker power/telemetry breakdown on multinode and disaggregated runs. power_invalid_reasons lists the producer reason codes (snake_case) on withheld rows (power_valid == 0), and power_audit carries the compact measurement-window audit (window bounds, GPU counts, sample statistics, producer identity) on valid and invalid rows alike; both are absent on rows ingested before the provenance contract. For measured-power requests, use powerValid=strictV2 to require power_valid == 1 and power_metric_schema_version == 2. It is the only supported power filter. Omit powerValid for general benchmark requests so results remain available even when they lack valid power measurements.',
+ '基准测试数据行可能包含实测功率、能耗和 GPU 遥测指标(avg_power_w、joules_per_*、avg_temp_c、peak_temp_c、avg_util_pct、avg_mem_used_mb)。power_valid 有三种状态:1 表示测量窗口已通过验证;0 表示验证失败,生产端会移除实测值,摄取端也会再次清除,若仍有残留,应视为不可靠;缺失表示该数据行早于验证机制。power_metric_schema_version == 2 规定所有无前缀的 joules_per_* 字段均按整个部署统计能耗。未标注版本的分离式部署数据中,这些字段曾记录单个角色的能耗,因此其统计口径不明确。多节点和分离式运行中,各 worker 的功率和遥测明细位于 workers[]。power_invalid_reasons 在实测值被扣留的行(power_valid == 0)上列出生产端的 snake_case 原因码;power_audit 在有效与无效行上都携带精简的测量窗口审计信息(窗口边界、GPU 数量、采样统计、生产者标识);早于溯源契约摄取的行均不含这两个字段。查询实测功率时,使用 powerValid=strictV2,仅保留 power_valid == 1 且 power_metric_schema_version == 2 的行。这是唯一支持的功率筛选值。常规基准测试请求应省略 powerValid,以保留缺少有效功率测量的结果。',
),
shape: 'BenchmarkRows',
example: {
diff --git a/packages/app/src/lib/api-route-catalog.ts b/packages/app/src/lib/api-route-catalog.ts
index e01c8e5a7..a00759cf7 100644
--- a/packages/app/src/lib/api-route-catalog.ts
+++ b/packages/app/src/lib/api-route-catalog.ts
@@ -76,7 +76,7 @@ export const apiRouteCatalog = [
en: 'UI-only overlay for unofficial workflow artifacts; upstream artifact availability and shape are not stable.',
zh: '仅供界面叠加非官方工作流制品;上游制品的可用性和结构并不稳定。',
},
- sourceSha256: '4a3f3da8399c741c26f0f502d44b1870a8ccdc05775edfd6ea3dee4e020df25c',
+ sourceSha256: '6973369b5ee4ded847754b7d38ec7b9db7c30f895c9b0e54c7aaf9b2bf7eb45d',
},
{
source: 'src/app/api/v1/agentic-aggregates/route.ts',
@@ -731,7 +731,7 @@ export const apiContractSourceDigests = [
},
{
source: '../db/src/queries/benchmarks.ts',
- sourceSha256: '486e34d55275170c7e0544af24c151199752eb5628191d38606b1ecec289dfdf',
+ sourceSha256: '0793d7901630d74301493a34436c8ad868cc4c938d46ba40256b19fc76b17332',
reviewArea: {
en: 'Benchmark row fields and latest, exact-run, history, and TCO query semantics.',
zh: '基准行字段以及最新、精确运行、历史和 TCO 查询语义。',
diff --git a/packages/app/src/lib/benchmark-api-view.test.ts b/packages/app/src/lib/benchmark-api-view.test.ts
index a2772c3b8..c74397c99 100644
--- a/packages/app/src/lib/benchmark-api-view.test.ts
+++ b/packages/app/src/lib/benchmark-api-view.test.ts
@@ -14,6 +14,8 @@ const rows = [
unused_debug_metric: 99,
},
workers: [{ rank: 0, avg_power_w: 700 }],
+ power_invalid_reasons: ['sampling_gap_exceeded'],
+ power_audit: { sample_count: 4800, producer_sha: null, exporter_image_sha256: null },
},
{
benchmark_type: 'single_turn',
@@ -46,6 +48,13 @@ describe('toCalculatorBenchmarkRows', () => {
]);
});
+ it('strips workers and the power audit provenance from the payload-trimmed view', () => {
+ const [row] = toCalculatorBenchmarkRows(rows, '1k/1k');
+ expect(row).not.toHaveProperty('workers');
+ expect(row).not.toHaveProperty('power_invalid_reasons');
+ expect(row).not.toHaveProperty('power_audit');
+ });
+
it('keeps all three cache tiers — the trim cannot know which one a row will use', () => {
// `measuredCacheHitRate` picks between external and CPU per row, so the allowlist
// has to pass all three through or the choice is made for it by the trim.
diff --git a/packages/app/src/lib/benchmark-api-view.ts b/packages/app/src/lib/benchmark-api-view.ts
index 372db5752..3f552267f 100644
--- a/packages/app/src/lib/benchmark-api-view.ts
+++ b/packages/app/src/lib/benchmark-api-view.ts
@@ -31,6 +31,8 @@ interface BenchmarkViewRow {
osl: number | null;
metrics: Record;
workers?: unknown;
+ power_invalid_reasons?: unknown;
+ power_audit?: unknown;
}
/**
@@ -45,7 +47,13 @@ export function toCalculatorBenchmarkRows(
return rows
.filter((row) => rowToSequence(row) === sequence)
.map((row) => {
- const { workers: _workers, ...rest } = row;
+ // Omit provenance with the measured-power fields it explains.
+ const {
+ workers: _workers,
+ power_invalid_reasons: _powerInvalidReasons,
+ power_audit: _powerAudit,
+ ...rest
+ } = row;
return {
...rest,
metrics: Object.fromEntries(
diff --git a/packages/app/src/lib/benchmark-transform.test.ts b/packages/app/src/lib/benchmark-transform.test.ts
index ffd358c35..1723c86db 100644
--- a/packages/app/src/lib/benchmark-transform.test.ts
+++ b/packages/app/src/lib/benchmark-transform.test.ts
@@ -280,6 +280,25 @@ describe('rowToAggDataEntry', () => {
expect(entry.joules_per_output_token).toBe(8.4);
});
+ it('passes through producer power_invalid_reasons on withheld rows', () => {
+ const entry = rowToAggDataEntry(
+ makeRow({
+ metrics: { power_valid: 0 },
+ power_invalid_reasons: ['thermal_throttle', 'sample_gap'],
+ }),
+ );
+ expect(entry.power_invalid_reasons).toEqual(['thermal_throttle', 'sample_gap']);
+ });
+
+ it.each([
+ ['legacy row without the field', {}],
+ ['API null (SQL NULL column)', { power_invalid_reasons: null }],
+ ['empty array', { power_invalid_reasons: [] }],
+ ])('leaves power_invalid_reasons undefined for %s', (_name, overrides) => {
+ const entry = rowToAggDataEntry(makeRow({ metrics: {}, ...overrides }));
+ expect(entry.power_invalid_reasons).toBeUndefined();
+ });
+
it('passes through versioned whole-deployment joules per successful query', () => {
const entry = rowToAggDataEntry(
makeRow({
diff --git a/packages/app/src/lib/benchmark-transform.ts b/packages/app/src/lib/benchmark-transform.ts
index 48ba55c1d..9587f5cf9 100644
--- a/packages/app/src/lib/benchmark-transform.ts
+++ b/packages/app/src/lib/benchmark-transform.ts
@@ -217,6 +217,13 @@ export function rowToAggDataEntry(row: BenchmarkRow): AggDataEntry {
// rows predating the field so downstream chart code can distinguish
// "no measurement" from "0 W" via createChartDataPoint's typeof guard.
power_valid: m.power_valid,
+ // Narrow at the trust boundary: API rows may carry null (SQL NULL) or
+ // omit the field entirely on legacy data; only a non-empty array of
+ // producer reason codes survives.
+ power_invalid_reasons:
+ Array.isArray(row.power_invalid_reasons) && row.power_invalid_reasons.length > 0
+ ? row.power_invalid_reasons
+ : undefined,
power_metric_schema_version: m.power_metric_schema_version,
power_tier: resolvePowerTier({
powerValid: m.power_valid,
diff --git a/packages/db/migrations/014_power_provenance.sql b/packages/db/migrations/014_power_provenance.sql
new file mode 100644
index 000000000..82916a4e2
--- /dev/null
+++ b/packages/db/migrations/014_power_provenance.sql
@@ -0,0 +1,140 @@
+-- Power audit provenance lives in dedicated JSONB columns so `metrics` stays
+-- a flat Record. NULL identifies rows from older producers.
+
+alter table benchmark_results
+ add column power_invalid_reasons jsonb;
+
+alter table benchmark_results
+ add column power_audit jsonb;
+
+-- Re-create latest_benchmarks because `br.*` freezes the column list when the
+-- materialized view is created. The body remains identical to the canonical
+-- definition in migration 012; line-selection changes require a new migration.
+
+drop materialized view if exists latest_benchmarks;
+
+create materialized view latest_benchmarks as
+with recursive run_lines as (
+ select
+ c.model,
+ c.hardware,
+ c.framework,
+ c.precision,
+ c.disagg,
+ case when br.benchmark_type = 'agentic_traces' then '' else c.spec_method end as line_spec_method,
+ br.benchmark_type,
+ br.isl,
+ br.osl,
+ br.offload_mode,
+ br.workflow_run_id,
+ br.date,
+ wr.run_started_at,
+ wr.append_only,
+ min(br.image) as image,
+ count(distinct br.image) as image_count,
+ bool_and(br.image is not null) as images_complete
+ from benchmark_results br
+ join configs c on c.id = br.config_id
+ join latest_workflow_runs wr on wr.id = br.workflow_run_id
+ where br.error is null
+ group by
+ c.model, c.hardware, c.framework, c.precision, c.disagg,
+ case when br.benchmark_type = 'agentic_traces' then '' else c.spec_method end,
+ br.benchmark_type, br.isl, br.osl, br.offload_mode,
+ br.workflow_run_id, br.date, wr.run_started_at, wr.append_only
+), ranked_runs as (
+ select
+ run_lines.*,
+ row_number() over (
+ partition by
+ model, hardware, framework, precision, disagg, line_spec_method,
+ benchmark_type, isl, osl, offload_mode
+ order by date desc, run_started_at desc nulls last, workflow_run_id desc
+ ) as run_rank
+ from run_lines
+), curve_runs as (
+ select
+ ranked_runs.*,
+ ranked_runs.image as root_image,
+ ranked_runs.date as snapshot_date,
+ ranked_runs.workflow_run_id as snapshot_workflow_run_id
+ from ranked_runs
+ where run_rank = 1
+
+ union all
+
+ select
+ older.*,
+ current.root_image,
+ current.snapshot_date,
+ current.snapshot_workflow_run_id
+ from curve_runs current
+ join ranked_runs older
+ on older.model = current.model
+ and older.hardware = current.hardware
+ and older.framework = current.framework
+ and older.precision = current.precision
+ and older.disagg = current.disagg
+ and older.line_spec_method = current.line_spec_method
+ and older.benchmark_type = current.benchmark_type
+ and older.isl is not distinct from current.isl
+ and older.osl is not distinct from current.osl
+ and older.offload_mode = current.offload_mode
+ and older.run_rank = current.run_rank + 1
+ where current.append_only
+ and current.image_count = 1
+ and current.images_complete
+ and older.image_count = 1
+ and older.images_complete
+ and older.image = current.root_image
+)
+select distinct on (
+ br.config_id,
+ br.benchmark_type,
+ br.isl,
+ br.osl,
+ br.offload_mode,
+ br.recipe_fingerprint,
+ br.conc
+)
+ br.*,
+ cr.snapshot_date,
+ cr.snapshot_workflow_run_id
+from curve_runs cr
+join benchmark_results br
+ on br.workflow_run_id = cr.workflow_run_id
+ and br.benchmark_type = cr.benchmark_type
+ and br.isl is not distinct from cr.isl
+ and br.osl is not distinct from cr.osl
+ and br.offload_mode = cr.offload_mode
+join configs point_c
+ on point_c.id = br.config_id
+ and point_c.model = cr.model
+ and point_c.hardware = cr.hardware
+ and point_c.framework = cr.framework
+ and point_c.precision = cr.precision
+ and point_c.disagg = cr.disagg
+ and case when br.benchmark_type = 'agentic_traces' then '' else point_c.spec_method end = cr.line_spec_method
+where br.error is null
+order by
+ br.config_id,
+ br.benchmark_type,
+ br.isl,
+ br.osl,
+ br.offload_mode,
+ br.recipe_fingerprint,
+ br.conc,
+ cr.run_rank;
+
+create unique index latest_benchmarks_pk
+ on latest_benchmarks (
+ config_id,
+ conc,
+ isl,
+ osl,
+ benchmark_type,
+ offload_mode,
+ recipe_fingerprint
+ )
+ nulls not distinct;
+create index latest_benchmarks_model_idx on latest_benchmarks (config_id);
diff --git a/packages/db/src/etl/benchmark-ingest.test.ts b/packages/db/src/etl/benchmark-ingest.test.ts
index d1a1db6a3..e97792ba6 100644
--- a/packages/db/src/etl/benchmark-ingest.test.ts
+++ b/packages/db/src/etl/benchmark-ingest.test.ts
@@ -7,8 +7,10 @@ import { describe, expect, it, vi } from 'vitest';
import type { Sql } from './db-utils';
import {
benchmarkPointIngestKey,
+ bulkIngestBenchmarkRows,
insertServerLogFilePaths,
insertServerLogFiles,
+ type BenchmarkPersistenceInput,
} from './benchmark-ingest';
const point = (recipeFingerprint: string | null) => ({
@@ -55,6 +57,77 @@ function fakeTransactionSql(linkedId: number | null) {
return { sql: tag as Sql, calls };
}
+function captureInsertSql() {
+ const calls: { text: string; values: unknown[] }[] = [];
+ const tag = vi.fn((strings: TemplateStringsArray, ...values: unknown[]) => {
+ calls.push({ text: strings.join('?').replaceAll(/\s+/gu, ' ').trim(), values });
+ return Promise.resolve([]);
+ }) as any;
+ tag.array = (value: unknown) => value;
+ return { sql: tag as Sql, calls };
+}
+
+describe('bulkIngestBenchmarkRows — power audit provenance lanes', () => {
+ const provenancedRow: BenchmarkPersistenceInput = {
+ configId: 7,
+ benchmarkType: 'single_turn',
+ isl: 1024,
+ osl: 1024,
+ conc: 64,
+ offloadMode: 'off',
+ image: 'img',
+ recipeFingerprint: null,
+ metrics: { power_valid: 0 },
+ powerInvalidReasons: ['sampling_gap_exceeded'],
+ powerAudit: {
+ window_start_unix: 1756174800,
+ window_end_unix: 1756175400,
+ expected_gpu_count: 8,
+ observed_gpu_count: 8,
+ sample_count: 4800,
+ max_sample_gap_s: 1.013,
+ producer_sha: null,
+ exporter_image_sha256: null,
+ },
+ };
+ const legacyRow: BenchmarkPersistenceInput = {
+ configId: 8,
+ benchmarkType: 'single_turn',
+ isl: 1024,
+ osl: 1024,
+ conc: 128,
+ offloadMode: 'off',
+ image: 'img',
+ recipeFingerprint: null,
+ metrics: { tput_per_gpu: 100 },
+ };
+
+ it('names both columns, adds two jsonb lanes, and refreshes both on conflict', async () => {
+ const { sql, calls } = captureInsertSql();
+ await bulkIngestBenchmarkRows(sql, [provenancedRow, legacyRow], 42, '2026-08-27');
+
+ const { text } = calls[0];
+ expect(text).toContain('metrics, workers, power_invalid_reasons, power_audit )');
+ expect(text.match(/::jsonb\[\]/gu)).toHaveLength(4);
+ expect(text).toContain('power_invalid_reasons = excluded.power_invalid_reasons');
+ expect(text).toContain('power_audit = excluded.power_audit');
+ });
+
+ it('serializes present fields and contributes null lanes for absent ones', async () => {
+ const { sql, calls } = captureInsertSql();
+ await bulkIngestBenchmarkRows(sql, [provenancedRow, legacyRow], 42, '2026-08-27');
+
+ const { values } = calls[0];
+ expect(values[10]).toEqual([
+ JSON.stringify(provenancedRow.metrics),
+ JSON.stringify(legacyRow.metrics),
+ ]);
+ expect(values[11]).toEqual([null, null]);
+ expect(values[12]).toEqual([JSON.stringify(provenancedRow.powerInvalidReasons), null]);
+ expect(values[13]).toEqual([JSON.stringify(provenancedRow.powerAudit), null]);
+ });
+});
+
describe('insertServerLogFiles', () => {
const files = [
{ fileName: 'results/benchmark.log', logText: 'benchmark' },
diff --git a/packages/db/src/etl/benchmark-ingest.ts b/packages/db/src/etl/benchmark-ingest.ts
index 617c51efe..122965e08 100644
--- a/packages/db/src/etl/benchmark-ingest.ts
+++ b/packages/db/src/etl/benchmark-ingest.ts
@@ -7,7 +7,7 @@ import path from 'node:path';
import type postgres from 'postgres';
import { cleanLogText, type ServerLogFile, type ServerLogFilePath } from './server-log-artifacts';
-import type { BenchmarkType, WorkerPower } from './benchmark-mapper';
+import type { BenchmarkType, PowerAudit, WorkerPower } from './benchmark-mapper';
import { kvCachePoolTokensFromServerLog } from './server-log-metrics';
type Sql = ReturnType;
@@ -23,6 +23,8 @@ export interface BenchmarkPersistenceInput {
recipeFingerprint: string | null;
metrics: Record;
workers?: WorkerPower[];
+ powerInvalidReasons?: string[];
+ powerAudit?: PowerAudit;
}
type BenchmarkPointIdentity = Pick<
@@ -82,17 +84,23 @@ export async function bulkIngestBenchmarkRows(
const images = deduped.map((r) => r.image);
const recipeFingerprints = deduped.map((r) => r.recipeFingerprint);
const metricsJsons = deduped.map((r) => JSON.stringify(r.metrics));
- // workers is optional — encode missing values as JSON null so the JSONB
- // unnest input has a homogeneous type (jsonb[]) and stores SQL NULL in the
- // column for rows that didn't emit a per-worker breakdown.
+ // Optional JSONB lanes use JSON null so each jsonb[] input remains
+ // homogeneous and missing payloads persist as SQL NULL.
const workersJsons = deduped.map((r) =>
r.workers === undefined ? null : JSON.stringify(r.workers),
);
+ const powerInvalidReasonsJsons = deduped.map((r) =>
+ r.powerInvalidReasons === undefined ? null : JSON.stringify(r.powerInvalidReasons),
+ );
+ const powerAuditJsons = deduped.map((r) =>
+ r.powerAudit === undefined ? null : JSON.stringify(r.powerAudit),
+ );
const result = await sql<{ inserted: boolean; id: number }[]>`
insert into benchmark_results (
workflow_run_id, config_id, benchmark_type, offload_mode, date,
- isl, osl, conc, image, recipe_fingerprint, metrics, workers
+ isl, osl, conc, image, recipe_fingerprint, metrics, workers,
+ power_invalid_reasons, power_audit
)
select
${workflowRunId},
@@ -106,7 +114,9 @@ export async function bulkIngestBenchmarkRows(
unnest(${sql.array(images)}),
unnest(${sql.array(recipeFingerprints)}),
unnest(${sql.array(metricsJsons)}::jsonb[]),
- unnest(${sql.array(workersJsons)}::jsonb[])
+ unnest(${sql.array(workersJsons)}::jsonb[]),
+ unnest(${sql.array(powerInvalidReasonsJsons)}::jsonb[]),
+ unnest(${sql.array(powerAuditJsons)}::jsonb[])
on conflict (
workflow_run_id, config_id, benchmark_type, isl, osl, conc, offload_mode,
recipe_fingerprint
@@ -120,7 +130,11 @@ export async function bulkIngestBenchmarkRows(
jsonb_build_object('kv_cache_pool_tokens', benchmark_results.metrics->'kv_cache_pool_tokens')
),
image = excluded.image,
- workers = excluded.workers
+ workers = excluded.workers,
+ -- Like workers, the fresh artifact is authoritative for provenance: a
+ -- re-ingest from an artifact without the fields deliberately nulls them.
+ power_invalid_reasons = excluded.power_invalid_reasons,
+ power_audit = excluded.power_audit
returning (xmax = 0) as inserted, id
`;
diff --git a/packages/db/src/etl/benchmark-mapper.test.ts b/packages/db/src/etl/benchmark-mapper.test.ts
index 0bc4f3b1a..d22c73f6e 100644
--- a/packages/db/src/etl/benchmark-mapper.test.ts
+++ b/packages/db/src/etl/benchmark-mapper.test.ts
@@ -1,6 +1,8 @@
-import { describe, it, expect } from 'vitest';
+import { describe, it, expect, vi } from 'vitest';
import { MEASURED_POWER_METRIC_KEYS } from '@semianalysisai/inferencex-constants';
import {
+ extractPowerAudit,
+ extractPowerInvalidReasons,
extractWorkers,
mapBenchmarkRow,
normalizePowerContractMetrics,
@@ -435,6 +437,13 @@ describe('mapBenchmarkRow', () => {
}
expect(result!.metrics).not.toHaveProperty('power_invalid_reasons');
expect(result!.metrics).not.toHaveProperty('power_audit');
+ expect(result!.powerInvalidReasons).toEqual(['window_too_short']);
+ expect(result!.powerAudit).toEqual({
+ window_start_unix: 1,
+ window_end_unix: 2,
+ producer_sha: null,
+ exporter_image_sha256: null,
+ });
});
});
@@ -888,6 +897,30 @@ describe('scrubWithheldPowerMetrics (direct — supplemental ingest path)', () =
expect(metrics).not.toHaveProperty(key);
}
});
+
+ it('recovers provenance companions nested under metrics and leaves the record flat', () => {
+ const metrics = supplementalMetrics({
+ power_valid: 0,
+ power_invalid_reasons: ['sampling_gap_exceeded', 'sampling_gap_exceeded', '
'],
+ power_audit: { sample_count: 12, producer_sha: 'abc123', unknown_key: true },
+ });
+ normalizePowerContractMetrics(metrics, metrics);
+ scrubWithheldPowerMetrics(metrics);
+ const reasons = extractPowerInvalidReasons(metrics.power_invalid_reasons);
+ const audit = extractPowerAudit(metrics.power_audit);
+ delete metrics.power_invalid_reasons;
+ delete metrics.power_audit;
+
+ expect(reasons).toEqual(['sampling_gap_exceeded']);
+ expect(audit).toEqual({
+ sample_count: 12,
+ producer_sha: 'abc123',
+ exporter_image_sha256: null,
+ });
+ expect(metrics).not.toHaveProperty('power_invalid_reasons');
+ expect(metrics).not.toHaveProperty('power_audit');
+ expect(metrics.tput_per_gpu).toBe(567.8);
+ });
});
describe('extractWorkers', () => {
@@ -974,6 +1007,236 @@ describe('extractWorkers', () => {
});
});
+describe('extractPowerInvalidReasons', () => {
+ it('keeps valid snake_case codes in first-seen order', () => {
+ expect(
+ extractPowerInvalidReasons(['sampling_gap_exceeded', 'expected_gpu_count_mismatch']),
+ ).toEqual(['sampling_gap_exceeded', 'expected_gpu_count_mismatch']);
+ });
+
+ it('deduplicates preserving first-seen order', () => {
+ expect(
+ extractPowerInvalidReasons([
+ 'telemetry_file_missing',
+ 'no_usable_power_samples',
+ 'telemetry_file_missing',
+ ]),
+ ).toEqual(['telemetry_file_missing', 'no_usable_power_samples']);
+ });
+
+ it.each([
+ ['non-string entry', [42]],
+ ['empty string', ['']],
+ ['hyphenated code', ['Bad-Reason']],
+ ['uppercase code', ['UPPER']],
+ ['leading digit', ['9lives']],
+ ['65-char code', ['a'.repeat(65)]],
+ ])('silently drops %s', (_name, raw) => {
+ expect(extractPowerInvalidReasons(raw)).toBeUndefined();
+ });
+
+ it('drops malformed entries while keeping valid siblings', () => {
+ expect(extractPowerInvalidReasons([42, 'sampling_gap_exceeded', '
', null])).toEqual([
+ 'sampling_gap_exceeded',
+ ]);
+ });
+
+ it('keeps a 64-char code (boundary)', () => {
+ const code = 'a'.repeat(64);
+ expect(extractPowerInvalidReasons([code])).toEqual([code]);
+ });
+
+ it('caps the result at 32 codes', () => {
+ const raw = Array.from({ length: 40 }, (_v, i) => `reason_${i}`);
+ const result = extractPowerInvalidReasons(raw);
+ expect(result).toHaveLength(32);
+ expect(result![0]).toBe('reason_0');
+ expect(result![31]).toBe('reason_31');
+ });
+
+ it.each([
+ ['empty array', []],
+ ['non-array object', { reason: 'x' }],
+ ['string', 'sampling_gap_exceeded'],
+ ['null', null],
+ ['undefined', undefined],
+ ])('returns undefined (never []) for %s', (_name, raw) => {
+ expect(extractPowerInvalidReasons(raw)).toBeUndefined();
+ });
+});
+
+describe('extractPowerAudit', () => {
+ const fullAudit = {
+ window_start_unix: 1756174800.25,
+ window_end_unix: 1756175400.75,
+ expected_gpu_count: 16,
+ observed_gpu_count: 16,
+ sample_count: 9600,
+ max_sample_gap_s: 1.013,
+ producer_sha: '887a6cb7c2ec174e5e2b977468a12ab34cd56ef7',
+ exporter_image_sha256:
+ 'sha256:0b7f1a2c3d4e5f60718293a4b5c6d7e8f9012a3b4c5d6e7f8091a2b3c4d5e6f7',
+ };
+
+ it('round-trips a full valid object across all 8 fields', () => {
+ expect(extractPowerAudit(fullAudit)).toEqual(fullAudit);
+ });
+
+ it('omits Infinity / NaN / junk-string numerics (partial audit beats none)', () => {
+ expect(
+ extractPowerAudit({
+ ...fullAudit,
+ window_start_unix: Number.POSITIVE_INFINITY,
+ window_end_unix: Number.NaN,
+ max_sample_gap_s: 'garbage',
+ }),
+ ).toEqual({
+ expected_gpu_count: 16,
+ observed_gpu_count: 16,
+ sample_count: 9600,
+ producer_sha: fullAudit.producer_sha,
+ exporter_image_sha256: fullAudit.exporter_image_sha256,
+ });
+ });
+
+ it('rejects negative and non-safe-integer counts', () => {
+ expect(
+ extractPowerAudit({
+ ...fullAudit,
+ expected_gpu_count: -1,
+ observed_gpu_count: Number.MAX_SAFE_INTEGER + 1,
+ sample_count: Number.POSITIVE_INFINITY,
+ }),
+ ).toEqual({
+ window_start_unix: fullAudit.window_start_unix,
+ window_end_unix: fullAudit.window_end_unix,
+ max_sample_gap_s: fullAudit.max_sample_gap_s,
+ producer_sha: fullAudit.producer_sha,
+ exporter_image_sha256: fullAudit.exporter_image_sha256,
+ });
+ });
+
+ it('keeps shas trimmed and collapses null / number / oversized shas to null', () => {
+ expect(
+ extractPowerAudit({
+ sample_count: 1,
+ producer_sha: ' abc123 ',
+ exporter_image_sha256: null,
+ }),
+ ).toEqual({ sample_count: 1, producer_sha: 'abc123', exporter_image_sha256: null });
+ expect(
+ extractPowerAudit({
+ sample_count: 1,
+ producer_sha: 42,
+ exporter_image_sha256: 'x'.repeat(129),
+ }),
+ ).toEqual({ sample_count: 1, producer_sha: null, exporter_image_sha256: null });
+ });
+
+ it('nulls the explicit-null numeric fields a producer emits without a benchmark window', () => {
+ expect(
+ extractPowerAudit({
+ window_start_unix: null,
+ window_end_unix: null,
+ expected_gpu_count: 8,
+ observed_gpu_count: 0,
+ sample_count: 0,
+ max_sample_gap_s: null,
+ producer_sha: null,
+ exporter_image_sha256: null,
+ }),
+ ).toEqual({
+ expected_gpu_count: 8,
+ observed_gpu_count: 0,
+ sample_count: 0,
+ producer_sha: null,
+ exporter_image_sha256: null,
+ });
+ });
+
+ it('drops unknown keys (fixed 8-key shape bounds the stored object)', () => {
+ expect(extractPowerAudit({ sample_count: 3, integration_method: 'trapezoid' })).toEqual({
+ sample_count: 3,
+ producer_sha: null,
+ exporter_image_sha256: null,
+ });
+ });
+
+ it.each([
+ ['string', 'audit'],
+ ['array', [1, 2]],
+ ['null', null],
+ ['undefined', undefined],
+ ['number', 7],
+ ])('returns undefined for non-object input: %s', (_name, raw) => {
+ expect(extractPowerAudit(raw)).toBeUndefined();
+ });
+
+ it('returns undefined for an empty husk (no numerics, both shas null)', () => {
+ expect(extractPowerAudit({})).toBeUndefined();
+ expect(extractPowerAudit({ producer_sha: null, exporter_image_sha256: 42 })).toBeUndefined();
+ expect(extractPowerAudit({ window_start_unix: 'junk' })).toBeUndefined();
+ });
+});
+
+describe('mapBenchmarkRow — power audit provenance', () => {
+ const reasons = ['sampling_gap_exceeded', 'expected_gpu_count_mismatch'];
+ const audit = {
+ window_start_unix: 1756174800,
+ window_end_unix: 1756175400,
+ expected_gpu_count: 8,
+ observed_gpu_count: 8,
+ sample_count: 4800,
+ max_sample_gap_s: 1.013,
+ producer_sha: null,
+ exporter_image_sha256: null,
+ };
+
+ it.each([
+ ['v1', makeV1Row],
+ ['v2', makeV2Row],
+ ['agentic', makeAgenticRow],
+ ])('lands the contract fields on BenchmarkParams for %s rows', (_name, makeRow) => {
+ const tracker = createSkipTracker();
+ const result = mapBenchmarkRow(
+ makeRow({ power_valid: 0, power_invalid_reasons: reasons, power_audit: audit }),
+ tracker,
+ );
+ expect(result!.powerInvalidReasons).toEqual(reasons);
+ expect(result!.powerAudit).toEqual(audit);
+ });
+
+ it('stores provenance from valid rows too (tolerance in both directions)', () => {
+ const tracker = createSkipTracker();
+ const result = mapBenchmarkRow(makeV2Row({ power_valid: 1, power_audit: audit }), tracker);
+ expect(result!.metrics.power_valid).toBe(1);
+ expect(result!.powerAudit).toEqual(audit);
+ expect(result!.powerInvalidReasons).toBeUndefined();
+ });
+
+ it('leaves both fields undefined on legacy rows', () => {
+ const tracker = createSkipTracker();
+ const result = mapBenchmarkRow(makeV2Row(), tracker);
+ expect(result!.powerInvalidReasons).toBeUndefined();
+ expect(result!.powerAudit).toBeUndefined();
+ });
+
+ it("never captures a malformed ['5'] reasons array as a numeric metric", () => {
+ const tracker = createSkipTracker();
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
+ try {
+ const result = mapBenchmarkRow(makeV2Row({ power_invalid_reasons: ['5'] }), tracker);
+ expect(result!.metrics).not.toHaveProperty('power_invalid_reasons');
+ expect(result!.powerInvalidReasons).toBeUndefined();
+ expect(warn.mock.calls.map((call) => String(call[0]))).not.toContainEqual(
+ expect.stringContaining('power_invalid_reasons'),
+ );
+ } finally {
+ warn.mockRestore();
+ }
+ });
+});
+
describe('mapBenchmarkRow — agentic interactivity normalization', () => {
it('derives *_intvty from 1/*_itl, discarding the artifact value', () => {
const tracker = createSkipTracker();
diff --git a/packages/db/src/etl/benchmark-mapper.ts b/packages/db/src/etl/benchmark-mapper.ts
index 0e6958f3b..bf45c175f 100644
--- a/packages/db/src/etl/benchmark-mapper.ts
+++ b/packages/db/src/etl/benchmark-mapper.ts
@@ -86,6 +86,10 @@ const NON_METRIC_KEYS = new Set([
// sibling of the metrics JSONB by mapBenchmarkRow so the metrics column
// stays Record for the index signature on BenchmarkRow.
'workers',
+ // Keep structured provenance outside flat numeric metrics. Explicitly
+ // excluding the keys also blocks Number(['5']) coercion.
+ 'power_invalid_reasons',
+ 'power_audit',
]);
/**
@@ -126,6 +130,21 @@ export interface WorkerPower {
avg_mem_used_mb?: number;
}
+/**
+ * Narrowed measurement-window audit. Fields are optional because malformed
+ * numerics are omitted; producer identity is null when unavailable.
+ */
+export interface PowerAudit {
+ window_start_unix?: number;
+ window_end_unix?: number;
+ expected_gpu_count?: number;
+ observed_gpu_count?: number;
+ sample_count?: number;
+ max_sample_gap_s?: number;
+ producer_sha?: string | null;
+ exporter_image_sha256?: string | null;
+}
+
export interface BenchmarkParams {
config: ConfigParams;
benchmarkType: BenchmarkType;
@@ -148,6 +167,8 @@ export interface BenchmarkParams {
* predating the multinode patch.
*/
workers?: WorkerPower[];
+ powerInvalidReasons?: string[];
+ powerAudit?: PowerAudit;
}
/**
@@ -357,6 +378,9 @@ export function mapBenchmarkRow(
// narrowing — anything other than a non-empty array of objects is dropped,
// and a withheld power verdict drops the payload entirely.
const workers = powerWithheld ? undefined : extractWorkers(row.workers);
+ // Audit metadata is independent of the verdict so valid-row provenance is retained.
+ const powerInvalidReasons = extractPowerInvalidReasons(row.power_invalid_reasons);
+ const powerAudit = extractPowerAudit(row.power_audit);
return {
config: {
@@ -378,6 +402,8 @@ export function mapBenchmarkRow(
recipeFingerprint,
metrics,
workers,
+ powerInvalidReasons,
+ powerAudit,
};
}
@@ -554,3 +580,78 @@ export function extractWorkers(raw: unknown): WorkerPower[] | undefined {
}
return out.length > 0 ? out : undefined;
}
+
+const POWER_REASON_CODE_RE = /^[a-z][a-z0-9_]*$/u;
+const MAX_POWER_REASON_CODES = 32;
+const MAX_POWER_REASON_LENGTH = 64;
+const MAX_POWER_AUDIT_SHA_LENGTH = 128;
+
+/**
+ * Keep at most 32 unique snake_case reason codes, each at most 64 characters.
+ * Empty results become undefined so persistence stores SQL NULL, not `[]`.
+ */
+export function extractPowerInvalidReasons(raw: unknown): string[] | undefined {
+ if (!Array.isArray(raw)) return undefined;
+ const out: string[] = [];
+ const seen = new Set();
+ for (const entry of raw) {
+ if (typeof entry !== 'string') continue;
+ const code = entry.trim();
+ if (code.length === 0 || code.length > MAX_POWER_REASON_LENGTH) continue;
+ if (!POWER_REASON_CODE_RE.test(code) || seen.has(code)) continue;
+ seen.add(code);
+ out.push(code);
+ if (out.length >= MAX_POWER_REASON_CODES) break;
+ }
+ return out.length > 0 ? out : undefined;
+}
+
+/** parseNum plus a finiteness guard: parseNum passes Infinity through. */
+function auditFiniteNum(v: unknown): number | undefined {
+ const n = parseNum(v);
+ return n !== undefined && Number.isFinite(n) ? n : undefined;
+}
+
+function auditCount(v: unknown): number | undefined {
+ const n = parseInt2(v);
+ return n !== undefined && Number.isSafeInteger(n) && n >= 0 ? n : undefined;
+}
+
+function auditSha(v: unknown): string | null {
+ if (typeof v !== 'string') return null;
+ const s = v.trim();
+ return s.length > 0 && s.length <= MAX_POWER_AUDIT_SHA_LENGTH ? s : null;
+}
+
+/**
+ * Narrow to the fixed {@link PowerAudit} shape: finite window/gap values,
+ * non-negative safe-integer counts, and bounded producer identifiers. Unknown
+ * keys are dropped; an empty result becomes undefined so persistence stores
+ * SQL NULL rather than `{}`.
+ */
+export function extractPowerAudit(raw: unknown): PowerAudit | undefined {
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined;
+ const e = raw as Record;
+ const audit: PowerAudit = {};
+
+ const window_start_unix = auditFiniteNum(e.window_start_unix);
+ if (window_start_unix !== undefined) audit.window_start_unix = window_start_unix;
+ const window_end_unix = auditFiniteNum(e.window_end_unix);
+ if (window_end_unix !== undefined) audit.window_end_unix = window_end_unix;
+ const max_sample_gap_s = auditFiniteNum(e.max_sample_gap_s);
+ if (max_sample_gap_s !== undefined) audit.max_sample_gap_s = max_sample_gap_s;
+ const expected_gpu_count = auditCount(e.expected_gpu_count);
+ if (expected_gpu_count !== undefined) audit.expected_gpu_count = expected_gpu_count;
+ const observed_gpu_count = auditCount(e.observed_gpu_count);
+ if (observed_gpu_count !== undefined) audit.observed_gpu_count = observed_gpu_count;
+ const sample_count = auditCount(e.sample_count);
+ if (sample_count !== undefined) audit.sample_count = sample_count;
+ const hasNumericField = Object.keys(audit).length > 0;
+
+ audit.producer_sha = auditSha(e.producer_sha);
+ audit.exporter_image_sha256 = auditSha(e.exporter_image_sha256);
+ if (!hasNumericField && audit.producer_sha === null && audit.exporter_image_sha256 === null) {
+ return undefined;
+ }
+ return audit;
+}
diff --git a/packages/db/src/ingest-supplemental.ts b/packages/db/src/ingest-supplemental.ts
index 18a2a8b55..b9ca0c684 100644
--- a/packages/db/src/ingest-supplemental.ts
+++ b/packages/db/src/ingest-supplemental.ts
@@ -27,7 +27,12 @@ import {
bulkUpsertAvailability,
type BenchmarkPersistenceInput,
} from './etl/benchmark-ingest';
-import { normalizePowerContractMetrics, scrubWithheldPowerMetrics } from './etl/benchmark-mapper';
+import {
+ extractPowerAudit,
+ extractPowerInvalidReasons,
+ normalizePowerContractMetrics,
+ scrubWithheldPowerMetrics,
+} from './etl/benchmark-mapper';
import { ingestEvalRow } from './etl/eval-ingest';
const sql = createAdminSql({
@@ -180,6 +185,8 @@ interface SupplementalBmk {
is_multinode?: boolean;
prefill_num_workers?: number;
decode_num_workers?: number;
+ power_invalid_reasons?: unknown;
+ power_audit?: unknown;
}
async function ingestSupplementalBmk(
@@ -271,6 +278,14 @@ async function ingestSupplementalBmk(
// then strip withheld measurements when power_valid=0.
normalizePowerContractMetrics(entry.metrics, entry.metrics);
scrubWithheldPowerMetrics(entry.metrics);
+ // Accept the supplemental format's metrics nesting, then remove the
+ // structured fields so persisted metrics remain numeric-only.
+ const powerInvalidReasons = extractPowerInvalidReasons(
+ entry.power_invalid_reasons ?? entry.metrics.power_invalid_reasons,
+ );
+ const powerAudit = extractPowerAudit(entry.power_audit ?? entry.metrics.power_audit);
+ delete entry.metrics.power_invalid_reasons;
+ delete entry.metrics.power_audit;
rows.push({
configId,
@@ -282,6 +297,8 @@ async function ingestSupplementalBmk(
image: entry.image,
recipeFingerprint: null,
metrics: entry.metrics,
+ powerInvalidReasons,
+ powerAudit,
});
}
diff --git a/packages/db/src/queries/benchmarks.test.ts b/packages/db/src/queries/benchmarks.test.ts
index bdebe5b82..0a4d1ead3 100644
--- a/packages/db/src/queries/benchmarks.test.ts
+++ b/packages/db/src/queries/benchmarks.test.ts
@@ -113,3 +113,43 @@ describe('append-only benchmark snapshots', () => {
expect(values).toEqual([['dsv4']]);
});
});
+
+describe('power audit provenance reads (tolerant to a not-yet-applied migration 014)', () => {
+ const TOLERANT_BR = [
+ "to_jsonb(br) -> 'power_invalid_reasons' AS power_invalid_reasons",
+ "to_jsonb(br) -> 'power_audit' AS power_audit",
+ ];
+
+ it('selects both columns via to_jsonb on the exact-run path', async () => {
+ const captured = captureSql();
+ await getBenchmarksForRun(captured.sql, 'dsv4', 123456);
+ const { text } = captured.query();
+ for (const piece of TOLERANT_BR) expect(text).toContain(piece);
+ expect(text).not.toMatch(/\b(?:br|lb)\.power_(?:invalid_reasons|audit)\b/u);
+ });
+
+ it('selects both columns via to_jsonb on the dated latest path', async () => {
+ const captured = captureSql();
+ await getLatestBenchmarks(captured.sql, 'dsv4', '2026-08-01');
+ const { text } = captured.query();
+ for (const piece of TOLERANT_BR) expect(text).toContain(piece);
+ expect(text).not.toMatch(/\b(?:br|lb)\.power_(?:invalid_reasons|audit)\b/u);
+ });
+
+ it('selects both columns via to_jsonb on the history path', async () => {
+ const captured = captureSql();
+ await getAllBenchmarksForHistory(captured.sql, 'dsv4', 8192, 1024);
+ const { text } = captured.query();
+ for (const piece of TOLERANT_BR) expect(text).toContain(piece);
+ expect(text).not.toMatch(/\b(?:br|lb)\.power_(?:invalid_reasons|audit)\b/u);
+ });
+
+ it('selects both columns via to_jsonb on the no-date matview path', async () => {
+ const captured = captureSql();
+ await getLatestBenchmarks(captured.sql, 'dsv4');
+ const { text } = captured.query();
+ expect(text).toContain("to_jsonb(lb) -> 'power_invalid_reasons' AS power_invalid_reasons");
+ expect(text).toContain("to_jsonb(lb) -> 'power_audit' AS power_audit");
+ expect(text).not.toMatch(/\b(?:br|lb)\.power_(?:invalid_reasons|audit)\b/u);
+ });
+});
diff --git a/packages/db/src/queries/benchmarks.ts b/packages/db/src/queries/benchmarks.ts
index 25aead1f6..20c39a614 100644
--- a/packages/db/src/queries/benchmarks.ts
+++ b/packages/db/src/queries/benchmarks.ts
@@ -1,6 +1,6 @@
import type { DbClient } from '../connection.js';
-import type { WorkerPower } from '../etl/benchmark-mapper.js';
-export type { WorkerPower } from '../etl/benchmark-mapper.js';
+import type { PowerAudit, WorkerPower } from '../etl/benchmark-mapper.js';
+export type { PowerAudit, WorkerPower } from '../etl/benchmark-mapper.js';
/**
* One entry in `BenchmarkRow.workers` — mirrors the runner's aggregate_power.py
@@ -47,6 +47,10 @@ export interface BenchmarkRow {
* aggregate_power.py's multinode patch — surfaced as undefined here.
*/
workers?: BenchmarkWorkerRow[];
+ /** Producer reason codes for withheld power; null/undefined on other rows. */
+ power_invalid_reasons?: string[] | null;
+ /** Narrowed measurement-window audit; null/undefined on legacy rows. */
+ power_audit?: PowerAudit | null;
date: string;
/** Producer identity and timestamp; preserved for per-point provenance. */
workflow_run_id?: number;
@@ -256,6 +260,11 @@ function executeRecursiveBenchmarkQuery(
br.recipe_fingerprint,
${plan.metricsExpression},
br.workers,
+ -- A bare br.power_* reference fails during query planning until the next
+ -- ingest applies migration 014. The jsonb lookup returns NULL before the
+ -- migration and the stored value afterward, making deploy order safe.
+ to_jsonb(br) -> 'power_invalid_reasons' AS power_invalid_reasons,
+ to_jsonb(br) -> 'power_audit' AS power_audit,
br.date::text,
br.workflow_run_id,
wr.run_started_at::text,
@@ -451,6 +460,10 @@ export async function getLatestBenchmarks(
lb.recipe_fingerprint,
lb.metrics,
lb.workers,
+ -- latest_benchmarks lacks these fields until migration 014 recreates it;
+ -- the jsonb lookup keeps reads safe during that deploy window.
+ to_jsonb(lb) -> 'power_invalid_reasons' AS power_invalid_reasons,
+ to_jsonb(lb) -> 'power_audit' AS power_audit,
lb.date::text,
lb.workflow_run_id,
wr.run_started_at::text,