docs: move golem15.user to sm-user-plugin and supersede the Phase 1 extraction deferral

- new note .planning/notes/core-plugins-own-repos.md: shared core plugins live in sm-<name>-plugin repos mounted as submodules
- 01-CONTEXT deferral points to the note; PROJECT constraint and Key Decisions row
- ROADMAP Phase 12 repos and the 12-01 entry, and Phase 12 plans 12-01, 12-02, 12-05 name sm-user-plugin and the submodule commit workflow
This commit is contained in:
Jakub Zych
2026-10-02 11:02:07 +02:00
parent b3fda9188f
commit 947aabee93
7 changed files with 53 additions and 11 deletions

View File

@@ -108,7 +108,7 @@ Phase 1 delivers the framework kernel in `summercms.go` only: config (`compass`)
- Scaffolding of model, migration, command, job and admin-controller stubs by `make:*` commands — Phase 4 (CLI-02). Phase 1's `make:plugin` produces only the compiling minimum.
- Admin config editor consuming `Persist` overrides — later admin phase; Phase 1 only provides the API.
- Extraction of shared stack plugins into their own repos — only when a second app (keios.eu) needs one.
- Extraction of shared stack plugins into their own repos — only when a second app (keios.eu) needs one. Superseded 2026-10-02 by `.planning/notes/core-plugins-own-repos.md` (golem15.user moved to sm-user-plugin).
- One-line corrections to research docs (module path, air, `plugins.<name>` sketch) — do as a docs commit alongside Phase 1 planning, not as code.
</deferred>

View File

