diff --git a/.planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-01-SUMMARY.md b/.planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-01-SUMMARY.md new file mode 100644 index 0000000..f1b765a --- /dev/null +++ b/.planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-01-SUMMARY.md @@ -0,0 +1,256 @@ +--- +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 "