Written 2026-08-14 for the QA agent taking over. Everything you need is here; you should not need to reconstruct history.
| What | Where |
|---|---|
| Main checkout | /home/ryan/code/Floppy (branch feat/591-pin-zxing, untouched) |
| This work | /home/ryan/code/floppy-jsonld (branch feat/jsonld-domain-context) |
| Base | latest (PR #742 merged; its branch was deleted) |
| PR | #769 (supersedes #763, auto-closed when its base branch went away) |
This was originally stacked on PR #742. #742 has since merged into latest,
so the branch now targets latest directly and latest is fully merged in.
GitHub auto-closed the original PR #763 when codex/openapi-grounding-contracts
was deleted; a closed PR's base cannot be retargeted, so the work moved to #769.
Test env: run uv sync in the worktree first. Test command is
SECRET=test-only scripts/test.sh (the SECRET prefix is mandatory; without
it settings refuse to load).
Publishes a committed 41-operation OpenAPI subset, a generated domain vocabulary, an offline API docs page, MCP hardening, and drift-gate tests.
- Conflicts against
latestwere resolved by mergingorigin/latest(8a26dcd0). The merge was textually clean, zero conflicted files. Pushed as6a667843. GitHub now reportsMERGEABLE. - Full review evidence, including a drift-gate mutation proof and measured
token costs, is at
~/.gstack/projects/dannyvfilms-Floppy/ryan-pr-742-conflicts-review-test-outcome-20260814-1500.md. Read that file before re-reviewing #742 so you do not repeat work. - Former local-only failure in
test_data_paths(python-decouple walking up to/home/ryan/code/Floppy/.env) is fixed upstream by #766, "isolate secret in data path test", now inlatest.
~/.gstack/projects/dannyvfilms-Floppy/ryan-fix-boosted-nav-entrance-guard-self-match-design-20260813-152535.md
(status APPROVED). Workstreams: W0 spike, W1 trustworthy OpenAPI, W2 JSON-LD,
W3 AsyncAPI, W4 handoff surfaces. #742 delivered W0 + W1 + focused W4.
Constraints that still bind and that QA should check against:
- Zero new runtime dependencies. Test-only deps must stay out of the image
(
Dockerfilerunsuv sync --no-default-groups). - Release-gating tests must live under
src/app/tests/, because.github/workflows/app-tests.ymlrunstest app users integrations lists eventsand omits theapilabel. A gate undersrc/api/tests/does not protect PRs. - Do not edit
.github/workflows/**— CI fails PRs that touch it. - Baseline Zero: ruff, lint and tests stay at zero failures.
/api/docs/must make zero external network requests and must not trigger schema generation.
Three commits:
39a79d31— generate the context from the vocabulary1c6dc98c— serve it and expose it to MCPeca2292d— close the review findings (see "Review round already applied")
Changes:
| File | Change |
|---|---|
src/app/domain_vocabulary.py |
render_jsonld_context(), class_name(), property_name(), CONTEXT_PATH, namespace constants; main() now writes/checks both artifacts; projected property names validated unique |
src/api/contracts/context.jsonld |
new generated artifact (JSON-LD 1.1) |
src/api/contract_views.py |
jsonld_context view — application/ld+json, byte-derived ETag, 1h public cache |
src/config/urls.py |
route api/context.jsonld, name jsonld-context |
mcp_server/floppy_mcp/client.py |
fetch_public_contract() — unauthenticated fetch for public contract paths outside /api/v1/ |
mcp_server/floppy_mcp/server.py |
optional floppy://domain-context resource |
src/templates/users/about.html |
"Domain Context (JSON-LD)" link |
src/templates/api/docs.html |
domain-context entry on the offline index |
src/app/tests/test_api_contracts.py |
JSONLDContextTests — 10 gates |
src/users/tests/views/test_about.py |
expects the new link; still asserts AsyncAPI absent |
src/app/tests/test_domain_vocabulary.py |
--check now covers context drift; tests isolated from committed artifacts |
mcp_server/tests/test_tools.py |
resource success + failure-does-not-block-tools |
pyproject.toml / uv.lock |
pyld==2.0.4 in the test group only |
Design decisions worth knowing before you judge the code:
- The context reads only
key,relationships,schema_org. Definitions, aliases and bounded contexts stay prose. This is revised premise P2: one vocabulary, separately maintained contracts, no projection engine. A test asserts prose fields never appear in the artifact. - Class terms are PascalCase from the key (
media_type→MediaType); property terms are camelCase from the relationship label. Where aschema_orgmapping exists the class expands to the schema.org IRI (Item→https://schema.org/CreativeWork); otherwise it stays inhttps://github.com/dannyvfilms/Floppy/ns#. - The MCP resource is optional by construction. No tool reads it. A test
asserts that when the context 404s, the resource returns a structured error
and a normal
search_mediacall still succeeds.
- Full fast suite on this branch: 3636 tests, OK (1 skipped). Note
test_data_pathspasses here, because this worktree lives at/home/ryan/code/floppy-jsonld, outside the main repo, so python-decouple never finds/home/ryan/code/Floppy/.env. That confirms the root cause. app.tests.test_api_contracts+test_domain_vocabulary+users.tests.views.test_about: 56/56 OKmcp_server/tests: 58/58 passedruff check src mcp_server/floppy_mcp mcp_server/tests: All checks passed- Drift-gate mutation proof: changing
floppy:consumption→floppy:MUTATEDin the committed context failedtest_committed_context_matches_the_vocabulary_rendererandtest_pyld_expands_the_context_to_absolute_iris; restoring made them green. The gate is load-bearing. - PyLD expansion verified across every class and relationship in the vocabulary (not a subset).
An independent review (Gemini 3.7 via agy) found three real defects, all
fixed in eca2292d:
validate_terms()checked raw relationship labels for uniqueness while the context maps the projected camelCase name."has user"and"Has user"both render ashasUser, so a collision could have silently collapsed two relationships into one property IRI. Now validated on the projected name, with a proof case.fetch_public_contract()did not follow redirects, so an instance behind a proxy doing http→https would have returned redirect HTML as the context body.- The
--checkgate never coveredcontext.jsonldalone, and the entry-point tests patched onlyGUIDE_PATH, somain([])wrote the real committed artifact during a test run. Both fixed; tests are now isolated.
It also named two gate gaps, both closed: property terms are now asserted to be
@id-typed, and PyLD expansion covers the whole vocabulary.
Run against a live instance on 2026-08-15. All passed:
/api/context.jsonld: 200,application/ld+json, bytes identical to the committed artifact,Cache-Control: public, max-age=3600, ETag match → 304, stale ETag → 200, HEAD → 200.- All four contract routes return 200 anonymously.
/api/docs/issues exactly one network request, the page itself — zero external refs, zero<script>, zero<img>, confirmed in the live network log.- New row renders and resolves to
/api/context.jsonld. - 320px and 375px: no page horizontal overflow; code blocks scroll internally.
- Dark mode correct; skip link first focusable with a 3px outline.
- Settings → About: four links, correct hrefs,
aria-hidden="true" focusable="false"icons, AsyncAPI absent. /floppysubpath renders/floppy/api/context.jsonld, no doubled prefix.
One finding: POST /api/context.jsonld returns 302 to login behind the real
middleware stack, not the 405 the Django test client sees. /api/openapi.yaml
and /api/docs/ behave identically, so it is pre-existing routing behaviour, not
a regression. The test now asserts the invariant true in both environments.
uv export --no-default-groups contains no pyld, lxml or frozendict; they
appear only under --only-group test. CI's "Smoke built image" job passes.
Artifact is 786 bytes.
Not done at all for this branch. Needed:
/api/context.jsonld— returns 200,application/ld+json, correct bytes, ETag revalidation gives 304, HEAD works. Unit tests cover this; confirm in a browser against a running instance./api/docs/— the new "Domain context (JSON-LD)" entry renders, the link resolves, and the page still makes zero external requests. Check the network log explicitly.- Settings → About — the new link renders with the right label and icon
a11y attributes (
aria-hidden="true",focusable="false"), and the group still reads as one capability. - Re-check 320px and 375px layouts, keyboard focus and skip link, dark mode,
and reduced motion on
/api/docs/, since a row was added. - Confirm
/floppysubpath rendering still produces correct URLs.
The design requires recording artifact bytes and a before/after container image
comparison, failing at 1 MiB or 0.1% growth, whichever is smaller. The new
context.jsonld is small (~700 bytes) but the gate has not been run for this
branch. PyLD must not appear in the image — verify with a clean Podman/Docker
build that uv sync --no-default-groups excluded it.
Not opened yet. Base must be codex/openapi-grounding-contracts, not latest.
A drafted channel_registry.py is at /tmp/claude-1000/w3-park/channel_registry.py.
It is not committed, not tested, and not reviewed — treat it as a sketch.
Verified facts it encodes, which you can trust because they were traced in source:
- Seven inbound message routes reaching six
WEBHOOK_PROCESSORS:webhook/plex/<token>,webhook/jellyfin/<token>,webhook/emby/<token>,webhook/jellyseerr/<token>,webhook/seerr/global/,webhook/kodi/<token>, andstremio-addon/<token>/subtitles/<type>/<id>.json(a GET subtitles request used as a throttled playback-start signal — not a webhook). - ListenBrainz has a separate inbound surface at
/apis/listenbrainz/1/submit-listens, handled synchronously, not viaprocess_webhook. - Three Celery queues:
celery(background, default priority 5),interactive(priority 0),discover. Priorities fromCELERY_TASK_ROUTESinsrc/config/settings.py:1331-1389. celery_queue_plan()insrc/config/runtime_profile.py:372has three branches but only two are reachable:minimal→ one combined worker oncelery,interactive,discover;constrainedandstandard→ background worker oncelery,discoverplus an isolatedinteractiveworker. The three-worker branch is dead code for the current tier set. Do not document a three-worker split as real.
Still required before W3 can ship, per the design:
- Pin the official AsyncAPI 3.0 JSON Schema as a test fixture outside
src/and validate with the already-installedjsonschema(4.26.0). Nonpx, no runtime dependency, no workflow change. Fetching that schema needs the maintainer's go-ahead. - Celery messages must declare
application/x-python-serializeand must not claim a portable JSON payload schema; logical task arguments go inx-floppy-*metadata only. - Exclude polling importers, Apprise destinations and base classes. A large or inaccurate AsyncAPI document is worse than none (design premise P7).
- Invert
assertNotContains(response, "AsyncAPI")insrc/users/tests/views/test_about.pyonly when the artifact actually ships.
The grounding evaluation failed its preregistered gate: treatment medians
were Q1 0, Q2 1, Q3 1 against a required 2
(docs/agents/evals/grounding_questions.md:502). W2 and W3 were both recorded
DEFER on that evidence.
The maintainer has since explicitly chosen to build W2 and W3 anyway. That is a deliberate scope override, not an oversight, and it is why this branch exists. Record it honestly in any PR body: the formats ship because the maintainer asked for them, not because the evaluation justified them. Do not restate the failed evaluation as a success.
These were raised and left open. They apply to the stack, not just #742.
src/api/contract_views.pyreads contracts at module import, so a missing artifact fails app startup rather than returning the 404 the design's failure-mode table promised. This branch follows the same pattern forcontext.jsonld, which doubles the blast radius. Worth a decision.src/api/schema_contract.py:786-811depends on drf-spectacular private API (GENERATOR_STATS._error_cache,._warn_cache,patched_settings). Pinned atdrf-spectacular==0.29.0, so it will not break silently, but the next upgrade can break the gate. The risk is not commented anywhere.mcp_server/floppy_mcp/server.pytreats a lookup HTTP 500 as "absent" whensource == "manual"and proceeds to write. Deliberate and tested (mcp_server/tests/test_tools.py:127, commit27cc61c4), but the adjacent comment explains only the 404 case. Documentation gap.