feat: eXIP-7.3.0.18 Spaces list portlet EXO-89465 - #6085
Conversation
3aea739 to
bcb585a
Compare
d44d362 to
9c27403
Compare
boubaker
left a comment
There was a problem hiding this comment.
AI review — Round #1
Cross-repo review of eXIP 7.3.0.18 "User Spaces List" against the approved Technical Specification (Tribe note 50524, revision 6) and the layer-1 norms: Meeds-io/social#6085, Meeds-io/analytics#446, Meeds-io/meeds#4281, Meeds-io/kudos#628, Meeds-io/gamification#2002. Reviewed as one delivery. First round, so there is no status table.
Findings are inline. Two are 🔴 and both are in test wiring rather than in the feature — but on an N1 delivery that is precisely where they hurt: the whole component/service test module does not compile, so UserSpacesRestTest and UserRestResourcesTest.testGetSpacesOfUserHidesTheHiddenSpacesAConnectionIsNotIn — the evidence for the REST contract and for the hidden-space filter on the deprecated endpoint — have never executed. Verified by running mvn -o -pl component/api,component/core,component/service -am test-compile, which fails with four errors; the base bcb585a608 compiles. component/api and component/core do compile, so the ACL matrix and the cache tests genuinely run.
One finding that cannot be anchored inline (untouched file)
🟡 Dead Less selectors for a dynamic container this eXIP removes
ProfileAboutMe/Style.less:24 and :48 style .profile-after-about-me-container, one of the seven addonContainers Meeds-io/meeds#4281 drops from the profile page. Grepping all five checkouts for profile-right-container and the six profile-before/after-* names across *.less *.css *.vue *.xml *.js *.jsp *.java returns exactly these two hits — nothing in meeds, analytics, gamification or kudos references any of the seven any more. Both blocks, including the @media override, are unreachable once the upgrade runs.
Fix: delete the .profile-after-about-me-container block and its @media counterpart in this PR — same eXIP, same release.
Verified conform
Checked and correct, so they need not be re-litigated:
- The ACL four-case matrix, on the viewer axis.
getEffectiveUserSpacesScopebranches on the viewer (viewerIdentity.isExternal()), before the requested scope is ever consulted; own profile →ALL; external owner →COMMON; an unresolvable viewer takes the restrictive path rather than failing open. TheisExternalUser(profileOwner)bug the specification records as fixed is fixed. I mutation-verified the pin rather than trusting it: reintroducing the owner-vs-viewer branch fails exactlyUserSpacesServiceTest.testExternalViewerCannotOptOutOfCommonScope(expected:<1> but was:<3>— the external viewer receives the permissive listing), so that test is real evidence and not a test that passes against its own bug. SpaceFilterKeyis the compliant form.viewerIdis aprivate finalfield on the Lombok@Dataclass, henceequals-compared, and the scope is carried bygetType()throughUSER_SPACES_COMMON/USER_SPACES_ALL. The explicitly rejected alternative — letting the discriminators reach the key only through the foldedObjects.hash(filter)int — was genuinely avoided. Only the sorting rides the hash, and it does not decide what a viewer may see.- Cache eviction: all three required assertions are real, against the real cache.
UserSpacesStorageTestruns on theSocialStorageCacheServicefromAbstractCoreTest— the KernelExoCacheharness the spec prescribes, not the app-center@Cacheablepattern — andSpaceStorageresolves toCachedSpaceStoragein the test container, so the assertions are not vacuous. Two viewers never share an entry; eviction on a membership change of either side; eviction when a space turns hidden. Two more cover the disclosing direction (wide-cached-then-narrow-requested, and missing-scope-after-wide). - The predicate.
EXISTSrather than a second membership join, so no row multiplication and no interference with the sort;(s.visibility <> :hiddenVisibility OR EXISTS(...))forALLand the bareEXISTSforCOMMON, on top of the owner axis. PRIVATE stays listed and only HIDDEN is filtered; only the MEMBER role counts; sub-spaces stay flat; the two scopes get distinct query-name suffixes so no cached JPQL string is shared between them. The count query carries the same predicate. - The index the specification's assumption asked to be named:
IDX_SOC_SPACES_MEMBERS_01 (SPACE_ID, USER_ID, STATUS), changeset1.0.0-100ofsocial-rdbms.db.changelog-1.0.0.xml. It matches theEXISTSsub-query column for column, so "no new index is required" holds; the owner axis is served by_03 (USER_ID, STATUS). Please paste that name into the PR description, which is where the spec asked for it. - No new entity, no changeset, no schema change.
@Secured("users")per decision D5. DTOs in.rest.modelper the package norm. Status contract holds: unknown owner → 404, badoffset/limit→ 400 with the message code, an unusable scope narrowed rather than refused.isMemberintoUserSpaceis in-memory, so no N+1 is added at the REST layer. Interface additions follow social'sdefault-throwing convention.getOrCreateUserIdentityon a client-supplied{username}cannot mint identity rows — it saves only when the org provider resolves the user, else returns null → 404. Nothing new was added tosocial/webapp: novue-apps/user-spaces-list, no portlet class, nogatein-resources.xml/portlet.xml/ webpack entry — the only frontend addition is the sharedSpaceService.getUserSpaces, which the spec sanctions.URLUtilsTest's pin stubs the remote user precisely so it cannot pass against the bug it targets.
What this delivery does well
The caching work is the strongest part, and strong in the way that matters. The two access-control discriminators are real key fields; the same normalizeUserSpacesScope feeds both the SQL predicate and the cache key, so the two cannot drift apart on what an absent scope means — and a drift there would be an access-control defect, not a staleness one; the storage refuses a blank viewer before the cache is consulted, so the exception is not surfaced wrapped from inside a loader; and the tests attack the key in the disclosing direction, not only the convenient one. That is a reviewer's test suite, not an author's. The offset > 0 bypass also shows the spec's open cardinality assumption was actually re-checked before merge, with the stated fallback applied rather than waived.
The comments throughout carry the reason rather than a restatement — why getStreamOwnerId cannot be getCurrentUser, why a failed listing hides the block instead of painting "no common spaces" over an error, why the profile filter runs at import time and what that costs. Several are things a later reader would otherwise get wrong, and one of them is the finding below that I would not have reached as quickly without it.
Process
- The title carries no
EXO-id (dev-lifecycle.md§4 wantsfeat: TITLE EXO-<TRIBE_TASK_ID>); the commits carryEXO-89465, so the id exists — it is just not in the title. Same onMeeds-io/analytics#446. - No
Knowledge:line in the body. These are thefeature/mipsintegration PRs, which is exactly wheredev-lifecycle.md§3b step 5 gates it. One concrete item it owes:backend-spring.md§2 namesSpaceFilterKey(this.hash = Objects.hash(filter)) as the anti-example of a folded ACL-bearing cache key — this delivery fixes it, so that paragraph now describes code that no longer exists. Anddomains/social.md§10 holds nothing onspacesCache's budget, its key families or its eviction policy. - PO deferred item 6 is still open: the hidden-space behaviour change on
GET /v1/social/users/{id}/spacesrode in underEXO-89465with no board story of its own, and the spec called that out as blocking the PR that carries it.
Classification
N1 — confirmed on the diff, not merely inherited from the spec: cross-user ACL decision code, a client-supplied scope narrowed in the Service, an ACL discriminator inside a shared cache key, a new REST surface plus a behaviour change on a live public endpoint. Max-severity aggregation makes the whole five-PR set N1.
This PR must be validated by an Architect/Senior Developer who knows it is N1 — not auto-merged on AI review alone, and author != approver applies. Note that until the two 🔴s are fixed, none of the service-layer evidence for this eXIP has ever run.
🤖 Generated with Claude Code
bcb585a to
5d1937c
Compare
9c27403 to
15ead30
Compare
MayTekayaa
left a comment
There was a problem hiding this comment.
AI review — Round #2 (follow-up)
Cross-repo review of eXIP 7.3.0.18 "User Spaces List" against the approved Technical Specification (Tribe note 50524, revision 6), the board (project 8368) and the layer-1 norms: Meeds-io/social#6085, Meeds-io/analytics#446, Meeds-io/meeds#4281, Meeds-io/kudos#628, Meeds-io/gamification#2002, reviewed as one delivery. Head reviewed: d38c1b5153. The two commits pushed since round 1 (15ead30156, d38c1b5153) were re-reviewed in full — the second one moves an access check and therefore re-entered classification as an N1 hunk.
Status of round #1
| # | Finding | Status |
|---|---|---|
| 1 | 🔴 UserSpacesRestTest on the Boot 3 test API |
✅ Fixed — UserSpacesRestTest.java:42-46 now imports org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc / AutoConfigureWebMvc and org.springframework.test.context.bean.override.mockito.MockitoBean; @MockitoBean at :97. |
| 2 | 🔴 InitContainerTestSuite lost the OtpRestTest import |
✅ Fixed — :23 import io.meeds.social.security.rest.OtpRestTest; restored beside the new import; OtpRestTest.class still listed at :79 and the file exists. |
| 3 | 🟡 Dead .profile-after-about-me-container Less |
✅ Fixed — ProfileAboutMe/Style.less keeps only its two @imports. |
| 4 | 🟡 Wrong cache budget citation in CachedSpaceStorage |
✅ Fixed — :369-373 cites plf-meeds-extension/.../conf/meeds/cache-configuration.xml, ${meeds.cache.social.SpacesCache.MaxNodes:4000} and FIFO; re-verified: no <implementation> on that region, so CacheServiceImpl yields SimpleExoCache extends ConcurrentFIFOExoCache. |
| 5 | 🟡 Own-profile sort vs spec §4 | ➖ Accepted — settled in-thread: the code follows the board (US05 alphabetical); the specification owes a /resync-spec of §4 and deferred item 1 (Architect item, not this PR). |
| 6 | 🟡 Access check left in REST | ✅ Fixed — SpaceService.checkUserSpacesAccess (interface :1200-1217, default-throwing per convention) implemented at SpaceServiceImpl.java:430-452; UserRest.java:1703-1709 only maps ObjectNotFoundException → 404 / IllegalAccessException → 403; @Operation now says "the super user". Trace below. |
| 7 | 🟡 Missing @DeprecatedAPI |
✅ Fixed — UserRest.java:1661 and :1750, both insist = true, pointing at UserSpacesRest. |
| 8 | 🟢 Undocumented 500 cap; 100 vs 500 bound | ✅ Documented (:1665 "An explicit limit is capped at 500") / ➖ the two bounds still differ — acceptable on a deprecated endpoint. |
| 9 | Process — index name in the PR body | ❌ Still open — the body does not name IDX_SOC_SPACES_MEMBERS_01 (SPACE_ID, USER_ID, STATUS), which is where the spec's assumption asked for it. One line to add. |
The ACL move (d38c1b5153), traced end to end
Compared with the pre-change REST code (719e11bd25:…/UserRest.java): the three admitted callers are unchanged — the profile owner, userAcl.getSuperUser(), and a viewer in a CONFIRMED relationship with the owner (relationshipManager.get(viewer, owner)); PENDING is refused both ways; an anonymous or unresolvable viewer is refused like a stranger. The one behavioural delta is deliberate and stated in the commit message: a deleted owner now answers 404 for every caller (an unknown username still hits the pre-existing 400 at :1697 first). Both usernames are parameters — no ConversationState read below REST. The super-user branch keeps the unfiltered getMemberSpaces(id) listing (:1717); connections go through getUserSpaces(..., ALL, ...) and lose the hidden spaces they are not in. RelationshipManagerImpl(IdentityManager, IdentityStorage, RelationshipStorage) and its storages depend on nothing space-related, so the new constructor injection creates no bean cycle. Four Service tests pin the gate (UserSpacesServiceTest:973-1010) and UserRestResourcesTest still asserts the 403 through REST; a no-op gate fails the refusal test, a dropped super-user branch fails checkUserSpacesAccess("root", OWNER). Not re-run here (no Maven in this session, CI carries it).
Verified conform
- ACL matrix and forced scope unchanged from round 1 and still pinned: own profile → ALL; viewer null or
isExternal()→ COMMON before the requested scope is read; external owner → COMMON; anonymous → empty/0. - Cache key:
viewerIdandtypeare equals-compared fields of the Lombok@DataSpaceFilterKey;normalizeUserSpacesScope(null) = COMMONis the single method feeding both the predicate and the key; every write path (saveSpace,renameSpace,deleteSpace,ignoreSpace,clearSpaceCached) ends inclearSpaceCache().UserSpacesStorageTestruns against the realSocialStorageCacheService: two viewers never share an entry, the disclosing direction, the missing-scope case, eviction on either side's membership change and on a space turning hidden, first-page-only caching. - Predicate:
EXISTSonSocSpaceMember(entity name and fields verified), only the MEMBER role, HIDDEN filtered and PRIVATE kept, count query built from the same predicates, distinct named-query suffixes per scope;hiddenVisibilitybound atSpaceDAO.java:248. - Cache-served spaces carry their member arrays (
SpaceData.members), somemberandmembersCountinUserSpaceare correct on a cache hit andisMemberstays in-memory — no N+1. - REST contract of
UserSpacesRest:@Secured("users")alone (D5);offset < 0/limitout of1..100→ 400 with message code; unknown owner → 404; scope forwarded untouched; count only withreturnSize;UserSpaceListomitssizewhen null. URLUtils.getStreamOwnerId()and its six-case test unchanged;SpaceService.js getUserSpacesmatches the controller path and parameter names.- Nothing added to
social/webappbeyond the shared JS service (D1/D2).
What this delivery does well
The access decision for the legacy endpoint now lives in one Service method whose interface entry documents the three admitted callers and both exceptions, and REST is reduced to status mapping — the layering the spec asked for, with a test set argued by mutation rather than by coverage. The cache-budget javadoc now cites the real registrar, the real default and the real eviction policy, which is what makes the first-page-only fallback checkable by the next reader.
Classification
N1 — unchanged: checkUserSpacesAccess and getEffectiveUserSpacesScope are ACL and identity-resolution hunks (ai-review-and-merge.md §2), the viewer-keyed cache is an ACL-bearing surface, and d38c1b5153 is itself an ACL move that re-entered classification. Its approver must be an Architect/Senior Developer who knows it is N1 and is not the author; no auto-merge on AI review alone. The Knowledge: line still reads "to follow" — the feature/mips gate needs the eng-standards PR reference before merge.
Still moving, so not the final round: items 9 above, the two nits, the Knowledge: reference, and — outside this PR — the spec resync (Architect) and PO items 3, 5 and 6.
🤖 Generated with Claude Code
d38c1b5 to
81c3717
Compare
…javadoc - EXO-89465 Review round 2 nits on #6085: SearchPageCard.vue and GroupMembersList.vue return to their feature/mips content (a self-closing rewrite and a whitespace-only hunk that widened an N1 diff), and the CachedSpaceStorage javadoc no longer says the REST endpoint owes a limit clamp that UserSpacesRest.MAX_LIMIT already provides. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ection receives - EXO-89465 Closing review round on #6085: the count query is offered through returnSize and no product screen sends it (the drawer paginates with an extra row), so the CachedSpaceStorage and UserSpaceList javadocs no longer attribute it to the drawer; the deprecated GET /v1/social/users/{id}/spaces description now says that an external connection receives the common spaces only and that a deleted user yields 404, which is what the Service does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Self-review close-out — #6085 (eXIP 7.3.0.18, head
|
| # | Finding | Status |
|---|---|---|
| 1 | 🔴 UserSpacesRestTest on the Boot 3 test API (@MockBean, old AutoConfigureMockMvc package) |
✅ Fixed 15ead30156 — @MockitoBean, org.springframework.boot.webmvc.test.autoconfigure.*; suite compiles and runs |
| 2 | 🔴 InitContainerTestSuite lost the OtpRestTest import |
✅ Fixed 15ead30156 |
| 3 | 🟡 Dead .profile-after-about-me-container Less |
✅ Fixed 15ead30156 |
| 4 | 🟡 CachedSpaceStorage javadoc: wrong cache budget, file and eviction policy |
✅ Fixed 15ead30156 — plf-meeds-extension/.../cache-configuration.xml, MaxNodes:4000, FIFO (SimpleExoCache extends ConcurrentFIFOExoCache) |
| 5 | 🟡 Own-profile sort order vs spec §4 | ➖ Waived — PO decision of 26/08/2026 (board US01/US02/US05: alphabetical); spec §4 resync owed by the spec author |
| 6 | 🟡 Access check of GET /v1/social/users/{id}/spaces left in REST |
✅ Fixed d38c1b5153 — SpaceService.checkUserSpacesAccess, REST maps 404/403 only; four Service tests pin the gate |
| 7 | 🟡 Missing @DeprecatedAPI on the two deprecated endpoints |
✅ Fixed 15ead30156 (insist = true) |
| 8 | 🟢 500 cap undocumented; 100 vs 500 bound | ✅ Documented / ➖ bounds left different on a deprecated endpoint |
| 9 | 🟢 Two cosmetic hunks outside the eXIP (SearchPageCard.vue, GroupMembersList.vue) |
✅ Fixed 97272a81d2 — files back to feature/mips content |
| 10 | 🟢 Stale "owes that clamp" javadoc | ✅ Fixed 97272a81d2 — cites UserSpacesRest.MAX_LIMIT |
| 11 | 🟢 Sibling getCommonSpacesOfUser keeps its gate in REST |
❌ Open — Architect/PO scope call; documented in this body as kept until removal |
| 12 | 🟢 Unknown owner 400 vs deleted owner 404 | ❌ Open — Architect/PO compatibility call; both now documented in the @Operation and this body |
| 13 | 🟡 Count query javadoc attributed to the drawer, which does not issue it | ✅ Fixed 611bd90f41 — offered via returnSize, no product screen sends it |
| 14 | 🟡 External confirmed connection narrowed to common spaces on the deprecated endpoint, undocumented | ✅ Documented 611bd90f41 (@Operation, this body); behaviour unchanged (recall-first, matrix row "external → common only"); D3 wording to be confirmed by PO/Architect |
| 15 | Process — EXO- id in title, index name and Knowledge: line in body |
✅ title / ✅ index name / see Knowledge: line above |
Verified conform (closing round, in source at 611bd90f41): the four-case ACL matrix on the viewer axis in SpaceServiceImpl.getEffectiveUserSpacesScope (own → ALL; external or unresolvable viewer → COMMON forced before the requested scope is read; external owner → COMMON), pinned by UserSpacesServiceTest (17 tests, the external-viewer pin fails the owner-vs-viewer mutant); SpaceFilterKey carries viewerId and the scope (SpaceType.USER_SPACES_*) as explicit equals-compared fields, the same normalizeUserSpacesScope feeding predicate and key; UserSpacesStorageTest (19 tests) runs against the real SocialStorageCacheService — two viewers never share an entry, eviction on either side's membership change and on a space turning hidden, first page only; SpaceDAO EXISTS predicate, HIDDEN filtered and PRIVATE kept, served by IDX_SOC_SPACES_MEMBERS_01; UserSpacesRest contract (@Secured("users"), 400 message codes, 404 unknown owner, in-memory member); no entity, changeset or index added; nothing added to social/webapp beyond SpaceService.getUserSpaces.
Still open for humans: Architect/PO — items 11, 12 and the D3 wording for external connections; spec author — /resync-spec of note 50524 (own-profile order in three places, deleted-owner 404, external-connection narrowing, upgrade "asynchronously", drawer count sentence); PO — deferred item 6 (board story for the users/{id}/spaces behaviour change) and the description of delivery task 90074.
Classification: N1 — cross-user ACL decision code (getEffectiveUserSpacesScope, checkUserSpacesAccess), a client-supplied scope narrowed in the Service, an ACL discriminator in a shared cache key, a new REST surface plus a behaviour change on a live public endpoint. This PR must be validated by an Architect/Senior Developer who knows it is N1 — not auto-merged on AI review alone, author ≠ approver. Release order 1 of 4.
🤖 Generated with Claude Code
boubaker
left a comment
There was a problem hiding this comment.
AI review — Round #2 (follow-up)
Re-review of Meeds-io/social#6085 at 611bd90f41 (the branch was rebased onto the current feature/mips since round 1, so the round-1 fix commits cited in the thread replies no longer exist under those SHAs — everything below was verified in the source at this head, and by execution, not from the replies).
Status of round-1 findings
| # | Finding | Status |
|---|---|---|
| 1 | 🔴 UserSpacesRestTest on the Boot 3 test API |
✅ Fixed — org.springframework.boot.webmvc.test.autoconfigure.* + @MockitoBean at :96. mvn -o -pl component/api,component/core,component/service -am test-compile is green, and the class ran for the first time: 9 tests, 0 failures |
| 2 | 🔴 InitContainerTestSuite lost the OtpRestTest import |
✅ Fixed — both imports at :23-24, OtpRestTest.class at :79, file present. UserRestResourcesTest.testGetSpacesOfUserHidesTheHiddenSpacesAConnectionIsNotIn — the Contract-2 pin — executed and passes |
| 3 | 🟡 Cache javadoc: wrong budget, file, eviction policy | ✅ Fixed — plf-meeds-extension/…/conf/meeds/cache-configuration.xml, MaxNodes:4000, FIFO; the "owes that clamp" residue now cites UserSpacesRest.MAX_LIMIT |
| 4 | 🟡 Own-profile sort order vs spec §4 | ➖ Accepted — board US01 (89465) and US05 (89468) say alphabetical, read directly from the board this round; /resync-spec of note 50524 §4 and deferred item 1 stays with the spec author |
| 5 | 🟡 Access check of GET /v1/social/users/{id}/spaces left in REST |
✅ Fixed — SpaceService.checkUserSpacesAccess (SpaceServiceImpl.java:430), REST maps 404/403 only; 4 new Service pins cover owner, super user, CONFIRMED (both directions), PENDING, anonymous, unresolvable and unknown owner. UserSpacesServiceTest: 17 tests green. Mutation-verified: accepting a PENDING relationship fails exactly testLegacyListingAccessIsRefusedWithoutAConfirmedRelationship — the pin is evidence, not decoration |
| 6 | 🟡 Missing @DeprecatedAPI |
✅ Fixed — insist = true on both getSpacesOfUser (:1661) and getCommonSpacesOfUser (:1750) |
| 7 | 🟡 Dead .profile-after-about-me-container Less |
✅ Fixed — no occurrence left in ProfileAboutMe/Style.less |
| 8 | 🟢 scope without defaultValue |
➖ Not taken — still required = false, no default (UserSpacesRest.java:85). Harmless, as stated in round 1 |
| 9 | 🟢 500 cap undocumented; 100 vs 500 | ✅ Documented in the @Operation / ➖ bounds kept different, fine on a deprecated endpoint |
| 10 | 🟢 Two formatting-only hunks | ✅ Fixed — both files out of the diff |
| 11 | 🟢 member vs isMember naming |
➖ spec-side, no change expected |
| — | Process: EXO- id in the title / index named in the body / Knowledge: line |
✅ / ✅ (IDX_SOC_SPACES_MEMBERS_01) / ❌ still open — the body says "to be opened before merge (number to be filled in here)". The feature/mips gate (dev-lifecycle.md §3b step 5) needs either the Meeds-io/eng-standards#NN reference or none — <reason>; a promise is neither |
New this round
The fix for #5 is new ACL code and re-entered full review (ai-review-and-merge.md §3 rule 5). It is correct; three residuals are anchored inline — two of them are decisions for the Architects Lead / PO rather than defects, and I am stating them as such:
- the unknown-owner
400still decided in REST before the Service's404— two existence outcomes, two statuses (inline atUserRest.java:1704); - an external CONFIRMED connection is now narrowed to common spaces on the deprecated endpoint — right by the matrix, but a step beyond what decision D3 promised, and it has no endpoint-level pin (inline at the
@Operation, :1665); - 🟢 the super-user "unfiltered listing" branch is still a REST-side business rule (inline at :1717).
Verified conform, by execution this time
component/api, component/core, component/service test-compile; UserSpacesServiceTest 17/17; UserSpacesRestTest 9/9; the UserRestResourcesTest hidden-space pin 1/1. checkUserSpacesAccess traced: resolves the owner first (deleted or unknown → ObjectNotFoundException), admits the owner and userAcl.getSuperUser(), resolves the viewer itself rather than trusting the caller, refuses anonymous and unresolvable viewers exactly like a stranger, and refuses anything but Relationship.Type.CONFIRMED. SpaceService gained the method as a default throwing UnsupportedOperationException, per the repo convention. Nothing else in the socle part of the diff moved: getEffectiveUserSpacesScope, SpaceFilterKey, the EXISTS predicate, the cache-key derivation and UserSpacesRest are byte-identical to round 1.
Not clean yet, but nothing left is a code blocker: one process item (the Knowledge: PR number) and three decisions. Once the decisions are recorded — in the spec's Functional divergences confirmed or in the code — the next round can be the close-out.
Classification: N1 — unchanged and re-confirmed on the fix commits themselves (checkUserSpacesAccess is ACL decision code). Architect/Senior Developer validation, author != approver, no auto-merge on AI review alone.
🤖 Generated with Claude Code
MayTekayaa
left a comment
There was a problem hiding this comment.
AI review — Round #3 (final)
Cross-repo review of eXIP 7.3.0.18 "User Spaces List" against the approved Technical Specification (Tribe note 50524, revision 6), the board (project 8368) and the layer-1 norms: Meeds-io/social#6085, Meeds-io/analytics#446, Meeds-io/meeds#4281, Meeds-io/kudos#628, Meeds-io/gamification#2002, reviewed as one delivery. Head reviewed: 611bd90f41 (rebased over feature/mips; the PR diff was compared hunk by hunk with the round-2 diff — the only deltas are the two fixes below and javadoc/Swagger wording, no code change).
Status of every finding
| Round | # | Finding | Status |
|---|---|---|---|
| 1 | 1 | 🔴 UserSpacesRestTest on the Boot 3 test API |
✅ Fixed (15ead30156, now 81c365f689) |
| 1 | 2 | 🔴 InitContainerTestSuite lost the OtpRestTest import |
✅ Fixed |
| 1 | 3 | 🟡 Dead .profile-after-about-me-container Less |
✅ Fixed |
| 1 | 4 | 🟡 Wrong cache budget citation in CachedSpaceStorage |
✅ Fixed — 4000 / plf-meeds-extension/.../cache-configuration.xml / FIFO, each re-verified |
| 1 | 5 | 🟡 Own-profile sort vs spec §4 | ➖ Accepted — the code follows the board (US05 alphabetical); /resync-spec of §4 and deferred item 1 owed by the spec author |
| 1 | 6 | 🟡 Access check left in REST | ✅ Fixed (d38c1b5153, now 81c3717772) — SpaceService.checkUserSpacesAccess, REST maps 404/403 only |
| 1 | 7 | 🟡 Missing @DeprecatedAPI |
✅ Fixed — insist = true on both legacy endpoints |
| 1 | 8 | 🟢 Undocumented 500 cap; 100 vs 500 | ✅ Documented / ➖ bounds kept distinct on a deprecated endpoint |
| 1 | 9 | Process — index name in the body | ✅ Fixed — body names IDX_SOC_SPACES_MEMBERS_01 (SPACE_ID, USER_ID, STATUS) and _03 for the owner axis |
| 2 | 1 | 🟢 Two cosmetic hunks unrelated to the eXIP | ✅ Fixed (97272a81d2) — GroupMembersList.vue and SearchPageCard.vue are out of the diff (23 files, was 25) |
| 2 | 2 | 🟢 Stale "owes that clamp" javadoc | ✅ Fixed (97272a81d2) — now "the REST endpoint bounds limit to UserSpacesRest.MAX_LIMIT" |
The last commit (611bd90f41) also corrects two statements this review had relied on: the count query is offered through returnSize and no product screen issues it (the drawer paginates with a lookahead row), and the legacy endpoint's @Operation now says that an external connection receives common spaces only and that a deleted user yields 404 — both verified against getEffectiveUserSpacesScope and checkUserSpacesAccess.
All findings from previous rounds are resolved; nothing outstanding from the AI review side.
Verified conform (closing pass by a fresh reviewer at this head)
Eviction audit: every write path that can change this listing — saveSpace (18 call sites in SpaceServiceImpl), renameSpace, deleteSpace — goes through a CachedSpaceStorage override that calls clearSpaceCache(); ignoreSpace and updateSpaceAccessed write only statuses this listing does not read, and SpaceMemberDAO has no writer outside SpaceStorage. Owner-axis predicate always added (statusList set unconditionally, branch SpaceWithStatuses), distinct named queries per scope, null sorting on the count path safe through SpaceFilter.getSorting()'s default. @JsonInclude from com.fasterxml.jackson.annotation is honoured by the Jackson 3 introspector on the platform. ACL matrix on the viewer axis with the forced scope; viewerId and scope as equals-compared cache-key fields with the shared null-scope normalisation; first-page-only caching against a 4000-entry FIFO region; EXISTS predicate on MEMBER role, HIDDEN filtered, PRIVATE kept, count parity; REST status contract 404 → 403 → 400 with message codes; @Secured("users") alone (D5); nothing added to social/webapp beyond the shared JS service; real-cache storage tests and the four-row matrix tests, plus the four tests pinning the relationship gate.
What remains for humans, outside this review
- Merge gate (
dev-lifecycle.md§3b step 5): the body'sKnowledge:line still says the eng-standards PR is to be opened; the patch exists (knowledge-update.patch, domain-doc refresh ofdomains/social.md§6/§7/§10/§11/§16, theSpaceFilterKeyparagraph ofbackend-spring.md§2, the late-install caveat infrontend-vue.md). Open it and put its number here before merge — the gate reads that field. - Spec resync (Architect,
/resync-specnote 50524): §4 own-profile sort bullet and deferred item 1 (alphabetical everywhere, notLASTVISITED); §4 "executed once, asynchronously" — the plugin runs synchronously (UpgradeProductPlugin.asyncUpgradeExecutiondefaults tofalseand the meeds configuration sets noplugin.upgrade.async.executionfor it); §2 query budget "the drawer does [issue the count query]" — the drawer paginates with a lookahead row and never sendsreturnSize, so no product screen issues the count today (a cheaper outcome than specified, worth recording). - PO: deferred items 3 (where weekly points and rank resurface), 5 (the block's title on the profile page — the control is back in the settings drawer), 6 (a board story and
EXO-id for the behaviour change onGET /v1/social/users/{id}/spaces).
Norm capture — proposed corpus additions from these rounds
Norms invoked across the three rounds that the corpus does not hold, or holds incompletely; each is one eng-standards change fragment:
@DeprecatedAPI(insist = true)beside@Deprecatedon a deprecated REST endpoint in social —backend-spring.md§6 asks only for the annotation and the comment; the runtime-surfacing aspect (DeprecatedAPIAspect,PropertyManager.isDevelopping()) is social's convention with 22 precedents and belongs indomains/social.mdwith a pointer from §6.- The
profilesattribute is evaluated at page import/store time, not at render (Container.buildChildren()), so an addon installed after the upgrade never gets its cell back without a layout-editor restore —frontend-vue.md"Placing portlets on pages" (in the drafted knowledge patch). - The profile name a page cell must use is the WAR's
add.profilesvalue, not its container dependency or context name — gamification's profile isgamificationwhile its dependency isgamification-portlets; no domain doc mentionsprofiles=oradd.profilestoday (gap found in round 2). Same section offrontend-vue.md. social.SpacesCacheis a FIFO region of 4000 entries registered inmeeds→plf-meeds-extension/.../cache-configuration.xml; theexo.cache.social.SpacesCache.*keys ofplf-configurationare inert —domains/social.md§10 (in the drafted patch) with a pointer fromdomains/meeds.md.- An
UpgradeProductPluginruns synchronously unlessplugin.upgrade.async.execution=true— worth one sentence next to theLayoutUpgradePluginreference infrontend-vue.md, since a spec repeated the opposite. - Integration-PR hygiene on N1: cosmetic or lint hunks outside the spec's scope do not ride an N1 integration PR — candidate line for the N1 checklist of
ai-review-and-merge.md§6.
Classification
N1 — final: ACL and identity-resolution hunks (checkUserSpacesAccess, getEffectiveUserSpacesScope), an ACL-bearing cache key and a new REST surface. Its approver must be an Architect/Senior Developer who knows it is N1 and is not the author; no auto-merge on AI review alone. Release order 1 of 5.
🤖 Generated with Claude Code
AI review — correction to Round #3The Round #3 (final) comment above (15:52 UTC) was drafted from a check made at 15:27 UTC and did not see the Round #2 (follow-up) posted by the other reviewing session at 15:38 UTC. Its "nothing outstanding from the AI review side" statement is therefore withdrawn for this PR: the review reopens on the three findings of that round — the two "owner does not exist" statuses (400 in REST vs 404 from the Service), the narrowing of an external CONFIRMED connection to common spaces on this endpoint (spec row under D3 + a REST-level pin owed), and the super-user listing rule still decided in REST (follow-up nit). Everything else in Round #3 stands: the round-1 and round-2 items are fixed at 🤖 Generated with Claude Code |
… access gate - EXO-89465 Review round 2 on #6085: the unknown-username 400 of GET /v1/social/users/{id}/spaces is kept for compatibility (spec decision D3 preserves the existing contract) while a deleted user answers 404 from the Service. The asymmetry is now stated in the operation description and explained in the code above the check, instead of looking accidental. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
AI review — Round #3 (follow-up)Re-review of Status of round-2 findings
Nothing new this round: the delta is one Swagger sentence, one code comment and one test, all read in full; no code path changed. Where this leaves the PRNo code finding is open. What remains, by owner:
The next round can be the close-out once the Classification: N1 — unchanged; the round-3 delta touches no decision code. Architect/Senior Developer validation, 🤖 Generated with Claude Code |
3312d01 to
ca80c72
Compare
The merge-base changed after approval.
…89465 (#6066) Prior to this change, there was no way to list the spaces of a given profile owner as seen by a viewer, so a profile page could only show the viewer's own spaces. This change will add a SpaceService listing of a profile owner's spaces, filtered by the viewer's access rights and narrowed to common spaces for external viewers, with the viewer as an explicit cache key field. It exposes that listing on GET /social/rest/users/{username}/spaces with offset paging, an optional total count and a bounded page size. It also adds the stream owner accessor and the space JS service call that the profile spaces widget and its See-all drawer use. --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…6067) Prior to this change, InitContainerTestSuite imported io.meeds.social.security.rest.OtpRestTest while its @SuiteClasses list no longer referenced OtpRestTest.class and OtpRestTest.java is not present on this branch, so the test compilation failed with "cannot find symbol" on the FB CI. This change will remove the orphan import. All 22 classes listed in @SuiteClasses resolve to existing test sources. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Prior to this change, the branch carried leftovers from the eXIP 7.3.0.18 merge: the UserSpacesRestTest imported the Boot 3 MockMvc auto-configuration and @MockBean packages that Boot 4 removed, InitContainerTestSuite listed OtpRestTest without importing it, the deprecated JAX-RS user spaces endpoints carried no @deprecatedapi marker, and ProfileAboutMe still styled the removed profile-after-about-me-container. This change will move the test to the Boot 4 webmvc test packages and @MockitoBean, restore the OtpRestTest import, mark both legacy endpoints with @deprecatedapi pointing at UserSpacesRest, correct their Swagger descriptions (super user, 500 cap), refresh the SpacesCache sizing comment to the real cache-configuration.xml source, and drop the dead Less rules. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…O-89465 Prior to this change, the deprecated GET /v1/social/users/{id}/spaces endpoint decided in the REST layer who may ask for a user's spaces (the owner, the super user or a confirmed connection), while the Technical Specification of eXIP 7.3.0.18 (Tribe note 50524, Security) committed to moving that check into the Service, as the layering norm requires. This change will add SpaceService.checkUserSpacesAccess, implemented in SpaceServiceImpl with the RelationshipManager it now receives, and make UserRest call it, mapping ObjectNotFoundException to 404 and IllegalAccessException to 403. The unfiltered super-user listing and the 400 on an unknown target are unchanged; a deleted profile owner now yields 404 for every caller, as the listing path already did. Four Service tests pin the gate; disabling it fails both the new refusal test and the existing REST 403 assertion of UserRestResourcesTest (mutation-verified). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…javadoc - EXO-89465 Review round 2 nits on #6085: SearchPageCard.vue and GroupMembersList.vue return to their feature/mips content (a self-closing rewrite and a whitespace-only hunk that widened an N1 diff), and the CachedSpaceStorage javadoc no longer says the REST endpoint owes a limit clamp that UserSpacesRest.MAX_LIMIT already provides. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ection receives - EXO-89465 Closing review round on #6085: the count query is offered through returnSize and no product screen sends it (the drawer paginates with an extra row), so the CachedSpaceStorage and UserSpaceList javadocs no longer attribute it to the drawer; the deprecated GET /v1/social/users/{id}/spaces description now says that an external connection receives the common spaces only and that a deleted user yields 404, which is what the Service does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…aces listing - EXO-89465 Prior to this change, the narrowing of an external CONFIRMED connection to common spaces on GET /v1/social/users/{id}/spaces was decided in the Service and stated in the Swagger description, but no REST-level test asserted it: UserRestResourcesTest pinned the hidden-space filter for an internal connection only. This change will add a REST-level pin: an external confirmed connection of the profile owner receives the common space only, while an internal one holding the same relationship receives the whole visible listing. The external flag is reset in a finally block, since the profile property outlives the test in the shared test database. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… access gate - EXO-89465 Review round 2 on #6085: the unknown-username 400 of GET /v1/social/users/{id}/spaces is kept for compatibility (spec decision D3 preserves the existing contract) while a deleted user answers 404 from the Service. The asymmetry is now stated in the operation description and explained in the code above the check, instead of looking accidental. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
0cc720a to
2f79c9c
Compare
|
…javadoc - EXO-89465 Review round 2 nits on #6085: SearchPageCard.vue and GroupMembersList.vue return to their feature/mips content (a self-closing rewrite and a whitespace-only hunk that widened an N1 diff), and the CachedSpaceStorage javadoc no longer says the REST endpoint owes a limit clamp that UserSpacesRest.MAX_LIMIT already provides. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ection receives - EXO-89465 Closing review round on #6085: the count query is offered through returnSize and no product screen sends it (the drawer paginates with an extra row), so the CachedSpaceStorage and UserSpaceList javadocs no longer attribute it to the drawer; the deprecated GET /v1/social/users/{id}/spaces description now says that an external connection receives the common spaces only and that a deleted user yields 404, which is what the Service does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… access gate - EXO-89465 Review round 2 on #6085: the unknown-username 400 of GET /v1/social/users/{id}/spaces is kept for compatibility (spec decision D3 preserves the existing contract) while a deleted user answers 404 from the Service. The asymmetry is now stated in the operation description and explained in the code above the check, instead of looking accidental. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… access gate - EXO-89465 Review round 2 on #6085: the unknown-username 400 of GET /v1/social/users/{id}/spaces is kept for compatibility (spec decision D3 preserves the existing contract) while a deleted user answers 404 from the Service. The asymmetry is now stated in the operation description and explained in the code above the check, instead of looking accidental. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…javadoc - EXO-89465 Review round 2 nits on Meeds-io#6085: SearchPageCard.vue and GroupMembersList.vue return to their feature/mips content (a self-closing rewrite and a whitespace-only hunk that widened an N1 diff), and the CachedSpaceStorage javadoc no longer says the REST endpoint owes a limit clamp that UserSpacesRest.MAX_LIMIT already provides. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ection receives - EXO-89465 Closing review round on Meeds-io#6085: the count query is offered through returnSize and no product screen sends it (the drawer paginates with an extra row), so the CachedSpaceStorage and UserSpaceList javadocs no longer attribute it to the drawer; the deprecated GET /v1/social/users/{id}/spaces description now says that an external connection receives the common spaces only and that a deleted user yields 404, which is what the Service does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>



Integration of the User Spaces List eXip (eXIP7.3.0.18) from feature/devx — this repo's delivery commits cherry-picked in their original order (#6066, #6067), verified patch-identical except the
OtpRestTestimport ofInitContainerTestSuite, which #6067 removed on feature/devx where that test does not exist and which feature/mips needs; restored in the review round 1 fix commit. Technical Specification: Tribe note 50524.Related PRs of the same delivery: Meeds-io/meeds#4281, Meeds-io/analytics#446, Meeds-io/kudos#628, Meeds-io/gamification#2002. Release order: this PR first (the addons build on social).
Index serving the membership predicate (spec §4 assumption, "named in the PR description"):
IDX_SOC_SPACES_MEMBERS_01 (SPACE_ID, USER_ID, STATUS), changeset1.0.0-100ofsocial-rdbms.db.changelog-1.0.0.xml, matches theEXISTSsub-query column for column; the owner axis is served byIDX_SOC_SPACES_MEMBERS_03 (USER_ID, STATUS). No new index, no entity, no changeset.Behaviour changes on the deprecated
GET /v1/social/users/{id}/spaces(decision D3 — kept for compatibility, hardened, not restricted): hidden spaces the caller is not a member of are no longer returned, and an external connection now receives the common spaces only (the viewer-axis matrix applies to this endpoint too); an explicitlimitis capped at 500; the access check (owner, super user, or a CONFIRMED connection) now lives inSpaceService.checkUserSpacesAccess, REST only maps the status — so a deleted profile owner answers 404 for every caller where it used to answer 200 (super user) or 403 (non-connection); an unknown username still answers 400.{userId}/spaces/{profileId}keeps its check in REST until removal. Both endpoints carry@Deprecated+@DeprecatedAPI(insist = true)pointing atGET /social/rest/users/{username}/spaces.Knowledge: Meeds-io/eng-standards PR to be opened before merge (number to be filled in here) — domain-doc refresh of
domains/social.md(§6, §7, §10, §11, §16), theSpaceFilterKeyparagraph ofbackend-spring.md§2, and the late-install caveat of theprofilespage pattern infrontend-vue.md; the patch is drafted and reviewer-checked (knowledge-update.patchfrom the self-review close-out).Classification: N1 — approver must be an Architect/Senior Developer (author ≠ approver); no auto-merge on AI review alone.