17 KiB
phase, plan, subsystem, tags, requires, provides, affects, actuals, plan_head_before, plan_head_after, app_repo_head_before, app_repo_head_after, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed, status
| phase | plan | subsystem | tags | requires | provides | affects | actuals | plan_head_before | plan_head_after | app_repo_head_before | app_repo_head_after | tech-stack | key-files | key-decisions | patterns-established | requirements-completed | duration | completed | status | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 14-domain-jobs-and-external-integrations | 03 | api |
|
|
|
|
|
2ee97c62f6 |
5101ecb49c |
ef34015a223a67e649e99bac410fb728337c0ee3 | 28349433d77ba16c0dec7c87e41ef486996169f2 |
|
|
|
|
|
54min | 2026-10-03 | complete |
Phase 14 Plan 03: Discogs routes Summary
All eight Discogs routes now run in Go with PHP's check order, bodies and limiters: album and wishlist release match and apply, draft match, the import box, the token test and the MCP cover-and-price fetch. They replay offline from 50 PHP-recorded cases whose Discogs exchanges, cover images included, are asserted request by request.
Performance
- Duration: 54 min
- Started: 2026-10-03T19:27Z
- Completed: 2026-10-03T20:21Z
- Tasks: 3
- Commits: 3 in fonoteka.go, 1 code commit in summercms.go (plus this summary)
- Files: 116 changed in fonoteka.go (+5300/-149), 6 in summercms.go (+115/-10)
Accomplishments
-
Match (D-07, INTG-01).
POST albums/{id}/match,POST albums/match(scored against an unsaved draft album) and the wishlist twin. Each checks in PHP's order: the album or wishlist scope 404, the gate 503, then the sharedfonoteka-discogs-missing:bucket (60 per 60 s, 429 withretry_after), then validation 422. After that comes the search, the year and medium filters over the first ten results, and the scores merged into each candidate in key order. -
Apply.
POST albums/{id}/apply-releaseand the wishlist twin run the 14-02Applicator. Fill-empty is the default;overwrite_all,cover_onlyand the album route'sdry_runbehave as PHP's applicator. A missing release answers 200discogs_no_match. The wishlist route never readsdry_runand never answersdraft. Rejected tokens map to 502, rate limits to 429discogs_rate_limitedand other failures to 502discogs_unavailable, logged by class only. -
Import.
POST albums/import/discogschecks the gate, then validation, then its ownfonoteka-discogs-import:bucket (20 per 60 s, 429 withoutretry_after), so an invalid body never counts. It answers the resolver's draft, candidates or no_match. -
Cover and price (MCP).
classes/discogs.CoverFetcherports AlbumCoverFetcher:- certainty through the stored id and corroboration, or exactly one barcode hit
nothing_missing,cover_already_presentandprice_already_presenthandled independently- suggestion-first market price with
market_price_source - one cover through the host-locked importer, with earlier
cover_import_failureskept - the attached-cover report after an unexpected error
The token route (
inv.scope:write,throttle:12,1) answers the outcome with null values dropped. -
Token test.
POST discogs-credential/testsends one/oauth/identityrequest with the inline token (trimmed and validated; a malformed one gets the Winter 500 page) or the stored one. A refused gate gets the disabled message without a request. No body or log carries the token. -
Route surface.
TestRouteTablePhase14pins the eight routes once each, with their group middleware, scope, throttle and{id}constraint.phase14Absentkeeps onlyPOST /ai-credential/test, and the Phase 08 isolation subtests now assert the real surfaces. The Phase 10.1 admin Discogs stubs are untouched. -
Parity. 50 cases were recorded against PHP through
summer parity:upstream, with 31 sidecars. This includes the re-recordeddiscogs-credential/testcase, which no longer calls the live vendor. The corpus has 165 routes ported and passing, 6 pending, andcheck_corpus --require-recorded --check-secretsis green.
Task Commits
fonoteka.go:
- Task 1 (tracer): album match, inbound limits, route table:
d8cd725(feat) - Task 2: draft match, apply-release, wishlist twins, guarded cover importer:
6a995a6(feat) - Task 3: import, cover-price, credential test, CoverFetcher, README:
2834943(feat)
summercms.go:
5101ecb(feat): tide keeps binary upstream bodies and masks every*_urlupload by shape (README and parity-testing docs)
Tracer gate: after Task 1, <verify> was re-run end to end and passed (vet, the four named tests with -race, the corpus, check_corpus), so the expansion tasks went ahead.
Deviations from Plan
Auto-fixed issues
1. [Rule 3 - Blocking] tide could not carry a cover image or compare cover_url
- Found during: recording design (Task 2/3)
- Issue: Sidecar bodies are YAML strings, so a JPEG response was corrupted on write. The normalizer masked only
urlandthumb_url, so the cover-price route's randomcover_urlpath could never match. - Fix: A non-UTF-8 response body is written as
!!binary, never masked, and replayed byte for byte. Every*_urlkey gets the upload-URL shape check. Tests, the tide README anddocs/services/parity-testing.mdwere updated, andTestDocsTreeanddocs:build --checkare green. - Commit:
5101ecb(summercms.go)
2. [Rule 2 - Correctness] Discogs-route covers through a seam-aware client
- Issue:
CoverImporter's defaultfetchguard.Fetchignoresfetchguard.WithTransport, so the replay would have reachedi.discogs.com. - Fix:
classes.NewGuardedCoverImporteris afetchguard.Clientin AllowHostsMode with the importer's cap and timeout and PHP's recordedUser-Agent: GuzzleHttp/7. The Discogs routes use it. The Phase 12 album routes are unchanged; see deferred-items.md. - Commit: 6a995a6
3. [Rule 1 - Test fragility] Two existing tests broke once the package created more rows and jobs
- The
TestRouteTablePhase13routing checks sent the literal id 12, and a routedDELETE wishlist/albums/12could remove another test's album. They now useunusedNumericID(2000000000). Commit 6a995a6. TestAlbumBroadcastSmoke's worker drained broadcast jobs that earlier tests left in the shared queue, so it missed its 5 s window. It now drops those stale jobs first. Amended into d8cd725.
4. [Rule 1 - Own omission] TestParityContract allow-list
- Newly ported routes must be listed.
phase14SeedRouteswas added and amended into d8cd725, so every commit stays green.
Plan statements that conflict with PHP (parity kept)
5. Idempotency truth. The plan says a second identical apply "imports no second cover, matching PHP's second response". PHP's recorded second response (twice case) does fill nothing again, but it re-imports both covers (filled: ["cover"], four photos). Go matches PHP, and TestApplyReleaseModes asserts that no field is filled again and that the covers are appended as PHP appends them.
6. "dry_run writes nothing" (prohibition). PHP's dry run, recorded as the dry-run case, stages only year, label, catalog_number, country and the tracklist. It still writes the covers, genre_id, barcode, edition, discogs_id, format and the market price. Go does the same, and the staged fields are never written. The verifier should treat the prohibition as "never writes a staged field".
7. No transaction around apply. The plan asked for one lagoon.Transaction. PHP has none, and a transaction would hold the album row lock while up to five covers download. Broadcasts fire from each model save, as PHP's model events do. They are not collapsed into one Phase 12-style event.
Plan details refined
8. NewCoverFetcher(env, importer, bucket) replaces (client, importer, db, clock), because the per-user client is created after the gate. CoverFetchOptions{ForceCover, ForcePrice} mirrors the route's force/refresh_price mapping.
9. Seeds: discogs is an additive extra in fonoteka_reset.php and the Go seed, and it also sets A Love Supreme's discogs_id to 2002. The old pending fixtures came from older seeds, and some came from live vendors. Each was replaced by a fresh case recording, and every route has more cases than the plan's minimum.
10. The import route answers failures PHP does not catch with Winter's 500 page.
Total deviations: 10. Four are auto-fixes, three are plan conflicts resolved for parity, and three are refinements. Impact: the only framework change is the small, documented tide change. There is no scope creep.
Issues Encountered
- The shell aliases
cpandrmto their interactive forms. A loop that held back fixtures between task commits consumed stdin through the prompts. Nothing was lost: every file was re-copied, and the later loops usecommand cp -f/command rm -f. - One full-suite run hit a testcontainers start timeout in the
updatespackage under parallel load. The package passes on its own and in later full runs. - The recording wrote covers into the PHP checkout's
storage/app/uploads/public/6ac/. That directory was removed afterwards. The recording also reset the isolated parity SQLite only.
Verification
- fonoteka.go:
go vet ./...passes.go test ./... -count=1andgo test ./plugins/golem15/fonoteka/... -count=1pass in every package. - Each task's
<verify>command ran with-raceand showed--- PASSfor every named test: TestDiscogsMatchRoute, TestDiscogsInboundLimits, TestApplyReleaseModes, TestDiscogsImportRoute, TestCoverPriceRoute, TestDiscogsCredentialTestRoute, TestCoverFetcherHostLock, TestRouteTablePhase12/13/14. There was no SKIP and no DATA RACE. - Parity:
TestParityCorpus/coverage(165 ported and passing, 6 pending),TestCheckCorpusPortedCaseStatus,TestUpstreamSidecarsAreReplayedandTestParityContractpass.check_corpus --routes … --require-recorded --check-secretsreports 171/171 recorded. Its one pending case-status mismatch isalbums/recognize(14-04). - A mutation check (an extra
filledtag) made the apply-release corpus case fail, so the new cases do compare bodies. - summercms.go:
go vet ./...andgo test ./modules/tide ./cmd/summer -run TestDocsTreepass, andsummer docs:build --checkreports no problems. - Acceptance greps:
expectedPortedRoutesis 165.cover-price/discogsappears once in routes.go, on the line carryinginv.scope:writeandthrottle:12,1.phase14Absenthas nodiscogs-credential/test, andfonoteka-discogs-missing:appears once in inbound_limits.go.
Known Stubs
None.
Threat Flags
None. The new surface (the eight routes, the cover downloads and the token test) is in the plan's threat register. T-14-16..T-14-21 are covered by the tests named above.
Next Phase Readiness
- 14-04 can port
ai-credential/testand recognize on the same recording setup: the per-case proxy loop, thediscogsextra pattern and binary sidecar bodies. - 14-06 picks up deferred-items.md: moving the Phase 12 cover imports onto the guarded importer, with re-recorded cover sidecars.
Phase: 14-domain-jobs-and-external-integrations Completed: 2026-10-03
Self-Check: PASSED
Every created file exists. Commits d8cd725, 6a995a6 and 2834943 (fonoteka.go) and 5101ecb (summercms.go) are present, and the 81 route fixtures and sidecars are on disk.