Files
summercms/.planning/phases/12-p-ytarium-api-collections-and-albums/12-01-SUMMARY.md

18 KiB

phase, plan, subsystem, tags, requires, provides, affects, actuals, plan_head_before, plan_head_after, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, coverage, duration, completed, status
phase plan subsystem tags requires provides affects actuals plan_head_before plan_head_after tech-stack key-files key-decisions patterns-established requirements-completed coverage duration completed status
12-p-ytarium-api-collections-and-albums 01 api
lagoon
validation
laravel
phrasebook
attach
webp
tide
multipart
beachcomber
typesense
user-groups
phase provides
05-data-layer-full-fidelity lagoon.Validate, attach File/Thumb/BlobKey, phrasebook catalogs
phase provides
11-jobs-realtime-and-search-infrastructure beachcomber Engine/Query, typesense engine, tide broadcast goldens
phase provides
07-user-plugin-and-authentication Go user plugin (sm-user-plugin) models, migrations and Postgres test harness
lagoon.ValidateRequest with Laravel 9 semantics and the pl/en lagoon::validation catalogs
ParseRules, In, CustomRule, UploadedFile/UploadedFileFromHeader, ErrorKeys
lagoon.Validate min/max/numeric-between message fix (todo folded)
attach.PublicURL, (*attach.File).URL, webp decoding (golang.org/x/image v0.46.0)
tide multipart Parts with fixed MultipartBoundary, upload URL masking, album date masking in publications
beachcomber PageSearcher/SearchResult/SearchPage and Query.QueryByWeights (typesense query_by_weights, MaxPerPage 250)
user_groups/users_groups tables, models.UserGroup, User.Groups, classes.UserGroupCodes/HasGroupCode
ROADMAP Phase 12-14 and REQUIREMENTS API-01/02/03/07, INTG-01 rewording
12-02
12-03
12-04
12-05
12.1
13
14
tokens tasks commits
55700 4 8
cfe568a214 1bdbdf041b
added patterns
golang.org/x/image v0.46.0 (direct
summercms.go)
Request rule tables are package-level []lagoon.RequestRule built with ParseRules; a bad rule panics at boot
Optional engine capability via a type-asserted interface plus a fallback helper (PageSearcher/SearchPage)
Multipart fixtures: file bytes beside the fixture, pinned by sha256, encoded with one fixed boundary
created modified
modules/lagoon/validate_request.go
modules/lagoon/validate_rules.go
modules/lagoon/validate_request_test.go
modules/phrasebook/lang/pl/validation.yaml
modules/phrasebook/lang/en/validation.yaml
modules/lagoon/attach/url_test.go
modules/tide/multipart.go
modules/tide/multipart_test.go
modules/beachcomber/searchpage_test.go
modules/beachcomber/typesense/searchpage_test.go
../fonoteka.go/plugins/golem15/user/models/user_group.go
../fonoteka.go/plugins/golem15/user/updates/202610020001_create_user_groups.go
../fonoteka.go/plugins/golem15/user/updates/user_groups_test.go
../fonoteka.go/plugins/golem15/user/classes/user_groups.go
modules/lagoon/validate.go
modules/lagoon/attach/thumb.go
modules/tide/flow.go
modules/tide/fixture.go
modules/tide/record.go
modules/tide/replay.go
modules/tide/diff.go
modules/tide/normalize.go
modules/tide/variables.go
modules/tide/centrifugo_golden.go
modules/beachcomber/searchable.go
modules/beachcomber/engines.go
modules/beachcomber/typesense/engine.go
../fonoteka.go/plugins/golem15/user/models/user.go
../fonoteka.go/parity/schema_diff_test.go
go.mod
Wildcard rules follow Laravel exactly: a wildcard with nothing to expand adds no attribute (the plan's 'absent parent still fires required' contradicted Validator.php and the live PHP validator)
ErrorKeys takes the rule table as a second argument: a Go map cannot carry the message-bag order, so the order is rebuilt from the rules (explicit attributes first, then wildcard expansions by rule, indexes ascending)
exists:table,column compares CAST(column AS TEXT) with the value's PHP string form, so a non-numeric value against an integer column is a validation failure, never a Postgres error
With a nil translator ValidateRequest returns Laravel's untranslated keys (validation.required); callers pass the app translator
lagoon.Validate numeric between failures use the new lagoon::validation.between.numeric line; every existing lagoon::validate text is unchanged
user_groups timestamps are TIMESTAMPTZ NULL, matching the plugin's other tables, rather than plain TIMESTAMP
Laravel validator parity is checked against the real Winter validator (vendor autoload + Winter Factory) before pinning Go expectations
API-01
API-02
id description requirement verification human_judgment
D1 lagoon.ValidateRequest reproduces Laravel 9 request validation (implicit stop, wildcards, size messages by type, character counts, decimal bounds) with pl/en Winter catalogs API-02
kind ref status
unit modules/lagoon/validate_request_test.go#TestValidateRequest* pass
kind ref status
other php vendor Winter Validator cross-check of 11 cases (posts [], wildcards, garbage date, min.numeric, presence, order, integer, email, boolean) pass
false
id description requirement verification human_judgment
D2 lagoon.Validate answers a numeric range failure with the bound that failed (min, max or numeric between) API-01
kind ref status
unit modules/lagoon/validate_test.go#TestValidateNumericRangeMessagePicksFailedBound pass
kind ref status
integration go -C ../fonoteka.go test ./parity -run TestUserAPINuxtFlows pass
false
id description requirement verification human_judgment
D3 attach.PublicURL/File.URL emit Winter URLs under the Winter layout config; webp originals decode and thumbnail as JPEG bytes API-02
kind ref status
unit modules/lagoon/attach/url_test.go#TestFileURLWinterLayout pass
kind ref status
unit modules/lagoon/attach/url_test.go#TestThumbWebP pass
false
id description requirement verification human_judgment
D4 tide records and replays multipart bodies byte-identically, refuses tampered part files, masks upload URLs by shape and album dates in publications API-02
kind ref status
unit modules/tide/multipart_test.go#TestMultipart* pass
kind ref status
unit modules/tide/multipart_test.go#TestNormalizeUploadURL* pass
kind ref status
unit modules/tide/multipart_test.go#TestNormalizePublicationAlbumDates pass
false
id description requirement verification human_judgment
D5 beachcomber exposes the engine found count through PageSearcher/SearchPage and sends query_by_weights API-02
kind ref status
unit modules/beachcomber/searchpage_test.go#TestSearchPage* pass
kind ref status
unit modules/beachcomber/typesense/searchpage_test.go#TestTypesenseSearchPage* pass
false
id description requirement verification human_judgment
D6 User groups tables, model, relation and group-code queries in sm-user-plugin with the user API unchanged API-01
kind ref status
integration ../fonoteka.go/plugins/golem15/user/updates/user_groups_test.go#TestUserGroups* pass
kind ref status
integration go -C ../fonoteka.go test ./parity -run '^(TestSchemaMatchesPHPSnapshot|TestUserAPINuxtFlows)$' pass
false
id description verification human_judgment rationale
D7 ROADMAP and REQUIREMENTS carry the D-03/D-04/D-06/D-19/D-20 Phase 12 boundary
true Planning-doc wording; grep checks pass, but whether it reads as the user intended is a human call
39min 2026-10-02 complete

Phase 12 Plan 01: Framework gaps, user groups and Phase 12 scope Summary

Laravel 9 request validator with WinterCMS pl/en catalogs, Winter upload URLs and webp, multipart tide fixtures with shape-masked upload URLs, a re-gateable search found count with field weights, and Winter user groups in sm-user-plugin.

Performance

  • Duration: 39 min
  • Started: 2026-10-02T09:09:24Z
  • Completed: 2026-10-02T09:48:46Z
  • Tasks: 4
  • Files modified: 57 (39 summercms.go, 8 fonoteka.go, 10 sm-user-plugin)

Accomplishments

  • lagoon.ValidateRequest ports Laravel 9 request validation: wildcard expansion in Laravel's attribute order, implicit-rule stop, bail, sometimes/nullable/blank skipping, size messages split by type, string length in characters and exact decimal bounds. {"posts":[]} against required|array|min:1 gives the single Pole posts jest wymagane. in Polish. The Go expectations were checked case by case against the real Winter validator run from the PHP vendor tree.
  • The lagoon::validation pl/en catalogs are generated verbatim from Winter's validation.php files. Keys missing in Polish (after_or_equal, before_or_equal) fall back to English.
  • The lagoon-validate-min-message todo is folded: lagoon.Validate now reports the bound that failed, and every user-plugin 422 fixture still replays.
  • attach.PublicURL and (*attach.File).URL produce Winter's File::getPath() URLs. The webp decoder is registered, and the thumbnailer reads the image size from the header before it decodes.
  • tide requests can carry multipart parts, encoded with the fixed tide.MultipartBoundary. File bytes stay beside the fixture, pinned by sha256. Upload URLs under url/thumb_url are compared by shape (default prefix /storage/app/uploads/public), and Carbon dates in the published album subtree are masked.
  • beachcomber.PageSearcher, beachcomber.SearchResult and beachcomber.SearchPage expose the engine's found count. Query.QueryByWeights is sent as query_by_weights. A Typesense page above 250 is refused before any request is sent.
  • sm-user-plugin gains user_groups (with the Guest and Registered seed rows) and the users_groups pivot, plus models.UserGroup, a User.Groups relation that is never serialized, and classes.UserGroupCodes/HasGroupCode. No controller changed.
  • ROADMAP Phases 12-14 and REQUIREMENTS API-01/02/03/07 and INTG-01 now state the locked Phase 12 boundary.

Task Commits

  1. Task 1: Laravel request validator (tracer): f9b7f2e (feat, summercms.go)
  2. Task 2: uploads, webp, tide multipart and masks: e06e0cc (feat, summercms.go); c65ab3c (build, sm-user-plugin); 6d6c827 (build, fonoteka.go)
  3. Task 3: search found/weights and user groups: 3ae49bb (feat, summercms.go); 8697007 (feat, sm-user-plugin); 02e1aa9 (feat, fonoteka.go, pointer bump)
  4. Task 4: roadmap and requirements wording: 1bdbdf0 (docs, summercms.go)

Per-repo plan ledgers (commits measured with git rev-list --count):

  • summercms.go: cfe568a..1bdbdf0, 4 commits
  • fonoteka.go: 18576db..02e1aa9, 2 commits
  • sm-user-plugin: 44f3c4c..8697007, 2 commits

Files Created/Modified

  • modules/lagoon/validate_request.go: ValidateRequest, RequestRule, Rule, CustomRule, In, UploadedFile, ErrorKeys, the expansion and message logic
  • modules/lagoon/validate_rules.go: ParseRules and the rule implementations (FILTER_VALIDATE_EMAIL, the Laravel url regex rewritten for RE2, dates, exists, mimes, size)
  • modules/lagoon/validate.go: numericRangeMessage (the min/between fix); removed the now-unused moneyInRange
  • modules/phrasebook/lang/{pl,en}/validation.yaml: Winter catalog ports
  • modules/lagoon/attach/thumb.go: PublicURL, File.URL, webp import, DecodeConfig before Decode
  • modules/tide/multipart.go (+ flow, fixture, record, replay, variables, diff, normalize, centrifugo_golden): Parts, MultipartBoundary, upload and album-date masking
  • modules/beachcomber/searchable.go, engines.go, typesense/engine.go: PageSearcher, SearchResult, SearchPage, QueryByWeights, MaxPerPage
  • ../fonoteka.go/plugins/golem15/user/...: UserGroup model, Groups relation, migration 202610020001_create_user_groups, group-code queries, README
  • ../fonoteka.go/parity/schema_diff_test.go, parity/migrate_test.go, plugins/golem15/fonoteka/classes/hidden_marshal_test.go: allow-list and count updates
  • READMEs and docs: modules/lagoon/README.md, modules/phrasebook/README.md, modules/tide/README.md, modules/beachcomber/README.md, docs/database/casts-and-validation.md, docs/database/attachments.md, docs/services/storage.md, docs/services/parity-testing.md, docs/services/search.md, docs/services/localization.md

Decisions Made

See key-decisions in the frontmatter. The two that change the plan's literal text are written up under Deviations.

Deviations from Plan

Auto-fixed Issues

1. [Rule 1 - Bug] Wildcard with an absent parent adds no attribute (plan said required still fires)

  • Found during: Task 1
  • Issue: The plan said an absent parent should expand to the literal attribute so that required still fires. Laravel's ValidationRuleParser::explodeWildcardRules drops a wildcard rule that has nothing to expand, and the real Winter validator returns [] for {} against posts.*.title: required.
  • Fix: Implemented Laravel's behaviour and pinned it with TestValidateRequestWildcardWithoutParentAddsNothing.
  • Files modified: modules/lagoon/validate_request.go
  • Commit: f9b7f2e

2. [Rule 3 - Blocking] ErrorKeys(errs, rules) takes the rule table

  • Found during: Task 1
  • Issue: The planned ErrorKeys(errs map[string][]string) cannot recover an order from a Go map.
  • Fix: Added the rule table as a second argument. The function rebuilds Laravel's message-bag order from the rules (cross-checked against PHP output).
  • Commit: f9b7f2e

3. [Rule 3 - Blocking] Acceptance grep grep -c 'Pole :attribute jest wymagane.' prints 7, not 1

  • Found during: Task 1
  • Issue: The faithful port carries every Winter key (filled, required_if, required_with and others). In grep, the unescaped . matches the space in wymagane gdy, so the pattern hits 7 lines.
  • Fix: None needed. The exact line Pole :attribute jest wymagane." appears for required and filled, as in Winter (grep -cF prints 2). The port stays faithful.

4. [Rule 2 - Missing critical] Thumbnailer checks dimensions before decoding

  • Found during: Task 2 (T-12-16)
  • Issue: File.Thumb decoded the whole image before its pixel check. A small webp or png that declares a huge canvas would allocate the full image first.
  • Fix: It now reads the bounded bytes, runs image.DecodeConfig, refuses anything over 4096x4096, and only then decodes.
  • Commit: e06e0cc

5. [Rule 3 - Blocking] Migration and model counts in existing tests

  • Found during: Task 3
  • Issue: Adding a sixth user migration and a fourth user model broke hard-coded counts in updates/organisations_test.go and updates/user_session_test.go (sm-user-plugin), and in parity/migrate_test.go (three assertions) and plugins/golem15/fonoteka/classes/hidden_marshal_test.go (fonoteka.go).
  • Fix: Updated the expected histories, rollback counts and model count. No production code changed.
  • Commits: 8697007, 02e1aa9

6. [Rule 3 - Blocking] tide RecordConfig.BaseDir, ReplayConfig.UploadPrefix and tide.DefaultUploadPrefix

  • Found during: Task 2
  • Issue: Recording needs a directory to read part files from, and the upload prefix had to be configurable.
  • Fix: Added both fields and the default constant. All are additive and documented in the tide README.
  • Commit: e06e0cc

7. [Orchestrator instruction] sm-user-plugin not pushed

  • The plan says to push sm-user-plugin master before the pointer bump. The orchestrator prompt says not to push anywhere, so commits c65ab3c and 8697007 exist locally only. The fonoteka.go pointer references them.

Total deviations: 6 auto-fixed (1 bug, 1 missing critical, 4 blocking) plus 1 instruction-driven. Impact on plan: All deliverables shipped. The deviations follow the PHP contract or keep existing tests honest; there is no scope creep.

Issues Encountered

  • One full go test ./... run in summercms.go failed in modules/lighthouse: its testcontainers Postgres did not become ready in time under parallel package load. lighthouse was not touched by this plan. It passed alone, and a second full run was green.
  • All commits were made on master in every repo (the project uses branching_strategy: none, and the orchestrator dispatched a sequential executor on the main tree). The generic protected-branch guard would refuse master. The orchestrator's instruction and the project convention were followed instead.
  • .planning/ROADMAP.md already held an uncommitted Phase 12.1 insertion that the executor did not make. The Task 4 commit staged only this plan's hunks.

Known Stubs

None.

User Setup Required

None. No external service configuration is required. The app config change that adopts the Winter upload prefix (storage.uploads.bucket_url / public_path_prefix in fonoteka.go config/storage.yaml) belongs to 12-02/12-04.

Next Phase Readiness

  • 12-02 can build the collection endpoints on ValidateRequest, HasGroupCode(..., "admin"), attach.File.URL and tide multipart.
  • 12-04 still has to set the Winter upload layout in config/storage.yaml and check that avatar URLs stay correct. It uses beachcomber.SearchPage for the D-19 recount.
  • The PHP-oracle approach (vendor autoload plus Winter\Storm\Validation\Factory) is available to 12-05 for table-driven validator coverage.

Self-Check: PASSED

All 15 created files listed above exist on disk. All 8 task commits exist: f9b7f2e, e06e0cc, 3ae49bb and 1bdbdf0 in summercms.go; 6d6c827 and 02e1aa9 in fonoteka.go; c65ab3c and 8697007 in sm-user-plugin. Plan-level verification passed: summercms.go go vet ./... && go test ./... (green on rerun), TestDocsTree, and fonoteka.go vet plus tests for the root module, the fonoteka plugin and the user plugin.


Phase: 12-p-ytarium-api-collections-and-albums Completed: 2026-10-02