Skip to content

location and ou stop pushing a value across a group (ADR 0038 decision 13) - #36

Merged
mastacontrola merged 1 commit into
mainfrom
claude/plugins-mass-edit
Sep 1, 2026
Merged

location and ou stop pushing a value across a group (ADR 0038 decision 13)#36
mastacontrola merged 1 commit into
mainfrom
claude/plugins-mass-edit

Conversation

@mastacontrola

Copy link
Copy Markdown
Member

Both plugins shipped a second hook file — AddLocationGroup, AddOUGroup — whose only job was to set one value on every member of a group. Neither was salvageable, for two separate reasons.

It was a copy, not a grant. The hook read the membership at the instant the button was pressed and wrote a row per member, so a host added to the group afterward got nothing and a host removed kept what it had. Exactly the model ADR 0038 removes from the core group page.

And it always clobbered. Every save ran a deletemass over every member before inserting, so "leave this host alone" was not a state the form could express. Saving the tab to set one host's location wiped it from every other member of the group.

Why they don't become grants

A location and an OU are single-valued per host — the write path has always been delete-then-insert-one. A grant is a set that several groups union into, and unioning two locations means nothing.

Single-valued settings go to the host list's mass edit, which is where ADR 0038 sent the core group page's imperative fields, and decision 13 added HOST_MASSEDIT_* precisely so a plugin could follow. These two are the first plugins to use that seam.

What changed

Each plugin contributes one field through HOST_MASSEDIT_FIELDS and applies it through HOST_MASSEDIT_APPLY. The group hook, its JS, and the 'group' entry in its PAGE_JS_FILES mapping are deleted. Net −2 files per plugin. Core draws the three-state action control, which is what makes "leave alone" expressible at all.

Two details worth naming:

  • The value control renders empty. There is no honest value to pre-fill from a selection whose hosts disagree, so what they hold is stated in the hint instead.
  • The hint goes through SharedHostValues::forHostRows(), not a hand-rolled query — a host with no location has no row at all, so counting rows would call three hosts out of five "in agreement". forHostRows() compares the row count to the selection size for that reason. Its answer is in the column's own terms, so a uniform selection comes back as an id; the name is looked up before rendering, and only when there is one name to show.

A set naming a record that does not exist throws rather than falling through to the delete. Treating it as a clear would strip the value off every selected host — the old clobber arriving by a new route.

Proof

tests/group-push-became-mass-edit.test.php drives the apply path for real. That needed FOG\Base\Hook in the stubs, which had never been there: every hook extends it, so no hook could be loaded at all and everything a hook decided had to be pinned by reading its source. Fine for a hook that echoes a form; wrong for one that writes, where the branch taken decides whether rows survive.

Proven by mutation, each turning it red:

Mutation Checks failed
Drop the leave guard (the old clobber) 1
Turn an invalid id into a clear 2
Register only the field event 1
Report the raw id in the hint 1
Drop the empty-selection guard 1
Restore the old group hook 2

The last trips a generic check as well as the filename one — no hook may deletemass over a group's membership — so a newly written group-push hook fails it too.

Full suite 16/16.

Follow-on: this unblocks removing the deprecated imperative cards from the core group page.

🤖 Generated with Claude Code

https://claude.ai/code/session_0166dqQEjAs9fhqUw5zCjvxM

…ecision 13)

Both plugins shipped a second hook file -- AddLocationGroup, AddOUGroup --
whose only job was to set one value on every member of a group. Neither
was salvageable, for two separate reasons.

It was a COPY, not a grant. The hook read the membership at the instant
the button was pressed and wrote a row per member, so a host added to the
group afterward got nothing and a host removed kept what it had. That is
exactly the model ADR 0038 removes from the core group page.

And it ALWAYS CLOBBERED. Every save ran a deletemass over every member
before inserting, so "leave this host alone" was not a state the form
could express. Saving the tab to set one host's location wiped it from
every other member of the group.

These do not become grants. A location and an OU are single-valued per
host -- the write path has always been delete-then-insert-one. A grant is
a SET that several groups union into, and unioning two locations means
nothing. Single-valued settings go to the host list's mass edit, which is
where ADR 0038 sent the core group page's imperative fields, and decision
13 added HOST_MASSEDIT_* precisely so a plugin could follow.

So each plugin now contributes one field through HOST_MASSEDIT_FIELDS and
applies it through HOST_MASSEDIT_APPLY, and the group hook, its JS, and
the 'group' entry in its PAGE_JS_FILES mapping are deleted. Core draws the
three-state action control, which is what makes "leave alone" expressible
at all.

Two details worth naming. The value control renders EMPTY: there is no
honest value to pre-fill from a selection whose hosts disagree, so what
they hold is stated in the hint instead. And the hint goes through
SharedHostValues::forHostRows() rather than a hand-rolled query, because
a host with no location has no row at all -- counting rows would call
three hosts out of five "in agreement". forHostRows() compares the row
count to the selection size for that reason. Its answer is in the
column's own terms, so a uniform selection comes back as an id; the name
is looked up before rendering, and only when there is one name to show.

A 'set' naming a record that does not exist THROWS rather than falling
through to the delete. Treating it as a clear would strip the value off
every selected host -- the old clobber arriving by a new route.

tests/group-push-became-mass-edit.test.php drives the apply path for
real. That needed FOG\Base\Hook in the stubs, which had never been there:
every hook extends it, so no hook could be loaded at all and everything a
hook decided had to be pinned by reading its source. That is the right
default for a hook that echoes a form and the wrong one for a hook that
writes, where the branch taken decides whether rows survive.

Proven by mutation: dropping the 'leave' guard, turning an invalid id
into a clear, registering only the field event, reporting the raw id in
the hint, dropping the empty-selection guard, and restoring the old group
hook each turn it red. The last of those trips a generic check as well as
the filename one, so a newly written group-push hook fails it too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166dqQEjAs9fhqUw5zCjvxM
@mastacontrola
mastacontrola merged commit 7327299 into main Sep 1, 2026
2 checks passed
@mastacontrola
mastacontrola deleted the claude/plugins-mass-edit branch September 1, 2026 23:42
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.

1 participant