Skip to content

feat(frontend): coverage — what the product can see about a person, and about how many people - #2410

Merged
dzarlax merged 18 commits into
constructorfabric:mainfrom
dzarlax:feat/coverage-readout
Aug 11, 2026
Merged

feat(frontend): coverage — what the product can see about a person, and about how many people#2410
dzarlax merged 18 commits into
constructorfabric:mainfrom
dzarlax:feat/coverage-readout

Conversation

@dzarlax

@dzarlax dzarlax commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Phase 1 of #2408 — the question that comes before every value on every other screen: is there anything here to read? Frontend only; no service change is required for any of it.

What it adds

A coverage model. For one person in one period, each part of their work is in one of three states: it reads, nothing was recorded, or no data reaches us. Three rather than two, because a part with no value is either a source that never arrives or a person who did none of that work, and collapsing them loses the only distinction separating "connect this" from "nothing happened".

An Overview tab built from that model as one argument read top to bottom:

  1. how many people are seen in fewer than half of their work — the verdict, as a number rather than a sentence;
  2. which parts are missing, where a part nothing reaches says so in words instead of being drawn as a bar at zero;
  3. how thinly people are seen, with the boundary the verdict is defined by drawn where it falls, and the rows below it in the colour of the number that counts them.

A level opens into the people at it, each linking to their page, with their missing parts split by which kind of absence applies. That split is the point: one is somebody's job to fix, the other is a person who does that work elsewhere or does not do it, and no amount of plumbing changes it.

Two bugs it fixes

A person's page called a section unmeasured when the comparison pool was empty. The pool is whoever the viewer can see, so a viewer with a narrow reach was told a live connector is missing — and the narrower the reach, the more often. It is the same shape of error as a statistic drawn from a truncated pool, in the more damaging direction: it tells somebody their tracker is broken while their colleagues' numbers sit on the next screen. Availability is a property of the tenant, so it now comes from the tenant's definition listing. groupPeersHaveData is deleted rather than left for a future caller — it is the inference, and leaving it available leaves the bug available.

The tab carried two counts of one thing. The radar measured coverage over each group's card-preview keys off the zone grid; the per-person block measured it over every key of every group from its own fetch. They agreed on the data in front of me and had no reason to keep agreeing, and a reader who catches two numbers for one fact is right to stop trusting both. The by-part cut is now derived from the same per-person states as the distribution, with a test that their totals must match.

