Files
summercms/.planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-01-SUMMARY.md

16 KiB

phase, plan, subsystem, tags, requires, provides, affects, actuals, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, coverage, duration, completed, status, plan_head_before, plan_head_after, fonoteka_plan_head_before, fonoteka_plan_head_after
phase plan subsystem tags requires provides affects actuals tech-stack key-files key-decisions patterns-established requirements-completed coverage duration completed status plan_head_before plan_head_after fonoteka_plan_head_before fonoteka_plan_head_after
13-p-ytarium-api-wishlist-notifications-csv-credentials-public 01 api
surf
servemux
conga
river
lagoon
validation
tide
parity
job-contract
phase provides
11 conga jobs on River, lighthouse publications, tide broadcast goldens
phase provides
12 fonoteka route groups, parity harness with the fonoteka seed hook, invitation mail job pattern
surf overlap families
PHP-style overlapping constrained routes boot and dispatch in registration order
conga ErrUnregisteredKindQueue and insert-only routing for job kinds whose worker ships later
lagoon request rule prohibited
tide Content-Disposition date mask and notification publication masks ($.data.payload.created_at, $.data.payload.id)
fonoteka classes/job_contract.go (CSV import, CSV match, wishlist digest, purchase mail kinds, queues, labels, args)
php_parity.sh QUEUE_CONNECTION override and rows subcommand; share:wishlist capture; check_corpus ported case-status check
ROADMAP/REQUIREMENTS reworded for the D-01/D-02/D-06 Phase 13/14 boundary
13-02
13-03
13-04
13-05
13-06
14
tokens tasks commits
24500 4 7
added patterns
Overlap family: routes ServeMux refuses side by side register under one generated method-less pattern; members are tried in registration order on literals and Where constraints
Workerless jobs: a kind with no registered job is inserted by the insert-only River client onto a queue no worker serves
Header masks assert the masked value's shape (real calendar date) and fall back to byte comparison
created modified
summercms.go/modules/surf/overlap.go
summercms.go/modules/surf/overlap_test.go
summercms.go/modules/conga/unregistered_kind_test.go
summercms.go/modules/tide/normalize_phase13_test.go
fonoteka.go/plugins/golem15/fonoteka/classes/job_contract.go
fonoteka.go/plugins/golem15/fonoteka/classes/job_contract_test.go
fonoteka.go/plugins/golem15/fonoteka/job_contract_worker_test.go
fonoteka.go/plugins/golem15/fonoteka/routes_overlap_test.go
summercms.go/modules/surf/router.go
summercms.go/modules/conga/conga.go
summercms.go/modules/lagoon/validate_rules.go
summercms.go/modules/tide/diff.go
summercms.go/modules/tide/centrifugo_golden.go
summercms.go/modules/tide/multipart_test.go
summercms.go/modules/{surf,conga,lagoon,tide}/README.md
summercms.go/docs/services/routing.md
summercms.go/docs/services/jobs.md
summercms.go/docs/services/parity-testing.md
summercms.go/docs/database/casts-and-validation.md
fonoteka.go/parity/php_parity.sh
fonoteka.go/parity/capture-rules.yaml
fonoteka.go/parity/check_corpus.go
fonoteka.go/parity/check_corpus_test.go
fonoteka.go/parity/manifest.yaml
fonoteka.go/parity/README.md
summercms.go/.planning/ROADMAP.md
summercms.go/.planning/REQUIREMENTS.md
River rejects dotted queue names (^[a-z0-9]+([_|-]?[a-z0-9]+)*$ on every insert), so the D-03 job contract keeps its kinds and dotted labels and spells the three workerless queues with underscores: fonoteka_csv_import, fonoteka_csv_match, fonoteka_wishlist_digest. No row carries them yet; flagged for the user.
conga refuses an unregistered kind's empty or served queue only while a worker runs, because plugin jobs are registered at worker start; a process without a worker keeps today's behaviour so work_in_serve: false deployments are not broken. Unregistered kinds always use the insert-only client, so a worker starting concurrently cannot route them through River's kind check.
surf keeps same-method same-shape routes (differing only in parameter names), {name...}/{$}/trailing-slash members and a more general route shadowing a member as boot errors; only constraint-separated overlaps become families.
check_corpus matches a case against every fixture step whose route_id names the route (fixtures carry setup steps of other routes); two ported oauth cases (consent 500, deny 404) and the D-15 invitation case were corrected to their recorded fixtures.
Overlapping constrained routes: declare them in routes.php order; surf builds the family and keeps Routes()/route:list one entry per route
Jobs whose worker ships later: Dispatch onto an unserved, River-valid queue; never list it in config/queue.yaml until the worker exists
API-03
API-04
API-05
API-06
API-07
id description requirement verification human_judgment
D1 surf registers PHP's overlapping constrained routes and dispatches them in registration order with per-member path values, middleware, 404 and ServeMux-equivalent 405/Allow API-03
kind ref status
unit summercms.go/modules/surf/overlap_test.go#TestOverlappingConstrainedRoutes pass
kind ref status
unit fonoteka.go/plugins/golem15/fonoteka/routes_overlap_test.go#TestWishlistOverlapPatternsDispatch pass
false
id description requirement verification human_judgment
D2 conga queues an unregistered job kind while a worker runs, refuses empty or served queues with ErrUnregisteredKindQueue, and the fonoteka job contract is pinned API-05
kind ref status
integration summercms.go/modules/conga/unregistered_kind_test.go#TestUnregisteredKindWithWorker pass
kind ref status
unit fonoteka.go/plugins/golem15/fonoteka/classes/job_contract_test.go#TestJobContract pass
kind ref status
integration fonoteka.go/plugins/golem15/fonoteka/job_contract_worker_test.go#TestJobContractDispatchWhileWorkerRuns pass
false
id description requirement verification human_judgment
D3 lagoon prohibited rule with Laravel 9 semantics and the literal validation.prohibited message API-03
kind ref status
unit summercms.go/modules/lagoon/validate_request_test.go#TestValidateRequestProhibited pass
false
id description requirement verification human_judgment
D4 tide masks Content-Disposition dates and notification publication id/created_at without hiding real differences API-05
kind ref status
unit summercms.go/modules/tide/normalize_phase13_test.go#TestNormalizeContentDispositionDate pass
kind ref status
unit summercms.go/modules/tide/normalize_phase13_test.go#TestNormalizeNotificationPublication pass
kind ref status
integration go -C ../fonoteka.go test ./parity -run '^(TestUserAPINuxtFlows|TestBroadcastGoldens)$' pass
false
id description requirement verification human_judgment
D5 Parity tooling: QUEUE_CONNECTION override, rows dump, share:wishlist capture, ported case-status check, D-15 manifest fix; corpus still 99 ported and passing API-07
kind ref status
unit fonoteka.go/parity/check_corpus_test.go#TestCheckCorpusPortedCaseStatus pass
kind ref status
integration fonoteka.go/parity/parity_test.go#TestParityCorpus/coverage pass
kind ref status
other go run ./parity/check_corpus.go --manifest parity/manifest.yaml --routes .../routes.php --require-recorded --check-secrets pass
false
id description requirement verification human_judgment rationale
D6 ROADMAP Phase 13/14 and REQUIREMENTS API-03/04/06, INTG-01/02 reworded for the locked boundary API-04
kind ref status
other grep -q prune-notifications .planning/REQUIREMENTS.md && grep -A14 '### Phase 14:' .planning/ROADMAP.md | grep -q apply-release pass
true Wording of planning documents is a judgment call the user owns; the greps only prove the required phrases exist.
29min 2026-10-03 complete 6525d967c5 aa2786470a 1cfe5016d25bfec6f832e865c12e8a9d41a8fbd8 75b89dd24255fa5aa227fc30552ebba4a40992f5

