docs(13-01): complete framework gaps, job contract and parity scaffolding plan

This commit is contained in:
Jakub Zych
2026-10-03 06:41:58 +02:00
parent aa2786470a
commit 1c303f1511
2 changed files with 265 additions and 0 deletions

View File

@@ -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 "<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`.

View File

@@ -0,0 +1,9 @@
# Phase 13 deferred items
Out-of-scope findings logged by plan executors. Not fixed by the plan that found them.
## From 13-01
- **fonoteka.go `TestPhase09SecurityRoutes` fails on the current framework.** `plugins/golem15/fonoteka/admin_phase09_security_test.go:52` reports the cabana admin file and relation routes (`/plytadmin/api/v1/{vendor}/{plugin}/{controller}/{id}/files/...`, `.../relations/{name}/records/{child}...`, `.../relations/{name}/pivot/{child}`) as unexpected. They were added by Phase 12.2 commits e54fd25, afb05b6 and fe9e8ba in summercms.go; the Phase 9 assembled-route inventory in fonoteka.go was not updated. Unrelated to 13-01 (no route was added or removed by the surf overlap change; `Routes()` is unchanged). Fix: extend the test's expected admin route set, or confirm with the 12.2 owner.
- **`FORCE_COLOR=3` in the agent shell fails `modules/bonfire` TestColorPolicy and TestInjectedOutputCapture.** They pass with the variable unset; the suite was run with `env -u FORCE_COLOR`. Environment, not code.
- **gofmt drift in files 13-01 did not touch:** `fonoteka.go/plugins/golem15/fonoteka/household_smoke_test.go`, `summercms.go/modules/tide/flow_test.go`, `summercms.go/modules/tide/headers_test.go`.