@@ -135,12 +135,12 @@ ROADMAP Phase 12 goal (verbatim, not in user-story form; MVP precedent of Phases
This plan's slice: the framework can produce every Laravel 422 body, record and replay multipart uploads, emit Winter-shaped upload URLs, decode webp, report the search engine's `found` count and rank fields by weight; the user plugin knows user groups; and the roadmap and requirements say what Phase 12 actually ships. Nothing here is user-visible on its own: plans 12-02 to 12-04 build every endpoint on these pieces.
<objective>
Close the framework gaps RESEARCH Finding 5 lists (summercms.go), add user groups to the Go user plugin (fonoteka.go, the user plugin lives at `../fonoteka.go/plugins/golem15/user`), and correct the planning-doc wording.
Close the framework gaps RESEARCH Finding 5 lists (summercms.go), add user groups to the Go user plugin (repo sm-user-plugin, module `git.golem15.com/golem15/sm-user-plugin`, mounted as a git submodule at `../fonoteka.go/plugins/golem15/user`), and correct the planning-doc wording.
Purpose: every Phase 12 endpoint needs Laravel-exact 422 bodies (D-21), multipart recordings (D-11), PHP upload URLs (D-22), webp (D-24), a re-gated search total (D-19) and the site-admin predicate (D-25).
Output: `lagoon.ValidateRequest` with pl/en catalogs; attach `URL`/`PublicURL` and webp; tide multipart and upload/publication masks; beachcomber `PageSearcher` and weights; user groups; README and docs updates; reworded ROADMAP/REQUIREMENTS.
Repos: summercms.go (framework and planning docs) and fonoteka.go (user plugin). Framework code, tests, READMEs and docs use neutral names (acme, blog, posts) and never name the application. Planning docs and code go in separate commits; never add co-author tags.
Repos: summercms.go (framework and planning docs), sm-user-plugin (user groups, committed inside the submodule and pushed to its origin master first) and fonoteka.go (parity schema allow-list plus the bumped submodule pointer). Framework code, tests, READMEs and docs use neutral names (acme, blog, posts) and never name the application. Planning docs and code go in separate commits; never add co-author tags.
</objective>
<execution_context>
@@ -268,11 +268,13 @@ Detector run on the Phase 12 ROADMAP section: detected=false. Considered D-25 (u
(2) Docs in the same change: modules/beachcomber/README.md and docs/services/search.md (PageSearcher, SearchResult, the helper, QueryByWeights, the 250 per-page limit, and that ids remain candidates only that callers re-gate in SQL).
(3) User groups, per D-25 (fonoteka.go user plugin, additive): models/user_group.go `UserGroup{ID uint; Name string; Code *string; Description *string; Permissions *string; CreatedAt, UpdatedAt *time.Time}` with TableName `user_groups`, registered in the models registry like the other models; add `Groups []UserGroup` to models.User with tag `gorm:"many2many:users_groups;joinForeignKey:user_id;joinReferences:user_group_id"` and `json:"-"` (GORM never writes it unless a caller associates groups; nothing in the user plugin does). Migration file updates/202610020001_create_user_groups.go (ID `202610020001_create_user_groups`): create `user_groups` (id serial primary key, name varchar(255) not null, code varchar(255) null with an index, description text null, permissions text null, created_at and updated_at timestamp null) and `users_groups` (user_id integer not null, user_group_id integer not null, primary key (user_id, user_group_id) named user_group), then insert the Guest/guest and Registered/registered rows with PHP's descriptions; Rollback drops both tables. classes/user_groups.go: `UserGroupCodes(ctx, db, userID uint) ([]string, error)` (codes of the user's groups ordered by user_groups.id, null codes skipped) and `HasGroupCode(ctx, db, userID uint, code string) (bool, error)`. The user API payload keeps `"groups":[]` exactly as today (D-25: contract unchanged); do not read the new table in any user-plugin handler.
(3) User groups, per D-25 (sm-user-plugin, additive): models/user_group.go `UserGroup{ID uint; Name string; Code *string; Description *string; Permissions *string; CreatedAt, UpdatedAt *time.Time}` with TableName `user_groups`, registered in the models registry like the other models; add `Groups []UserGroup` to models.User with tag `gorm:"many2many:users_groups;joinForeignKey:user_id;joinReferences:user_group_id"` and `json:"-"` (GORM never writes it unless a caller associates groups; nothing in the user plugin does). Migration file updates/202610020001_create_user_groups.go (ID `202610020001_create_user_groups`): create `user_groups` (id serial primary key, name varchar(255) not null, code varchar(255) null with an index, description text null, permissions text null, created_at and updated_at timestamp null) and `users_groups` (user_id integer not null, user_group_id integer not null, primary key (user_id, user_group_id) named user_group), then insert the Guest/guest and Registered/registered rows with PHP's descriptions; Rollback drops both tables. classes/user_groups.go: `UserGroupCodes(ctx, db, userID uint) ([]string, error)` (codes of the user's groups ordered by user_groups.id, null codes skipped) and `HasGroupCode(ctx, db, userID uint, code string) (bool, error)`. The user API payload keeps `"groups":[]` exactly as today (D-25: contract unchanged); do not read the new table in any user-plugin handler.
(4) parity/schema_diff_test.go: add allowedDiffs entries `user_groups` and `users_groups` with a reason naming D-25 and that the frozen PHP snapshot predates the user-plugin dump (the same justification as user_throttle). Keep every other entry.
(5) Tests: updates/user_groups_test.go on the plugin's Postgres harness: migrate up creates both tables and the two seed rows, RollbackLast drops them, re-up works; UserGroupCodes returns `admin` for a user linked to a group with code admin and an empty slice for a user with no groups.</action>
(5) Tests: updates/user_groups_test.go on the plugin's Postgres harness: migrate up creates both tables and the two seed rows, RollbackLast drops them, re-up works; UserGroupCodes returns `admin` for a user linked to a group with code admin and an empty slice for a user with no groups.
(6) Commits: commit the user-plugin files inside the submodule (`git -C ../fonoteka.go/plugins/golem15/user`), push its master, then commit parity/schema_diff_test.go together with the bumped submodule pointer in fonoteka.go, no co-author tags.</action>
<verify>
<automated>go vet ./... &amp;&amp; go test ./modules/beachcomber/... -count=1 -v -run '^(TestSearchPage.*|TestTypesenseSearchPage.*)$' &amp;&amp; go test ./cmd/summer -run TestDocsTree -count=1 &amp;&amp; go -C ../fonoteka.go vet ./... &amp;&amp; go -C ../fonoteka.go test ./plugins/golem15/user/updates -count=1 -v -run '^(TestUserGroups.*)$' &amp;&amp; go -C ../fonoteka.go test ./parity -run '^(TestSchemaMatchesPHPSnapshot|TestUserAPINuxtFlows)$' -count=1</automated>
<fails_when>Any command exits non-zero; a verbose run prints "no tests to run", "--- FAIL" or "--- SKIP", or lacks "--- PASS" for a TestSearchPage test and a TestUserGroups test; TestSchemaMatchesPHPSnapshot reports an extra Go table; TestUserAPINuxtFlows fails (user payload changed).</fails_when>
@@ -283,7 +285,7 @@ Detector run on the Phase 12 ROADMAP section: detected=false. Considered D-25 (u
- `grep -c 'PageSearcher' modules/beachcomber/README.md` and `grep -c 'PageSearcher' docs/services/search.md` each print at least 1.
- `grep -c '"user_groups"' ../fonoteka.go/parity/schema_diff_test.go` and `grep -c '"users_groups"' ../fonoteka.go/parity/schema_diff_test.go` each print 1.
- `grep -c 'json:"-"' ../fonoteka.go/plugins/golem15/user/models/user.go` increases by one relative to HEAD (the Groups field is never serialized).
- `grep '"groups":' ../fonoteka.go/plugins/golem15/user/controllers/api_controller.go | grep -c '\[\]any{}'` prints 1, and this task's fonoteka.go commit touches no file under `plugins/golem15/user/controllers/` (payload untouched).
- `grep '"groups":' ../fonoteka.go/plugins/golem15/user/controllers/api_controller.go | grep -c '\[\]any{}'` prints 1, and this task's sm-user-plugin commit touches no file under `controllers/` (payload untouched).
</acceptance_criteria>
<done>Callers can ask the search engine for one page of candidate ids and the total it found, ranked with explicit weights, and any plugin can ask which group codes a user has without the user API changing.</done>
</task>

View File

@@ -142,7 +142,7 @@ Repo: fonoteka.go only (summercms.go untouched). No Nuxt or fonoteka-mcp change.
@../fonoteka.go/parity/README.md
<interfaces>
- From 12-01: `lagoon.ValidateRequest(ctx, tx, input, []lagoon.RequestRule{{Field, Rules: lagoon.ParseRules("...")}}, tr)`, `lagoon.UploadedFileFromHeader`, `attach.PublicURL`, `(*attach.File).URL()`, tide `Request.Parts`, user plugin `classes.HasGroupCode(ctx, db, userID, "admin")`.
- From 12-01: `lagoon.ValidateRequest(ctx, tx, input, []lagoon.RequestRule{{Field, Rules: lagoon.ParseRules("...")}}, tr)`, `lagoon.UploadedFileFromHeader`, `attach.PublicURL`, `(*attach.File).URL()`, tide `Request.Parts`, user plugin `classes.HasGroupCode(ctx, db, userID, "admin")` (import path `git.golem15.com/golem15/sm-user-plugin/classes`).
- fonoteka today: `classes.AccessibleByMembership(userID)`, `classes.ResolveActiveCollection(ctx, gdb, userID)` (JWT default path only; callers: controllers/genre_controller.go ListGenres and controllers/api/oauth_consent_controller.go), `persistContext` (INSERT ... ON CONFLICT (user_id)), `classes.SaveCollection(ctx, gdb, c, requested)` with `CollectionFillFields = {name, description}`, `classes.FieldErrors`, `classes.SerializeCollection` (4-key stub), `models.Collection{ID, OwnerID, Owner, Name, Description, Kind, PublicToken, PublicEnabled, PublicTokenGeneratedAt, ReservationsAllowed, Editors, DeletedAt, CreatedAt, UpdatedAt}` with `BeforeDelete` doing a bulk album delete, `models.ApiToken{CollectionIDs lagoon.Jsonable[[]uint], Scopes}`, `models.UserCollectionContext`, `models.OrgAiCredential`, `models.UserAiCredential`, `models.UserDiscogsCredential`, `models.OrgDiscogsCredential`, `models.AlbumMarketCurrencies`.
- Request context: `bouncer.User(ctx) (*bouncer.Principal, bool)`; the personal token is `cred, _ := bouncer.Credential(ctx); tok, _ := cred.(*models.ApiToken)` (middleware/token_scope.go); `wire.WriteJSON` (no trailing newline), `wire.WriteOpaque500`; responses carry `Cache-Control: no-cache, private` and `Content-Type: application/json`.
- surf: `g.Get/Post/Put/Delete(path, h, mw...)`, `g.Where(param, regex)` constrains only the immediately preceding route (Pitfall 2), inline `throttle:N,M`, `body.limit:<bytes>`.

View File

@@ -196,7 +196,7 @@ Repos: summercms.go (framework tests, gate script, planning docs) and fonoteka.g
(3) scripts/check-phase12.sh modelled on check-phase11.sh: stages `--go` (vet and test both repos), `--parity` (TestParityCorpus with 99 ported and 0 failing parsed from the coverage line, TestBroadcastGoldens all four passing, TestFonotekaNuxtFlows both subtests, check_corpus --require-recorded --check-secrets), `--named` (every test named in 12-VALIDATION.md run by exact name with -run '^Name$', refusing skip, fail and "no tests to run"), `--removal` (anchor-exact mutations restored byte for byte with cmp: drop AlbumsAccessibleBy from the search re-gate, drop the token narrowing in AccessibleBy, drop the owner check in share, add owner_id to CollectionFillFields, drop the sha256 lookup in accept, swap PublicOnly for no policy in the manual fetcher, make the invitation args carry the plaintext token; each must make its named test fail on an assertion), `--coverage` (go test -coverprofile per listed package; refuse any package below 80% with its number), `--evidence` (12-SECURITY-REVIEW.md lists every T-12 id with a passing test, 12-VALIDATION.md has no pending or TBD row and nyquist_compliant true), `--all`, and `--self-test` proving each detector fails closed on planted inputs. It never names the application in framework-facing output beyond the paths it must call.
(4) Planning docs (separate commit): 12-SECURITY-REVIEW.md (reviewer note: self-performed by the executor if no reviewer agent can be spawned, per the 08-10 precedent; table of T-12-01..T-12-34 and T-12-SC with category, severity, disposition, protecting file and the test name that was run and seen failing under --removal); 12-VALIDATION.md: replace the seeded Per-Task Verification Map rows with the final rows (task ids 12-01-T1..12-05-T3, the exact commands each plan ran, file-exists ticks, statuses), tick Wave 0 items, set status validated, nyquist_compliant true, wave_0_complete true; REQUIREMENTS.md traceability rows API-01 and API-02 set to Complete and their checkboxes ticked.</action>
(4) Planning docs (separate commit): 12-SECURITY-REVIEW.md (reviewer note: self-performed by the executor if no reviewer agent can be spawned, per the 08-10 precedent; table of T-12-01..T-12-34 and T-12-SC with category, severity, disposition, protecting file and the test name that was run and seen failing under --removal); 12-VALIDATION.md: replace the seeded Per-Task Verification Map rows with the final rows (task ids 12-01-T1..12-05-T3, the exact commands each plan ran, file-exists ticks, statuses), tick Wave 0 items, set status validated, nyquist_compliant true, wave_0_complete true; REQUIREMENTS.md traceability rows API-01 and API-02 set to Complete and their checkboxes ticked. The user plugin test file lives in the sm-user-plugin submodule; commit it there and push its master, then commit the bumped pointer in fonoteka.go with the other fonoteka.go test changes.</action>
<verify>
<automated>scripts/check-phase12.sh --self-test &amp;&amp; scripts/check-phase12.sh --all</automated>
<fails_when>Non-zero exit; output contains "refuse:" or "FAIL", a package coverage line below 80%, a named test reported skipped or with "no tests to run", a --removal mutation whose named test still passes, or the evidence stage reporting a pending row or a T-12 id without a test.</fails_when>