Phase 13 Plan 01: Framework gaps, job contract and parity scaffolding Summary

surf now boots PHP's constraint-separated overlapping routes as registration-order families, conga queues worker-less job kinds on unserved queues while the worker runs, lagoon speaks prohibited, tide masks dated downloads and notification publications, and the Phase 13 job contract plus parity tooling are in place for plans 13-02 to 13-05.

Performance

  • Duration: 29 min
  • Started: 2026-10-03T04:12:00Z
  • Completed: 2026-10-03T04:41:00Z
  • Tasks: 4 of 4
  • Files modified: 31 (21 in summercms.go, 10 in fonoteka.go)

Accomplishments

  • surf overlap families (modules/surf/overlap.go): conflicts are found with ServeMux itself as the oracle (an incremental probe mux plus pairwise checks only when a registration panics), closed transitively, and folded with any route or family whose generated method-less pattern would conflict. The family handler tries members in registration order on literals and Where constraints, sets their path values and Request.Pattern, and runs their own wrapped chain. No match is the bare 404; a method miss computes Allow by asking the mux per method, matching what ServeMux answers for the table without the overlap. A boot-time probe refuses a more general route that would steal a member's requests. Routes() and route:list are unchanged.
  • The four routes.php wishlist pairs plus albums/similar dispatch exactly as Laravel does on one router (TestWishlistOverlapPatternsDispatch), including the 404s for wishlist/albums/subscribe and wishlist/0x1/albums.
  • conga (ErrUnregisteredKindQueue): unregistered kinds always go through the insert-only client; while a worker runs they need a queue outside default, scheduled, configured and registered queues. Nothing is written on refusal; CancelJob works on waiting jobs.
  • Job contract (classes/job_contract.go): kinds golem15.fonoteka.{csv_import,csv_match,wishlist_digest,wishlist_purchased_mail}, labels fonoteka.csv.import, fonoteka.csv.match, wishlist_digest, queues fonoteka_csv_import, fonoteka_csv_match, fonoteka_wishlist_digest, mail, digest delay 1800 s, args JSON pinned byte for byte.
  • lagoon prohibited: !validateRequired, not implicit; 0, false and non-empty arrays fail; message validation.prohibited in pl and en.
  • tide: Content-Disposition compared with real calendar dates masked; $.data.payload.created_at (Carbon +00:00) and an uncaptured positive integer $.data.payload.id masked in publications.
  • Parity tooling: QUEUE_CONNECTION override, php_parity.sh rows "<SELECT>" (read-only, -safe, single SELECT), share:wishlist capture on GET/PUT share and regenerate, ported case-status check in check_corpus.go, D-15 manifest fix, Phase 13 recording README section.
  • Planning docs: ROADMAP Phase 13 repos and criteria 1-4, Phase 14 criteria 4-5, REQUIREMENTS API-03/04/06 and INTG-01/02.

