Phase 6 — Group Register (consolidation stage 1) Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.
Goal: A register, kept by one organization (the group owner), of the entities in a group, dated shareholdings, approved relationship assessments (Companies Act s.4, MFRS 10, MFRS 128), beneficial owners, and the consent grants member organizations give the owner. No consolidation runs, eliminations or translation.
Architecture: a new platform/group package. The register tables carry the owner’s org_id with the standard org_isolation policy, so tenancy is unchanged. A consent grant carries the member’s org_id and a composite foreign key (group_id, grantee_org_id) → company_group (id, org_id) that makes the grantee rule a constraint. The one cross-organization read, group_read_allowed, is an allowlisted SECURITY DEFINER function that checks an unrevoked grant and audits in the member’s chain. Relationships are never stored: they are derived by a read function and confirmed or overridden by an approved relationship_assessment.
Tech Stack: Go, pgx v5, Postgres (goose migrations), gin, oapi-codegen, React + TanStack Router/Query + i18next, vitest + msw.
Spec: docs/superpowers/specs/2026-10-03-group-register-design.md (owner decisions in §9, all five settled; the Companies Act 2016 s.4 wording is unverified, see Gate G1).
Gates (owner or adviser, before the step they block)
Section titled “Gates (owner or adviser, before the step they block)”- G1 — adviser confirms Companies Act 2016 s.4 (board control, more than half the voting power, more than half the issued shares excluding preference). Blocks Task 3’s labels (
subsidiary/not_subsidiary) and Task 6’s display of them; until confirmed the derivation returnspresumed_*only. Tasks 1, 2, 4, 5 do not wait. - G2 — adviser confirms the accounting-records retention period that the retention grants (Task 1) and the revocation rule rely on. Task 1 uses the platform’s existing seven-year working figure and records it as provisional.
Global Constraints
Section titled “Global Constraints”- Migration range:
00920–00939(this plan uses00920–00923). Task 1 declares it ininternal/kernel/db/migrationranges_test.go;TestEveryMigrationIsNumberedInItsOwnersRangefails otherwise. - A float never touches money or shares. Percentages
NUMERIC(9,6), share countsNUMERIC(20,2); strings on the wire, a decimal type in Go. - Every business table has
org_id,ENABLEandFORCE ROW LEVEL SECURITY, policyorg_isolation; every tenant foreign key is composite onorg_id, except the one grant key above, which is the deliberate cross-tenant reference. - Every constraint is named; every function pins
SET search_path = pg_catalog, public, pg_tempand schema-qualifies reads. - Retention:
shareholding,relationship_assessment,beneficial_ownerandgroup_access_grantnever grantDELETEto the application role; rows are ended by date or revoked. - Vocabulary values are strings: entity kinds, tests, outcomes, grant scopes, basis (
mfrs10). - Every user-facing name is a translation row; permission keys get en/ms/zh labels (
TestEveryRegisteredPermissionAndModuleHasATeamLabelInEveryLocale). - No handler reads an organization from a request header. The owner is the session’s organization; a member is named by a body or path id and validated against the grant.
- The contract is
api/openapi.yaml;make generateleaves the tree clean. Generated files are never edited by hand. - Layering:
platform/groupimportskernelandplatform/audit-level packages only;platform/hrand others do not import it. - Drift tests: presence before absence, each watched failing once (AGENTS.md). Iterate on touched packages; run
go test -p 2 ./...once, in Task 7, on a clean Docker. - Commits: one per task, ending with the attribution line the session specifies. Merging and pushing are the owner’s.
Rulings (where the spec is silent)
Section titled “Rulings (where the spec is silent)”- GR-1 Change a holding = end and insert.
effective_tois exclusive. An edit never updates a percentage;group.ChangeShareholdingends the current row on the change date and inserts the new one in the same transaction. - GR-2 The 100% cap is a constraint trigger per
(subject, share_class, day)over the periods that overlap the written row, taking the subject’s row lock first, so two concurrent writes cannot both pass. - GR-3 Indirect holdings. Ownership multiplies along a chain; voting passes through an entity only while that entity is presumed or assessed as controlled. The walk has a depth limit of 12 and records a cycle as
circularinstead of looping. - GR-4 Presumptions. Voting above 50% (direct or through controlled entities) is
presumed_control; 20% up to that ispresumed_influence. An approved assessment for the same holder, subject, test and date wins, either way. Board control comes only from an assessment or a recordedboard_controlfact. - GR-5 Assessments need a second person.
approved_bydiffers from the asserter, enforced by a CHECK, and an unapproved row is ignored by every derivation. - GR-6 Beneficial-owner candidates are individuals at or above 20% of shares or voting, directly or through any chain, with no declared
beneficial_ownerrow covering the date. They are listed, never auto-declared. - GR-7 Grants. Only
group.grant.manageholders of the member make or revoke one. Revoke setsrevoked_at; a revoked grant is never reused.group_read_allowed(group, member, scope)is true only for an unrevoked grant whose scope covers the request; it audits every call in the member’s chain. - GR-8 Revocation keeps history. Revoking stops new reads of the member. Records the owner already produced stay readable to the owner.
- GR-9 Membership of several groups is allowed:
group_entityis unique on(group_id, member_org_id), not onmember_org_id; grants are per group. - GR-10 Beneficial-owner personal data is behind
group.register.sensitive: identity numbers are masked for others, and every read of them is audited.
Task 1 — Migration 00920, schema guards, range
Section titled “Task 1 — Migration 00920, schema guards, range”Files: db/migrations/00920_group_register.sql, internal/kernel/db/migrationranges_test.go, internal/platform/group/migration00920_test.go
- Tables
company_group,group_entity,shareholding,relationship_assessment,beneficial_owner,group_access_grantwith RLS, named constraints, the grant composite FK, exclusion constraint on overlapping holdings (btree_gist), the 100% trigger (GR-2), the approver CHECK (GR-5), retention grants. - Tests (each watched failing once): grant FK refuses a grantee that is not the owner (presence: the owner is accepted); overlapping holdings refused (presence: the adjacent non-overlapping row is accepted); over-100% refused under two concurrent writes; unapproved-by-same-person refused;
DELETEprivilege absent on the four retention tables; Down migration restores the prior state. - Verify:
go test -p 2 ./internal/kernel/db/... ./internal/platform/group/....
Task 2 — Register service
Section titled “Task 2 — Register service”Files: internal/platform/group/{group,entity,shareholding,beneficial_owner}.go and tests
- Create group, entities (company or individual, optional
member_org_id),ChangeShareholding(GR-1), beneficial owners, each audited. - Verify: create, change over time, history reproduces a past date, tenant isolation across two owners.
Task 3 — Derivation and assessments
Section titled “Task 3 — Derivation and assessments”Files: internal/platform/group/{derive,assessment}.go and tests · Gate G1 for the legal labels
RelationshipsAt(group, date)per GR-3/4/6, returningpresumed_*, assessed outcomes,circular, and the BO candidates.- Tests: chain multiplication (60% of 50% is 30%), de facto control via assessment below 50%, assessment overriding above 50%, unapproved assessment ignored, cycle reported, depth limit.
- Until G1 clears, the legal test is exposed only as
presumed_*.
Task 4 — Grants and group_read_allowed
Section titled “Task 4 — Grants and group_read_allowed”Files: db/migrations/00921_group_read_allowed.sql, internal/platform/group/grant.go, the SECURITY DEFINER allowlist test, tests
- Function per GR-7; allowlist entry; not executable by PUBLIC.
- Tests: answers with an unrevoked grant (presence), refuses without one, refuses after revoke, refuses a scope that is not covered, refuses a different group; an audit row lands in the member’s chain each call.
Task 5 — Permissions, routes, contract
Section titled “Task 5 — Permissions, routes, contract”Files: internal/modules/group/, api/openapi.yaml, constants.yaml (then make generate), web/src/lib/i18n/locales/{en,ms,zh}/team.json, migration 00922 (Owner grants for existing organizations)
- Keys
group.register.read,.manage,.sensitive,group.grant.manage, Owner-only by default; routes behindhttpx.Authorize; route-authorization and label drift tests green. - Verify: handler tests for 403 without each key, masked identity numbers without
.sensitive.
Task 6 — Web
Section titled “Task 6 — Web”Files: web/src/features/group/*, nav entry, i18n
- Register screens: group, entities, shareholdings with a history view, relationship view (labels gated on G1), beneficial owners; member-side consent screen.
- Verify: vitest, typecheck, prettier; a manual en/ms/zh, phone-width and dialog-focus pass on a seeded stack including the data-dependent screens (the Phase 5 pass could not reach them).
Task 7 — Close-out
Section titled “Task 7 — Close-out”- Tick Phase 6 stage 1 in
docs/ROADMAP.md, add the lock order for the subject row (Task 1) todocs/LOCK_ORDER.md, rungo test -p 2 ./...and the web suite once on a clean Docker.
Execution record: owner rulings (2026-10-04)
Section titled “Execution record: owner rulings (2026-10-04)”The owner accepted the recommendations of Part B of docs/superpowers/briefs/2026-10-04-phase-6-adviser-and-owner-questions.md. Built on phase6-group-rulings, one commit each, migrations in the group range:
- B1 (00923).
DELETErevoked fromerp_apponcompany_groupandgroup_entity.internal/kernel/db/retention_test.gomoved both to the no-delete (amendable) list;TestMigration00923RevokesDeleteOnGroupAndEntityshows the privilege held at 00922, then absent. Watched both fail with the revoke commented out. - B3 (00924).
shareholding_immutableBEFORE UPDATE trigger: only an open row’seffective_tomay be set, once, and nothing else may change.source_document_idcannot be attached afterwards (no path does). It fires beforeshareholding_within_whole, so an edit of a percentage now fails withshareholding_immutable; the 100% trigger still guards inserts.TestAHoldingMayOnlyBeEndedOnceruns as the table owner: the end date is accepted, then each other column, a re-end and a re-open are refused. Watched it fail with the trigger disabled. Not applied to the other three retained tables (follow-on question in the brief). - B2 (00925).
company_group.namedropped; names aretranslationrows (entity_kind = 'company_group', fieldname, locales en, ms, zh; en required). Existing names backfilled as en rows. One name per owner is the partial unique indextranslation_company_group_name_onceon the English row, mapped toErrDuplicateGroupName.GETreturnsname(caller’s locale, English fallback) andnames;POSTtakesnames. Web form has three inputs. Tests: translation rows, fallback for an en-only group, per-owner uniqueness (present for another owner), shape, migration backfill and Down. Watched uniqueness and fallback fail once.group_entity.legal_nameis unchanged. - B4. No change:
grantee_org_idstays;TestAGrantMayOnlyNameTheOwnerOfTheGroupalready covers the composite key refusing a non-owner.
Open: adviser confirmation of s.4 and the retention period; manual en/ms/zh, phone-width and focus pass on a seeded stack (not done here).
Not in this plan
Section titled “Not in this plan”Consolidation runs, inter-company eliminations, currency translation, consolidated statements, data-returning cross-org read functions, group-level chart templates (architecture spec §20.2). Each needs its own spec.