Things worth knowing for review

  • The visibility check on /v1/metric-results is all-or-nothing. One id outside the caller's visible set refuses the whole request rather than filtering it, and does not disclose which id was at fault. The id list is therefore the roster the portal was served, exactly — never widened, never guessed. There is a test on the request rather than the answer, because this failure is invisible in the returned shape.
  • Only the period view is requested. entityChunkSize will not chunk a collection carrying a timeseries, breakdown or histogram view, and an unchunked roster-sized request meets the projected-row limit. Asking for the one view this needs keeps the existing chunk-and-merge path available. A richer collection here would work at team scale and fail at organisation scale — the failure appears only for a reader with wide visibility, who is both the most important one and the last to be tested against.
  • This section is exempt from the never-observed gate. That gate reports "source isn't ingested for this org yet" when no metric of a lens was ever observed, which on the screen built to report which sources are not ingested would withhold the answer exactly when it is worth most.
  • The count is over the people the viewer can see, and says so. That compromise is affordable for a count and is not for a statistic: a quantile over a subset is a different quantity from the same quantile over the whole group, and biased (GRAPH 7: [EPIC] Cohorting is a GROUP BY over identity attributes #2028). A count over a subset stays true about that subset as long as its size travels with it, which is why counted is not optional.
  • No estimate is offered of who connecting a missing source would reveal. The people doing that work are invisible precisely because it is missing, so any such number would be invented; the return shape has nowhere to put one.

Deliberately not here

Checks

tsc -b clean, eslint clean, 843 unit tests pass. Verified live against a populated instance, including the level drill-down and the empty-level case.

Refs #2408, #2391, #2028

Summary by CodeRabbit

  • New Features

    • Added “What we can see” coverage views showing data availability by work area and person.
    • Added expandable person-level details with links to individual views.
    • Added explanations for unavailable data sources, empty coverage levels, and limited coverage.
    • Coverage summaries now include distributions, unreachable areas, and thin-coverage indicators.
    • Metric definitions now identify whether metrics are built-in or custom.
  • Improvements

    • Updated Overview navigation with a clearer label and refreshed icon.
    • Improved data availability handling when comparison groups are empty or metrics lack observations.

@dzarlax
dzarlax requested a review from a team as a code owner August 10, 2026 17:17
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@dzarlax, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 40 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a66c632f-ca9a-4021-981f-b0bd652e7a31

📥 Commits

Reviewing files that changed from the base of the PR and between 0add897 and a4db1ae.

📒 Files selected for processing (3)
  • src/frontend/src/lib/portal/use-person-sections.test.tsx
  • src/frontend/src/lib/portal/use-scope-coverage.test.tsx
  • src/frontend/src/lib/portal/use-scope-coverage.ts
📝 Walkthrough

Walkthrough

The PR replaces the coverage radar with coverage-level analysis. It adds metric reachability and scope aggregation, updates person-section semantics, and renders expandable coverage details. Portal labels and configuration now use “What we can see.” Bug-report image instructions now use manual attachment.

Changes

Coverage levels

Layer / File(s) Summary
Coverage classification and aggregation
src/frontend/src/api/metric-definitions-client.ts, src/frontend/src/lib/insight/coverage.ts, src/frontend/src/lib/insight/coverage.test.ts, src/frontend/src/api/metric-definitions-client.test.ts, src/frontend/src/queries/metric-definitions.test.ts, src/frontend/src/screens/metric-definitions.test.tsx
Metric definitions now identify builtin and custom origins. Coverage utilities classify reachable data, calculate person and part coverage, distributions, thin coverage, and unreachable parts.
Period-scoped coverage hook
src/frontend/src/lib/portal/use-scope-coverage.ts, src/frontend/src/lib/portal/use-scope-coverage.test.tsx
useScopeCoverage requests period-only data for selected members and returns aggregate coverage, pending, and error state.
Portal configuration and navigation
src/frontend/src/lib/portal/lens-configs.ts, src/frontend/src/lib/portal/overview-configs.ts, src/frontend/src/lib/portal/nav-model.ts, src/frontend/src/components/portal/context-pane.test.tsx, src/frontend/src/lib/portal/lens-configs.test.ts, src/frontend/src/components/portal/metric-groups-view.test.tsx
The health section changes from coverage-radar to coverage-levels and uses the “What we can see” label and icon.
Coverage-level portal rendering
src/frontend/src/components/portal/domain-lens-view.tsx, src/frontend/src/components/portal/domain-lens-view.test.tsx
The radar visualization is replaced with coverage levels, expandable person lists, connector explanations, and personal-view links.
Person section reachability
src/frontend/src/lib/portal/use-person-sections.ts, src/frontend/src/lib/portal/use-person-sections.test.tsx, src/frontend/src/lib/insight/group-data.ts
peersHaveData now reflects tenant-wide metric reachability. The comparison-pool helper is removed.

Bug insight documentation

Layer / File(s) Summary
Issue image workflow
.claude/skills/file-bug-insight/SKILL.md
The skill instructs users to attach PNG images manually after issue creation instead of using API upload commands.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DomainLensView
  participant useScopeCoverage
  participant MetricDefinitions
  participant MetricCollections
  participant CoverageUtilities
  DomainLensView->>useScopeCoverage: selected member IDs
  useScopeCoverage->>MetricDefinitions: load definitions
  useScopeCoverage->>MetricCollections: request period-only collections
  useScopeCoverage->>CoverageUtilities: compute coverage results
  CoverageUtilities-->>useScopeCoverage: person and aggregate coverage
  useScopeCoverage-->>DomainLensView: render coverage levels
Loading

Possibly related PRs

Suggested reviewers: cyberantonz

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 68.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the frontend coverage reporting for individual people and population-level coverage.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Alexey Panfilov added 9 commits August 10, 2026 19:18
The judgement before every other one: is there anything here to read?
Three states, not two — a part with no value is either a source that never
reaches us or a person who did none of that work, and collapsing them loses
the only distinction separating "connect this" from "nothing happened".

Reachability comes from the tenant-wide definition listing (last_observed_date,
schema_status, is_enabled), never from nobody in view having a value. A viewer
whose visible set is small may see no user of a system that is connected and
busy elsewhere, and would report a live connector as missing — more often the
smaller their reach. That is the same shape of error as a statistic drawn from
a truncated pool, and it is avoided rather than mitigated: none of the three
judgements here is a population statistic.

coverageDistribution reports how many people it counted. Not decoration: the
same distribution is a true statement about the people counted and a false one
about the organisation, and that number is the whole difference.

unreachableParts offers no estimate of who connecting a source would reveal.
The people doing that work are invisible precisely because it is missing, so
any such number would be invented; the return shape has nowhere to put one.

Refs constructorfabric#2408

Signed-off-by: Alexey Panfilov <alexey.panfilov@constructor.tech>
Both ways this can go wrong are in the request, not the answer, so both are
tested there.

The visibility check on the metrics endpoint is all-or-nothing: one id outside
the caller's visible set refuses the whole request rather than filtering it,
and does not say which id was at fault. The id list is therefore the tree
identity served, exactly — never widened, never guessed.

Only the period view is requested. entityChunkSize refuses to chunk a
collection carrying timeseries, breakdown or histogram views, and an unchunked
roster-sized request meets the backend's projected-row limit; asking for the
one view this needs keeps the existing chunk-and-merge path available at
roster scale.

Reachability rides the definition listing's existing query key rather than
issuing a second request. It is a different question from the availability
gate next to it: that one asks which metrics may be requested, this asks which
have ever answered, and the gap between them is exactly the nulls that must
read as "no data reaches us" rather than as an idle person.

Refs constructorfabric#2408

Signed-off-by: Alexey Panfilov <alexey.panfilov@constructor.tech>
The question that comes before every value on every other tab: is there
anything here to read? A distribution of people by how many parts of their
work reach us, and the parts nothing reaches at all.

Distinct from the Health radar next to it, which reports per domain what share
of people show activity there. That cannot tell whether the same people are
covered everywhere or different people each time — and it is that difference,
not the per-domain share, which decides whether anything compound may be built
on top. It also has two states where this has three: a domain reading zero is
either unconnected or idle, and the radar cannot say which.

The section fetches its own period-only collection rather than widening the
zone grid: coverage asks whether ANY metric of a group reads, so it needs every
group's keys, and adding those to the shared grid would make one tab pay for
all of them.

The counted-people line under the bars is load-bearing, not a caption. The same
distribution is a true statement about the people counted and a false one about
the organisation, and that line is the whole difference.

Placed after the value tabs in the nav on purpose: a reader reaches for it when
a number looks thin.

Refs constructorfabric#2408

Signed-off-by: Alexey Panfilov <alexey.panfilov@constructor.tech>
…the scale

Two halves of one question, so one tab. The radar answers it per domain: what
share of people show activity in each. The levels answer it per person: how
many domains we can see for each of them.

Neither implies the other, which is the reason to show both. Five domains at
60% is one situation if it is the same people every time and a very different
one if it is not — everyone thinly covered versus most people covered well and
some not at all. The radar renders those identically; only the per-person
distribution separates them, and which of the two is true decides whether
anything compound may be built on top.

The bars are unreadable without saying what the scale is, so the parts are
named in place and "4 of 5" is spelled out — including that it says nothing
about how well the person did in any of them. It is the first question anyone
asks and the answer is one sentence.

Also exempts a coverage lens from the never-observed gate. That gate shows
"source isn't ingested for this org yet" when no metric of the lens was ever
observed — which on the screen built to report which sources are not ingested
would withhold the answer exactly when it is worth most.

Refs constructorfabric#2408

Signed-off-by: Alexey Panfilov <alexey.panfilov@constructor.tech>
…cale

The section described a shape and left the reader to work out why they should
care. A distribution is not a finding: what a reader needs first is whether the
rest of the product can bear weight, and for whom it cannot.

So it now opens with how many people are seen in fewer than half of their work,
and what that means — every metric, comparison and flag about those people
rests on that fraction of what they do. The scale follows, since "4 of 5" only
needs explaining once there is a reason to read it.

"Fewer than half" rather than a tuned threshold: it needs no defending and
holds its meaning when the number of parts changes, which a fixed count would
not. With an odd number of parts the midpoint is not a level anyone can sit at,
so the boundary is unambiguous; with an even one, exactly half counts as
covered, since drawing it into the thin group would overstate the problem.

Refs constructorfabric#2408

Signed-off-by: Alexey Panfilov <alexey.panfilov@constructor.tech>
…people

The tab carried two counts of the same thing. The radar measured coverage over
each group's card-preview keys off the zone grid; the per-person block measured
it over every key of every group from its own fetch. They agreed on the data in
front of me and had no reason to keep agreeing, and a reader who catches two
numbers for one fact is right to stop trusting both. Now the by-part cut is
derived from the same per-person states as the distribution, with a test that
their totals must match.

Read top to bottom it is an argument rather than two charts: how many people we
cannot speak confidently about, then which parts are missing, then how thinly
people are seen. The verdict is a number rather than a sentence because it is
meant to be seen and not parsed, and the amber blocks in the last cut ARE the
number at the top — the shape reads without the labels.

A part nothing reaches is not drawn as a bar at zero. Zero says people did none
of that work, which is the one thing it does not mean, so it says so in words
instead. Same reason the radar could not carry this: an axis has one position
for two different facts.

The prose that used to explain the screen is gone. Two rounds of feedback said
the meaning did not land, and both times I added text; text was the problem.
What survives is one line of fine print, below everything, for the reader who
wants the rule rather than the finding.

Refs constructorfabric#2408

Signed-off-by: Alexey Panfilov <alexey.panfilov@constructor.tech>
The headline counts the rows below it and nothing said so. Asked where the
number came from, I could answer by adding three row values — which means the
screen was making the reader do that, and a reader who has to work it out is in
the same position as one who does not know it.

Two changes, no extra prose. The number takes the colour of the rows it sums,
so the eye makes the link unaided. And the rule that defines it — fewer than
half the parts — is drawn as a line where it actually falls in the list, with
the tally on it, so the boundary is visible rather than inferred from which
bars happen to be amber.

Refs constructorfabric#2408

Signed-off-by: Alexey Panfilov <alexey.panfilov@constructor.tech>
A level says how much of somebody we cannot see. What a reader does next is
find out who, so the rows open into the people at that level, each linking to
their page.

The names are not the point — the missing parts are, and they are split by
which kind of absence applies. "No connector" is somebody's job to fix.
"Nothing recorded" is a person who does that work elsewhere or does not do it,
and no amount of plumbing changes it. Those lead to different places, which is
why the three-state model exists at all; this is the first surface where the
difference does any work.

A level nobody is at cannot be opened, rather than opening onto nothing.

The component test needed the set form of the metric-results query and the
definition listing, which this file did not mock before — the coverage section
fetches its own period-only collection instead of riding the zone grid, and
reads reachability from the tenant listing.

Refs constructorfabric#2408

Signed-off-by: Alexey Panfilov <alexey.panfilov@constructor.tech>
…ant, not the pool

A person's page said "no data reaches us" for a section whose comparison pool
held no readings. The pool is whoever the viewer can see, so a viewer with a
narrow reach would be told a live connector is missing — and the narrower the
reach, the more often. It is the same shape of error as a statistic drawn from
a truncated pool, and it produced the more damaging direction: telling somebody
their tracker is broken while their colleagues' numbers sit on the next screen.

Availability is a property of the tenant, so it now comes from the tenant's
definition listing — the one place that holds it, and the same place the
org-wide coverage screen reads. Before this the two surfaces answered one
question two ways and were free to disagree about the same person on the same
day.

groupPeersHaveData is deleted rather than left for a future caller: it is the
inference, and leaving it available is leaving the bug available.

The old test asserted the pool rule, so it now asserts the listing rule, with a
companion for the case that used to be wrong — an empty pool no longer means
anything at all.

Refs constructorfabric#2408

Signed-off-by: Alexey Panfilov <alexey.panfilov@constructor.tech>
@dzarlax
dzarlax force-pushed the feat/coverage-readout branch from 8b582ad to 1765771 Compare August 10, 2026 17:18
dzarlax and others added 5 commits August 10, 2026 19:21
…about people

Review findings, and the two serious ones are the same mistake: this screen
guards carefully against a biased inference and did not guard at all against a
request that never answered.

A failed definition listing left the reachable set empty, so every part read
"no data reaches us" for every person and the headline announced that everyone
is thinly seen. A failed metrics chunk did the same by dropping entities. Both
printed at full confidence the moment the query settled. The hook now carries
isError and the section says it could not check, claiming nothing about anyone.

The person page had the pending half of the same hole: the definition listing's
loading state was never folded into the section's, so between the metrics
answering and the listing answering the page said nothing reaches us about any
part of that person, then flipped. The old pool inference read the query whose
pending state was already tracked, so this arrived with the new source.

Custom metrics were being reported as unreachable. The listing warns in as many
words that it stamps freshness from materialized relations only, so a custom
metric's last_observed_date stays absent however much data it serves — and the
frontend type did not carry `origin` at all, so the check could not be made.
Latent today since every compiled-in key is builtin, but latent inside the one
function whose whole job is not to fabricate absence.

Also: the hook returned a second, unused way of computing unreachability — in
the PR that removed the radar for computing coverage a second way. Deleted.
`unlinked` documented a protection no code path provided, with a test pinning
it; the parameter is gone rather than left describing something untrue. The
unreachable-part copy no longer names a connector as the cause, since a
disabled or schema-broken metric lands there too and sending someone to plumb a
live connector is the wrong direction to be wrong in. The amber headline gained
its light-theme pair, which its own comment had claimed it already had. The
disclosure rows gained aria-controls and the list an id. And the sentence about
the top of the scale is rewritten — a careful reader took it as a contradiction,
which is the only test that phrasing has.

Refs constructorfabric#2408

Signed-off-by: Alexey Panfilov <alexey.panfilov@constructor.tech>
Updating this branch from main pulled in three unrelated documentation commits
touching an agent-tooling skill file. They have nothing to do with coverage and
agent tooling does not belong in an upstream pull request, so the file is
restored to what main carries. The commits themselves are untouched wherever
else they live; only this branch's diff is narrowed back to the change under
review.

Signed-off-by: Alexey Panfilov <alexey.panfilov@constructor.tech>
@dzarlax
dzarlax enabled auto-merge August 11, 2026 08:22

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (5)
src/frontend/src/components/portal/domain-lens-view.tsx (2)

690-698: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

id is shadowed inside the row loop.

The component takes an id prop and applies it to the <ul> on Line 690. The loop on Line 694 binds a second id for the group key. The current code is correct, but the two meanings of id in one function invite a wrong reference during later edits.

♻️ Proposed rename
-        for (const [id, state] of p.states) {
-          const title = titleById.get(id) ?? id;
+        for (const [groupId, state] of p.states) {
+          const title = titleById.get(groupId) ?? groupId;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/src/components/portal/domain-lens-view.tsx` around lines 690 -
698, Rename the inner `id` binding in the `rows.map` state loop to a distinct
name such as `groupId`, and update its uses when retrieving `titleById` and
constructing the fallback title. Preserve the component `id` prop used by the
`<ul>` unchanged.

528-535: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

The error state offers no retry.

CoverageLevelsSection renders the failure with Pending, which is the loading component. The reader gets no way to retry. Every other failing section in this file passes a refetch callback, for example compRefetch in CompositionSection. useScopeCoverage exposes isError but no refetch, so no retry is possible today.

Expose a refetch from useScopeCoverage that fans out to the collection-set and definitions queries, then render the failure with the same error component the other sections use.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/src/components/portal/domain-lens-view.tsx` around lines 528 -
535, Update useScopeCoverage to expose a refetch function that triggers both the
collection-set and definitions queries, then update CoverageLevelsSection’s
isError branch to use the established error component with that refetch callback
instead of Pending. Match the retry behavior and error rendering used by
sections such as CompositionSection and compRefetch.
src/frontend/src/components/portal/domain-lens-view.test.tsx (2)

79-84: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The coverage error path has no test.

The useMetricDefinitionsResponse mock omits isError, and each collectionSet entry omits isError. The hook reads both fields, so both resolve to undefined and the error branch never runs. CoverageLevelsSection Line 533 renders "Could not read coverage — the check did not complete, so nothing is claimed about anyone." This branch is a stated safety property of the PR: it stops an infrastructure fault from reading as a verdict about named people. It is currently unverified at the component level.

Add isError to both mocks, then add a test that sets it and asserts the failure text.

Also applies to: 453-484

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/src/components/portal/domain-lens-view.test.tsx` around lines 79
- 84, Add isError fields to the useMetricDefinitionsResponse mock and each
collectionSet entry, then add a component test that sets the relevant error
state and asserts CoverageLevelsSection renders “Could not read coverage — the
check did not complete, so nothing is claimed about anyone.”

Source: Coding guidelines


486-513: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Derive the part count from GROUPS instead of hardcoding 5.

coverageWorld builds the fixture from GROUPS, but the assertions hardcode /1 of 5/ and /5 of 5/. The rendered label is {level} of {GROUPS.length}. If a group is added or removed, both tests fail with a "unable to find role button" message that does not name the cause.

🧪 Proposed change
-    await userEvent.click(screen.getByRole("button", { name: /1 of 5/ }));
+    const parts = GROUPS.length;
+    await userEvent.click(
+      screen.getByRole("button", { name: new RegExp(`1 of ${parts}`) }),
+    );
-    expect(screen.getByRole("button", { name: /5 of 5/ })).toBeDisabled();
+    expect(
+      screen.getByRole("button", {
+        name: new RegExp(`${GROUPS.length} of ${GROUPS.length}`),
+      }),
+    ).toBeDisabled();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/src/components/portal/domain-lens-view.test.tsx` around lines
486 - 513, Update the two button-label assertions in the coverage-level tests to
derive the total from GROUPS.length instead of hardcoding 5: use the existing
level values with GROUPS.length for both the clickable and disabled button
queries. Keep the current test behavior and coverageWorld setup unchanged.
src/frontend/src/lib/portal/use-scope-coverage.test.tsx (1)

99-106: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an isPending assertion for the empty scope.

This test verifies the request only. It does not assert the returned state. With an empty roster the hook returns isPending: true permanently, because data.get(def.id)?.isPending ?? true has no map entry to read. The consumer then renders a permanent loading label. An assertion here would fix the gap in the hook.

🧪 Proposed test addition
     expect(state.lastCall?.entity.ids).toEqual([]);
     expect(state.lastCall?.collections).toEqual([]);
+    const { result } = renderHook(() => useScopeCoverage(state.members));
+    expect(result.current.isPending).toBe(false);
+    expect(result.current.distribution.counted).toBe(0);
   });

As per coding guidelines: "Ensure new and changed lines achieve at least 80% test coverage."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/src/lib/portal/use-scope-coverage.test.tsx` around lines 99 -
106, Extend the empty-scope test around useScopeCoverage to capture the hook
result and assert that isPending is false once the scope has no members. Update
the hook’s pending-state logic to handle an empty roster without relying on a
missing data map entry, while preserving pending behavior for unresolved
non-empty scopes.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.claude/skills/file-bug-insight/SKILL.md:
- Line 222: Update the image-attachment guidance near the “Images — the honest
constraint” section to provide an executable manual step: for PNGs intended for
the issue body, tell the user to open Edit, drag them into the body editor, and
save; otherwise direct them to attach the PNGs in the issue comment box.

In `@src/frontend/src/lib/portal/use-person-sections.test.tsx`:
- Around line 17-28: Update the useMetricDefinitionsResponse mock in the test
setup to return mocks.isPending instead of a hardcoded false, then add a
regression test covering the definitions.isPending branch in usePersonSections.
Ensure the new test exercises the pending definition-query behavior and
maintains the required coverage for changed lines.

In `@src/frontend/src/lib/portal/use-scope-coverage.ts`:
- Around line 107-109: Update the useScopeCoverage hook so isPending is false
when rosterIds.length === 0, before evaluating group pending states. In
src/frontend/src/lib/portal/use-scope-coverage.ts#L107-L109, apply this change;
in src/frontend/src/lib/portal/use-scope-coverage.test.tsx#L99-L106, extend the
empty-roster test to assert isPending is false and distribution.counted is 0.

---

Nitpick comments:
In `@src/frontend/src/components/portal/domain-lens-view.test.tsx`:
- Around line 79-84: Add isError fields to the useMetricDefinitionsResponse mock
and each collectionSet entry, then add a component test that sets the relevant
error state and asserts CoverageLevelsSection renders “Could not read coverage —
the check did not complete, so nothing is claimed about anyone.”
- Around line 486-513: Update the two button-label assertions in the
coverage-level tests to derive the total from GROUPS.length instead of
hardcoding 5: use the existing level values with GROUPS.length for both the
clickable and disabled button queries. Keep the current test behavior and
coverageWorld setup unchanged.

In `@src/frontend/src/components/portal/domain-lens-view.tsx`:
- Around line 690-698: Rename the inner `id` binding in the `rows.map` state
loop to a distinct name such as `groupId`, and update its uses when retrieving
`titleById` and constructing the fallback title. Preserve the component `id`
prop used by the `<ul>` unchanged.
- Around line 528-535: Update useScopeCoverage to expose a refetch function that
triggers both the collection-set and definitions queries, then update
CoverageLevelsSection’s isError branch to use the established error component
with that refetch callback instead of Pending. Match the retry behavior and
error rendering used by sections such as CompositionSection and compRefetch.

In `@src/frontend/src/lib/portal/use-scope-coverage.test.tsx`:
- Around line 99-106: Extend the empty-scope test around useScopeCoverage to
capture the hook result and assert that isPending is false once the scope has no
members. Update the hook’s pending-state logic to handle an empty roster without
relying on a missing data map entry, while preserving pending behavior for
unresolved non-empty scopes.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9c6c2d36-8626-49e1-b6e7-5190316898e1

📥 Commits

Reviewing files that changed from the base of the PR and between 574a241 and 0add897.

📒 Files selected for processing (20)
  • .claude/skills/file-bug-insight/SKILL.md
  • src/frontend/src/api/metric-definitions-client.test.ts
  • src/frontend/src/api/metric-definitions-client.ts
  • src/frontend/src/components/portal/context-pane.test.tsx
  • src/frontend/src/components/portal/domain-lens-view.test.tsx
  • src/frontend/src/components/portal/domain-lens-view.tsx
  • src/frontend/src/components/portal/metric-groups-view.test.tsx
  • src/frontend/src/lib/insight/coverage.test.ts
  • src/frontend/src/lib/insight/coverage.ts
  • src/frontend/src/lib/insight/group-data.ts
  • src/frontend/src/lib/portal/lens-configs.test.ts
  • src/frontend/src/lib/portal/lens-configs.ts
  • src/frontend/src/lib/portal/nav-model.ts
  • src/frontend/src/lib/portal/overview-configs.ts
  • src/frontend/src/lib/portal/use-person-sections.test.tsx
  • src/frontend/src/lib/portal/use-person-sections.ts
  • src/frontend/src/lib/portal/use-scope-coverage.test.tsx
  • src/frontend/src/lib/portal/use-scope-coverage.ts
  • src/frontend/src/queries/metric-definitions.test.ts
  • src/frontend/src/screens/metric-definitions.test.tsx
💤 Files with no reviewable changes (2)
  • src/frontend/src/lib/portal/lens-configs.test.ts
  • src/frontend/src/lib/insight/group-data.ts

Comment thread .claude/skills/file-bug-insight/SKILL.md Outdated
Comment thread src/frontend/src/lib/portal/use-person-sections.test.tsx
Comment thread src/frontend/src/lib/portal/use-scope-coverage.ts Outdated
@dzarlax
dzarlax disabled auto-merge August 11, 2026 08:27
Alexey Panfilov added 3 commits August 11, 2026 10:30
Review findings.

With no members the hook sends no collections, so no group ever gets an entry
and `data.get(id)?.isPending ?? true` held every one of them pending for good.
The section sat on its loading label permanently instead of saying there is
nobody in this scope. Pending now only considers the groups when there is
somebody to fetch for.

The person-page test mocked the definition listing as never pending, so the
branch added for its loading window could not be exercised at all — the fix
shipped without a test that could fail if it regressed. The mock is
configurable now and a case covers it: while the listing loads, no section
claims anything.

Refs constructorfabric#2408

Signed-off-by: Alexey Panfilov <alexey.panfilov@constructor.tech>
Updating this branch from main left four files at an older revision than
upstream carries — an agent-tooling doc and three CI files. This restores them,
so the diff under review is the frontend change and nothing else.

An earlier commit on this branch explained the same restore by saying the
fork's main carries commits upstream does not. That was wrong: the fork's main
held no commit of its own, it was simply far behind, and merging it left the
branch with stale copies. The commits in question are upstream's own. The
action was right either way; the reasoning was not, and it is corrected here
rather than left in the history for a reader to trust.

Signed-off-by: Alexey Panfilov <alexey.panfilov@constructor.tech>
@dzarlax
dzarlax force-pushed the feat/coverage-readout branch from 78a13d0 to 79534c1 Compare August 11, 2026 08:38
@dzarlax
dzarlax enabled auto-merge August 11, 2026 08:55
@dzarlax
dzarlax added this pull request to the merge queue Aug 11, 2026
Merged via the queue into constructorfabric:main with commit 4d94815 Aug 11, 2026
53 checks passed
@dzarlax
dzarlax deleted the feat/coverage-readout branch August 11, 2026 08:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants