Three follow-ups from the smoke run, all additive:
1. Status asserts: bruno_gen.go parses each doc's documented success
status (## Response (2NN), 'Success is **201**', '**204 No Content**')
and emits a runtime tests script (expect(res.status).to.eql(N)) on
148 of 191 mirrors. GETs default to 200 (uniform across this API);
mutating verbs stay assert-less unless documented — no guessed
asserts. Verified live: asserts execute and fail loudly (deliberate
999 probe), pass on real responses.
2. OpenAPI: the same -update run now also writes
docs/developer/openapi.yaml (141 paths, tags from docs areas,
security schemes per auth label, request examples literal, no
invented schemas — the docs site remains the shape reference).
Verify mode fails when the file is missing.
3. Scenarios: 7 safe negative scenarios rebuilt (auth registration
matrix 409/403/400, invalid-uuid 400, unknown 404 x2, invalid range
400) — all no-mutation, repeatable, run via bru: 7/7 tests pass.
These replace a fraction of the deleted broken suite; mutations-
heavy scenarios stay out until a design for setup chains exists.
created_at RFC3339 deliberately NOT done — it is the one item that
would break things (Android parses current formats; plan reserves it
as a compat decision).bru run of mutating mirrors also proven to touch
live data (a 'To Read' collection was created and deleted in testing);
only id-less GET mirrors and negative scenarios are safe for blanket
runs.
make test green; generation idempotent (byte-identical re-runs);
no stray smoke rows in users.
The generator moves into the Go toolchain per repo preference:
internal/docs/bruno_gen.go emits the OpenCollection mirrors (same output
contract — gate-verified example-request bodies, env-var params,
device-token headers, login/register/refresh token-capture scripts) and
TestBrunoMirrorsDocs grows a -update flag: verification by default,
regeneration on demand:
go test ./internal/docs -run TestBrunoMirrorsDocs -update
Generator and checker are now the same code — they cannot drift apart.
scripts/gen_bruno_from_docs.py deleted; opencollection.yml README points
at the go-test command.
Also fixed vs the Python pass: mirror placement follows the docs tree
folder (RelPath), not the area frontmatter (several docs — highlights,
notes, ratings, users, progress — carry a different area); docs blocks
emit the collection's mapping form (content + type), not a bare scalar;
JSON body sanity is checked at generation time.
Idempotent: back-to-back -update runs are byte-identical.
make test green (9 pkgs).
WO4. The old collection was the drifted artifact AGENTS.md warned about:
bodies pre-truncated at their first {{var}}, routes renamed server-side
but not here (/api/notes/:media_id, /api/book-matching/search,
/api/collections/:id/rules/test), an orphan universal-progress suite for
routes that no longer exist.
scripts/gen_bruno_from_docs.py emits one OpenCollection request per
frontmatter'd endpoint doc (191 requests): gate-verified example-request
JSON as body, {{env}} params per the environment file, device-token
routes get an explicit Bearer {{device_token}} header, login/register/
refresh keep the token-capture after-response scripts that chain the
collection. Old 1:1 mirrors deleted (128); hand-authored NewDevDBSetup,
environments, and edge-case suites kept. 30 scenario files with
truncated bodies (the documented corruption) deleted — they could not
run.
internal/docs/drift_test.go gains TestBrunoMirrorsDocs: every
frontmatter'd doc must have a Bruno request with matching method+URL
shape — the collection can never silently drift from the docs again.
opencollection.yml README block replaced with the real run instructions.
make test green (9 pkgs, parity test included in internal/docs).
Highlights + notes CRUD (media-item scoped, jwt), progress delete,
reading-stats analytics, and the OPDS trio frontmatter'd. OPDS docs are
the first XML entries: example-response intentionally omitted (Atom
responses), gate contract honored via nil-Response registry pins.
Gate fix: group .Use() middleware now parsed (opds.Use(DeviceAuth...
Authenticate) previously resolved to auth 'none'). Registry: 13 entries.
Drift fixed by gate: reading-stats key names (total_reading_time_minutes,
most_active_day_of_week, daily_reading_minutes — draft invented
reading_time_hours/books_completed); notes/highlights list endpoints
return BARE arrays (no {notes:[]}/{highlights:[]} wrapper); create-note
request has no percentage_location/epubcfi_location. New
/tmp/merge_arrays.py handles array-example merges.
make test 9/9.
14 endpoints backfilled: devices pending/approve/reject; users
password/profile/profile-delete self-service (admin variants in prose);
admin list_users + update_user_max_devices; progress history; book
matching; new password-rules doc.
Gate fix: authLabel missed JWT middleware written inline as
createJWTMiddleware(cfg) (uppercase 'JWT') — reported auth 'none' for
/api/auth self-service routes and would have for any wrapper-inlined
group. Labeled 'JWTMiddleware' + fallthrough 'jwt' case.
Claude registry: 12 entries. Drift fixed by gate: list_users on-device —
handler returns raw []database.Users (bare array, full model incl.
password_hash — real leak the API emits; pinned + documented; earlier
draft had invented {users,total,limit,offset} wrapper). max-devices
response is {message} only. books/match is GET /api/books/match
{matches, action}. progress history sessions = ReadingHistory rows
(pgtype fields). make test 9/9.
Scanner surface frontmatter'd (scan/start/stop/cancel/status/active/
active-summary + watch start/stop/status) with provenance comments;
active-summary on a non-admin group.
Claude registry: 10 entries. Fixed handler drift Beam missed: scan_library
request has no "recursive"; scanner/start binds folder_paths (not
invented interval_seconds); scanner/stop binds no body; response is
{message, job_id, status} with 409 coalescing variant
{error, active_job_id}.
Gate fix in drift_test seed: registerScannerRoutes group seeds used bare
"jwt"/"admin" tokens invisible to authLabel; and silently passed before
only because the seeded set was unchecked. Seeds now 'jwtMiddleware'/
'AdminMiddleware' markers. Also: cleanup of a stale-merge-script residue
in collections/update_rule.md example (extra null keys). make test 9/9.
Beam drafted 15 frontmatter'd docs (saved-filters CRUD, dashboard
sections/preferences/restore, system config/settings, series list + books);
Claude verified shapes against handlers and pinned the registry:
- saved-filters: SavedFilterResponse pinned by type (RFC3339 timestamps here,
unlike most of the API); create/update share CreateSavedFilterRequest
(resource_type oneof media-items|collections|devices)
- dashboard: preferences GET pin {hidden_collections, collection_order,
items_per_section}; PUT returns full UserDashboardPreferences row; sections
wrapper {sections}
- system: GET+PUT /api/system/config are admin-defined key/value maps — new
AnyJSONObject sentinel (gate shape-checks without key comparison; request
side too). UpdateSystemConfiguration returns {status, message} — verified,
not guessed. GET /api/system/settings returns []SettingEntry.
- series: {series[{name, library_id, book_count, total_in_series,
cover_paths, last_entry_at}], total, limit, offset}; books by
?name=&library_id= with family-lens 403
Dedupe: deleted dashboard.md, system/config.md, system/settings.md — each
covered the same endpoints as Beam's per-endpoint files (content merged by
the gate-verified shapes; old prose recoverable from git).
Beam clarifications confirmed: scanner routes are WO5 (docs exist);
progress/books-match are WO5.
New frontmatter'd docs with real shapes (sqlc models pinned directly):
- bookmarks: get/create/update/delete (sync-aware rows: dedup_key,
last_modified_*, tombstones; delete is 204)
- review: upsert (empty string DELETES the review) / get (200 with
{review:null} when absent) / delete
- get single note + highlight (full sqlc row shapes)
- annotation history: restore (POST, kind via query or body) and purge
(DELETE, kind via required query; permanent)
Gate hardening forced by this batch:
- required-field walk now guards non-struct Request types (RawMessage pins)
- update_media_bookmark needed a full example block (no cross-reference
shortcuts — the gate checks every doc standalone)
New frontmatter'd docs with handler-compatible shapes:
- get_library_types.md (public; []LibraryTypes)
- list_libraries.md (admin; {data: ListLibrariesRow})
- get_library_folders.md (admin; []LibraryFolders)
- list/get stats/resolve/delete processing issues (admin; family-lens 403s
documented)
- scan_library.md (202 + 409 coalescing with active_job_id)
- get/update scan-settings (ScanSettingsResponse)
Gate improvements forced by this batch:
- Array responses: registered slice -> doc example must be a JSON array; key
set compared against the element type
- RawMessage pins may now be arrays
- update scan-settings uses a RawMessage pin: handler always sets Message but
the struct's omitempty would reject correct docs via zero-value marshal
Registry: sqlc models pinned directly (LibraryTypes, LibraryFolders,
ListLibrariesRow — json tags verified in the generated code).
Every API doc that declares frontmatter (method/path/auth/area) is now
checked by a unit test against three sources of truth:
1. Route registration: internal/router/*.go is parsed the way echo resolves
it (group prefixes concatenate; scanner.go's parameterized groups seeded).
Doc paths that no longer exist fail. (This is the check that would have
caught the /api/api/admin/hash-conflicts split.)
2. Auth: frontmatter auth label must match the middleware chain
(none|jwt|jwt+admin|device-token|optional-jwt).
3. Shapes: internal/docs/schema.go registry — example-request decoded with
unknown fields DISALLOWED (typo guard), required-by-validate fields must
appear (staleness guard), example-response top-level key set compared
both directions against the marshaled handler type (or a RawMessage pin
for hand-built map responses).
Five collections docs are the pilots with real handler shapes; notable
corrections they pin: update returns 201 (not 200), list excludes smart
collections unless include_auto=true, rule priority is 1-10 higher-wins
(docs claimed 1=highest), created_at is Go time.String() not RFC3339,
hide_finished/show_up_next toggles documented. CollectionResponse hoisted
out of GetCollections/GetBookCollections as CollectionSummary /
BookCollectionSummary so the registry can pin real types.
The gate fails on missing registry entries by design — that is the
maintenance contract. Known pre-existing (not this commit): vet copy-lock
warnings in media.go.