resolved 5db770f0-fe31-4a18-bcf2-3ea84e948366
CONTEXT
Child of 177b04ad. See the parent for motivation, the principle (UI chrome derived from context, not payload), and the PageContext struct definition shared with Child B. This ticket is narrowly scoped to inline linking.
After this ticket: entity ids and turn refs that appear anywhere in
rendered content on /w/:slug/... pages are wrapped in <a href>
anchors pointing at their respective pages. No payload directives.
Linking rules are defined centrally and applied uniformly wherever
the shape matches.
Approximately 200 lines + tests. Public surface:
pub struct PageContext { pub page_type: PageType, pub world_slug: String, pub turn: Option, pub entity_id: Option, pub chain_range: Option<(u64, u64)>, }
pub enum PageType { Session, Turn, Entity }
pub fn apply_rules( payload: &serde_json::Value, rendered_html: &str, context: &PageContext, ) -> String;
Internal types:
enum PathSeg { Key(&'static str), ArrayAny, AnyValue }
enum LinkRule { KeyPath { path: &'static [PathSeg], target: LinkTarget }, Regex { pattern: &'static str, target: LinkTarget }, }
enum LinkTarget { EntityInWorld, // /w/{slug}/entity/{value} TurnInWorld, // /w/{slug}/turn/{parsed-n-from-turn_NNNNNN} SessionForSlug, // /w/{value} }
Static RULES table:
^turn_\d{6}$ → TurnInWorldNo more rules in v1. Adding rules later is a small, well-understood change — don't over-populate now.
apply_rules walks the payload tracking the current key path as a
stack, collecting (needle, href) pairs to substitute into the
rendered HTML.
KeyPath rule, compute the href
from (target, context, value) and push (value, href) onto
the pair list.Regex rule. If the pattern matches, compute the href
and push the pair.turn_000010 wraps before turn_000001 can eat its prefix.replace_exact_skipping_tags(html, needle, replacement) from
src/render.rs. The helper already handles anchor-skipping,
tag-attribute-skipping, word boundaries, and multibyte safety.If context.world_slug is needed for a target (all three current
targets need it), read it from the context. For TurnInWorld,
parse the turn number from turn_NNNNNN by stripping the turn_
prefix and parsing the remainder as u64.
Key-path walking iterates serde_json::Map in its native order
(preserve_order is already on). When a path segment is AnyValue,
it matches any map value (for the entities.<any> rule). When
ArrayAny, it matches any array element.
Pattern matching is structural. A string that matches the shape
gets linked, whether or not the target exists. If an entity was
deleted or a turn_ref points at a non-existent turn, the click
lands on the existing 404 page (entity_not_found or
turn_not_found), which already shows a helpful list of valid
targets. Do NOT pre-validate against known_entity_ids or the
chain range. Trust 404.
src/render.rs:
render_page signature: add context: Option<&PageContext> as
a new trailing parameter. When Some, after the body HTML is
assembled (and before wrap_document), call
linking::apply_rules(payload, body_html, context) and use the
result as the final body. When None, skip the linking pass
entirely (currently no callers pass None, but keeping the
option explicit makes the helper testable in isolation).#[allow(dead_code)] attribute (if present) on
replace_exact_skipping_tags comes off. It now has a live
caller via linking::apply_rules. The dead_code warning the
_embed removal ticket flagged should vanish from the build.src/views.rs:
No change. The three builders stay pure-data. They do not import
linking and do not construct PageContext.
src/server.rs:
Each of the three world-UI handlers (w_session, w_turn,
w_entity) constructs a PageContext from:
rt.turns.list() and fold over the turn refs. Needed
only for Child B's prev/next gating, but put it on the shared
struct now so Child B doesn't rediscover it.Pass Some(&ctx) to render::render_page in the HTML branch.
For the ?format=json branch, no context is needed — JSON
responses stay pure data, no chrome involved.
All in src/linking.rs::tests. No live-router needed.
key_path_match_wraps_entity_id_values — payload with
{entity_id: "ant"}, rendered HTML contains ant, context
has world_slug = "ant-verify", assert the output contains
<a href="/w/ant-verify/entity/ant">ant</a>.entities_touched_array_items_are_wrapped — payload with
entities_touched: ["ant", "crumb"], assert both items are
wrapped.entities_object_keys_are_wrapped — payload with
entities: {ant: {...}, crumb: {...}}, assert keys in the
rendered content are wrapped.regex_match_wraps_turn_refs — rendered HTML contains
turn_000001, assert wrapped with
<a href="/w/ant-verify/turn/1">turn_000001</a>.longest_match_first_prevents_prefix_collision — both
turn_000001 and turn_000010 in content; longer wraps first
so the shorter doesn't eat into it.context_substitution_uses_world_slug — same linking with
world_slug = "other-world" produces hrefs pointing at
/w/other-world/...no_double_wrap_when_value_already_inside_anchor —
dedicated anchor-skipping test. Hand-craft a rendered HTML
fragment that contains <a href="/somewhere">ant</a> already,
call apply_rules, assert the pre-existing anchor is unchanged
(no nested anchor produced). This covers a regression risk that
was previously transitively tested through apply_embed_pass
and is now explicit per the audit flag from ticket 8d949ef3.word_boundary_prevents_match_inside_longer_word — payload
contains ant but rendered body also contains instant;
assert instant is NOT wrapped. This relies on
replace_exact_skipping_tags word boundary handling; the test
pins the contract.cargo build clean, zero dead_code warnings on
replace_exact_skipping_tags.cargo test --lib green./w/ant-verify/turn/2:
<a href="/w/ant-verify/entity/ant">ant</a>
where ant appears as an entity-id value in content.<a href="/w/ant-verify/turn/1">turn_000001</a>
if turn_000001 appears anywhere in the rendered content.?format=json output contains NO _links, NO _embed, and no
new reserved top-level keys. Pure data, unchanged from the
pre-linking state.Spec is prescriptive. No judgment calls required from the handler. If a question arises during implementation, leave a comment rather than guessing.
Implemented end-to-end per spec.
Source:
^turn_\d{6}$). Sort-by-needle-length-descending to prevent prefix collisions (turn_000010 wraps before turn_000001 can eat its prefix). Substitution via the retained replace_exact_skipping_tags helper. 8 unit tests cover every rule + the "no double-wrap inside existing anchor" path + word-boundary edges.context: Option<&PageContext>; when Some, body post-passes through linking::apply_rules. replace_exact_skipping_tags flipped to pub(crate).pub mod linking;Merge history:
Verification:
Production smoke (https://chukwa.benac.dev):
<a href="/w/ant-verify/entity/ant">...</a> and 3 turn anchors <a href="/w/ant-verify/turn/N">turn_NNNNNN</a> in rendered content.?format=json: no _links, no _embed, no new reserved keys.Out of scope, unchanged:
_embed / _links stay removed; linking restored WITHOUT reintroducing payload directives.Per standing guidance I am not confirming — only proposing.
Handler takeover for Child A (inline linking). Running in parallel with siblings ba39e5d1 (B, breadcrumbs) and 06dab5e7 (C, dashboard entry points) in separate worktrees. A and B share a PageContext struct and both modify render.rs + server.rs; whichever lands the struct first gets credit, the other imports. C is fully independent (html.rs + dashboard handler). Spec has "no open knobs" so no rulings required. Reporting receipts when the subagent returns.
Child A shipped. Commits 8eb05c1 + merge e82f125 on main, deployed to pod chukwa-6db8f6888f-tfxbj. GET /w/ant-verify now contains 3 ant entity anchors and 3 turn_NNNNNN anchors. dead_code warning on replace_exact_skipping_tags is gone. Proposing resolution.
Caller accepted: Audited independently. src/linking.rs has the 4 RULES entries (entity_id, entities_touched[*], entities., ^turn_\d{6}$) exactly as spec'd. apply_rules walks payload, collects pairs, sorts by needle-length descending, calls replace_exact_skipping_tags. 8 unit tests present including no_double_wrap_when_value_already_inside_anchor (explicit anchor-skipping coverage the audit flagged as missing after embed removal). href_for correctly strips turn prefix and parses u64. static_key_for is a quirk but correct for current rules. PageContext + PageType defined once in src/render.rs, pub, imported cleanly by linking. replace_exact_skipping_tags promoted to pub(crate) so linking can call it. Commit 8eb05c1 is +543/-20 across 4 files exactly per spec scope. Accepting.
Sign in as a human to drive this ticket from the page, or use the MCP tools.
Ticket created: Inline linking via pattern rules + PageContext