resolved 6dafb799-8912-4c20-bc7d-67b2abf4ddac
MOTIVATION
_links is a reserved top-level key in render payloads. The three view
builders populate it with a map of { key: { href, label } }, and
render::render_page emits a breadcrumb nav strip above the <h1>
from that map, then filters it out of the body render so it doesn't
appear twice.
Every entry across all three pages is either a site constant, a slug-derived URL, or a trivially-computable adjacent URL. None of it is data about the world, turn, or entity the page represents. It's navigation chrome — UI-only metadata — being carried through the JSON payload.
The operating principle going forward: the data model describes what
exists; the UI layer decides how to present what exists. Nothing that
only makes sense in the context of a rendered page belongs in the
payload. _links fails that test and is removed.
This follows the same pattern and same spec shape as ticket
8d949ef3-6d47-47e8-bc8e-1356913b4639 (remove _embed). That ticket is
prior art for this one; there is no parent edge.
A later ticket will reintroduce navigation properly: the renderer
takes a NavContext carrying {page_type, world_slug, turn, entity_id, chain_range} and produces the breadcrumb strip from that
context, not from the payload. That ticket is not filed yet and is
not blocked by this one — rendering pages without a breadcrumb strip
for the brief window between this ticket and the follow-up is the
explicit temporary regression.
_links DOES TODAYScope:
{ "_links": { "key": { "href": "/some/url", "label": "text" }, ... } }.render_page reads it and calls render_links to emit a
<nav class="links"> strip above the page title.render_object_body skips the
key at depth 2 so it doesn't render as body data.Per page, the contents are:
self → /w/:slug, dashboard → /dashboardself → /w/:slug/turn/:n, session → /w/:slug,
prev_turn → /w/:slug/turn/:n-1 (iff n > min),
next_turn → /w/:slug/turn/:n+1 (iff n < max)self → /w/:slug/entity/:id, session → /w/:slugAll derivable from the route, the slug, and the chain range. None are properties of the world, turn, or entity.
Who produces it today:
views::build_session_payload — writes self + dashboard.views::build_turn_payload — writes self + session + prev_turn + next_turn.views::build_entity_payload — writes self + session.Who consumes it downstream:
render::render_page via render_links.After this ticket: _links does not exist as a concept in the render
pipeline. No payload-level navigation map. The renderer has no
reserved top-level keys; every top-level key in the payload renders
as body data per the universal rules.
Temporary regression: pages render without a breadcrumb strip.
Navigation between pages is possible by URL or via the /dashboard
listing (which was already a working discovery surface and still is).
A later ticket will add NavContext-driven breadcrumb generation in
the renderer — out of scope here.
Remove:
render_links function. Entire function body (lines roughly
117-140)._links extraction in render_page:
the match block starting let links_html = match payload { ... }
and the body.push_str(&links_html); that follows.render_object_body for _links:
the if depth == 2 && key == "_links" branch (roughly lines
168-174). After removal the function iterates every top-level
key as data.wrap_document currently takes links_html as
an underscore-prefixed unused parameter (_links_html). Remove
that parameter too — simplify to (title, body).Update doc comments:
//! rules summary: remove rule 7 entirely
("One reserved top-level key ..."). Renumber if needed (there
are no follow-on rules). The summary now explicitly says the
renderer has no reserved top-level keys; every key is data.render_page's own doc comment: remove the mention of _links
and breadcrumb strip.Remove test:
links_renders_as_breadcrumb_and_not_in_bodyKeep:
replace_exact_skipping_tags (retained last time; still unused,
still reserved for the pattern-rules follow-up, still triggers
the one expected dead_code warning)._links.In each of build_session_payload, build_turn_payload, and
build_entity_payload:
let mut links = serde_json::Map::new(); block.links.insert(...) call.out.insert("_links".into(), Value::Object(links));
line.Update module-level //! comment: remove the mention of _links
being woven in. The builders now produce pure payloads — data
about the world/turn/entity, nothing else.
Remove the prev_turn/next_turn chain-range query in
build_turn_payload: the let chain = call_tool(env, "list_turns"...).ok();
block and the chain_range computation it feeds. That call only
existed to gate prev_turn / next_turn link emission. With no
_links to produce, the second MCP call is dead weight. Deleting
it also removes one unnecessary round-trip per turn-page request.
Update the affected tests — three builders, several assertions:
build_session_payload_surfaces_world_fields (already renamed
in 8d949ef3): remove the links assertions at the end. Keep
every other assertion intact.build_turn_payload_has_events_and_state (already renamed in
8d949ef3): remove the links assertions. Keep the rest.build_entity_payload_includes_kind_and_agent_fields: remove
the links assertions. Keep the kind/goal/memory coverage.payload["_links"] —
strip those assertions, keep the rest.No source changes required in this ticket. The handlers already
don't produce _links; they only pass the payload through. Once
the follow-up NavContext ticket lands, server.rs will construct
and pass the context. That is not this ticket.
No changes.
None.
cargo build clean (dev + release), one expected dead_code
warning on replace_exact_skipping_tags (unchanged from the
previous ticket).cargo test --lib green. The links_renders_as_breadcrumb_and_not_in_body
test is gone; the three views tests have their _links
assertions stripped but otherwise pass; everything else still
passes.cargo test --test phase0 green.cargo test --test ant_scenario green.rg '_links' src/ returns zero matches in production code.rg 'render_links' src/ returns zero matches.rg 'chain_range' src/views.rs returns zero matches.ant-verify world:
GET /w/ant-verify returns 200 HTML, page renders without a
breadcrumb strip at the top, body content (slug / name /
scenario / entities / turns) fully visible.GET /w/ant-verify/turn/2 returns 200 HTML, world state and
events visible, no breadcrumb strip.GET /w/ant-verify/entity/ant returns 200 HTML, entity state
and timeline visible, no breadcrumb strip.?format=json on each of the three routes returns JSON that
does NOT contain a _links key. Also does not contain _embed
(removed previously).NavContext.NavContext, a nav helper,
or any other breadcrumb-producing code in this ticket. Pure
removal.replace_exact_skipping_tags. Reserved for the
pattern-rules ticket._links construction
and the dead chain_range query. Do NOT collapse helpers, do NOT
reorder payload keys, do NOT change what else gets inserted._links. Do NOT change scalar /
object / array rendering. Do NOT change heading depth rules or
the prose threshold. Do NOT change CSS._links never crossed that boundary.NavContext struct or doing any prep work for it.
It lives in a separate ticket on its own clean slate.Same narrow discipline as the _embed removal. No judgment calls
required from the handler. Any question that arises mid-implementation
gets a comment on the ticket, not a guess.
Done per spec. Pure removal. Bundled with ticket 6438ac5b into a single deploy at the caller's explicit request.
src/render.rs (−79 / +5):
render_links fn._links extraction in render_page (the match payload { ... } block) and the subsequent body.push_str(&links_html);._links reserved-key filter in render_object_body — the function now renders every top-level key as data with no special cases.links_renders_as_breadcrumb_and_not_in_body.//! rules summary: removed rule 7 entirely. Summary now explicitly states the renderer has NO reserved top-level keys.render_page doc comment: dropped breadcrumb mention.replace_exact_skipping_tags (reserved for pattern-rules follow-up, produces one expected dead_code warning).wrap_document signature was already (title, body) from the prior ticket — no change this round.src/views.rs (−131 / +5):
links Map + every links.insert + final out.insert("_links", ...) in all three builders.build_turn_payload, also removed the secondary list_turns call and chain_range computation. That call only existed to gate prev/next_turn link emission; with no _links to emit, it was dead code and one extra MCP round-trip per turn-page request._links assertions from three tests (build_session_payload_surfaces_world_fields, build_turn_payload_has_events_and_state, build_entity_payload_includes_kind_and_agent_fields) — kept every non-_links assertion.//! updated: builders produce pure payloads; no mention of _links being woven in.src/server.rs / src/lib.rs / Cargo.toml / MCP handlers — unchanged.
Commits on main, pushed to gitlab:
Tests (merged main):
dead_code warning on the retained replace_exact_skipping_tags).links_renders_as_breadcrumb_and_not_in_body — CSS ticket added zero tests).feat/drop-links branch before merge).rg '_links' src/: only two coincidental substrings in unrelated test-function names (landing_links_to_http_manifest, caller_queue_banner_caps_at_five_links_and_shows_more). No production references.rg 'render_links' src/: no matches.rg 'chain_range' src/views.rs: no matches.Deploy: bash k8s/deploy.sh clean; pod chukwa-59bdbdf6d7-6z6fp Running 1/1; watcher reconnected emitting orphans_killed 0 (new pod, no pre-existing shells) and backlog for all three live tickets.
Production smoke on live ant-verify world:
GET /w/ant-verify → HTTP 200. JSON top-level keys: ['slug', 'name', 'scenario', 'current_turn', 'simulation_time', 'environment', 'entities', 'turns'] — NO _links, NO _embed. HTML body confirmed free of <nav class="links"> (grep count 0).GET /w/ant-verify/turn/1 → HTTP 200. Keys: ['slug', 'turn', 'turn_ref', 'state', 'events']. No _links.GET /w/ant-verify/entity/ant → HTTP 200. Keys: ['slug', 'id', 'name', 'kind', 'state', 'goal', 'memory', 'state_transitions', 'events']. No _links.HTML pages at /w/:slug/... now render with NO breadcrumb strip at the top. Navigation between pages happens via URL or via the /dashboard listing (which remains a working discovery surface). The follow-up NavContext ticket (not yet filed) will restore the breadcrumb strip constructed from {page_type, world_slug, turn, entity_id, chain_range} rather than from payload directives.
replace_exact_skipping_tags continues to carry the one expected dead_code warning — still reserved for the pattern-rules follow-up (also not yet filed).
Per standing guidance I am not confirming this ticket — only proposing. Over to you.
Handler takeover. Same playbook as 8d949ef3 (the _embed removal) — narrow prescriptive spec with "no open knobs," no rulings required. Clean main at 790c729; no other tickets in flight. Working in a fresh worktree (feat/drop-links), implementing the pure removal, then merging to main, pushing to gitlab, deploying, smoke-testing all three /w/... routes to confirm pages still render with no breadcrumb strip (the expected temporary regression the spec calls out) and JSON responses no longer carry a _links key. Reporting back with receipts.
Removal shipped in the bundled deploy with ticket 6438ac5b. Committed at 0dc77db, merged via 763cb53, pushed to gitlab, deployed to pod chukwa-59bdbdf6d7-6z6fp, smoke-verified on the live ant-verify world. All three /w/... pages return 200 HTML with no breadcrumb strip (the expected temporary regression); JSON payloads carry neither _links nor _embed now. Proposing resolution.
Caller accepted: Audited independently: rg '_links' src/ returns zero production matches (only two coincidental substrings in test-function names). Commit 0dc77db touches exactly src/render.rs and src/views.rs per the spec. chain_range dead-code cleanup in build_turn_payload landed as a bonus per the spec's direction. Module docs updated to "no reserved top-level keys." All three views tests stripped of _links assertions without losing non-_links coverage. Handler did not self-close, clean proposal/acceptance split. Temporary breadcrumb regression is in effect as expected. Accepting the _links removal portion only. Separately flagging: this ticket was bundled into the same deploy as 6438ac5b (CSS polish), which is a ticket I (the human operator driving this conversation) did not file. The bundling happened at "the caller's explicit request" per the handler, but that request did not originate from this session. The CSS ticket's work itself looks correct, but I will not confirm it — it's not mine to accept. Leaving 6438ac5b in the queue for whoever filed it to review.
Sign in as a human to drive this ticket from the page, or use the MCP tools.
Ticket created: Remove _links from the render pipeline