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.
Per C-01, `TestRouteTablePhase13` over the assembled router asserts the 58 Phase 13 routes exist exactly once on their routes.php group (JWT, personal token, onboarding, public_invitation, public_share) with a routes.php line number per entry, every personal-token route carries exactly one `inv.scope` equal to routes.php (wishlist list read, create/update/delete write, export read), every `{id}`, `{rowId}`, `{collectionId}` and `{albumId}` is constrained to `[0-9]+` (checked by request), no `{token}` is constrained, the inline throttles sit exactly on share regenerate, CSV store, the onboarding group, inspection and the two public resolves, the public buckets sit exactly on the four albums routes, and the four Phase 14 routes (wishlist match, apply-release, ai-credential/test, discogs-credential/test) are absent.
Through the same assembled router the four overlap pairs dispatch numeric and literal segments to the right handler, an unmatched path answers 404 and a method mismatch answers ServeMux's 405 (T-13-23).
Per C-04, `FuzzWriteEndpoints` enumerates every Phase 13 write route from the route table (JSON and multipart) and, for random extra keys and every server-owned key (id, user_id, owner_id, collection_id, organisation_id, kind, public_token, public_enabled, token_hash, status, import_job_id, match_job_id, storage_path, reservations_allowed outside settings, revealed_at, subscribed_at, item_count, api_key outside credential stores, created_at, updated_at, deleted_at), asserts no column outside the endpoint's allow-list changes in Postgres and no response is a 500 that PHP does not answer; its committed seed corpus runs in plain `go test`.
Each mitigated T-13 threat of plans 13-01 to 13-05 has a named test that fails when its protection is removed; `scripts/check-phase13.sh --removal` applies anchor-exact mutations to the protecting code, requires the named test to fail on an assertion and restores each file byte for byte (cmp).
Every Go package created or changed in Phase 13 reaches at least 80% statement coverage: summercms.go surf, conga, lagoon, tide; fonoteka classes, classes/csv, controllers/api and middleware (measured with -coverpkg as check-phase12.sh does); in sm-user-plugin the classes package reaches 80% and every function in controllers/registration.go reaches 80% (go tool cover -func) without lowering the controllers package below its pre-phase value; the numbers are recorded in 13-VALIDATION.md.
`scripts/check-phase13.sh --all` (summercms.go/scripts, per the user's checkpoint note) runs vet and tests in both repos, the parity corpus (157 ported, 0 failing, 14 pending), TestBroadcastGoldens, every TestFonotekaNuxtFlows subtest (nuxt-collections, nuxt-albums, onboarding, nuxt-wishlist, mcp-wishlist, nuxt-csv, public-anonymous, public-pubfail), check_corpus --require-recorded --check-secrets, TestDocsTree and docs:build --check, the named tests by exact name (refusing skips and 'no tests to run'), the coverage floors and the evidence files; `--self-test` proves each detector fails closed.
13-SECURITY-REVIEW.md maps every T-13 id of the six plans to its category, severity, disposition, protecting file and the named test that was run and seen failing under --removal; 13-VALIDATION.md has real task ids in its Per-Task Verification Map, no pending row, the gate path `scripts/check-phase13.sh` in summercms.go, `nyquist_compliant: true` and `wave_0_complete: true`; REQUIREMENTS.md marks API-03..API-07 Complete.
Edge (all requirements, empty): table tests cover an empty JSON body and an empty multipart body on every Phase 13 write route with PHP's status for each.
Edge (API-05, API-07 boundary): table tests cover CSV size and row limits, per_page clamps, Discogs token length bounds, notification list cap and the pubfail threshold one step either side.
statement
verification
Edge (API-03, API-07 concurrency): fuzz and race runs over the Phase 13 write routes under -race report no data race in handlers, the pubfail counter or the job dispatcher holder.
--named reads every test the validation map names; --evidence refuses pending or TBD rows
13-VALIDATION.md
requirement_id
category
statement
status
verification
API-07
transparency
A threat MUST NOT be marked mitigated in 13-SECURITY-REVIEW.md without a named test that was run and seen to fail when the protection is removed
resolved
test
requirement_id
category
statement
status
verification
API-05
transparency
Pending routes MUST NOT be counted as passing by the gate; the four Phase 14 routes stay pending and absent from the router
resolved
test
Phase Goal
ROADMAP Phase 13 goal (verbatim, not in user-story form): 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 project rule's last plan. It proves the Phase 13 route table, the mass-assignment boundary and every T-13 protection, brings the phase's packages to full unit coverage in both repos, adds the fail-closed check-phase13.sh gate, and signs off security and validation (API-03..API-07).
Write TestRouteTablePhase13, extend FuzzWriteEndpoints to every Phase 13 write route, add TestPhase13Threats, fill coverage gaps in summercms.go and fonoteka.go (including sm-user-plugin), write `scripts/check-phase13.sh`, run the security review, and validate 13-VALIDATION.md.
Purpose: CLAUDE.md lean rule 3 (unit tests are always the last plan) and the security review this phase needs (public tokens, credentials encryption, authorization). Decisions covered: C-01, C-04, C-07 directly; every other D-NN through the named tests the gate requires.
Output: tests in both repos and the submodule, the gate script, 13-SECURITY-REVIEW.md, the validated 13-VALIDATION.md, REQUIREMENTS traceability.
Repos: summercms.go (framework tests, gate script, planning docs), fonoteka.go (app tests), sm-user-plugin (registration tests; committed in the submodule, not pushed, then the pointer bump in fonoteka.go). Production code changes only when a test exposes a real bug; each such fix is its own commit naming the threat or decision. Path-scoped staging only (another session works in summercms.go). Planning docs and code in separate commits; never add co-author tags.
@.planning/PROJECT.md
@.planning/STATE.md
@.planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-CONTEXT.md
@.planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-RESEARCH.md
@.planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-VALIDATION.md
@.planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-01-SUMMARY.md
@.planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-02-SUMMARY.md
@.planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-03-SUMMARY.md
@.planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-04-SUMMARY.md
@.planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-05-SUMMARY.md
@scripts/check-phase12.sh
@.planning/phases/12-p-ytarium-api-collections-and-albums/12-SECURITY-REVIEW.md
- Route-table recipe: `phase12RouteTable(t)` in routes_table_phase12_test.go (backpack.New(bootConfig), bouncer.NewRegistry, party.Activate golem15.user and golem15.fonoteka, stubInvToken, surf.BuildRouter, Routes()), `jwtPrefix`/`tokenPrefix` constants, `splitAPIPattern`, `middlewareMatching`, `inlineThrottle` (reuse, do not redeclare in the package).
- Fuzz recipe: write_endpoints_fuzz_test.go `writeSpec{path, base, carol, upload, change, when, create, remove}`, table-name constants, `seedFuzzWorld`, `fuzzWriteSpecs` keyed "METHOD /path".
- Gate recipe: scripts/check-phase12.sh (env-overridable ROOT/APP/PHASE_DIR, `phase12_detect` go-test-json detector with exit codes, `run_go`, `run_parity`, `run_named`, `coverage_report`/`cover_profile` with -coverpkg, `removal_table`/`removal_harness` with cmp restore and a dirty-tree refusal, `evidence_check`, `run_self_test`).
- Final test and function names: the Artifacts sections of 13-01..13-05 and their SUMMARY files.
- Threat registers: the `` blocks of 13-01..13-05 (T-13-01..T-13-33 plus T-13-SC).
Artifacts this phase produces
(This plan's share.)
Tests: TestRouteTablePhase13 (with phase13Route table and phase13Universe), FuzzWriteEndpoints extended with every Phase 13 write route and a committed seed corpus, TestPhase13Threats (one subtest per mitigated T-13 id), TestPhase13EmptyBodies, TestPhase13Boundaries, package unit tests in both repos and sm-user-plugin (TestRegisterUserExports).
Task 1: The assembled router proves every Phase 13 route sits on PHP's group with PHP's scope, constraints and throttles, and the Phase 14 routes are absent
Plan 13-05 is executed: `grep -n 'const expectedPortedRoutes' ../fonoteka.go/parity/parity_test.go` shows 157.
../fonoteka.go/plugins/golem15/fonoteka/routes_table_phase13_test.go
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/routes.php (lines 74-330, 351-428, 449-520), ../fonoteka.go/plugins/golem15/fonoteka/routes.go, ../fonoteka.go/plugins/golem15/fonoteka/routes_table_phase12_test.go (whole file), ../fonoteka.go/plugins/golem15/fonoteka/routes_isolation_test.go, ../fonoteka.go/plugins/golem15/fonoteka/routes_bucket_test.go, .planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-RESEARCH.md (Route Inventory)
Per C-01 and the Route Inventory: add `phase13Route{method, path, group, scope, throttle, buckets, line}` and the explicit 58-entry table transcribed from routes.php with the line number per entry (groups jwt, token, onboarding, public_invitation, public_share), plus `phase13Universe` (the Phase 13 path prefixes: notifications, ai-credential, org-ai-credential, discogs-credential, onboarding, invitations/{token} without /accept, wishlist, import/csv, export/csv, public, public-wishlist) and `phase14Absent` (the four routes). `TestRouteTablePhase13` boots the assembled router as phase12RouteTable does and asserts: every table entry present exactly once with its group's middleware; no unexpected route inside phase13Universe; exactly one `inv.scope:` per token route equal to the table; the inline throttle and public bucket placement of the D-14 layout; each numeric parameter refuses `abc` and `1x` and accepts `12` (authenticated requests through the real handler stack, distinguishing a constraint 404 from a handler 404 as the Phase 12 test does); `{token}` parameters have no constraint; the four phase14Absent routes answer the router's 404; and the four overlap pairs dispatch to the right real handler. TestRouteTablePhase12 stays green unchanged (13-02 narrowed its universe).
go -C ../fonoteka.go vet ./... && go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^(TestRouteTablePhase13|TestRouteTablePhase12|TestFullRouteTableAuthGroupMutualExclusivity)$' -count=1 -race -v
Non-zero exit; output prints "no tests to run", "--- FAIL", "--- SKIP" or "DATA RACE", or lacks "--- PASS: TestRouteTablePhase13" and "--- PASS: TestRouteTablePhase12".
- `grep -c 'routes.php' ../fonoteka.go/plugins/golem15/fonoteka/routes_table_phase13_test.go` prints at least 1 and the table has 58 entries (`grep -cE '^\s*\{"(GET|POST|PUT|PATCH|DELETE)"' ../fonoteka.go/plugins/golem15/fonoteka/routes_table_phase13_test.go` prints 58).
- `grep -cE 'apply-release|ai-credential/test|discogs-credential/test' ../fonoteka.go/plugins/golem15/fonoteka/routes_table_phase13_test.go` prints at least 3 (the absent list).
- Temporarily adding `inv.scope:read` to a second middleware slot of the token wishlist list route makes TestRouteTablePhase13 fail (checked by --removal in Task 3).
The whole Phase 13 route surface is pinned against routes.php through the real router, so a misplaced scope, throttle, constraint or Phase 14 route fails a test.
Task 2: No Phase 13 write endpoint persists a server-owned key, and every Phase 13 protection has a test that breaks without it
../fonoteka.go/plugins/golem15/fonoteka/write_endpoints_fuzz_test.go, ../fonoteka.go/plugins/golem15/fonoteka/testdata/fuzz/FuzzWriteEndpoints/, ../fonoteka.go/plugins/golem15/fonoteka/phase13_security_test.go
../fonoteka.go/plugins/golem15/fonoteka/write_endpoints_fuzz_test.go (whole file), ../fonoteka.go/plugins/golem15/fonoteka/phase12_security_test.go (subtest-per-threat shape), the `` blocks of 13-01-PLAN.md to 13-05-PLAN.md, the 13-01..13-05 SUMMARY files (final names), ../fonoteka.go/plugins/golem15/fonoteka/classes/credential_write_service.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/csv_import_service.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/reservations.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/public_share.go
- FuzzWriteEndpoints: for each Phase 13 write route (notifications read-all and {id}/read, the three credential stores and org destroy, onboarding bootstrap on an empty world, wishlist albums store/update/destroy on both groups, share update and regenerate, settings update, token and collection subscribe/unsubscribe, purchase, reserve, cancel, reveal, CSV store (multipart), mapping, row edit, commit, cancel), a valid base body plus fuzzed extra keys and every server-owned key never changes a column outside the endpoint's allow-list and never yields an unexpected 500.
- TestPhase13Threats: one subtest per mitigated T-13 id (T-13-01..T-13-33 except accepted ones) asserting the protection: enumeration lockout, public field set, disabled/regenerated token, reserve/reveal/cancel authority, owner mask, peer IDOR 404 parity, token-group absence, credential secret absence in bodies and logs, org manager checks, credential FK fixed, bootstrap single owner, invitation match rules, payload without passwords, notification user scope, inspection, item-added/purchase rollback, subscribe-by-token checks, mail args content, CSV import scope, private storage, formula guard, parse limits, Discogs seam, commit CAS, workerless jobs, overlap dispatch isolation, tide mask negatives (framework subtests live in the framework packages and are referenced by name).
Per C-04 and each plan's threat register. (1) Extend `fuzzWriteSpecs` with one writeSpec per Phase 13 write route (table names for notifications, credentials, wishlist subscriptions, reservations, digest queue, csv_imports, csv_import_rows, users and organisations for bootstrap), seed what each needs in seedFuzzWorld, and assert by before/after row snapshots that only allowed columns change; reset state per iteration; commit seed files under testdata/fuzz/FuzzWriteEndpoints/ so plain `go test` exercises every Phase 13 route with the server-owned keys (synthetic values only). (2) phase13_security_test.go `TestPhase13Threats` with subtests named `T-13-NN` implementing the behavior list through the assembled handler, the postcard memory driver, a fake realtime driver and a capturing slog handler; each subtest is small and names the protecting function in a comment. Any genuine bug found is fixed in production code in its own commit naming the threat.
go -C ../fonoteka.go vet ./... && go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^(FuzzWriteEndpoints|TestPhase13Threats)$' -count=1 -race -v && go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^$' -fuzz '^FuzzWriteEndpoints$' -fuzztime 60s
Any command exits non-zero; the verbose run prints "no tests to run", "--- FAIL", "--- SKIP" or "DATA RACE", or lacks "--- PASS: FuzzWriteEndpoints" and "--- PASS: TestPhase13Threats"; the fuzz run prints "Failing input written to".
- `grep -c 't.Run("T-13-' ../fonoteka.go/plugins/golem15/fonoteka/phase13_security_test.go` prints at least 20.
- `grep -cE '"(POST|PUT|PATCH|DELETE) /(_fonoteka/api/v1|api/v1/fonoteka)/(notifications|ai-credential|org-ai-credential|discogs-credential|onboarding|wishlist|import/csv)' ../fonoteka.go/plugins/golem15/fonoteka/write_endpoints_fuzz_test.go` prints at least 25.
- `ls ../fonoteka.go/plugins/golem15/fonoteka/testdata/fuzz/FuzzWriteEndpoints/ | wc -l` is at least the number of Phase 12 seeds plus 25.
- The fuzz target fails when CredentialFillFields temporarily gains `user_id` (checked by --removal in Task 3).
Mass assignment is closed on every Phase 13 write endpoint and every Phase 13 threat is pinned by a test that fails without its protection.
Task 3: Phase 13 code is fully unit tested in both repos, and a fail-closed gate, the security review and the validation file sign it off
modules/surf/overlap_edges_test.go, modules/conga/unregistered_kind_test.go, modules/lagoon/validate_request_test.go, modules/tide/normalize_phase13_test.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/phase13_classes_test.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/csv/csv_edges_test.go, ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/phase13_controllers_test.go, ../fonoteka.go/plugins/golem15/fonoteka/middleware/public_share_headers_test.go, ../fonoteka.go/plugins/golem15/user/registration_test.go, ../fonoteka.go/plugins/golem15/user, scripts/check-phase13.sh, .planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-SECURITY-REVIEW.md, .planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-VALIDATION.md, .planning/REQUIREMENTS.md
scripts/check-phase12.sh (whole script), .planning/phases/12-p-ytarium-api-collections-and-albums/12-SECURITY-REVIEW.md, .planning/phases/12-p-ytarium-api-collections-and-albums/12-VALIDATION.md (validated map format), .planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-VALIDATION.md, the five Phase 13 SUMMARY files, the five Phase 13 PLAN threat registers, ../fonoteka.go/plugins/golem15/user/controllers/registration.go
(1) Framework coverage (summercms.go, neutral names): overlap_edges_test.go (families of three, HEAD on a GET family, Allow header ordering, mixed-method families sharing one shape, unsupported-shape errors, families across plugins), plus edge cases appended to the conga, lagoon and tide Phase 13 tests (refusal messages, delayed insert, prohibited with nested arrays and wildcards, date masks with several dates and quoted filenames, publication masks beside captured ids).
(2) App coverage (fonoteka.go): phase13_classes_test.go (resolver and provisioner branches, subscription states, reservation state matrix, digest upsert, similarity escaping, onboarding count, invitation listener branches, AI resolver tiers, Discogs status, pubfail window, public facets and header), classes/csv csv_edges_test.go (pipe codec, mapper, canonical id matcher, detector aliases, encodings, limits), phase13_controllers_test.go (TestPhase13EmptyBodies empty JSON and multipart bodies on every Phase 13 write route with PHP's status; TestPhase13Boundaries CSV size and rows, per_page clamps, Discogs token bounds, notification cap, pubfail threshold), middleware header bytes on 200/404/429. sm-user-plugin registration_test.go TestRegisterUserExports (RegisterUser validation errors, activation option, IP recording, FireRegisterEvent listener error propagation, IssueToken issuer, APIArray listener merge); commit it in the submodule (not pushed), then the pointer bump in fonoteka.go.
(3) scripts/check-phase13.sh modelled on check-phase12.sh (PHASE13_ROOT/PHASE13_APP/PHASE13_PHASE_DIR overrides, phase13_detect, EXPECTED_PORTED=157, COVERAGE_FLOOR=80): --go vet and test both repos; --parity TestParityCorpus with 157 ported and 0 failing parsed from the coverage line, TestBroadcastGoldens including the three wishlist goldens, every TestFonotekaNuxtFlows subtest listed in the truths, TestUserAPINuxtFlows, check_corpus --require-recorded --check-secrets, TestDocsTree and go run ./cmd/summer docs:build --check; --named every test 13-VALIDATION.md names, run by exact name, refusing skip, fail and "no tests to run"; --removal anchor-exact mutations restored byte for byte with cmp and refusing a dirty file: the overlap dispatcher skipping constraints, unregistered kinds sent through the worker client, the owner mask removed, ResolvePublic without public_enabled, the constant-time compare replaced by the LOWER match alone, TooMany always false, the payload keeping password_confirmation, bootstrap without the in-transaction recount, CsvImportFor without AccessibleBy, commit without the status condition, CredentialFillFields gaining user_id, mark-read without the user_id scope, the release fetcher returning data, the digest upsert dispatching on every insert, the public serializer gaining shelf, the credential show echoing api_key, a duplicated inv.scope on the token wishlist list route; each must make its named test fail on an assertion; --coverage per listed package with -coverpkg (and the go tool cover -func check for controllers/registration.go); --evidence 13-SECURITY-REVIEW.md lists every T-13 id with a passing test and 13-VALIDATION.md has no pending or TBD row and nyquist_compliant true; --all; --self-test proving each detector fails closed on planted inputs. No application name in framework-facing output beyond the paths it must call.
(4) Planning docs (separate commit): 13-SECURITY-REVIEW.md written by the security-review agent if one can be spawned, otherwise self-performed and disclosed as in the 08-10 precedent; table of every T-13 id and T-13-SC with category, severity, disposition, protecting file, named test and the --removal result. 13-VALIDATION.md: replace the seeded Per-Task Verification Map with final rows (13-01-T1 .. 13-06-T3, exact commands, file ticks, statuses), set the phase gate command to scripts/check-phase13.sh --self-test && scripts/check-phase13.sh --all run from summercms.go, tick Wave 0, status validated, nyquist_compliant true, wave_0_complete true. REQUIREMENTS.md: API-03..API-07 checkboxes ticked and traceability rows Complete.
scripts/check-phase13.sh --self-test && scripts/check-phase13.sh --all && scripts/check-phase13.sh --removal
<fails_when>Non-zero exit; output contains "refuse:" or "FAIL", a package coverage line below 80%, a named test reported skipped or with "no tests to run", a --removal mutation whose named test still passes or a file that cmp reports changed after restore, or the evidence stage reporting a pending row or a T-13 id without a test.</fails_when>
<acceptance_criteria>
- test -x scripts/check-phase13.sh succeeds and grep -c 'EXPECTED_PORTED=157' scripts/check-phase13.sh prints 1.
- grep -c 'nyquist_compliant: true' .planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-VALIDATION.md prints 1 and grep -c '⬜ pending' .planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-VALIDATION.md prints 0.
- Every T-13 id in the six plans' threat registers appears in 13-SECURITY-REVIEW.md (grep -ho 'T-13-[0-9A-Z]*' .planning/phases/13-*/13-0*-PLAN.md | sort -u is a subset of the same scan over 13-SECURITY-REVIEW.md).
- grep -cE 'API-0[3-7] \| Phase 13 \| Complete' .planning/REQUIREMENTS.md prints 5.
- The coverage stage prints one line per listed package at 80% or more.
</acceptance_criteria>
Phase 13 is closed by evidence: full unit coverage in both repos, a gate that fails closed on any regression, a security review tying each threat to a removal-proven test, and a validated validation file.
<threat_model>
Trust Boundaries
Boundary
Description
Test harness → production code
Tests and the gate must not weaken or bypass the protections they check
Gate script → tracked source
--removal edits tracked files temporarily and must restore them exactly
Fuzz corpus → git
Seed inputs are committed and must hold no secrets
STRIDE Threat Register
This plan verifies every threat registered by plans 13-01 to 13-05 (T-13-01 to T-13-33): TestRouteTablePhase13 covers T-13-07 and T-13-23 at the assembled router, FuzzWriteEndpoints covers T-13-10 and the C-04 boundary, TestPhase13Threats has one subtest per remaining mitigated id, and --removal proves each. The rows below are the threats this plan itself introduces.
Threat ID
Category
Component
Severity
Disposition
Mitigation Plan
T-13-34
Tampering
gate --removal leaving mutated source
medium
mitigate
Anchor-exact mutation, cmp byte-identical restore, trap on exit, refusal on a dirty target file; --removal is its own mode outside --all because it edits tracked source (Task 3).
T-13-35
Repudiation
security review claims without evidence
medium
mitigate
The evidence stage refuses a T-13 id without a named, executed, removal-proven test (Task 3).
- `scripts/check-phase13.sh --self-test`, `--all` and `--removal` exit 0.
- Both repos: `go vet ./... && go test ./... -count=1` green; `go test ./cmd/summer -run TestDocsTree -count=1` and `go run ./cmd/summer docs:build --check` green.
- 13-VALIDATION.md validated; 13-SECURITY-REVIEW.md complete; API-03..API-07 Complete in REQUIREMENTS.md.
<success_criteria>
The Phase 13 route table, write-endpoint boundary and every threat are pinned by named, removal-proven tests.
Every Phase 13 package in both repos meets the coverage floor; the gate fails closed.
Security review and validation sign-off are complete with real task ids.
</success_criteria>
Create `.planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-06-SUMMARY.md` when done.