Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
4.1 KiB
id, title, status, wave, depends_on, plan, spec
| id | title | status | wave | depends_on | plan | spec |
|---|---|---|---|---|---|---|
| 01-unit-tree-storage | Unit tree storage and placement | done | 1 | plan.md | ../../specs/workspaces/requirements/org-units.md |
Task 01: Unit Tree Storage and Placement
Outcome
org_units and unit_members exist, every organization has a root unit, every
user has a personal unit, and every workspace has a unit_id. Reach still comes
from visibility, so behaviour is unchanged at the end of this order.
In scope
org_unitsandunit_memberstables with the indexes named in the design.workspaces.unit_id, not null.internal/orgunit: unit lifecycle, membership rows, tree invariants (single parent, no cycles, one organization, empty before delete), and materialized path maintenance including subtree reparenting.- Root unit created with an organization; personal unit created with a user.
- The one-shot data migration described in the design's Persistence section.
- Carrying
unit_idthrough every migration that recreatesworkspacesortasks.
Out of scope
- Reading the tree for authorization; that is order 02.
- HTTP surface; that is order 03.
- Removing
workspaces.visibility; that is order 05.
Requirements
REQ-WORKSPACES-ORG-UNITS-001, REQ-WORKSPACES-ORG-UNITS-003.
Acceptance criteria: AC-WORKSPACES-ORG-UNITS-001.1 through .7,
AC-WORKSPACES-ORG-UNITS-003.1, .5, .6.
System design
Organization units, sections Data and contracts, Personal units, Persistence.
Implementation acceptance
- A fresh database has one root unit per organization, one personal unit per
user, and no workspace with an empty
unit_id. - Reparenting a unit rewrites the path of every descendant, and a move that would create a cycle is refused.
- The migration places every previously private workspace in its owner's personal unit, and no user reaches a workspace they could not reach before.
Verification
cd apps/backend
go test ./internal/orgunit/... ./internal/task/repository/sqlite/... ./internal/office/repository/sqlite/...
go test ./internal/task/service/ -run 'TestUnitPlacement|TestMigrationDoesNotWidenReach'
Likely files
apps/backend/internal/orgunit/(new)apps/backend/internal/task/repository/sqlite/schema and migrationsapps/backend/internal/office/repository/sqlite/base_migrations.gorebuild listsapps/backend/internal/user/store/personal unit creationapps/backend/internal/org/service.goroot unit creation
Results
Done. internal/orgunit owns the tree, its membership, and its invariants:
single root and single personal unit per user enforced by unique indexes, a
cycle refused on move, protected units refused on move and delete, a personal
unit refusing members, and delete failing closed when the occupancy seam is
unwired. Ancestry is a materialized path, and reparenting rewrites the whole
subtree in one transaction.
workspaces.unit_id is added with its index, and the one-shot backfill gives
every organization a root, every account a personal unit, and every existing
workspace a home. The placement rule is the one that keeps an upgrade from
widening access: an owned workspace lands in its owner's personal unit and never
under the root, including when its owner's account is gone.
New workspaces are placed at creation through a lazy seam rather than a user-creation hook, so an account created by any path gets a personal unit the first time it needs one, with no ordering to get wrong.
Verified on a fresh instance: an unowned pre-authentication workspace sits at the root, and both workspaces created by a signed-in user sit in her personal unit. Every test is mutation-checked; two mutants that only broke the build were rerun as valid mutations rather than counted as caught.
RED/GREEN:
- RED: removing the owner branch from the backfill fails
TestBackfillDoesNotWidenReachandTestBackfillIsolatesWorkspacesOfMissingOwners. - RED: dropping the descendant rewrite from
ReparentfailsTestReparentRewritesDescendantPaths. - GREEN:
go test ./internal/orgunit/... ./internal/task/... ./internal/office/...