Skip to content

feat(matrix): business roles off the rows by default, with a wizard option to fold them back in (#370 + #937) - #933

Merged
WimvandenHeijkant merged 24 commits into
mainfrom
dor/issue-370
Sep 12, 2026
Merged

feat(matrix): business roles off the rows by default, with a wizard option to fold them back in (#370 + #937)#933
WimvandenHeijkant merged 24 commits into
mainfrom
dor/issue-370

Conversation

@fortigi-ci-bot

@fortigi-ci-bot fortigi-ci-bot Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Closes #370. Also carries and closes #937 — the two were merged into one item at the requestor's request; #937's PR (#1183) and the interim stacked PR (#1184) are closed in favour of this one.

What you get

#937 is the default. Business roles and access packages are no longer resource rows: they are governance intent and already appear as the business-role columns, so the same role stopped showing up on both axes at once.

#370 is the option. The wizard's Resources step gains "Show business roles as foldable rows", off by default and saved with the matrix (so a shared or saved matrix renders the same for everyone). Tick it and you get the whole layer:

  • each business role on a row of its own, with the resources it grants drawn underneath it — indented, same triangle and elbow as an expanded nested group — and the role directly above them in the row order;
  • fold a role to hide those rows, leaving an "N resources folded" chip; Fold roles / Unfold roles does every role at once, reducing the grid to "business roles + resources no role grants". Fold state is remembered per matrix;
  • a resource several roles grant appears under each of them, with a BR / BR+3 chip that counts and names the others;
  • markers for both directions of drift — fewer than the role assigns, more than it assigns, and held outside the role that grants it — on the resource rows and rolled up onto a folded role;
  • the Excel export mirrors that structure, always unfolded, so folding can never leave access out of a review file.

Leave it unticked and the matrix is what it was before, minus the business-role rows.

How the option is enforced

The gate is the data, not a pile of UI conditionals. With no business-role rows on the resource axis, analyseRoleRows finds nothing foldable, no row carries a roleParentId, and every affordance disappears on its own.

Two things could have been drawn from the SOLL columns alone, so they take an explicit showBusinessRoles: the red + "more than the role assigns" mark, and the legend entries for markers a default grid can never draw. That makes "the default matrix is the old matrix" a property tests pin rather than an emergent one — see the new describe blocks in MatrixView.mount.test.jsx, MatrixGroupRow.mount.test.jsx, MatrixLegend.test.js and matrix-business-role-rows.spec.js.

Migration 061 vs #937's decision 5

Reconciled without a flag of its own: the coverage view's self arm stays unconditional (holding a business role is governed access), and the scope/governed counts follow the option because the resource axis does — #937's choke point already keeps business-role resources out of the pair counts. Pinned from both directions in scopeHistory.test.js.

Rebuilt, not merged as-is

This branch was force-pushed onto a rebuild against current main, which has since extracted accessPackageModel.js, MatrixCell.helpers.js and the row-cell components — so the fold, the staircase promotion and the marker strip land in those instead of in the monolith they were written against. The previous implementation stays readable in this PR's earlier commit range.

Not gated, on purpose

Two things here are not about business roles and stay on for everyone: the resizable grid (useViewportFitHeight / useResizableGridHeight / GridResizeHandle, which also fixes a page-overflow the current measurement has), and the cell marker strip geometry that stops markers overlapping their neighbours' badges. One behaviour change does reach the default matrix: the Gaps row filter now uses the same comparison as the amber !, so the Gaps view and the marker can no longer disagree.

Tests

  • UI 1522 passed, API 2735 passed against current main merged in (incl. the vitest 5 bump).
  • Both coverage suites green against their thresholds.
  • New mutation scope app/ui/stryker.roles.config.json for coverageDeviation.js + useBusinessRoleFold.jsmeasured 93.54%, floor 90, registered in the weekly workflow. The five presentational/measurement files that landed with the feature are excluded with written reasons.
  • The two Python ratchets (file length, complexity) could not be run locally — no working interpreter on this box — so CI is the first check there.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

🤖 Built + verified on dev-docker-03. e2e on the live env + full CI green. Functional testing: https://3.build.identityatlas.io. Awaiting requestor acceptance on #370.

IdentityAtlas DoR agent and others added 15 commits August 4, 2026 14:28
Brings 61 commits of main (up to ac657ec) onto the #370 fold branch so the
PR stops sitting on a 3-day-old base and picks up the bug fixes landed since.

Conflict resolution, by tier:

* .github/** — taken from main wholesale. The DoR agent had edited its own
  scripts during this build, but every one of those edits has since landed on
  main independently (#932, #936, #967), in several cases verbatim. Verified
  line-by-line that nothing unique was dropped: the only four lines absent from
  main were changed_summary's two-dot git range (main uses the better
  origin/main...HEAD) and the hardcoded sk:[sk3,sk5,…] fan-out matrix, which
  main deliberately replaced with derived runner labels + direct dispatch to
  the reservation holder (#950, #961).

* MatrixGroupRow.jsx / MatrixView.jsx — real reconciliation. Main's Contexts
  column (#924) and its extraction of getRoleBadge into the shared
  getApRoleBadge (#949) now coexist with the fold affordance: the local badge
  copy is dropped in favour of the shared helper, and the row renders the fold
  toggle in the name cell followed by main's MatrixContextsCell. Confirmed the
  fold's synthetic role-child rows inherit `contexts` for free — they spread
  existing row objects rather than building new ones — so the new column is
  populated on the rows this feature adds.

* matrix.spec.js / the mount tests — add/add of independent blocks; both sides
  kept. The e2e file was reassembled from the merge stages because both new
  describes opened with the same filter literal; verified main left the trailing
  scrollbar describe untouched, so the branch's edits to it survive intact.

* Demo dataset — parts list is the union (role-drift + shared-grants + volume).
  Main's #928 test pinned the default company at 39 resources; #370 grew it to
  46 (Verify-DemoDataset.ps1 already expected 46/7, and the doc's count tables
  already said so). Updated that pin and the three prose references main added
  that still said 39, so the dataset's documented and asserted counts agree.

Verified: API 2123/2123, UI 1086/1087, demo-dataset Pester 44/46, lint clean
(0 errors) on both packages. The one UI failure is a pre-existing en-NL locale
artifact on this machine (toLocaleString gives 1.500, the test pins 1,500); the
two Pester failures pre-date this merge and are fixed in the follow-up commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
All three pre-date the main merge (verified by running the suites against the
pre-merge branch tip) and none change product behaviour.

* DemoDataset: `$pid` is a read-only PowerShell automatic variable, so
  assigning to it threw before the assertion ever ran and the "one resource,
  two business roles" case was never actually checked. Renamed to
  `$principalId`. PSScriptAnalyzer flags this as PSAvoidAssignmentToAutomaticVariable.

* DemoDataset: the business-role count still pinned 5 after #370 added the
  role-drift and shared-grant roles. Verify-DemoDataset.ps1 and the doc's count
  table already said 7; this brings the last pin into line.

* MatrixScopePanel: the tile-naming test hardcoded '1,500' while the panel
  formats with toLocaleString() and no explicit locale, so it passed only on an
  en-US runner and failed on e.g. en-NL. Asserts against the same formatter now.

Verified: demo-dataset Pester 46/46, UI 1087/1087, API 2123/2123.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The branch added 061_business_role_covers_itself.sql but never ran the
record step every migration-adding PR needs, so the gate reported "new
migration not recorded". It went unnoticed because the workflow had been
sitting at action_required on every prior push and had never actually run.

Additive-only, as the ratchet requires: one line appended, no existing hash
touched. Generated with a Node stand-in for tools/migrations/ratchet.py
--update (no Python on this machine) that mirrors its hashing and output
byte-for-byte; verified the gate's own check logic now passes over all 63.

Note this makes 061 immutable: any further schema change coming out of
functional testing needs a new 062 rather than an edit to 061.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The two CI failures the main merge finally let run. Neither was caused by
the merge; both are the collision of #370's new row model with definitions
that lived somewhere else.

Excel export (main's #949 e2e, "exported cells disagree with the grid"):
the export still read `orderedGroups`, the row list from before business
roles adopt their resources, so it had one row per resource while the grid
now draws a resource under every role that grants it. The export now comes
from the same buildRoleLayout() the grid uses — no second row model to
drift — but is built with an empty fold set, so it mirrors the grid's
structure without inheriting its fold state. Folding is a reading aid; an
access-review artifact must never quietly lose a resource because someone
collapsed a role before hitting Export. That is what the hook's own header
comment already claimed ("never what is fetched, counted or exported") and
is now actually true.

Governed history (integration, ts G=75 vs live G=124): migration 061 gave
vw_UserPermissionAssignmentViaBusinessRole a second arm — holding a
business role is itself governed access — but scopeHistory.js rebuilds that
definition in SQL rather than reading the view, so 061 never reached it.
Live counted the role-membership cells as governed, history did not, and
the latest timeseries point disagreed with live scope-stats for the same
instant. Added the matching arm, mirroring 061 exactly. Historical governed
percentages rise: they were understating governance, not misreporting data.

Both paths still restate one definition in two places, which is what let
this drift. Worth collapsing later; noted rather than done here so the fix
stays reviewable.

Tests: hook-level (export ignores folds, row identity stays usable for cell
lookups), mount-level (Export after "Fold roles" still receives the folded
resources), and SQL-shape parity for 061's arm. UI 1090/1090, API 2124/2124,
lint 0 errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e cell

The export e2e (#949) scraped the row name out of the name cell's innerText,
stripping only a leading expander glyph. #370 put more in that cell — the
fold toggle, the nesting elbow, and the "BR"/"BR+3" overlap chips — so the
scrape started returning "SG-Monitoring-Tools\nBR" and every row failed to
pair with its export row. The export itself was already correct; the reader
was reading the affordances as part of the name. (The multi-line names are
also why the failure listed phantom "BR (Group)" rows: one wrapped string.)

Gives the display name an element of its own (`data-row-name`) and reads
that, so anything else the name cell grows later can't be mistaken for the
resource name again. A mount test locks the contract the e2e depends on.

UI 1091/1091, lint 0 errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@TaekeK
TaekeK marked this pull request as draft August 10, 2026 12:19
TaekeK added a commit that referenced this pull request Aug 10, 2026
…gate notice (#994)

Three problems, all surfaced by #370 → PR #933, where the requestor said the PR
should never have existed because they had not agreed to the solution.

1. The PR was opened ready-for-review on the FIRST build and every feedback round
   re-advertised "CI green". #933 sat like that through eight rounds of the
   requestor rejecting the design — one of which added a schema migration. Green
   checks mean the agent's own tests pass; they say nothing about whether the
   solution is the one that was asked for, and a green ready-for-review PR reads
   as mergeable. Open it as a draft; dor-acceptance.yml takes it out of draft on
   `approve`, the signal it already detects. CI runs on drafts unchanged (no
   workflow here filters on draft), so verify_loop is unaffected — the only thing
   a draft cannot do is merge. Branch protection and the human merge review are
   untouched.

2. The value gate asked the wrong person the wrong question. Its notice was a
   hardcoded @-list and its framing was purely spend ("nothing runs or spends
   until you do"); it never showed the spec, which by then was days up-thread. On
   #370 a maintainer approved and the PR then claimed it was built "from the
   approved spec" — while the requestor had never confirmed it. Address the
   requestor of record (authorize already resolves it; now exported), deep-link
   the certified spec, and ask whether it describes the solution they want.
   Anyone may still approve so an absent requestor cannot stall a build — the
   gate now records who did and says plainly when it was not the requestor.

3. #977 moved that notice INTO the `gate` job, which holds
   `environment: build-approval`. A job gated by required reviewers runs no step
   until approved, so the notice could only arrive after the approval it was
   asking for: an issue needing the gate would wait with nothing said on it.
   Latent so far — the one build since #977 took the autonomous path and skipped
   the gate. Split into an ungated `notify` job (asks) and the gated `gate` job
   (records, post-approval by construction).

test/ci-scripts/test-dor-gate-notice.sh guards all three structurally, including
the exact #977 shape: the job posting the notice must carry no `environment:`.
12 tests; 9 fail against the pre-change tree. Wired into the existing ci-scripts
job in pr.yml.

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
fortigi-ci-bot Bot and others added 3 commits September 11, 2026 11:33
…937)

The business-role row view built for #370 — roles as parent rows, their
resources folded underneath, the BR chips and the deviation markers — is
now the opt-in half of #937's "Show business roles as foldable rows".
Ticked, a matrix gets the whole layer; left alone (the default) it is the
matrix that was there before, minus the business-role rows #937 removed.

The gate is the data, not a pile of UI conditionals: with no business-role
rows on the resource axis, analyseRoleRows finds nothing foldable, no row
carries a roleParentId, and every affordance disappears on its own. Only
the two things that could be drawn from the SOLL columns alone — the
over-grant mark and the legend entries for markers the grid cannot draw —
take an explicit showBusinessRoles, so "the default matrix is the old
matrix" is a property tests pin rather than an emergent one.

Migration 061 is reconciled with #937's decision 5 without a flag of its
own: the coverage view's self arm stays unconditional, and the scope
counts follow the flag because the resource axis does.

Rebuilt on top of #937 rather than merged as-is: main had since extracted
accessPackageModel.js, MatrixCell.helpers.js and the row-cell components,
so the fold, the staircase promotion and the marker strip land in those
instead of in the monolith they were written against.

Adds app/ui/stryker.roles.config.json for coverageDeviation.js and
useBusinessRoleFold.js — measured 93.54%, floor 90.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@WimvandenHeijkant

Copy link
Copy Markdown
Contributor

Superseded by #1184, which rebuilds this work on top of #937 (#1183) and puts the whole business-role row view behind that PR's wizard option — default off, so the matrix stays as it is on main unless a matrix asks for the layer.

Closed unmerged on purpose, per the scope amendment on #370: this branch was written against a main that has since extracted accessPackageModel.js, MatrixCell.helpers.js and the row-cell components. Its implementation stays reachable in the history of dor/issue-370 as the reference it was.

Note for whoever watches the pipeline: the dor-reset leg for this PR is dispatched to sk3, whose runner is currently registered offline, so the sidekick wipe will sit queued until that runner service is back up. The box itself is up and still serving this build at 3.build.

@WimvandenHeijkant WimvandenHeijkant changed the title [Feature] Collapse managed resources under business roles in the matrix feat(matrix): business roles off the rows by default, with a wizard option to fold them back in (#370 + #937) Sep 12, 2026
@WimvandenHeijkant

Copy link
Copy Markdown
Contributor

🚀 Deployed to dev-docker-03 for functional testing → https://3.build.identityatlas.io (Fortigi-tenant sign-in via authentik). Serving dor/issue-370 at 5fd87ff, version 5.663.20260912.1231.

Put there by hand rather than by the deploy-to-sidekick label: that workflow dispatches to whichever dor-build runner is idle and every other box in the pool is reserved, so there was no way to aim it at sk3. The sk3 runner itself had been crash-looping since 2 September on a half-finished self-update to 2.337.0 (System.Xml.ReaderWriter failing to load) — its binaries have been re-extracted and it is connected and listening again, with bin.2.336.0 left in place as a fallback.

What to check

Default (no option ticked) — this should look like the matrix you already know, minus the business-role rows:

  • no business roles among the resource rows, but the business-role columns and their colouring unchanged;
  • no "Fold roles" buttons, no BR chips, no red markers; the legend lists only the markers the grid can draw;
  • resource/assignment counts and the scope panel count what is actually rendered.

Then open the wizard → Resources step → tick "Show business roles as foldable rows":

  • each role gets its own row with the resources it grants underneath it; fold one with the chevron, or all of them with Fold roles;
  • a resource two roles grant appears under both, each copy carrying a BR chip;
  • a folded role keeps a red and an amber count per subject for access it does not cover and access it assigns that is missing;
  • Export Excel: the file mirrors the grid but always unfolded.

The option is saved with the matrix, so a shared link renders the same for whoever opens it.

@WimvandenHeijkant
WimvandenHeijkant marked this pull request as ready for review September 12, 2026 12:52
WimvandenHeijkant and others added 3 commits September 12, 2026 15:04
The complexity ratchet caught MatrixView at cyclomatic 22 against a ceiling
of 20: the opt-in added four decision points to an orchestrator that was
already at the limit.

Extracted them into useMatrixBusinessRoleLayer — the flag, the fold, the
export-row choice and the deviation tallies as one named thing the grid
switches on or off, next to the column model and the nested-row builder
that came out of MatrixView for the same reason. MatrixView now reads
`roleLayer.rows` / `roleLayer.enabled` and holds no business-role logic of
its own; the row React key moves to a module-level helper.

The hook ships with its own tests, and they are about the SWITCH rather
than the pieces it composes: off, the rows come back untouched, the export
is the plain row order and there are no tallies; a truthy-but-not-true flag
(a string "true" off the wire, say) leaves it off, matching the API parser.
Added to the roles mutation scope alongside the two modules it composes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The roles scope measured 93.54% under vitest 4.1.11. main's vitest 5 bump
(#1185) lands mid-PR, and under it the measurement collapses repo-wide —
the untouched pilot scope, floored at 91 from a measured 93.8, now reports
18.07 with usePermissions.js flat at 0.00 while its own suite is green.
That is a tooling regression, not a suite that stopped discriminating, so
the floor stays where the real measurement put it and the config says why
a red weekly run is not evidence against these tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The grid-resize spec pinned the height after the reset against one taken
before the drag. The fit is MEASURED from the chrome above the grid, and
that chrome is not identical after a reload with a custom height in place:
the two retries of the same click read 403 and 445 against an expected 250.
That is the assertion drifting, not the feature.

It now checks what the button actually promises — the chosen height is
dropped (button gone, nothing left in storage) and the grid is measured
again (taller than the dragged height, still exactly one scroller). A reset
that failed to clear the stored height leaves the grid at the dragged size
with the button on screen, and fails all three.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@WimvandenHeijkant

Copy link
Copy Markdown
Contributor

CI is green on 1c91a4f: 32 passing, 2 skipped, nothing failing. https://3.build.identityatlas.io now serves this exact commit (redeployed after the two fixes below), so what was validated is what merges.

Two things needed fixing after the first green-on-my-machine run, both worth knowing about:

The complexity ratchet caught MatrixView at cyclomatic 22 against a ceiling of 20. The opt-in added four decision points to an orchestrator already at the limit. Rather than golfing them away, the business-role layer moved out into useMatrixBusinessRoleLayer — the flag, the fold, the export-row choice and the deviation tallies as one named thing the grid switches on or off, next to the column model and the nested-row builder that left MatrixView for the same reason. It ships with tests about the switch rather than the pieces it composes: off, the rows come back untouched, the export is the plain row order, there are no tallies; and a truthy-but-not-true flag (a string "true" off the wire) leaves it off, matching the API parser.

The grid-resize e2e pinned a pixel snapshot across a reload. It compared the height after "Fit to window" against one taken before the drag. That fit is measured from the chrome above the grid, and the chrome is not identical after a reload with a custom height in place — the two retries of the same click read 403 and 445 against an expected 250. It now asserts what the button promises: the chosen height is dropped (button gone, nothing left in storage) and the grid is measured again, still with exactly one scroller. A reset that failed to clear the stored height fails all three.

Unrelated finding, flagged rather than fixed here. main's vitest 4 → 5 bump (#1185) breaks Stryker's per-test coverage repo-wide. My roles scope measured 93.54% under vitest 4.1.11; under 5.0.0 it reports 23.16. That is not this branch: the untouched stryker.pilot.config.json, documented and floored at 91 from a measured 93.8, now reports 18.07 with usePermissions.js flat at 0.00 while its own suite is green. Every scope in the weekly js-mutation.yml will be red until that is sorted (a Stryker release that supports vitest 5, or coverageAnalysis: "all" as a stopgap). I left the roles floor at the real measurement and wrote the reason into the config — please don't re-baseline any floor to a vitest-5 number, that would bake the outage into the ratchet permanently. Mutation testing does not run on PRs, so it does not block this merge.

@WimvandenHeijkant
WimvandenHeijkant merged commit 0cefe16 into main Sep 12, 2026
34 checks passed
@WimvandenHeijkant
WimvandenHeijkant deleted the dor/issue-370 branch September 12, 2026 14:10
@github-actions

Copy link
Copy Markdown
Contributor

♻️ PR merged. Reset dev-docker-03 (stack down, volumes + images pruned) and released it back to the dor-build pool.

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.

[FEATURE] Don't who Business Roles as a row in the matrix [Feature] Collapse managed resources under business roles in the matrix

1 participant