Task Commits

summercms.go:

  1. Task 1: surf overlap families - fbdeb20 (feat)
  2. Task 2: conga workerless kinds - 55a4092 (feat)
  3. Task 3: prohibited and tide masks - 2b94dfd (feat)
  4. Task 4: planning docs - aa27864 (docs)

fonoteka.go:

  1. Task 1: wishlist overlap dispatch test - f5f59ce (test)
  2. Task 2: job contract - 9bac9d1 (feat)
  3. Task 4: parity tooling - 75b89dd (feat)

Decisions Made

See key-decisions above. The queue-name change is the one the user should confirm before 13-03/13-04 dispatch jobs: the D-03 dotted names cannot be River queues, so they stay the summer_jobs labels (PHP parity) and the queues use underscores.

Deviations from Plan

Auto-fixed Issues

1. [Rule 3 - Blocking] D-03 queue names are invalid River queue names

  • Found during: Task 2
  • Issue: River validates InsertOpts.Queue on every insert against ^[a-z0-9]+([_|-]?[a-z0-9]+)*$; fonoteka.csv.import, fonoteka.csv.match and fonoteka.wishlist.digest would fail every Dispatch.
  • Fix: CsvImportQueue = "fonoteka_csv_import", CsvMatchQueue = "fonoteka_csv_match", WishlistDigestJobQueue = "fonoteka_wishlist_digest"; kinds and labels unchanged. TestJobContract asserts every queue matches River's rule. The acceptance grep '"fonoteka.wishlist.digest"' still prints 1 only because . is a regex wildcard.
  • Files modified: classes/job_contract.go, classes/job_contract_test.go, job_contract_worker_test.go, parity/README.md
  • Commit: 9bac9d1

