Skip to content

feat(manager): a list view over the pool, with multi-select - #210

Merged
stormer78 merged 3 commits into
mainfrom
feat/attribute-list-multi-select
Sep 9, 2026
Merged

feat(manager): a list view over the pool, with multi-select#210
stormer78 merged 3 commits into
mainfrom
feat/attribute-list-multi-select

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

The map's subject is reach — what one selection touches — so it draws one selection at a time. That is the right picture and the wrong tool for the job a holder arrives with once the pool is large: delete these four stale numbers, put these six into a new face. On the map that is opening, reading and confirming one card at a time, and no amount of layout fixes it, because one-at-a-time is what the map is for.

So this adds a second view over the same graph, behind a Map/List toggle. Same AttributeNodes, same families, same words, no new wire records — and a selection that is a set rather than a single node.

What it does

  • Rows group under the family headings the map already uses (attribute-family.ts), with a tri-state checkbox per heading.
  • Shift-click selects the range that was on screen — grouping reorders, so a range computed over the pool selects attributes that were never between the two rows clicked.
  • Two bulk actions: delete and add to a face. Both use tasks the console already carries.

Three things worth reviewing closely

Bulk visibility is deliberately absent, and the screen says why. persona/attribute/put is a replace, and this pane lists without includeSensitive on purpose, so a sensitivity: high attribute is in hand with value: undefined. A bulk visibility change written through put would send an empty value for every sensitive row and blank it — silently, with no attribute/get and no version history to restore from. whyNoBulkVisibility() is asserted in a test, so adding a third action means deleting that assertion.

Delete asks the second question first. The agent refuses to delete an attribute a face still references unless cascade is set. Sending twelve and discovering five are in use is five refusals after seven irreversible successes. The preview counts them up front across all three referencing forms — {ref}, {ref, pinVersion}, {ref, override}; reading only attributeIds under-counts — names the faces that would lose an entry, and puts cascade on Destructive's existing force tick.

attribute-words.ts is an extraction, not an addition. provenanceWords, labelSaysSomethingElse and staleWords were private to persona-map.tsx while the map was the only surface drawing an attribute. A second copy is how the map comes to say you said so beside a row the list calls credential · storm.ws.

A bug this found

manager-form-state.test.mts failed on AttributeEditor rendered without a key. It seeds useState from existing, which runs on mount and never again — so opening a second attribute from the list showed the first one's value, and on a put, which replaces, that writes one attribute's value over another's. Fixed with key={editing.attributeId}.

Tests

24 new — 16 model (manager-attribute-list.test.mts), 8 rendered (persona-list.render.test.mts). Full suite 440/440, tsc -b clean.

clickWith is new on the test harness so a modifier click goes through act like every other event, rather than a raw dispatch that warns and settles in a different order than a browser would.

Not in this PR

Grouping faces into worlds (persona/facet/*) is a separate chain that starts with a spec change. This needs none of it.

…eview

The identity map answers "what reaches what", one selection at a time. That
is the right picture for a pool of five and the wrong one for a pool of
fifty: a holder who wants to tidy has to open, read and confirm one card at
a time, which is the clunkiness the map cannot design its way out of because
one-at-a-time is its whole subject.

This is the model for a second view over the same graph — the same
`AttributeNode`s, the same families, no new wire records, and a selection
that is a set rather than a single node. Out of the component for the reason
`identity-graph.ts` is: the interesting parts are the ordering and the set
arithmetic, and both are testable with no DOM.

**The grouping is the one that already exists.** Rows group by `familyOf` in
`FAMILY_ORDER`, with `familyStyle`'s words as headings — not a second
taxonomy invented for this view. Two groupings of one pool that disagree is
the defect where a person counts six in one place and five in the other.

**A range selects over the rendered order, not the pool.** Grouping
reorders, so a range computed from the unsorted pool selects attributes that
were never between the two rows the person clicked.

**One bulk action is deliberately missing.** `persona/attribute/put` is a
replace and this console lists without `includeSensitive` on purpose, so a
`sensitivity: high` attribute reaches the client with `value: undefined`. A
bulk visibility change written through `put` would send an empty value for
every sensitive one and blank it — silently, with no `attribute/get` and no
version history to restore from. That is the hazard `AttributeEditor`
fetches a value to avoid, multiplied by the size of the selection. So
`BULK_ACTIONS` has two members, and `whyNoBulkVisibility` exists so the
screen can say why rather than leaving a gap someone fills in later.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
`persona/attribute/delete` refuses an attribute a profile still names unless
`cascade` is set — "a profile silently projecting one fewer claim is a
failure the holder discovers from the far side of a disclosure". A bulk
delete of twelve where five are in use is therefore five refusals, arriving
one at a time, after the seven that already succeeded.

The preview now counts them up front, so the confirm step can ask the second
question before anything is sent — the way `Destructive`'s `force` tick
does. Overriding a refusal the agent makes on purpose is its own decision
and does not follow from pressing Delete.

It reads all three referencing forms, not just live references:
`attributeIds` carries only `{ref}`, while a pin and an override name the
attribute too and the agent refuses on those as well. Counting only live
references would under-count and put the holder back in the
one-refusal-at-a-time state the preview exists to prevent.

`facesAffected` names the faces rather than counting them — "Work loses two"
is the sentence that changes a mind, and it is not available anywhere else
on the screen.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
The map's subject is reach — what one selection touches — so it draws one
selection at a time. That is the right picture and the wrong tool for the
job a holder arrives with once the pool is large: delete these four stale
numbers, put these six into a new face. On the map that is opening, reading
and confirming one card at a time, and no amount of layout fixes it, because
one-at-a-time is what the map is for.

So: a second view over the same graph, behind a toggle. Same
`AttributeNode`s, same families, same words, no new wire records, and a
selection that is a set. Rows group under the family headings the map
already uses, headings carry a tri-state checkbox, shift-click takes the
range that was on screen, and the bulk bar offers exactly two actions.

**`attribute-words.ts` is new and is an extraction, not an addition.**
`provenanceWords`, `labelSaysSomethingElse` and `staleWords` were private to
`persona-map.tsx` while the map was the only surface drawing an attribute. A
second copy is how the map comes to say *you said so* beside a row the list
calls *credential · storm.ws* — the vocabulary defect in the channel a
person is most likely to act on.

**Bulk visibility is not offered and the screen says why.** `attribute/put`
is a replace and this pane lists without `includeSensitive`, so a bulk
visibility write would blank every sensitive value in the selection.

**Delete asks the second question first.** The preview counts what faces
still reference, names them, and `cascade` rides on `Destructive`'s existing
`force` tick.

`AttributeEditor` is keyed on the record it edits, which
`manager-form-state.test.mts` caught: it seeds `useState` from `existing`,
so an unkeyed second open shows the first attribute's value — and on a put,
writes it over the second's.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@stormer78
stormer78 merged commit 79de016 into main Sep 9, 2026
3 checks passed
@stormer78
stormer78 deleted the feat/attribute-list-multi-select branch September 9, 2026 06:02
@affinidi-appsecurity-bot

Copy link
Copy Markdown

🛡️ AI Agentic Security Code Review

🔎 A manual security review is recommended before merging. Please contact the Security team for specifics and remediation guidance.

ℹ️ Detailed findings are not published on public repositories; the Security team holds the complete report.

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