Run metrics
Every finished run writes one flat metrics point into a Workers Analytics Engine dataset, beside the one commit its record already makes (decision 0063). The bot computes the point (pointOf in the node-free src/core/runMetrics.ts) and sends it beside the record on POST /runs/put and POST /runs/finish; the state Worker's RunHistoryDO — the only writer of finished records — decides whether that write is the run's terminal close on an unlatched run (pointEmits, record 0097) and writes the point to the dataset AFTER its transaction, so a run is counted at most once however many times its record is written (the tombstone, a restarting close, the finish, a review-artifact rewrite, a retry), and an unclaimed restart is closed by the store's own sweep. The binding is optional on the Worker and the whole path is advisory: without it, without a point, or with a sink that throws, every answer is byte-identical. The record itself is untouched — the point travels beside it, never on it (run-history.md items 27, 33 and 44 name the three writes this rule governs). The price table is costs.md's costs.prices. On the read side, metrics trend answers the trend report on every command surface from the dataset over the Analytics Engine SQL API — three weighted queries per read, no snapshot — behind the metrics capability (items 7–9), while the review tabs render the distinct metrics reviews report through the protected review adapter. The retained generic trend adapter remains a separate compatibility contract (item 10); the index selects the review adapter for /metrics, /metrics/health and /metrics.json.
- Code:
scripts/web-preview.ts;web/green/pages/SystemHealth.vue,web/green/lib/reviews.ts,src/channels/webSeed.ts,scripts/web-review-fixture.ts,scripts/web-green-fixtures.ts;web/green/lib/navigation.ts;web/green/pages/Metrics.vue(outcome count tooltip and tile qualifiers);src/core/reviewFeedbackReport.ts,web/src/components/metrics/ReviewFeedback.vue,src/core/reviewFeedback.ts,src/core/reviewFeedbackWorker.ts,src/execution/githubReviewFeedback.ts,deploy/cloudflare-memory/reviewFeedbackStorage.ts,src/core/intakeMetrics.ts,src/core/runMetrics.ts(the point contract:RunMetricsPoint,POINT_COLUMNS,pointOf,dominantFriction,isRunMetricsPoint,isTerminalClose,pointEmits,outcomeOf,surfaceOf,workspaceWordOf,REVIEW_POINT_COLUMNS,reviewPointOf,isReviewMetricsPoint),src/core/runStoreWorker.tsandsrc/core/runLedgerWorker.ts(the clients that compute and send it;metricsDatasetWarning),src/core/runStore.ts(BuildRunStoreDeps.prices),deploy/cloudflare-memory/runMetricsSink.ts(the sink seam),deploy/cloudflare-memory/worker.ts(Env.RUN_METRICS?,parseRunPut,upsertInTransaction,closeUnclaimedRestarts,metricsHealth,featuresOf),src/deploy/profile.ts(metrics.dataset),deploy/cloudflare-memory/wrangler.template.jsonc(the{{#if metrics}}block),src/core/metrics.ts(the reader:parseMetricsConfig,metricsQueries, the sources,buildMetricsReport),src/core/metricsService.ts(MetricsService,NullMetricsService,METRICS_OFF_MESSAGE),src/core/reviewsReport.tsandsrc/core/reviewsService.ts(the review report, item 12),src/core/commands/metrics.ts(metrics.trend),src/core/capabilities.ts(themetricsaxis),src/channels/metricsView.ts(parseMetricsRoute,createMetricsViewHandler— serves the shared web shell with aMetricsSeed, see live-view.md's Rendering paragraph),src/index.ts(gates/metrics*behind Access next to/costs*; the startup line names the dataset or the reason the reader is off); the page rendering inweb/:web/src/components/metrics/TrendLineChart.vue,web/src/components/costs/CostChart.vue(reused for the stacked day series),web/src/lib/metrics.ts(the view model: tiles, chart geometry, the footer's sentences) - Tests:
web/green/frontend.test.ts;web/green/lib/reviews.test.ts;src/core/reviewsReport.test.ts,src/core/reviewsService.test.ts,src/core/reviewsComparison.test.ts;web/green/pages/metrics.test.ts;src/core/commands/metricsFeedback.test.ts,src/core/reviewFeedbackReport.test.ts,src/core/reviewFeedback.test.ts,src/core/reviewFeedbackWorker.test.ts,src/execution/githubReviewFeedback.test.ts,deploy/cloudflare-memory/reviewFeedback.test.ts,src/core/intakeMetrics.test.ts,deploy/cloudflare-memory/runLedger.test.ts,src/core/runMetrics.test.ts,deploy/cloudflare-memory/runs.test.ts(inside workerd, against the real object),src/core/runStoreWorker.test.ts,src/core/runLedgerWorker.test.ts,src/core/runStore.test.ts,src/deploy/profile.test.ts,src/deploy/wranglerTemplate.test.ts,src/deploy/plan.test.ts,src/core/metrics.test.ts,src/core/metricsService.test.ts,src/core/commands/metrics.test.ts,src/channels/metricsView.test.ts(routing + seed),web/src/lib/metrics.test.ts(generic trend helpers) - Docs: run-history.md (the record, the store and the three writes), costs.md (the price table the point's dollars come from), decision 0063 (why a second store, and why this shape).
Behavior
The point.
RunMetricsPoint = { indexes: [string]; blobs: string[]; doubles: number[] }— the platform's own positional shape.POINT_COLUMNSis the ONE ordered table naming every position: twenty blobs (schema— the literal"1"—agent,preset,model,status,error type,dominant friction,channel,repository,machine class,route class,reply,lineage,requester,identity,run id, and, appended by record 0097 so no position changes meaning,surface,workspace,setup reason,outcome) and twenty doubles (wall=finishedAt − (receivedAt ?? startedAt); the seven partition terms ofdiagnosis.shape, zero when absent;turns; the four token counts summed overusage.byModel;dollars=llmUsdOfUsage(usage, prices).usd;steps;tool calls;events=eventCount;unpriced tokens=llmUsdOfUsage(...).unpricedTokens;minutes— the profile's, zero when absent;finished at), the index the agent name (unknownfor a record naming none). It is typed so a position added without a name fails to compile, andblobColumn/doubleColumnresolve a name to itsblob<n>/double<n>— a reader's query never carries a magic number.pointOf(record, prices?)returnsundefinedfor a record withprovisional: trueand for nothing else; an absent optional field is the empty string (blobs) or zero (doubles);route classisroutedwhen the record carries aroute, elsechosen;replyisok/failed/noneoffreplyOk;lineageischildwhenparentRunIdis present, elseroot;dominant frictionis theFrictionCategorywith the largestdurationMsamong those with a count (dominantFriction), empty when none has one.outcomeanderror typeareoutcomeOf(record): the terminal status decides first, then the first matching row of record 0097's table —cancellationfor a stop;errorwithrestart_unclaimedorinterruptedfor an interrupted run; for a failed runfailurewithpolicy_refusal, elseerrorwith the failure kind, the setup result's lost word,reply_failed, elsetimeoutwith a budget ending, elseerrorwithunclassified; for a completed runtimeoutwith a budget ending,errorwithanswer_interrupted,cancellationafter a stop, elsesuccess. Error type is empty exactly forsuccessandcancellation.surfaceis the channel's namespace when it isslack,mcp,web,httporcli(surfaceOf), else empty;workspacefolds the setup result into one word (workspaceWordOf:resident,resident_restored,sandbox_snapshot,sandbox_fresh,sandbox_seed_failed,sandbox_cold,sandbox_blank,localore2bfor a run on that backend,nonefor a lost word, empty without a setup result);setup reasonis the setup result'sreasonor empty, so a cancellation keeps it. Every blob and the index are capped at 96 bytes, andpointOfnever readsactivity,label,events,verdict,handoff,dispositionsordiagnosis.verdict— no free text reaches the dataset.isRunMetricsPointaccepts exactly one index, sixteen or twenty strings (RUN_POINT_BLOB_WIDTHS: this item's width and the width record 0097 widens the row to, accepted together so a deploy's old and new writers both land) each at most 96 bytes, twenty finite numbers, and nothing else; the Worker validates an incoming point with it and answers400by name (point must be a RunMetricsPoint) for anything else.The emission rule: the terminal close (record 0097, amending 0063's first non-provisional close). A close is terminal when it is neither
provisionalnorrestarting(isTerminalClose).pointEmits(latched, stored)is true exactly whenstoredis a terminal close and the run's emission latch is unset — there is deliberately nofinishedAt > startedAtterm (a plainputof aninterruptedrecord over no row is a run counted once). The latch is therunsrow'smetrics_emittedcolumn, set in the same transaction as the write that emits and never cleared by a later write; rows a store kept before the column existed start latched unless they are provisional, since 0063's rule had already counted them.upsertInTransactionreads the row'sprovisionalandrestartingalone fromsummary_json(never the whole summary) and returnsemitsbesidestored,retainedandrewritten.putandfinishcallsink.write(point)AFTER their transaction — thesendRunFinishedplacement — whenemits && storedand a point was sent; arestartingclose, a review-artifact rewrite (run-history.md item 44) and an identical retry write no point, and a record the write itself trimmed (stored: false) writes none. Arestartingclose's point is kept on its row (restart_point_json) for the sweep in item 3a.The guard: a provisional record never overwrites a non-provisional row, and a
restartingrecord never overwrites a terminal one.upsertInTransactionanswers either as{ ok, stored: true, retained, rewritten: false, emits: false }and writes NOTHING — the row, its events and the sessions table stay exactly as they were. This is the store-side floor under run-history.md item 27's in-process stand-down (which is unchanged): without it, a tombstone or drain write landing after a close would replace the run's end, or erase arestartingrow's deadline and kept point so the sweep could never count it, and arestartingclose a refused ledger finish falls back toputwith, landing after the successor's finish, would replace the run's terminal close. 3a. An unclaimed restart is closed by the store. The store's alarm (closeUnclaimedRestarts) finds each unlatchedrestartingrow whoserestartUntilhas passed while nolive_runsrow holds its id, and in one transaction per row — re-checking the same deadline and the absent live row — rewrites it as aninterruptedterminal close withfailure.kind: restart_unclaimed,restartingandrestartUntilremoved, latched. After the commit it writes the point therestartingclose carried, withstatusinterruptedandfailure kindrestart_unclaimed; the store never prices a record itself. A successor still holding the id keeps the sweep a no-op, and its own terminal close emits the run's only point. 3b. The review row and the store's failed writes (record 0097, "Rows and columns"). A review run's terminal close with a verdict also writes a review row (reviewPointOf), under the run row's emission decision and latch: indexreview, its own schema wordreview-1(so 0063's reader, filtering on"1", never counts it), eight blobs (schema,run id,repository,pull request number,head revision,verdict,requester,surface) and seven doubles (finished at,duration,findings,blocking,major,minor,nit) named inREVIEW_POINT_COLUMNS. The target is the run record's own —repo,targetPr(the repository context's pull request, resolved before the model starts),reviewHeadfalling back toheadSha— neverreviewPost.target; a branch review with no pull request writes an empty number. The verdict readsapprovedorchanges_requested. The store computes the row from the record it committed — its clamped stamps, never the input's — after the commit (it needs no price), so an older client's runs get one too. Every row the sink refuses is counted by kind (run,review) inmetrics_write_failures, andPOST /runs/metrics-health {storeKey}answers{ failedWrites: { run, review } }: delivery stays at most once and best effort, and a drop after an accepted write is not detectable here.The wire.
POST /runs/putandPOST /runs/finishaccept an optionalpointbesiderecord(parseRunPut); a body without one is exactly today's, and the answers of both routes are byte-identical with and without the binding, with and without a point, and when the sink's write throws (emitsstays internal — the put route strips it).WorkerRunStore.putandWorkerRunLedger.finishcomputepointOf(record, prices)and send it when defined;buildRunStoreandbuildRunLedgertakeprices?: ModelPriceTablein their deps andsrc/index.tspasses the same tableRunsServiceprices with.InMemoryRunStoreandFileRunStoreare unchanged — they send no point.The off state and the sink.
RunMetricsSink(write(point): void) has two implementations:AnalyticsEngineSinkover the optionalEnv.RUN_METRICSbinding andNullSink, selected at the object's construction — theSHIP_COORDINATOR?shape: the Worker deploys without the binding and nothing changes. The write sits in atrywhose catch is ONEconsole.warnline carrying the run id and the error's constructor name, never the point's contents; it can fail no put and no finish.GET /healthz'sfeaturesgainsrunMetrics:<dataset>when the binding is present (featuresOf), the name from theRUN_METRICS_DATASETvar rendered beside the binding, so the bot's boot probe can compare names.The binding in the deploy, and the name held on both sides. The deployment profile's optional
metrics: { dataset }(release-and-deploy.md item 14; Analytics Engine's own dataset-name rule — 1–64 letters, digits and underscores, not starting with a digit — refused by field) is what the state Worker template's{{#if metrics}}block rendersanalytics_engine_datasets: [{ binding: "RUN_METRICS", dataset }]and theRUN_METRICS_DATASETvar from, the{{#if artifacts}}shape: a profile without one renders today's file byte for byte, and the committed example names none.deploy plannames the binding in the memory step when the profile names a dataset; no resource is created ahead of the deploy — the platform creates a dataset on first write — and the step needs no extra credential capability. On the other side, the bot's boot probe of the state Worker (the same/healthzread that checksruns) compares its configuredmetrics.dataset(themetrics:config block; onlydatasetis read until the reader lands) with the Worker'srunMetrics:<dataset>feature (metricsDatasetWarning) and logs ONE[runs]warning when the two differ or when only one side names one — saying which side is missing what — and nothing when both agree or both are absent; neither side refuses to start.The reader's config and capability.
metrics: { dataset, days? }is a config block, validated at load byparseMetricsConfigand refused by field:datasetunder the platform's dataset-name rule (item 6's),daysa whole number in 1..90 (default 30). Themetricscapability is on exactly when the block names a dataset AND thecostsblock with its Cloudflare analytics token are present — the reader queries the SQL API on the same read-only credential the spend dashboard holds, so no new secret exists (capabilities.md item 1). The three installation fixtures carry the axis (cloud-fullon, the others off).The source and the three queries.
MetricsSourceisquery(sql) → Promise<MetricsRow[]>with three implementations:AnalyticsEngineSqlSourcePOSTs the text to the account'sanalytics_engine/sqlendpoint with the bearer in the header ONLY andFORMAT JSONappended, parsesdata, and turns a non-2xx into aMetricsSourceErrorcarrying the status and never the token;InMemoryMetricsSource(points)evaluates the same texts over held points (each stamped at itsfinished atdouble, weight 1);NullMetricsSourceis the off state.metricsQueries({ sinceMs, untilMs, agent? }, dataset)returns three texts —byDayStatus(day, status,sum(_sample_interval)),byAgent(runs,sumIf(_sample_interval, … = 'failed'),quantileExactWeighted(0.5/0.95)of the wall, dollars, unpriced tokens and turns assum(double × _sample_interval)),byDayAgentP50— each overtimestamp >= toDateTime(<since>) AND timestamp < toDateTime(<until>); every aggregate is_sample_interval-weighted (no query counts rows or averages a raw column) and every column position resolves fromPOINT_COLUMNS. The agent filter is the ONLY user string a query can carry: validated against the agent-name rule before interpolation and quoted; the range is two integers.The report and the command.
buildMetricsReport(rows, range)is pure: the tiles (runs, failed, failure rate, p50/p95 wall — the runs-weighted mean of the per-agent quantiles — dollars, unpriced tokens, turns),byDayzero-filled for every UTC day in range with a count per status,byAgentlargest first,p50ByDayAgent, and arangenamingbucket: "write time",retentionDays: 90,pricing: "at finish",completeness: "at most once"; an empty result is a zero-filled report, never an error.metrics.trendis a registry command (actionmetrics:read— thecosts.bygrant shape: a browser session's baseline, a grant for a Slack user or a token, never a chat baseline — effect read,enabledWhen: caps.metrics), options--days1..90 and--agent; derived formsmetrics trendin chat and on the CLI,GET /api/metrics.trend, themetrics_trendMCP tool; the JSON is the report exactly, and the text render is a header (range, agent filter, dataset), the tiles as one·-joined bullet, one line per agent row largest first, then the footer sentence (bucket, pricing, completeness). Off isunavailablewithMETRICS_OFF_MESSAGE; a source error isunavailablenaming the error's class.MetricsService.report({ days?, agent? })reads whole UTC days ending today (today included, still filling), the default range from config;NullMetricsServiceis the off state the catalogue binds without the capability.The review dashboard and retained trend adapter. The current browser routes are
GET /metrics(Usage),GET /metrics/health(System health),GET /metrics/week(Week over week),GET /metrics.jsonandGET /metrics/week.json, selected bycreateReviewsMetricsViewHandlerinsrc/channels/metricsView.ts. They are GET-only and use the same protected current-actor and global Mine/Person/Everyone view as other work pages.fromandtoare whole UTC finish days, both included, at most 90 days; malformed dates or an inverted range return400before reading. Every tab and JSON link preserves the exact range. ThereviewsMetricsseed and JSON twin carry the same scopedReviewsReport(item 12); the Week over week seed and/metrics/week.jsonadd itscomparison, read with the same scope; filtering happens before aggregation, and current proof is checked again before output. Known access loss returns403; a provider/source failure or an off reader returns503without private details or global totals.Usage renders review-run counts, successful-duration populations, daily surface counts, requester/surface durations, the histogram, verdicts and findings. System health renders recorded ratios with good/total denominators, outcomes, workspace/setup/error facts and supplied duration populations. It does not infer uptime. Week over week renders the comparison's two windows, six tiles with the earlier value and change, a side-by-side table (counts and run time change in percent, ratios in percentage points, durations as a signed difference, no data when either side has no population), runs per day beside the same weekday one shift earlier, and runs per requester; an earlier window outside retention is said, never compared with zero. Sampling, legacy/unclassified outcomes, missing populations and unavailable diagnostics stay explicit. No model-price totals or averaged per-agent quantiles are presented as review metrics. These pages use the shared charts and theme; the primary sidebar offers Metrics only when its capability is on. Captures cover the three tabs on desktop and phone in both themes.
The previous all-agent trend presentation, agent selector and 7/30/90 range pills are retired. The generic
metrics.trendcommand, global service math, shared trend chart/formatting helpers andcreateMetricsViewHandlerremain supported compatibility contracts, with their owndays/agentvalidation andMetricsSeed/JSON parity. They are not the selected browser review dashboard.Intake telemetry. An opted-in
POST /runs/intakeemits oneintake-1point to the existingRUN_METRICSbinding after a measured receipt is inserted. Both addressed and silent decisions are emitted; receipt replay, legacy callers and unmeasured receipts emit nothing. Sink failures cannot change the committed decision. New clients opt in withtelemetry:trueonly after their run-metrics reader isolates schema1, so the memory-first rollout cannot contaminate an older bot's completed-run reports. Points carry experiment, arm, model, coordinates, verdict, failure source, latency, usage, estimated cost and Jev probabilities; no prompt, message text or model-written reason. Probe rows are marked separately. Analytics Engine retains telemetry independently of the receipt cleanup policy. The intake report uses sampling weights for event counts, sums and percentiles, names distinct threads as observed when sampling can omit them, and returns unknown cost as null. Optional samples are bounded; explicit thread exclusions keep operator tests separate from ordinary traffic.The review report (record 0097, "The reader and the page";
src/core/reviewsReport.ts,src/core/reviewsService.ts).reviewsQueries(range, dataset)builds the eight query texts for a half-open range of at most 90 days over the review agent's run rows (schema = "1", indexreview) and the review rows (review-1): runs by finish day, outcome, surface and requester with the time spent and the largest sample interval; weighted p50/p95 duration of successes by requester, by surface and overall; a whole-minute duration histogram with an open last bucket; review rows by repository, pull request and verdict with findings by severity; runs by finish day, workspace, setup reason and error type; and p50 duration and time to ready by workspace. Every count issum(_sample_interval)and every quantilequantileExactWeighted, and every function a query calls is one Analytics Engine's SQL reference documents (the histogram's open bucket is anif); every aggregate filters and groups on the row's ownfinished at, and the scan is bounded by write time a day wider on each side.buildReviewsReportis pure over their rows: ausagesection (runs, successes, time spent, durations, by day and surface, by requester, by surface, the histogram, pull requests, reviews per pull request — absent when any row was sampled — verdicts and findings) and asystemsection (four tiles as good over total — run success rate without cancellations and unrecorded outcomes, warm-start share over runs that asked for a resident and got a workspace (a local or E2B run whose setup reason is a fallback word included), lost at setup, reviews with a verdict — the unclassified and unrecorded counts, outcomes, workspaces by day, setup reasons by class, error types, and the per-workspace durations). Setup reasons are classed bysetupReasonClass: every lost word is a failure, record 0097's design words are design, every other word a defect.createReviewsServiceruns the eight reads in parallel and reuses run aggregates for five minutes while its range includes today (REVIEWS_CACHE_OPEN_MS) and a day once it ended, holds at most 32 ranges, and never reuses a failed read. Enabled human feedback uses the existing retained feedback store and report builder on every read, including closed ranges. A feedback-store failure marks only feedback unavailable. Retained feedback has no requester facts, so Mine, Person and other narrower predicates receive unavailable feedback; unscoped and exact Everyone reports retain feedback under the current host authorization;NullReviewsServicerefuses withMETRICS_OFF_MESSAGE.metrics.reviews(actionmetrics:read, hidden whencaps.metricsis off) answers the report on every surface —metrics reviewsin chat and on the CLI,GET /api/metrics.reviews, themetrics_reviewsMCP tool — for--fromand--to, whole UTC days with both included (default the seven days ending today, at most 90); a day the calendar does not have, a range that ends before it starts or one that runs past 90 days isinvalid_inputbefore any read, the off state and a failing source areunavailable, and its text render is a header, a usage bullet, the four tiles as good of total, one line per requester and the footer, chat-safe. With--comparethe answer addscomparison(reviewsComparisonOf,src/core/reviewsComparison.ts): the range beside the same range whole weeks earlier (shiftDays, seven for a range of one to seven days), so weekdays line up. A range that includes today is cut in both windows at the latest five-minute mark (REVIEWS_COMPARE_CUT_MS), so a partial day is compared with the same partial day. Both windows are ordinary reports from the same service and cache, without dataset or feedback. An earlier window that starts more than 90 days back isunavailable: "before_retention"and is not read. The text render adds one week-over-week line; a rate whose tile has no eligible runs readsno data, never a measured0%.Exact human review feedback.
metrics.feedback.enabledopts the bot into a minute refresh loop over the existing durable run-history Worker.excludedActorIdsnames automation User node IDs andexcludedPullRequestsnames disposableowner/repository#numberacceptance cohorts. The native review invitation asks whether the review helped decide what to change. The original run's matching pending-to-accepted transaction retains a compact receipt, including missing optional native identity as a tracking gap, independently of ordinary run retention. Only accepted invitation-bearing publications belong to this cohort; old receipts and uncertain writes do not. The retained receipt is the index and repair source, unique per repository/native review (record 0107).The feedback seam is
src/core/reviewFeedback.ts, with the in-memory implementation andsrc/core/reviewFeedbackWorker.ts;deploy/cloudflare-memory/reviewFeedbackStorage.tsowns SQLite retention, leases and snapshots through the Worker's authenticated/runs/review-feedback/{list,claim,complete,cooldown}routes.src/execution/githubReviewFeedback.tsreads GitHub using the bot credential, never a model tool. It verifies the exact review ID, repository, PR, head and body, then fetches every reaction page before replacing the current votes. Partial errors, changed body, inaccessible nodes and pagination failures preserve the previous complete snapshot with an unavailable/changed status. Shared rate-limit cooldown and fenced, expiring collection leases survive restart. Recent publications refresh hourly for seven days; older retained publications refresh daily. Each pass and request is bounded bysrc/core/budgets.ts. Page loads never read GitHub. Reporting clamps to 90 days; physical cleanup obeys the existing run-history maintenance gate. Required acceptance-journal failure keeps the existing uncertain/no-repost behavior; optional reaction collection cannot change publication or approval.Human usefulness on the existing metrics report.
src/core/reviewFeedbackReport.tsbuilds exact counts over accepted publications in the same half-open UTC range as the operational report. Only complete snapshots within the report's explicit freshness limits enter usefulness ratios. Each review counts once: positive-only, negative-only or mixed when effective votes exist; otherwise ambiguous-only, unknown-only or unrated. A human using both signs on a review is ambiguous and supplies no effective vote. Bot and configured automation reactions are excluded; missing actor identities remain unknown. Helpful share among clear ratings is positive-only / (positive-only + negative-only), with mixed reviews alongside it. Vote positivity is secondary. Rating coverage is rated reviews / complete fresh reviews; native-link and complete-sync coverage use all eligible tracked publications, including missing identity and failed reads. No denominator yieldsnull, never a zero quality score. Last-known ratings on stale, inaccessible or edited reviews stay separate. Configured acceptance PRs are excluded before any count.The same optional
feedbackobject ridesMetricsService.report, the Access-gated/metricspage and/metrics.json, andmetrics trendon command surfaces. Page loads read the durable store, never GitHub. Feedback-store failure marks only this section unavailable, leaving operational metrics usable. An agent filter other thanreviewmakes feedback not applicable. Weekly groups use Monday-start publication weeks in UTC and format version; later votes/removals revise those groups. Repository and findings-bearing/clean groups reuse the same count rules. The detail list shows at most 100 latest reviews while totals cover the complete range; each available link targets the native review body. The page uses Pacific time for refresh/publication display, labels the cohort start and freshness, shows sample counts without quality targets, and exposes gaps rather than treating them as no votes.web/src/components/metrics/ReviewFeedback.vuerenders the stored report under the existing range controls.Feedback investigation through MCP.
metrics.feedbackis a read-only command under the existingmetrics:readgrant and metrics capability, exposed asmetrics_feedbackon MCP andmetrics feedbackon other command surfaces. It reads exact retained feedback directly, without Analytics Engine queries, a GitHub write or a model run. List queries use the full publication cohort rather than the dashboard's latest-100 detail limit.daysselects whole UTC publication days;repo/prselect a target, andrating/stateselect evidence. Positive/negative filters include mixed and last-known effective votes;state: completeselects current data. Bots, configured automation actors and ambiguous pairs do not count as effective votes. The summary uses the repo/PR/day scope;matchingand the page apply rating/state filters. Results use publication order, bounded pages andnextCursor; a cursor that no longer belongs to the same mutable query refuses explicitly rather than silently skipping reviews.runretrieves one retained publication and its current/last-known raw reaction snapshot over the full 90-day retention. It is separate from list filters. Exact detail includes native review identity and URL, original review run ID, reviewed head and body hash, read/completion stamps, reaction IDs, actor IDs/types and signs, plus the automation actor IDs excluded from counts. Missing/expired/excluded publications returndetail: null, not an empty complete reaction snapshot. Thumb reactions contain no person's explanation; the evidence limit is stated. An investigator can read the native review/PR and askruns_getfor the original run when available, then send a normal authorized Switchboard investigation request with those exact references. Reading feedback does not start that investigation or authorize a fix; repository/run access and dispatch authorization remain their existing gates. Collection must be enabled and the durable store accessible; unavailable data refuses by name. Unexpected read failures emit the report's bounded, redacted server warning while callers receive a generic unavailable response; invalid cursors remain input errors.
Validation criteria
| Criterion | Proof |
|---|---|
| 15: the public command finds negative feedback beyond the dashboard limit, returns exact retained reaction evidence by run ID, supports target filters and ordered pagination, marks last-known evidence, and refuses invalid or unavailable queries | [unit] src/core/commands/metricsFeedback.test.ts::metrics.feedback::* |
| 15: MCP advertises the new read tool and returns exact native identity only to the metrics-read caller; other command surfaces share parsing, output and authorization | [unit] src/core/commandConformance.test.ts::command conformance — catalogue fences::MCP exposes exact review feedback by run ID only after the metrics read grant; src/core/commandConformance.test.ts::command conformance — metrics.feedback::* |
15 live: the advertised metrics_feedback tool lists an accepted reaction-bearing review, exact lookup returns its snapshot and native identity, and a read-only investigation follows those references | [gap] Requires the containing bot build, enabled collection and a retained publication; local command/adapters prove the contract only. |
| 14: every ratio uses the complete fresh subset; review-level counts, per-review/actor ambiguity, unknown/bot exclusions, old daily freshness, mutable weekly cohorts, acceptance exclusions and bounded details retain exact totals | [unit] src/core/reviewFeedbackReport.test.ts::human review feedback report::* |
| 14: the shared service reads stored feedback, isolates its failure, logs a bounded/redacted server-side reason without returning it, keeps an empty cohort's scores null and respects the selected agent | [unit] src/core/metricsService.test.ts::createMetricsService::serves exact stored feedback on the shared report and isolates its failures from operational metrics |
| 14: commands preserve the feedback JSON object and distinguish unrated from unavailable feedback in text | [unit] src/core/commands/metrics.test.ts::metrics.trend::carries exact feedback into JSON and distinguishes an unrated cohort from unavailable feedback in text |
| 14: the active review page renders the shared human-feedback component with exact ratings and native links, distinguishes an empty cohort from unavailable feedback, and retains review usage tiles | [unit] web/green/pages/metrics.test.ts::Green review metrics::shows current human feedback, empty ratings and unavailable feedback beside review usage |
| 14: each review report reads current retained votes even when its closed-range run queries are cached; Mine and Person cannot receive global feedback totals, and feedback failure can recover without invalidating healthy run aggregates | [unit] src/core/reviewsService.test.ts::createReviewsService::refreshes exact retained feedback while reusing the closed range's run queries; src/core/reviewsService.test.ts::createReviewsService::keeps feedback storage failure separate from a healthy report and recovers without cached failure |
| 14 live: a disposable review-body vote added and removed changes the same dashboard/JSON cohort once at the deployed build; a real person's first rating confirms ingestion | [gap] Requires the configured App, enabled collection and excluded disposable PR at the deployed build. Local fixtures and CI do not prove live ingestion or human usefulness. |
| 13: only the original owner's matching pending-to-accepted transition retains one compact cohort row; native identity is optional, uncertain writes excluded, removed votes replace only complete reads, stale collection owners refused and shared backoff survives the next pass | [unit] src/core/reviewFeedback.test.ts::exact review feedback storage::* |
| 13: authenticated Worker routes retain feedback beyond ordinary run deletion, fence late collection owners, preserve previous votes on failure, and refuse malformed input | [integration] deploy/cloudflare-memory/reviewFeedback.test.ts::review feedback through the state Worker::* |
| 13: the durable client reads all pages and refuses malformed or looping responses | [unit] src/core/reviewFeedbackWorker.test.ts::durable feedback client::* |
| 13: the restart fixture checkpoints the feedback store independently of the run ledger, retaining its own data and constructing a fresh clock rather than serializing functions | [unit] src/channels/slackGoldenFlow.test.ts::Slack golden flow::* |
| 13: the native reader verifies review/repository/PR/head/body, bounds token acquisition within the read deadline, cancels an opted-in mint, preserves unknown actors, ignores other emoji, refuses partial reads and honors primary, secondary and HTTP-200 GraphQL rate limits | [unit] src/execution/githubReviewFeedback.test.ts::native review-body reactions::*; src/execution/githubApp.test.ts::resolveGithubToken::cancels an opted-in mint request and can mint again after cancellation |
| 13 live: the configured App reads an actual review's reactions; adding/removing a disposable account vote, restart and refresh change the retained snapshot once, without another publication | [gap] Requires the additive state Worker, enabled collection, configured App credential and disposable PR at the deployed build. |
| 10: the review page retains successful-run duration qualifiers and marks sampled rows, unproved surface quantiles and unavailable PR distributions explicitly | [unit] web/green/pages/metrics.test.ts::Green review metrics::shows review counts and successful duration samples without model cost claims; web/green/pages/metrics.test.ts::Green review metrics::keeps sampled PR distribution and unproved surface quantiles unavailable |
| 10: Review tiles identify recorded counts and successful-duration populations without inventing model prices; shared generic trend and USD formatters remain separate | [unit] web/green/pages/metrics.test.ts::Green review metrics::shows review counts and successful duration samples without model cost claims, web/src/lib/metrics.test.ts::the tile and cell words::* |
| 11: intake points omit free text, include either verdict and full coordinates; the report isolates schema and experiment, bounds the decision window, excludes probes by default and weights counts and quantiles; sampled thread counts and unpriced costs are explicit | [unit] src/core/intakeMetrics.test.ts::intake telemetry::* |
| 11: a committed receipt emits at most one point, legacy callers and duplicate writes emit nothing, and a throwing sink cannot change receipt persistence; completed-run queries exclude the intake schema | [integration] deploy/cloudflare-memory/runLedger.test.ts::run ledger — intake receipts (item 59)::emits telemetry after one committed receipt, never on replay or legacy writes, and contains sink failures; [unit] src/core/metrics.test.ts::metricsQueries — the three texts, pinned::excludes intake telemetry from completed-run aggregates |
1: a final record fills every blob and double in POINT_COLUMNS order, the agent as the index; a ship child with usage.turns 14 and a price table yields double14 equal to llmUsdOfUsage of the same inputs and blob13 = "child" | [unit] src/core/runMetrics.test.ts::pointOf — every position by its POINT_COLUMNS name::fills the twenty blobs and twenty doubles…, ::a ship child with usage.turns 14… |
1: a provisional record yields undefined; a final finishedAt === startedAt record yields a zero wall; an absent optional field is the empty string or zero | [unit] src/core/runMetrics.test.ts::pointOf — every position by its POINT_COLUMNS name::a provisional record yields undefined… |
1: route present yields blob11 = "routed", absent "chosen"; a failed run carries its status and failure kind | [unit] src/core/runMetrics.test.ts::pointOf — every position by its POINT_COLUMNS name::a route on the record yields blob11…, ::a failed run carries its status and failure kind… |
1: no free text reaches a blob — a sentinel in activity, label, diagnosis.verdict and an event body appears in no blob; every blob and the index are capped at 96 bytes | [unit] src/core/runMetrics.test.ts::pointOf — every position by its POINT_COLUMNS name::free text never reaches a blob…, ::caps every blob and the index at 96 bytes |
1: an unpriced model's tokens land in unpriced tokens and dollars counts only the priced ones | [unit] src/core/runMetrics.test.ts::pointOf — every position by its POINT_COLUMNS name::an unpriced model's tokens land in unpriced tokens… |
1: dominantFriction picks slow_tool { count 3, durationMs 9000 } over failed_tool { count 5, durationMs 800 }; all zero counts yield "" | [unit] src/core/runMetrics.test.ts::dominantFriction::picks the category with the largest durationMs… |
1: isRunMetricsPoint rejects seventeen blobs, a 97-byte blob, a NaN double, a second index; the Worker answers a malformed point 400 point must be a RunMetricsPoint on put and finish, before any write | [unit] src/core/runMetrics.test.ts::isRunMetricsPoint::accepts what pointOf builds and rejects…, deploy/cloudflare-memory/runs.test.ts::run metrics — the point, the guard and the emission rule::a malformed point is refused by name on put and finish… |
1: isRunMetricsPoint accepts sixteen and twenty blobs and refuses fifteen, seventeen, nineteen and twenty-one; the Worker stores and writes a twenty-column point beside a sixteen-column one | [unit] src/core/runMetrics.test.ts::isRunMetricsPoint::accepts the sixteen-column row and the twenty-column row…, deploy/cloudflare-memory/runs.test.ts::run metrics — the point, the guard and the emission rule::a twenty-column point is stored and written beside a sixteen-column one… |
1: outcomeOf decides every status and ending combination record 0097's table lists, and leaves the error type empty exactly for success and cancellation | [unit] src/core/runMetrics.test.ts::outcomeOf — the terminal status decides first, then the first matching row::* |
1: surfaceOf reads the five namespaces and nothing else; workspaceWordOf names every setup shape, and a local or E2B backend by its own word | [unit] src/core/runMetrics.test.ts::surfaceOf::reads the channel's namespace, and nothing for one it does not know, src/core/runMetrics.test.ts::workspaceWordOf — the setup result folded into one word::names each shape the record's table lists, src/core/runMetrics.test.ts::workspaceWordOf — the setup result folded into one word::names a local or E2B backend for every shape it ran, never a Cloudflare sandbox, and keeps a lost workspace none |
1: the four appended columns land at blob17–20 with the error type still blob6; a cancellation keeps its setup reason; the sweep's relabel sets error type and outcome | [unit] src/core/runMetrics.test.ts::pointOf — the four columns record 0097 appends::*, deploy/cloudflare-memory/runs.test.ts::run metrics — the point, the guard and the emission rule::the terminal close (record 0097)::the sweep closes an unclaimed restart past its deadline as interrupted with restart_unclaimed, writing the run's one point |
| 2: a close is terminal when neither provisional nor restarting; it emits exactly on an unlatched run | [unit] src/core/runMetrics.test.ts::the terminal close and the emission latch (record 0097)::a close is terminal when it is neither provisional nor restarting, src/core/runMetrics.test.ts::the terminal close and the emission latch (record 0097)::emits exactly when a terminal close lands on a run whose latch is unset |
2: inside workerd — a restarting close writes no point and the successor's terminal close under the same id writes the run's only one; rows kept before the latch count as emitted unless provisional | [unit] deploy/cloudflare-memory/runs.test.ts::run metrics — the point, the guard and the emission rule::the terminal close (record 0097)::a restarting close writes no point; the successor's terminal close under the same id writes the run's only one, deploy/cloudflare-memory/runs.test.ts::run metrics — the point, the guard and the emission rule::the terminal close (record 0097)::rows a store wrote before the latch existed count as emitted unless provisional, so a later terminal rewrite writes no second point |
2: inside workerd — a tombstone then the finish writes ONE point; a review-artifact rewrite answers rewritten: true, emits: false and an identical retry writes none more | [unit] deploy/cloudflare-memory/runs.test.ts::run metrics — the point, the guard and the emission rule::a tombstone then the finish writes one point… |
2: a plain put of an interrupted record over no row writes one point; a record the write itself trimmed (stored: false) writes none | [unit] deploy/cloudflare-memory/runs.test.ts::run metrics — the point, the guard and the emission rule::a plain put of an interrupted record over no row writes one point… |
3: a provisional put over a final row answers stored: true, rewritten: false, emits: false, writes nothing to the row, its events or the sessions table, and a following get returns the final record unchanged | [unit] deploy/cloudflare-memory/runs.test.ts::run metrics — the point, the guard and the emission rule::a provisional put over a final row writes nothing… |
3: a late restarting write over a terminal row stores nothing and writes no point | [unit] deploy/cloudflare-memory/runs.test.ts::run metrics — the point, the guard and the emission rule::the terminal close (record 0097)::a late restarting write never replaces a terminal row, and writes no point |
3: a late provisional write over a restarting row stores nothing, so its deadline and kept point survive and the sweep still closes it | [unit] deploy/cloudflare-memory/runs.test.ts::run metrics — the point, the guard and the emission rule::the terminal close (record 0097)::a late tombstone over a restarting row writes nothing, so the sweep still closes the orphaned restart |
3a: the sweep closes an unclaimed restart past its deadline as interrupted with restart_unclaimed and writes its one point; it leaves a restart before its deadline and one a successor's live row holds | [unit] deploy/cloudflare-memory/runs.test.ts::run metrics — the point, the guard and the emission rule::the terminal close (record 0097)::the sweep closes an unclaimed restart past its deadline as interrupted with restart_unclaimed, writing the run's one point, deploy/cloudflare-memory/runs.test.ts::run metrics — the point, the guard and the emission rule::the terminal close (record 0097)::the sweep leaves a restart alone before its deadline, and while a successor's live row holds the id |
| 3b: a review run's terminal close writes its review row beside its run row, once; the row is written without a client point and not without a verdict | [unit] deploy/cloudflare-memory/runs.test.ts::run metrics — the point, the guard and the emission rule::the review row (record 0097)::a review run's terminal close writes its run row and its review row; a later write writes neither again, deploy/cloudflare-memory/runs.test.ts::run metrics — the point, the guard and the emission rule::the review row (record 0097)::a review row is written even when the client sent no run point, and not for a run without a verdict |
3b: the review row's columns come from the run record's target, never the post; an approval reads approved; a branch review writes an empty number; no row for a non-review, a verdict-less or a non-terminal close | [unit] src/core/runMetrics.test.ts::reviewPointOf — the review row::* |
| 3b: the review row is built from the committed record, so a finish dated past the skew limit is written at its clamped time and duration | [unit] deploy/cloudflare-memory/runs.test.ts::run metrics — the point, the guard and the emission rule::the review row (record 0097)::the review row is the committed record's: a finish dated past the skew limit is written at the clamped time |
3b: a throwing sink is counted by row kind and served on /runs/metrics-health | [unit] deploy/cloudflare-memory/runs.test.ts::run metrics — the point, the guard and the emission rule::the review row (record 0097)::a throwing sink is counted by row kind and served on /runs/metrics-health |
4: WorkerRunStore.put sends pointOf(record, prices) beside the record and none for a provisional one; WorkerRunLedger.finish does the same; buildRunStore carries deps.prices onto the client | [unit] src/core/runStoreWorker.test.ts::WorkerRunStore::put sends the record's metrics point beside it…, src/core/runLedgerWorker.test.ts::WorkerRunLedger::finish posts the record's metrics point beside it…, src/core/runStore.test.ts::…::worker with its bearer → WorkerRunStore… |
4, 5: the JSON answers of put and finish are deep-equal across a recording sink, a throwing sink and a Worker without the binding; the throw is one console.warn with the run id and the error's constructor name | [unit] deploy/cloudflare-memory/runs.test.ts::run metrics — the point, the guard and the emission rule::a throwing sink leaves the JSON answers deep-equal… |
5: features carries runMetrics:<dataset> exactly when the binding is present | [unit] deploy/cloudflare-memory/runs.test.ts::run metrics — the point, the guard and the emission rule::features names the dataset only when the binding is present |
| 12: the query texts weight every count and quantile, bound the scan by write time a day wider each side, filter on finish time, read run rows of the review agent and review rows by their own schema word, and refuse a bad dataset, an inverted range or one over 90 days | [unit] src/core/reviewsReport.test.ts::reviewsQueries::* |
| 12: every query calls only functions Analytics Engine's SQL reference documents | [unit] src/core/reviewsReport.test.ts::reviewsQueries — only documented SQL functions::calls no function outside Analytics Engine's SQL reference |
| 12: setup reasons class as failure, design or defect | [unit] src/core/reviewsReport.test.ts::setupReasonClass::names every lost word a failure, the record's design words design, and every other fallback word a defect |
| 12: the report zero-fills days, sums usage, counts reviews per pull request without branch reviews, states each tile as good over total (a local or E2B stand-in for a resident in the warm-start total, a cold one not), classes setup reasons, and suppresses reviews per pull request when sampled | [unit] src/core/reviewsReport.test.ts::buildReviewsReport::* |
| 12: eight reads per report; a range including today is reused five minutes and an ended one a day; a failed read is never reused and a bad range reads nothing; the cache is bounded; the off state refuses by name | [unit] src/core/reviewsService.test.ts::createReviewsService::*, src/core/reviewsService.test.ts::NullReviewsService::refuses with the metrics-off sentence |
12: metrics reviews asks for whole UTC days with --to included and defaults to the seven ending today; refuses a day the calendar does not have, a backwards or an over-90-day range before reading; off and a failing source are unavailable; renders the range, usage, the four tiles and the requesters chat-safe | [unit] src/core/commands/metrics.test.ts::metrics.reviews::* |
12: --compare shifts by whole weeks, cuts a range that includes today at the same five-minute mark in both windows, compares a closed range whole, keeps a non-empty window after midnight, and names an earlier window outside retention without reading it | [unit] src/core/reviewsComparison.test.ts::reviewsComparisonRangesOf::*, src/core/reviewsComparison.test.ts::reviewsComparisonOf::*, src/core/commands/metrics.test.ts::metrics.reviews::--compare adds the same range a week earlier, read through the same service, and one text line, src/core/commands/metrics.test.ts::metrics.reviews::--compare says no data for a rate with no eligible runs on either side, and keeps a measured 0% |
10: Week over week reads the scoped report and both comparison windows, its seed and /metrics/week.json agree exactly, and the other tabs carry no comparison | [unit] src/channels/metricsView.test.ts::protected review metrics HTTP::serves Week over week as the scoped report and a comparison a week earlier, with exact JSON parity |
| 10: Week over week renders only the seed's comparison (windows, tiles, side by side, days, requesters) and says when the earlier range is out of retention | [unit] web/green/pages/metrics.test.ts::Green review metrics::compares the range with a week earlier: tiles, side by side, days and requesters, all from the seed, web/green/pages/metrics.test.ts::Green review metrics::says the earlier range is out of retention instead of comparing with zero, and links the third tab |
12 live: the eight texts run against the production dataset, including toDateTime(intDiv(…)) over the finish double, and a 90-day report's uncached latency is recorded | [gap] needs the reader's metrics:read grant live (infrastructure PR 195); the first receipt on the tracker |
6: metrics.dataset is optional in the profile, kept when valid, refused by field when it breaks the dataset-name rule; the committed example names none | [unit] src/deploy/profile.test.ts::the profile's image mode::\metrics.dataset` is optional, kept when valid, and refused by field when it breaks the dataset-name rule` |
6: the state Worker template renders the binding and the RUN_METRICS_DATASET var exactly when the profile names a dataset; without one the render equals today's file byte for byte | [unit] src/deploy/wranglerTemplate.test.ts::templateView / renderTemplate for a bot-only profile::the committed state Worker template binds the run-metrics dataset (analytics_engine_datasets + RUN_METRICS_DATASET) exactly when the profile names one |
6: deploy plan names the RUN_METRICS binding in the memory step exactly when the profile names a dataset | [unit] src/deploy/plan.test.ts::planDeploy::the memory step names the RUN_METRICS binding exactly when the profile names a metrics dataset |
| 6: the boot probe warns once on a name mismatch and on a one-sided name — naming both sides — and says nothing when both agree or both are absent | [unit] src/core/runStoreWorker.test.ts::metricsDatasetWarning — the name held on both sides::* |
7: parseMetricsConfig keeps a well-formed block (30-day default), refuses a bad dataset name or days by field, and validateConfig refuses the block at load | [unit] src/core/metrics.test.ts::parseMetricsConfig::*, src/config.test.ts::metrics config::loads a well-formed block and refuses a bad dataset name or days by field |
7: the metrics capability is on exactly when the block names a dataset and the costs block's credential is in the env; the three installation fixtures carry the axis | [unit] src/core/capabilities.test.ts::capabilitiesFrom — every axis, on and off::metrics: a metrics block naming the dataset AND the costs block with its token…, src/core/capabilitySurfaces.test.ts::capability fixtures — real configurations::* |
8: the three query texts are pinned — each _sample_interval-weighted, columns resolved from POINT_COLUMNS (double1, double9, double14, blob5, index1), the range two integers; the agent filter is validated against the agent-name rule and quoted | [unit] src/core/metrics.test.ts::metricsQueries — the three texts, pinned::* |
8: AnalyticsEngineSqlSource posts the text with the bearer and FORMAT JSON; a 403 is a MetricsSourceError carrying the status and never the token | [unit] src/core/metrics.test.ts::AnalyticsEngineSqlSource::* |
9: buildMetricsReport over rows from the in-memory source equals the report over the same points computed by hand; a 7-day range over points on three of the days yields seven byDay rows with four zero rows; an agent filter leaves other agents' points out of every table; an empty window is a zero-filled report | [unit] src/core/metrics.test.ts::buildMetricsReport over the in-memory source::* |
9: the service reads whole UTC days ending today, the default range from config, --days/--agent through to the queries, 1..90 held; the Null Object answers the off message | [unit] src/core/metricsService.test.ts::createMetricsService::*, src/core/metricsService.test.ts::NullMetricsService::answers the off message… |
9: metrics trend on every surface with the catalogue's guarantees — metrics:read held by a browser session's baseline and by the grant, hidden without the metrics capability; off is unavailable with the off message, a source error unavailable naming its class; the render heads with the range, filter and dataset, one bullet of tiles (a failed share of 2 in 20 renders as 10%), one line per agent largest first, the footer sentence | [unit] src/core/commandConformance.test.ts::command conformance — metrics.trend::*, src/core/authz/policy.test.ts::POLICY coverage::*, src/core/commands/metrics.test.ts::metrics.trend::* |
10: the retained generic trend adapter matches /metrics and /metrics.json (and nothing else), GET-only; the page carries the report as the seed and the twin answers it exactly; ?days/?agent pass through validated, a malformed one is 400 before any read | [unit] src/channels/metricsView.test.ts::parseMetricsRoute::matches the page and its JSON twin, and nothing else, src/channels/metricsView.test.ts::createMetricsViewHandler::* |
10: the retained generic trend adapter answers off as 503 with METRICS_OFF_MESSAGE on its page and twin; a failing source is 503 naming the error's class and never its detail | [unit] src/channels/metricsView.test.ts::createMetricsViewHandler::answers 503 with the off message when the reader is off — the Null service — on the page and the twin, src/channels/metricsView.test.ts::createMetricsViewHandler::answers 503 naming the error's class when a source fails, never its detail |
| 10: the generic trend helpers retain their weighted chart and provenance mapping; the review page uses the distinct review report below | [unit] web/src/lib/metrics.test.ts::runsChartModelOf — the stacked day series::*, web/src/lib/metrics.test.ts::the footer and the hrefs::* |
9, 10: shared generic trend helpers draw a gap where a day has no value, one line per agent in the table's order; the tile words are the command's (2 in 20 → 10%, seconds under two minutes) | [unit] web/src/lib/metrics.test.ts::trendChartModelOf — the day lines::*, web/src/lib/metrics.test.ts::the tile and cell words::* |
| 10: Metrics is offered only when its capability is on; in-app navigation loads its seed | [unit] web/src/components/AppNav.test.ts::Global shell navigation::removes unavailable report and schedule paths while keeping Home, run details, Settings, and docs, web/src/lib/seedRouting.test.ts::installSeedRouting::an in-app navigation to /metrics resolves with its seed |
| Criterion | Proof |
|---|---|
| Hovering a day in the replacement Metrics review chart shows the total and each surface as run counts, without currency formatting. Shared cost charts retain their USD formatter. | [unit] web/green/pages/metrics.test.ts::Green review metrics::shows hovered surface totals as counts and preserves the range on the health tab, web/src/pages/costs.test.ts::CostsPage::hovering a day shows a compact tooltip: the day and its total on top, then one row per component, largest first, the estimate tagged; leaving hides it |
Current-user page aggregates
The retained generic trend adapter and its JSON twin accept the current run-read predicate and global work view before aggregation. Mine selects current proved owner identities; Person keeps the selected person's read ceiling; Everyone retains the actual actor's current permission boundary. Query parameters cannot choose another owner. The existing generic metrics command keeps its unscoped behavior. No global fallback replaces an unavailable personal read, and shared system health remains unavailable to personal views when it cannot be attributed.
The SQL reader compiles this common predicate against stored requester, channel and repository fields. Missing visibility facts remain unknown. The in-memory reader receives the same predicate through the existing query seam, requires a matching canonical query, and applies the reference predicate evaluator before any sums or quantiles. A missing or mismatched scoped predicate fails closed. Point schema, immutable writers, outcome fields, model and pricing formats remain unchanged.
| Criterion | Proof |
|---|---|
| Mine, read-only Person, authorized Everyone and a denied reader return literal distinct-owner aggregate totals on both page and JSON; forged owner/all query knobs cannot retarget the view. | [unit] src/channels/metricsView.test.ts::current work metrics projection::filters Mine, read-only Person and authorized Everyone before aggregation on the page and JSON twin |
| Unavailable scoped source reads return 503 without global totals or provider details. | [unit] src/channels/metricsView.test.ts::current work metrics projection::does not substitute global totals when scoped source reads are unavailable |
| Stored owner/repository scope filters before aggregation, missing or mismatched scope refuses, and unscoped queries preserve their existing totals. | [unit] src/core/metrics.test.ts::scoped metrics aggregation::filters stored owner and repository facts before aggregating and refuses missing or mismatched scope |
| An empty conjunction remains denied in both SQL and the stored-point evaluator. | [unit] src/core/metrics.test.ts::scoped metrics aggregation::keeps an empty conjunction denied in both SQL and stored-point aggregation |
Scoped review report reader
Browser review reports receive the same current run-read predicate and global data view as other work pages. Every one of the existing eight queries applies that predicate before aggregation. Run and review rows use their own stored requester and repository columns. Scoped repository comparisons are case-insensitive; an empty stored repository is absent, including in exclusion predicates. The legacy global query and default run-metrics translator stay unchanged; a constraint whose fact is absent from the table denies that branch. No point schema or permission rule changes. A scoped service cache includes the actual caller, dataset, range and compiled predicate. Missing scoped proof refuses rather than using the legacy global default; existing unscoped CLI and MCP report calls retain that default. Weighted successful surface counts accompany quantiles so a known lack of samples differs from a measured zero duration; old rows without a population remain unknown.
| Criterion | Evidence |
|---|---|
| Every aggregate uses the table's exact requester/repository columns; absent facts deny, supported OR branches remain usable and scope strings cannot alter the SQL | [unit] src/core/reviewsReport.test.ts::scoped reviewsQueries::*; src/core/metrics.test.ts::table-specific metrics read predicate::* |
| Public service reads produce literal separate Mine, Person and Everyone totals over SQLite-filtered stored rows, preserve cache separation across actual callers, datasets and permission changes, exclude missing or case-variant forbidden repositories, allow case-variant authorized repositories, and refuse missing scope | [unit] src/core/reviewsService.test.ts::scoped review reports::keeps literal Mine, Person and Everyone totals separate across caller, dataset and permission changes |
| Failed scoped reads are evicted and can recover without a global fallback | [unit] src/core/reviewsService.test.ts::scoped review reports::evicts scoped failed reads without poisoning the same caller's recovery |
| Surface duration populations distinguish no successful sample from a measured zero | [unit] src/core/reviewsReport.test.ts::review duration population::* |
Review report tabs
Usage and System health render one protected reviewsMetrics seed. They do not combine the generic metrics.trend report with review data. The host must supply the selected current-user scope before aggregation; the page cannot choose another requester. A missing report has no numeric tiles. These frontend proofs use fictional reports, not live Analytics Engine reads.
| Criterion | Proof |
|---|---|
| Usage renders recorded review counts, successful-duration populations, verdicts and findings without model-price claims; histogram uses the recorded open bucket. | [unit] web/green/pages/metrics.test.ts::Green review metrics::shows review counts and successful duration samples without model cost claims |
| Both tabs retain the exact UTC finish range and JSON address; ninety days render ninety buckets without network reads, with at-most-once/best-effort provenance. | [unit] web/green/pages/metrics.test.ts::Green review metrics::renders a ninety-day report without fetching and keeps finish-time provenance, web/green/lib/reviews.test.ts::Review report display::keeps the exact finish range on each tab and preserves supplied daily totals |
| Sampled PR distribution is unavailable when omitted; missing successful-sample counts remain unknown. Zero duration with a proved population remains valid. | [unit] web/green/pages/metrics.test.ts::Green review metrics::keeps sampled PR distribution and unproved surface quantiles unavailable, web/green/lib/reviews.test.ts::Review report display::keeps missing populations unknown, empty populations separate, and zero duration valid |
| Health shows report ratios with good/total, unclassified and legacy outcomes, and no inferred uptime, write-failure counts or rollout date. Missing/mismatched reports remain separate from successful empty reports. | [unit] web/green/pages/metrics.test.ts::Green review metrics::renders health ratios with denominators, legacy outcomes and unavailable diagnostics, web/green/pages/metrics.test.ts::Green review metrics::distinguishes a successful empty report from missing and mismatched seeds |
| Requester names are display-only and the page contains no local owner selector or mutation controls. | [unit] web/green/pages/metrics.test.ts::Green review metrics::renders only the supplied scoped requester rows with protected display names and no owner selector |
| Actual index wiring, native proof provider, Analytics Engine reads and final theme/mobile captures are qualified on the composed host. | [gap] page rendering and injected HTTP reader proofs are local; live composition and captures remain separate integration work |
Protected review HTTP adapter
The review handler is additive to the retained generic trend adapter. It reads only the authenticated host's actual actor and attached global work view. A fresh native actor/view check precedes aggregation and response. URL knobs never choose a requester or cache subject. Both tabs share the report-only JSON twin. Person requests stay data-only.
| Criterion | Proof |
|---|---|
| Usage, System health and JSON return literal distinct-owner Mine/Person/authorized Everyone totals with exact report parity; forged owner/all parameters do not retarget the view and non-GET requests are refused. | [unit] src/channels/metricsView.test.ts::protected review metrics HTTP::routes the three tabs and their JSON twins without claiming unrelated addresses, src/channels/metricsView.test.ts::protected review metrics HTTP::renders Mine, read-only Person and authorized Everyone with exact JSON parity and ignores forged owner knobs |
| Missing/unknown actual subjects, absent attached scope, denied readers, unauthorized Everyone and missing or mismatched native proofs refuse before querying. | [unit] src/channels/metricsView.test.ts::protected review metrics HTTP::refuses missing, unknown, unscoped, denied and unauthorized Everyone callers before reading |
| UTC date validation reuses the registry helper, includes the final day, and refuses invalid/duplicate/backward/over-90-day input before reading. | [unit] src/channels/metricsView.test.ts::protected review metrics HTTP::keeps UTC ranges exact and refuses invalid dates before any reader call |
| Final native proof/view loss withholds the completed report; off and provider failures produce unavailable responses with no provider details or global fallback. | [unit] src/channels/metricsView.test.ts::protected review metrics HTTP::withholds finished reports after scope loss or native proof failure and hides provider details |
| Display names include returned requester IDs only, and the JSON twin remains the exact report object. | [unit] src/channels/metricsView.test.ts::protected review metrics HTTP::adds names only for returned requesters while the JSON twin remains the exact report |
Production review dashboard wiring
The browser entry point serves Usage, System health and JSON through the same scoped review service. Each protected read refreshes the original credential through the existing browser context and organization read guard. Names are looked up for returned ids only. The generic trend command remains separate.
| Criterion | Proof |
|---|---|
| Usage and System health retain desktop and phone captures in both themes | [unit] src/docs/screenshotManifest.test.ts::expectedFiles::is one PNG per surface per theme, named <surface>-<theme>.png, src/docs/screenshotManifest.test.ts::capture profiles::preserves all existing desktop outputs and themes |
| Actual server wiring serves both tabs and JSON for the current native account, with current proof before and after the reader | [agent] On the final staging candidate, use an authenticated account to open both tabs and JSON, preserve the date range, verify current-user scope and then authorized selected-person/Everyone scopes. Confirm a lost or unavailable native proof returns no report. Local handler proofs do not establish this live callback. |
Local review preview parity
| Criterion | Proof |
|---|---|
| The marked local review preview's two pages and JSON twin carry the same fictional report. The preview transport intercepts the twin before the retained generic fixture. | [unit] web/green/frontend.test.ts::Green browser frontend::keeps the marked review preview page and its JSON twin identical |
| Snapshot evaluation receives one finite initial green fixture session frame; ordinary browser fixture SSE remains open and retains the same literal actor/messages. Production streams are unchanged. | [unit] web/green/frontend.test.ts::Green browser frontend::ends only snapshot preview event reads after their real initial session frame |