--- phase: 13-p-ytarium-api-wishlist-notifications-csv-credentials-public plan: 01 subsystem: api tags: [surf, servemux, conga, river, lagoon, validation, tide, parity, job-contract] requires: - phase: 11 provides: conga jobs on River, lighthouse publications, tide broadcast goldens - phase: 12 provides: fonoteka route groups, parity harness with the fonoteka seed hook, invitation mail job pattern provides: - 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 affects: [13-02, 13-03, 13-04, 13-05, 13-06, 14] actuals: tokens: 24500 tasks: 4 commits: 7 tech-stack: 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" key-files: created: - 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 modified: - 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 key-decisions: - "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." patterns-established: - "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" requirements-completed: [API-03, API-04, API-05, API-06, API-07] coverage: - id: D1 description: "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" requirement: API-03 verification: - kind: unit ref: "summercms.go/modules/surf/overlap_test.go#TestOverlappingConstrainedRoutes" status: pass - kind: unit ref: "fonoteka.go/plugins/golem15/fonoteka/routes_overlap_test.go#TestWishlistOverlapPatternsDispatch" status: pass human_judgment: false - id: D2 description: "conga queues an unregistered job kind while a worker runs, refuses empty or served queues with ErrUnregisteredKindQueue, and the fonoteka job contract is pinned" requirement: API-05 verification: - kind: integration ref: "summercms.go/modules/conga/unregistered_kind_test.go#TestUnregisteredKindWithWorker" status: pass - kind: unit ref: "fonoteka.go/plugins/golem15/fonoteka/classes/job_contract_test.go#TestJobContract" status: pass - kind: integration ref: "fonoteka.go/plugins/golem15/fonoteka/job_contract_worker_test.go#TestJobContractDispatchWhileWorkerRuns" status: pass human_judgment: false - id: D3 description: "lagoon prohibited rule with Laravel 9 semantics and the literal validation.prohibited message" requirement: API-03 verification: - kind: unit ref: "summercms.go/modules/lagoon/validate_request_test.go#TestValidateRequestProhibited" status: pass human_judgment: false - id: D4 description: "tide masks Content-Disposition dates and notification publication id/created_at without hiding real differences" requirement: API-05 verification: - kind: unit ref: "summercms.go/modules/tide/normalize_phase13_test.go#TestNormalizeContentDispositionDate" status: pass - kind: unit ref: "summercms.go/modules/tide/normalize_phase13_test.go#TestNormalizeNotificationPublication" status: pass - kind: integration ref: "go -C ../fonoteka.go test ./parity -run '^(TestUserAPINuxtFlows|TestBroadcastGoldens)$'" status: pass human_judgment: false - id: D5 description: "Parity tooling: QUEUE_CONNECTION override, rows dump, share:wishlist capture, ported case-status check, D-15 manifest fix; corpus still 99 ported and passing" requirement: API-07 verification: - kind: unit ref: "fonoteka.go/parity/check_corpus_test.go#TestCheckCorpusPortedCaseStatus" status: pass - kind: integration ref: "fonoteka.go/parity/parity_test.go#TestParityCorpus/coverage" status: pass - kind: other ref: "go run ./parity/check_corpus.go --manifest parity/manifest.yaml --routes .../routes.php --require-recorded --check-secrets" status: pass human_judgment: false - id: D6 description: "ROADMAP Phase 13/14 and REQUIREMENTS API-03/04/06, INTG-01/02 reworded for the locked boundary" requirement: API-04 verification: - kind: other ref: "grep -q prune-notifications .planning/REQUIREMENTS.md && grep -A14 '### Phase 14:' .planning/ROADMAP.md | grep -q apply-release" status: pass human_judgment: true rationale: "Wording of planning documents is a judgment call the user owns; the greps only prove the required phrases exist." duration: 29min completed: 2026-10-03 status: complete plan_head_before: 6525d967c50d3a01c4b3170648257f48ed965e52 plan_head_after: aa2786470ac8d168162e73dff4549724ef9092e6 fonoteka_plan_head_before: 1cfe5016d25bfec6f832e865c12e8a9d41a8fbd8 fonoteka_plan_head_after: 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 "