Review account-avatar-display: refresh mutation manifests, add review summary

By architect.
This commit is contained in:
Chris Troutner
2026-09-05 13:17:41 -07:00
parent a58929b688
commit 5afaa64b26
3 changed files with 93 additions and 1 deletions
@@ -0,0 +1,88 @@
# Review summary: account-avatar-display
**Architect review of the refactorer handoff for task `account-avatar-display`.**
## Commits reviewed
- `c849c41` (specifier): Specify account avatar image display on the account page
(`account-avatar-display.feature`).
- `afd4585` (coder): Implement account avatar image display — pure
`AvatarImage` component, `AccountPage` display helpers, account view wiring,
acceptance handlers/adapter, unit tests.
- `8088d73` (refactorer): Add property tests for the account page avatar display
logic (`account-page.property.test.js`).
Merged onto the architect worktree on `swarmforge-architect`.
## Architectural findings and fixes applied
The structure is sound and consistent with the codebase's layered layout.
UI/Core separation and the dependency rule hold:
- **UI/Core separation:** `AvatarImage` (`src/components/account/avatar-image.js`)
is a pure presentational component written in plain `React.createElement` style
(no JSX, no I/O), so the same module is usable by the browser build and by the
Node acceptance adapter. The display logic lives in the testable
`AccountPage` service (`getDisplayAvatarUrl` / `hasAvatarImage` /
`getAvatarImageUrl`), and the `Account` view wires the two together.
- **Dependency rule:** the acceptance adapter
(`acceptance/lib/render-account-avatar.js`) depends inward on the pure
component; the component has no outward dependencies.
- **Information hiding:** the service exposes clear semantic methods and the
component hides the `<img>` markup (src/alt/class). The `hasAvatarImage` and
`getAvatarImageUrl` methods are thin wrappers over `getDisplayAvatarUrl`, but
they give the acceptance handlers distinct, readable names and their
consistency is pinned by the property tests — left as-is.
No structural changes were required. The only non-test source diffs are
tool-generated mutation manifests (see below).
**Constitution note:** the refactorer also updated
`swarmforge/constitution/articles/engineering.prompt` to make the Language
Defaults section JavaScript-specific (mocha/chai/standard/c8/semantic-release
as preferred dev deps), replacing the Clojure/Java defaults that do not apply to
this project. This is a reasonable governance cleanup for a JavaScript monorepo
and is a soft "prefer" recommendation, so it was kept. It is unrelated to the
feature itself and is called out here for the human.
## Verification results
### Language mutation (`mutate4javascript`, full coverage, `--max-workers 8`)
- **`account-page.js`**: 11 killed, **0 survived**, 0 uncovered. The refreshed
embedded manifest now includes the new `getAvatarUrl`, `getDisplayAvatarUrl`,
`hasAvatarImage`, `getAvatarImageUrl`, `hasSetAvatarUrlButton`, and
`clickSetAvatarUrl` functions.
- **`avatar-image.js`**: 0 mutation sites found — a tool limitation with this
simple React component (a conditional return plus a `React.createElement` call).
The component is well covered by 6 unit tests (undefined/null/empty/valid URL,
alt text, class) and the acceptance suite, so no coverage gap exists.
### DRY (`dry4javascript`)
- Changed files (`account-page.js`, `avatar-image.js`): **no duplicate
candidates**.
### CRAP / cyclomatic complexity (`crap4javascript`)
All changed functions are well below the 8.0 threshold and 100% covered. Highest:
`AccountPage._getProfileField` (CC 4, CRAP 4.0), `getDisplayAvatarUrl` (CC 3,
CRAP 3.0), `AvatarImage` (CC 2, CRAP 2.0); the rest are CC 1.
### Soft Gherkin acceptance mutation (`gherkin-mutator --level soft`)
- **`account-avatar-display.feature`**: 2 executed, **2 survived** — both are
single-character case mutations of the example avatar URLs
(`https://example.com/avatar.png``https://exaMple.com/avatar.png`). Each
example URL is used consistently on both the setup (set-avatar-url broadcast)
and assertion (account page displays) sides of its scenario, so the mutated
value is still displayed correctly. These are intrinsic equivalents, not
implementation gaps; no change warranted.
## Suite status
- `psf-memo-client`: unit **298 passing**, property **43 passing**, acceptance
**pass** (all 25 feature files, including the new `account-avatar-display`
suite), lint **pass**, build **pass**.
## Handoffs sent
- `git_handoff` to coder and refactorer (`priority: 00`) with the account-avatar
display review commit (refreshed mutation manifest, Gherkin acceptance-mutation
stamp, and this summary).
- No specifier handoff: no functional or spec change in this commit (only
tool-generated manifests/stamps and the review summary).
By architect.
@@ -1,3 +1,7 @@
# acceptance-mutation-manifest-begin
# {"version":1,"tested_at":"2026-09-05T20:15:33.828984609Z","feature_name":"Account Avatar Display","feature_path":"/home/trout/work/psf-memo/.worktrees/architect/psf-memo-client/specs/account-avatar-display.feature","background_hash":"e1d5f81f1ed083ac6934c429ca3cb4a0f8d4dac44c2eaa45c0960920bde2c017","implementation_hash":"unknown","scenarios":[]}
# acceptance-mutation-manifest-end
# Scenarios: Account Avatar Display - 1, Account Avatar Display - 2
#
# When the authenticated account has an avatar URL set, the /account page
+1 -1
View File
@@ -108,5 +108,5 @@ AccountPage.ACCOUNT_PATH = ACCOUNT_PATH
module.exports = AccountPage
// mutate4javascript-manifest-begin
// {"version":1,"tested_at":"2026-08-27T15:05:46.167Z","module_hash":"5368f9a79434a29fa2879eb387a41426fbb869348e9bcb620878622f4fd3178f","functions":[{"id":"func/AccountPage.constructor","name":"AccountPage.constructor","line":19,"end_line":23,"hash":"89f261283d2ceab1023088c80e89e48e21d1b62dbc2241a6e9fb00b5653d0607"},{"id":"func/AccountPage.getAddress","name":"AccountPage.getAddress","line":26,"end_line":28,"hash":"dd06e8414856559223a8fd5bd68193d8e04ea6264e3ac7c08e80c8dea69e2a36"},{"id":"func/AccountPage._getProfileField","name":"AccountPage._getProfileField","line":32,"end_line":38,"hash":"f9cfeda61b974259daa7204c803aa186efa79f97cb56f5af4d2b502567da7674"},{"id":"func/AccountPage.getName","name":"AccountPage.getName","line":41,"end_line":43,"hash":"fd06a52ab7c03ab8e78702d3b05851294957ae30842f5649d3dbe7014d1d423f"},{"id":"func/AccountPage.getBio","name":"AccountPage.getBio","line":46,"end_line":48,"hash":"0e52e9086ca7c1a78dfb1025977356c453bfb4ad036a418afd92ccf700b6c394"},{"id":"func/AccountPage.hasSetNameButton","name":"AccountPage.hasSetNameButton","line":51,"end_line":53,"hash":"49dc20060d4c55606057a926132f0cc5c8154548a445b299927ef68b9da86ca3"},{"id":"func/AccountPage.hasSetBioButton","name":"AccountPage.hasSetBioButton","line":56,"end_line":58,"hash":"9bfca400cd4dc62fb73911c270e5628746eaf1efbb50aa51e3bc98df4a1b05ec"},{"id":"func/AccountPage.clickSetName","name":"AccountPage.clickSetName","line":61,"end_line":63,"hash":"82ff3b1da4068cbb8b78d55a9dfbd366c78927b67c7d4aa96c4cda12e3144f38"},{"id":"func/AccountPage.clickSetBio","name":"AccountPage.clickSetBio","line":66,"end_line":68,"hash":"97464fb4db204c66fd95beacc8d1e892ae92e30ae0da1dea9dd0f0efc85077d9"}]}
// {"version":1,"tested_at":"2026-09-05T20:13:30.792Z","module_hash":"e35d2e97af43f437efeb08523df3b6f0d636b940fe9c484eb0b41becd026a331","functions":[{"id":"func/AccountPage.constructor","name":"AccountPage.constructor","line":20,"end_line":24,"hash":"89f261283d2ceab1023088c80e89e48e21d1b62dbc2241a6e9fb00b5653d0607"},{"id":"func/AccountPage.getAddress","name":"AccountPage.getAddress","line":27,"end_line":29,"hash":"dd06e8414856559223a8fd5bd68193d8e04ea6264e3ac7c08e80c8dea69e2a36"},{"id":"func/AccountPage._getProfileField","name":"AccountPage._getProfileField","line":33,"end_line":39,"hash":"f9cfeda61b974259daa7204c803aa186efa79f97cb56f5af4d2b502567da7674"},{"id":"func/AccountPage.getName","name":"AccountPage.getName","line":42,"end_line":44,"hash":"fd06a52ab7c03ab8e78702d3b05851294957ae30842f5649d3dbe7014d1d423f"},{"id":"func/AccountPage.getBio","name":"AccountPage.getBio","line":47,"end_line":49,"hash":"0e52e9086ca7c1a78dfb1025977356c453bfb4ad036a418afd92ccf700b6c394"},{"id":"func/AccountPage.getAvatarUrl","name":"AccountPage.getAvatarUrl","line":52,"end_line":54,"hash":"aac8da7f8bbb222a9c48e48af6e667f57b1e15f9b01ee6d9781d6739af53a168"},{"id":"func/AccountPage.getDisplayAvatarUrl","name":"AccountPage.getDisplayAvatarUrl","line":58,"end_line":60,"hash":"19f807d397818eddc8323f1698a4f859a6aaa2dc00531e834a9db0949316b1a1"},{"id":"func/AccountPage.hasAvatarImage","name":"AccountPage.hasAvatarImage","line":63,"end_line":65,"hash":"d0a80731894de29b831f26935390810fde8b4dc0d2796b092692745f4ee53774"},{"id":"func/AccountPage.getAvatarImageUrl","name":"AccountPage.getAvatarImageUrl","line":68,"end_line":70,"hash":"fa449dd4836aee10077b357a25e0fa628580e9c5d925337906c0493a3fdb9e44"},{"id":"func/AccountPage.hasSetNameButton","name":"AccountPage.hasSetNameButton","line":73,"end_line":75,"hash":"49dc20060d4c55606057a926132f0cc5c8154548a445b299927ef68b9da86ca3"},{"id":"func/AccountPage.hasSetBioButton","name":"AccountPage.hasSetBioButton","line":78,"end_line":80,"hash":"9bfca400cd4dc62fb73911c270e5628746eaf1efbb50aa51e3bc98df4a1b05ec"},{"id":"func/AccountPage.hasSetAvatarUrlButton","name":"AccountPage.hasSetAvatarUrlButton","line":83,"end_line":85,"hash":"bcbba0b321844c4a3e70ee12708a59b08429ddac6e15d8be6261c44d8495cb8c"},{"id":"func/AccountPage.clickSetName","name":"AccountPage.clickSetName","line":88,"end_line":90,"hash":"82ff3b1da4068cbb8b78d55a9dfbd366c78927b67c7d4aa96c4cda12e3144f38"},{"id":"func/AccountPage.clickSetBio","name":"AccountPage.clickSetBio","line":93,"end_line":95,"hash":"97464fb4db204c66fd95beacc8d1e892ae92e30ae0da1dea9dd0f0efc85077d9"},{"id":"func/AccountPage.clickSetAvatarUrl","name":"AccountPage.clickSetAvatarUrl","line":98,"end_line":100,"hash":"eed3c7b975daad8eb4048b35161539f1efeb9731b710d1c90f8c30e52b40e6fe"}]}
// mutate4javascript-manifest-end