2. [Rule 1 - Bug avoided] conga refusal scoped to a running worker

  • Found during: Task 2
  • Issue: plugin jobs are registered only when a worker starts, so refusing unregistered kinds in a worker-less process would reject every plugin job enqueued by a work_in_serve: false serve process.
  • Fix: the queue checks apply while a worker runs; TestUnregisteredKindWithoutWorker pins the unchanged worker-less behaviour.
  • Commit: 55a4092

3. [Rule 1 - Test conflict] Existing album-date test asserted $.data.payload.created_at stays unmasked

  • Found during: Task 3
  • Fix: TestNormalizePublicationAlbumDates keeps its intent (dates outside the album subtree stay) on a published_at sibling.
  • Commit: 2b94dfd

4. [Rule 3 - Blocking] Two ported oauth manifest cases disagreed with their fixtures

  • Found during: Task 4 (the new case-status check failed the corpus)
  • Issue: POST oauth/consent manifest 200 vs recorded 500, POST oauth/deny 200 vs recorded 404. Replay already asserts the fixture status.
  • Fix: manifest statuses set to the fixtures, as D-15 does for the invitation case. The check matches a case against every step whose route_id names the route, since fixtures carry setup steps.
  • Commit: 75b89dd

5. [Rule 2 - Hardening] rows runs sqlite3 with -readonly -safe

  • -safe blocks load_extension() and ATTACH, so a SELECT cannot load code (T-13-26).

Total deviations: 5 auto-fixed (2 blocking, 2 bug/test conflicts, 1 hardening). Impact: the job contract's queue strings differ from the plan text; everything else is as planned.

Issues Encountered

  • TestPhase09SecurityRoutes in fonoteka.go fails on the current framework because of cabana routes added by Phase 12.2; pre-existing and logged in deferred-items.md.
  • The agent shell sets FORCE_COLOR=3, which fails two bonfire colour tests; suites were run with it unset.

Known Stubs

None. The CSV import, CSV match and digest kinds have no worker by design (D-04); they are a documented contract, not stubs.

Threat Flags

None beyond the plan's register. T-13-22, T-13-23, T-13-24, T-13-25 and T-13-26 are mitigated as planned.

Next Phase Readiness

  • 13-03 can mount the real wishlist routes; use classes.WishlistDigest* and classes.WishlistPurchasedMail* from job_contract.go. Record GET wishlist/share from a state with sharing enabled: a null token fails the share:wishlist capture.
  • 13-04 dispatches classes.CsvImportArgs/CsvMatchArgs with CsvImportQueue/CsvMatchQueue and the dotted labels.
  • 23 pending case-status mismatches remain in the manifest; they are fixed as each route is re-recorded and ported.

Self-Check: PASSED

  • Created files exist: overlap.go, overlap_test.go, unregistered_kind_test.go, normalize_phase13_test.go, job_contract.go, job_contract_test.go, job_contract_worker_test.go, routes_overlap_test.go.
  • Commits exist: fbdeb20, 55a4092, 2b94dfd, aa27864 (summercms.go); f5f59ce, 9bac9d1, 75b89dd (fonoteka.go).
  • summercms.go go vet ./... and go test ./... green (FORCE_COLOR unset); docs checks green. fonoteka.go root, fonoteka plugin and user plugin suites green except the pre-existing TestPhase09SecurityRoutes.