diff --git a/docs/reviews/account-avatar-display-summary.md b/docs/reviews/account-avatar-display-summary.md new file mode 100644 index 0000000..e0ceee9 --- /dev/null +++ b/docs/reviews/account-avatar-display-summary.md @@ -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 `` 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. diff --git a/psf-memo-client/specs/account-avatar-display.feature b/psf-memo-client/specs/account-avatar-display.feature index bebdcc2..c533d24 100644 --- a/psf-memo-client/specs/account-avatar-display.feature +++ b/psf-memo-client/specs/account-avatar-display.feature @@ -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 diff --git a/psf-memo-client/src/services/account-page.js b/psf-memo-client/src/services/account-page.js index 87ee1c1..f3800ea 100644 --- a/psf-memo-client/src/services/account-page.js +++ b/psf-memo-client/src/services/account-page.js @@ -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