Six sequential plans: framework gaps, notifications/credentials/onboarding, wishlist, CSV, public views, unit tests and gate. Research open questions marked resolved per the plan-count checkpoint.
48 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, estimate, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | estimate | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 13-p-ytarium-api-wishlist-notifications-csv-credentials-public | 01 | execute | 1 |
|
true |
|
|
|
Phase Goal
ROADMAP Phase 13 goal (verbatim, not in user-story form; the MVP precedent of Phases 11, 11.2 and 12 is to quote it): The remaining core API surface — wishlist, notifications, CSV import/export, per-user/org credentials, and onboarding/public/invitation routes — is ported with byte-compatible shapes and their own public rate-limit buckets.
This plan's slice: the framework can register PHP's overlapping constrained routes, queue a job whose worker ships later without failing or losing it, validate prohibited, and compare dated download names and notification publications; the app has one pinned job contract; the parity harness can record with a real queue and dump rows; the roadmap says what Phase 13 ships. Plans 13-02 to 13-05 build every route on these pieces.
Purpose: without the surf change the app panics at boot once the wishlist routes exist; without the conga change commit, mapping and wishlist item-add return 500 in production (work_in_serve: true). Decisions implemented: D-01, D-02, D-03, D-04, D-06, D-08 (contract), D-13 (row dump tooling), D-15, C-01, C-07.
Output: framework features with README and docs updates, classes/job_contract.go, parity tooling, reworded ROADMAP/REQUIREMENTS.
Repos: summercms.go (framework, planning docs) and fonoteka.go (job contract, tests, parity tooling). Another session is committing Phase 12.2 work in summercms.go concurrently: stage only the paths of the task at hand (git add <paths>), never git add -A or git add ., and rebase on HEAD before committing if needed. Framework code, tests, READMEs and docs never name the application (CLAUDE.md). Planning docs and code go in separate commits; never add co-author tags.
<execution_context>
@/.claude/gsd-core/workflows/execute-plan.md
@/.claude/gsd-core/templates/summary.md
</execution_context>
Artifacts this phase produces
(This plan's share.)
- surf: constraint-aware overlap dispatch inside
compile(unexportedoverlapFamilies, family dispatcher in modules/surf/overlap.go); no exported API change; README and docs/services/routing.md section "Overlapping constrained routes". - conga:
ErrUnregisteredKindQueue; unregistered kinds insert through the insert-only client; README and docs/services/jobs.md section "Jobs whose worker ships later". - lagoon: request rule
prohibited. - tide: Content-Disposition date masking in header comparison;
$.data.payload.created_atand$.data.payload.idpublication masks. - fonoteka classes (job contract): constants
CsvImportKind,CsvImportQueue,CsvImportLabel,CsvMatchKind,CsvMatchQueue,CsvMatchLabel,WishlistDigestKind,WishlistDigestJobQueue,WishlistDigestLabel,WishlistDigestDelay,WishlistPurchasedMailKind,WishlistPurchasedMailQueue,WishlistPurchasedMailAttempts; typesCsvImportArgs{CsvImportID},CsvMatchArgs{CsvImportID},WishlistDigestArgs{SubscriberID, WishlistCollectionID},WishlistPurchasedMailArgs{SubscriberID, AlbumName, WishlistName}, each withKind() string. - Tests:
TestOverlappingConstrainedRoutes,TestUnregisteredKindWithWorker,TestValidateRequestProhibited,TestNormalizeContentDispositionDate,TestNormalizeNotificationPublication,TestWishlistOverlapPatternsDispatch,TestJobContract,TestJobContractDispatchWhileWorkerRuns,TestCheckCorpusPortedCaseStatus. - Parity tooling:
php_parity.sh rows <sql>,QUEUE_CONNECTIONoverride, captureshare:wishlist.
Job contract (D-03, D-08) — the one place the names are stated
Phase 14 workers consume these kinds and args, and queued river_job rows in a live database carry them; renaming later needs a coordinated worker change and a row migration. The user signed this table off at the plan-count checkpoint on 2026-10-02, so it is recorded here without a new checkpoint.
| Job | Kind | Queue | summer_jobs label | Args JSON | Insert | Worker |
|---|---|---|---|---|---|---|
| CSV import (PHP AlbumCsvImportJob) | golem15.fonoteka.csv_import |
fonoteka.csv.import (unserved until Phase 14) |
fonoteka.csv.import |
{"csv_import_id":N} |
conga.Dispatch, Count = row_count, Metadata {"csv_import_id":N} |
Phase 14 (JOBS-02) |
| CSV match (PHP AlbumCsvMatchJob) | golem15.fonoteka.csv_match |
fonoteka.csv.match (unserved) |
fonoteka.csv.match |
{"csv_import_id":N} |
conga.Dispatch, Count = row_count, Metadata {"csv_import_id":N} |
Phase 14 (JOBS-02) |
| Wishlist digest (PHP WishlistDigestJob) | golem15.fonoteka.wishlist_digest |
fonoteka.wishlist.digest (unserved) |
wishlist_digest |
{"subscriber_id":U,"wishlist_collection_id":C} |
conga.Dispatch, Delay 1800 s, Count 0, Metadata nil (stores "") |
Phase 14 (JOBS-03) |
| Wishlist purchase mail (D-07) | golem15.fonoteka.wishlist_purchased_mail |
mail |
none (Enqueue writes no summer_jobs row) | {"subscriber_id":U,"album_name":S,"wishlist_name":S} |
conga.Enqueue, 3 attempts |
Phase 13 (13-03) |
Known difference (RESEARCH Finding 3): PHP's JobManager rows get user_id = NULL on JWT requests; conga.Dispatch stores the request principal. No client reads the column; it is recorded in parity/README.md.
Assumption-delta decision
<assumption_delta_decision>
Detector run on the Phase 13 ROADMAP section: detected=false. Considered by hand: public-wishlist mirrors the public collection view with kind='wishlist' (a second kind of shared collection, not a second identity); the share token stays on the collection row and the collection id stays the identity. Noun primary: Collection. Decision: no-change. Rationale: kind already models both; no anchor moves.
</assumption_delta_decision>
Flagged assumptions
- A2 (RESEARCH): the
prohibitedmessage is the literalvalidation.prohibited; plan 13-03 records a wishlist store withconditionset to settle it. - The overlap dispatcher's 405 contract is defined as "what ServeMux answers for the same table without the conflict"; PHP answers 404 for paths no route matches, which the 404 truth covers.
(1) modules/surf/overlap.go: before compile registers anything, compute overlap families over r.routes. Two routes overlap-conflict when ServeMux would panic for them: same method (a GET route also answers HEAD as ServeMux does), the same segment count, every segment pair either equal literals or containing a single-segment wildcard, and neither pattern matching a strict subset of the other's paths. Families are the transitive closure of that relation. Routes outside every family keep the current one-pattern-per-route registration through handleRoute. A family member whose pattern uses a multi-segment wildcard, the end anchor or a host fails compile with an error naming both routes (unsupported, never silently misrouted).
(2) Register each family under one generated pattern whose segments are the members' shared literal where they all agree and a generated wildcard elsewhere. The family handler walks members in registration order; the first member whose literal segments equal the request's segments and whose constraints all match gets its own named path values set with Request.SetPathValue, then runs its own fully wrapped handler (the same wrap output compile builds today, so middleware, body limit, locale and recovery stay per member). No member matching answers http.NotFound. The registration must not create a 405 ServeMux would not have answered: a request whose path only the generated pattern matches answers 404 for every method, and a method mismatch on a path a member or another route matches answers ServeMux's 405 with the same Allow header as a mux holding the same table without the conflict (one way: register the family pattern without a method and resolve method, 405 and Allow inside the family handler from the router's full route list; families of different methods that generalise to the same shape share one registration).
(3) Routes() and route:list stay one entry per route (no family entries). Add a short paragraph to the surf README (Features, and a Usage note) and a section "Overlapping constrained routes" to docs/services/routing.md in prose: when it applies, registration-order dispatch, 404/405 behaviour, unsupported shapes. Neutral examples only (shelves, items).
(4) modules/surf/overlap_test.go TestOverlappingConstrainedRoutes with neutral names mirroring the four app shapes (for example GET /shelves/token/{token}/follow vs GET /shelves/{shelfId}/items/{itemId}, GET|DELETE /shelves/{shelfId}/follow vs GET|DELETE /shelves/items/{id}, GET /shelves/items/{id} vs GET /shelves/{shelfId}/items, plus a literal GET /shelves/items/similar): subtests boot (no compile error), dispatch (numeric and literal segments reach the right handler with the right PathValue names), constraint-404 (/shelves/items/follow, /shelves/0x1/items answer 404 text/plain), method-405 (status and Allow equal a reference ServeMux built from the same table minus the conflicting partner), unknown-method-404 (POST to a path only the family pattern matches answers 404), middleware (each member's own middleware runs, the other member's does not), route-table (Routes() lists every member once), unsupported (a family with {rest...} is a compile error).
(5) fonoteka.go routes_overlap_test.go TestWishlistOverlapPatternsDispatch: build a surf router with stub handlers that record which route ran, registered with the exact routes.php wishlist patterns and constraints under /_fonoteka/api/v1 (lines 171-176, 197-206, 224-225), and assert each of the four conflicting pairs plus GET wishlist/albums/similar dispatch as PHP would. This proves the real shapes before plan 13-03 mounts the real handlers.
go vet ./... && go test ./modules/surf -count=1 -v -run '^(TestOverlappingConstrainedRoutes)$' && go test ./modules/surf -count=1 && go test ./cmd/summer -count=1 -run '^(TestDocsTree|TestDocsCommandsMirrorGeneratedMain)$' && go run ./cmd/summer docs:build --check && go -C ../fonoteka.go vet ./... && go -C ../fonoteka.go test ./plugins/golem15/fonoteka -count=1 -v -run '^(TestWishlistOverlapPatternsDispatch|TestRouteTablePhase12|TestFullRouteTableAuthGroupMutualExclusivity)$'
<fails_when>Any command exits non-zero; a verbose run prints "no tests to run", "--- FAIL" or "--- SKIP", or lacks "--- PASS: TestOverlappingConstrainedRoutes" and "--- PASS: TestWishlistOverlapPatternsDispatch"; docs:build --check or TestDocsTree reports an unknown identifier or broken link.</fails_when>
<acceptance_criteria>
- grep -c 'SetPathValue' modules/surf/overlap.go prints at least 1.
- grep -c 't.Run(' modules/surf/overlap_test.go prints at least 8.
- grep -ci 'overlapping constrained routes' docs/services/routing.md prints at least 1 and grep -ci 'overlap' modules/surf/README.md prints at least 1.
- grep -rniE 'fonoteka|plytarium|płytarium|wishlist' modules/surf/overlap.go modules/surf/overlap_test.go modules/surf/README.md docs/services/routing.md prints nothing (framework stays app-agnostic).
- grep -c 'wishlist/{collectionId}/albums/{albumId}' ../fonoteka.go/plugins/golem15/fonoteka/routes_overlap_test.go prints at least 1.
</acceptance_criteria>
The framework registers PHP-style overlapping constrained routes and the app's exact wishlist shapes dispatch correctly, so plan 13-03 can mount the real routes without a boot panic.
(1) conga.go: add exported ErrUnregisteredKindQueue. In Dispatch and Enqueue, when args.Kind() has no registered job: require a non-empty queue in the opts and refuse a queue that the known-queue set contains (knownQueues plus QueueScheduled), returning an error that wraps ErrUnregisteredKindQueue and names the kind and queue; then insert through the insert-only client (build it lazily even while a worker runs; keep it separate from m.worker). A registered kind keeps today's client choice and behaviour. CancelJob, Get and the status helpers keep working for workerless jobs (they act on summer_jobs and cancel through a client that never checks kinds).
(2) Tests modules/conga/unregistered_kind_test.go TestUnregisteredKindWithWorker on the conga Postgres harness with neutral names: register one real job (queue acme.mail), start a worker (poll-only, short poll interval as the existing worker tests do), then subtests dispatch-unregistered (Dispatch of kind acme.pending_import on queue acme.pending returns an id, the summer_jobs row has the label and river_job_id, the river_job row is available with attempt 0 and still is after three poll intervals), enqueue-delayed (Enqueue with a delay gives scheduled), refuse-served (onto default, scheduled and acme.mail errors with ErrUnregisteredKindQueue and inserts nothing), refuse-empty (empty queue errors), cancel (CancelJob stops the summer_jobs row and cancels the River job), registered-unchanged (the registered kind still runs on the worker).
(3) Docs in the same change: conga README (API reference entry for ErrUnregisteredKindQueue, a Usage note) and a docs/services/jobs.md section "Jobs whose worker ships later" in prose: dispatch onto a queue nothing serves, the refusal rules, rows wait until a worker for the kind is registered and its queue becomes served. Neutral names.
(4) fonoteka.go classes/job_contract.go: the constants and args types listed in the Job contract table and the Artifacts section, with JSON tags exactly csv_import_id, subscriber_id, wishlist_collection_id, album_name, wishlist_name, WishlistDigestDelay = 1800 * time.Second, WishlistPurchasedMailQueue = "mail", WishlistPurchasedMailAttempts = 3, and a doc comment pointing at the PHP job classes and stating that only the purchase mail kind gets a worker in Phase 13. classes/job_contract_test.go TestJobContract pins every kind, queue, label and the marshalled args JSON byte for byte, and asserts no Phase 13 queue name appears in ../fonoteka.go/config/queue.yaml (read the file).
(5) fonoteka.go job_contract_worker_test.go TestJobContractDispatchWhileWorkerRuns: boot the app plugins' jobs into a conga Manager on the plugin test database, start a worker, Dispatch classes.CsvImportArgs{CsvImportID: 1} with label CsvImportLabel, queue CsvImportQueue, Count 3 and Metadata {"csv_import_id":1}; assert success, the summer_jobs row (label, status in progress, progress_max 3, metadata JSON) and an unworked river_job row of kind CsvImportKind; then Dispatch WishlistDigestArgs with WishlistDigestDelay and assert a scheduled row.
go vet ./... && go test ./modules/conga -count=1 -v -run '^(TestUnregisteredKindWithWorker)$' && go test ./modules/conga -count=1 && go test ./cmd/summer -count=1 -run '^(TestDocsTree|TestDocsCommandsMirrorGeneratedMain)$' && go run ./cmd/summer docs:build --check && go -C ../fonoteka.go vet ./... && go -C ../fonoteka.go test ./plugins/golem15/fonoteka ./plugins/golem15/fonoteka/classes -count=1 -v -run '^(TestJobContract|TestJobContractDispatchWhileWorkerRuns)$'
<fails_when>Any command exits non-zero; a verbose run prints "no tests to run", "--- FAIL" or "--- SKIP", or lacks "--- PASS: TestUnregisteredKindWithWorker", "--- PASS: TestJobContract" and "--- PASS: TestJobContractDispatchWhileWorkerRuns".</fails_when>
<acceptance_criteria>
- go doc ./modules/conga ErrUnregisteredKindQueue exits 0 and grep -c 'ErrUnregisteredKindQueue' modules/conga/README.md prints at least 1.
- grep -ci 'worker ships later' docs/services/jobs.md prints at least 1.
- grep -c '"fonoteka.wishlist.digest"' ../fonoteka.go/plugins/golem15/fonoteka/classes/job_contract.go prints 1 and grep -c 'golem15.fonoteka.csv_match' ../fonoteka.go/plugins/golem15/fonoteka/classes/job_contract.go prints 1.
- grep -cE 'fonoteka\.(csv|wishlist)\.' ../fonoteka.go/config/queue.yaml prints 0.
- grep -rniE 'fonoteka|plytarium|wishlist' modules/conga/conga.go modules/conga/unregistered_kind_test.go modules/conga/README.md docs/services/jobs.md prints nothing.
</acceptance_criteria>
A request can queue a Phase 14 job in its write transaction while the server's worker runs, the job waits unworked, and every name Phase 14 will consume is pinned by a test.
(2) tide, per RESEARCH Finding 6 point 2: in header comparison, compare Content-Disposition after replacing every YYYY-MM-DD token on both sides with one placeholder, but only after checking each replaced token parses as a real calendar date on its side; a token that does not parse, or a date present on one side only, keeps the byte comparison so the Diff stays visible. No application name or filename stem is hard-coded.
(3) tide, per RESEARCH Finding 8 (masking only): in normalizer.walk, mask $.data.payload.created_at holding a Carbon +00:00 value as {{datetime}} with the same shape check used for album dates, and mask $.data.payload.id holding a positive integer as {{id}} only when no captured id variable names it (a captured value keeps its {{id:name}} placeholder). Leave every other path unchanged.
(4) Tests modules/tide/normalize_phase13_test.go: TestNormalizeContentDispositionDate (same stem on different days: no Diff; different stem: Diff; 2026-13-45: Diff; date only on one side: Diff; header absent on got: Diff) and TestNormalizeNotificationPublication (a notification:new body with id and created_at normalizes equal across two runs with different ids and times; a Z created_at and a string id stay visible; an album publication's existing masks are unchanged). README and docs/services/parity-testing.md describe both masks in prose with neutral examples.
go vet ./... && go test ./modules/lagoon ./modules/tide -count=1 -v -run '^(TestValidateRequestProhibited|TestNormalizeContentDispositionDate|TestNormalizeNotificationPublication)$' && go test ./modules/lagoon ./modules/tide -count=1 && go test ./cmd/summer -count=1 -run '^(TestDocsTree|TestDocsCommandsMirrorGeneratedMain)$' && go run ./cmd/summer docs:build --check && go -C ../fonoteka.go test ./parity -count=1 -run '^(TestUserAPINuxtFlows|TestBroadcastGoldens)$'
<fails_when>Any command exits non-zero; the verbose run prints "no tests to run", "--- FAIL" or "--- SKIP", or lacks a "--- PASS" line for each of the three named tests; TestBroadcastGoldens or TestUserAPINuxtFlows fails (an existing golden or recorded body regressed).</fails_when>
<acceptance_criteria>
- grep -c '"prohibited"' modules/lagoon/validate_rules.go prints at least 1 and grep -c 'prohibited' modules/phrasebook/lang/pl/validation.yaml prints 0.
- grep -c 'prohibited' modules/lagoon/README.md and grep -c 'prohibited' docs/database/casts-and-validation.md each print at least 1.
- grep -c 'payload.created_at' modules/tide/centrifugo_golden.go prints at least 1.
- grep -ci 'content-disposition' modules/tide/README.md and grep -ci 'content-disposition' docs/services/parity-testing.md each print at least 1.
- grep -rniE 'fonoteka|plytarium' modules/tide/diff.go modules/tide/centrifugo_golden.go modules/tide/normalize_phase13_test.go modules/lagoon/validate_rules.go prints nothing.
</acceptance_criteria>
The validator speaks Laravel's prohibited rule, and the parity diff can compare a dated download and a notification publication recorded on another day without hiding a real difference.
(2) capture-rules.yaml: copy the collection share rule for the wishlist share surface: GET and PUT /_fonoteka/api/v1/wishlist/share and POST /_fonoteka/api/v1/wishlist/share/regenerate, capturing the token path PHP returns as share:wishlist, category share.
(3) check_corpus.go, per RESEARCH Finding 5: for a route with status: ported, fail when any case's manifest status differs from the recorded status in its fixture, naming route, case and both numbers; pending routes are reported only as a count line (they are re-recorded by plans 13-02 to 13-05). check_corpus_test.go TestCheckCorpusPortedCaseStatus with a planted mismatch on a ported route (fails) and on a pending route (passes with the count line).
(4) manifest.yaml, per D-15: set the case status of GET /_fonoteka/api/v1/invitations/{token} public_invitation to 200 to match its fixture (the route stays pending until 13-02 re-records and ports it).
(5) parity/README.md: a "Phase 13 recording" section: QUEUE_CONNECTION=database for the nuxt-wishlist and nuxt-csv recordings, php_parity.sh rows for row goldens (digest queue, apparatus job rows), share:wishlist, the shared anonymous throttle:10,1 budget (reset before each anonymous recording, at most 10 inline-throttled cases per route), and the known summer_jobs.user_id difference from the Job contract table. Commit the fonoteka.go changes path-scoped.
(6) Planning docs (a separate docs-only commit in summercms.go; use Edit, never a whole-file Write), per D-01, D-02, D-06: ROADMAP Phase 13 **Repos:** becomes fonoteka.go, sm-user-plugin (submodule, D-09/D-11 additive exports) and summercms.go (13-01 framework gaps). Criterion 1 adds that the wishlist Discogs match/apply-release routes move to Phase 14 and the digest job body is Phase 14 (JOBS-03). Criterion 2 becomes notifications list, unread count and mark-read (one and all), with pruning as the Phase 14 fonoteka:prune-notifications console command. Criterion 3 adds that commit and mapping enqueue the CSV import and match jobs whose bodies are Phase 14 (JOBS-02). Criterion 4 adds that the live ai-credential/test and discogs-credential/test routes move to Phase 14. Phase 14 criterion 4 (Discogs) adds the wishlist match/apply-release routes, discogs-credential/test and the CSV row-edit Discogs pick; criterion 5 (AI) adds ai-credential/test and the backend global vision model behind the AI resolver's admin tier. REQUIREMENTS: API-03 notes match/apply-release in Phase 14; API-04 reads list, unread count, mark read, with pruning a Phase 14 console command; API-06 notes the live /test routes in Phase 14; INTG-01 and INTG-02 gain the moved routes. Leave every status column and the traceability table untouched.
bash -n ../fonoteka.go/parity/php_parity.sh && grep -q 'QUEUE_CONNECTION:-sync' ../fonoteka.go/parity/php_parity.sh && go -C ../fonoteka.go vet ./... && go -C ../fonoteka.go test ./parity -count=1 -v -run '^(TestCheckCorpusPortedCaseStatus|TestParityCorpus)$' && go -C ../fonoteka.go run ./parity/check_corpus.go --manifest parity/manifest.yaml --routes /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/routes.php --require-recorded --check-secrets && grep -q 'prune-notifications' .planning/REQUIREMENTS.md && grep -A14 '### Phase 14:' .planning/ROADMAP.md | grep -q 'apply-release'
<fails_when>Non-zero exit: the script has a syntax error or lacks the override; a verbose run prints "--- FAIL" or "no tests to run" or lacks "--- PASS: TestCheckCorpusPortedCaseStatus" and "--- PASS: TestParityCorpus/coverage"; check_corpus reports a ported case-status mismatch, a secret or an unrecorded route; REQUIREMENTS.md lacks the prune wording or the Phase 14 section lacks apply-release.</fails_when>
<acceptance_criteria>
- grep -c 'share:wishlist' ../fonoteka.go/parity/capture-rules.yaml prints at least 1.
- grep -A12 'invitations/{token} public_invitation' ../fonoteka.go/parity/manifest.yaml | grep -c 'status: 200' prints 1.
- grep -c 'rows)' ../fonoteka.go/parity/php_parity.sh prints at least 1 and grep -c 'QUEUE_CONNECTION=database' ../fonoteka.go/parity/README.md prints at least 1.
- grep -A14 '### Phase 13:' .planning/ROADMAP.md | grep -c 'sm-user-plugin' prints at least 1.
- The REQUIREMENTS.md traceability rows for API-03..API-07 still read Phase 13 | Pending.
- git log -1 --stat of the planning commit lists only .planning files, and the fonoteka.go commit lists only parity/ files.
</acceptance_criteria>
Recordings can show queued-not-run jobs and dump the rows D-13 compares, a ported route can no longer disagree with its fixture, and the roadmap and requirements match the locked Phase 13/14 boundary.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| HTTP request path → surf dispatch | Untrusted path segments select the handler and its middleware chain |
| Request write transaction → River queue | A queued job waits for a worker that ships later; it must neither fail the write nor be lost |
| Recorded fixtures and goldens → tide comparison | Masks decide what the parity diff is allowed to ignore |
| Isolated PHP SQLite → row goldens | The rows subcommand reads the parity database for committed goldens |
STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|---|---|---|---|---|---|
| T-13-23 | Elevation of Privilege | surf overlap dispatch | high | mitigate | Members dispatch in registration order only after their literals and constraints match; each member runs its own wrapped middleware chain; no match is 404; TestOverlappingConstrainedRoutes covers middleware isolation and 404/405 (Task 1). |
| T-13-22 | Denial of Service / Repudiation | conga unregistered kinds | high | mitigate | Unregistered kinds go through the insert-only client; empty or served queues are refused with ErrUnregisteredKindQueue so a job is never fetched and discarded; TestUnregisteredKindWithWorker and TestJobContractDispatchWhileWorkerRuns (Task 2). |
| T-13-24 | Repudiation | tide date and publication masks | medium | mitigate | Each mask asserts the masked value's shape (real calendar date, Carbon +00:00, positive integer) and leaves every other path visible; negative tests prove a wrong stem, a bad date, a Z date and a string id still diff (Task 3). |
| T-13-25 | Tampering | lagoon prohibited rule | medium | mitigate | Laravel semantics ported with a truth table including 0 and false; plan 13-03 records the wishlist 422 and its fuzz proves condition/shelf never persist (Task 3). |
| T-13-26 | Information Disclosure | php_parity.sh rows and share capture | medium | mitigate | rows accepts one SELECT only and prints to stdout; share tokens are captured as {{share:wishlist}}; check_corpus --check-secrets stays in the verify (Task 4). |
| T-13-SC | Tampering | package installs | low | accept | No new dependency in this plan; no npm/pip/cargo installs. |
| </threat_model> |
<success_criteria>
- Overlapping constrained routes register and dispatch with registration-order semantics; the route table is unchanged.
- Workerless jobs can be dispatched while a worker runs and wait unworked; the job contract is pinned in one file.
prohibited, the Content-Disposition date mask and the notification publication masks exist with docs.- Parity tooling supports a database queue, row dumps, the wishlist share capture and the ported case-status check; ROADMAP and REQUIREMENTS carry D-01, D-02 and D-06. </success_criteria>