# Cross-Layer Thinking Guide > **Purpose**: Think through data flow across layers before implementing. --- ## The Problem **Most bugs happen at layer boundaries**, not within layers. Common cross-layer bugs: - API returns format A, frontend expects format B - Database stores X, service transforms to Y, but loses data - Multiple layers implement the same logic differently --- ## Before Implementing Cross-Layer Features ### Step 1: Map the Data Flow Draw out how data moves: ``` Source → Transform → Store → Retrieve → Transform → Display ``` For each arrow, ask: - What format is the data in? - What could go wrong? - Who is responsible for validation? ### Step 2: Identify Boundaries | Boundary | Common Issues | | --------------------- | --------------------------------- | | API ↔ Service | Type mismatches, missing fields | | Service ↔ Database | Format conversions, null handling | | Backend ↔ Frontend | Serialization, date formats | | Component ↔ Component | Props shape changes | ### Step 3: Define Contracts For each boundary: - What is the exact input format? - What is the exact output format? - What errors can occur? --- ## Common Cross-Layer Mistakes ### Mistake 1: Implicit Format Assumptions **Bad**: Assuming date format without checking **Good**: Explicit format conversion at boundaries ### Mistake 2: Scattered Validation **Bad**: Validating the same thing in multiple layers **Good**: Validate once at the entry point ### Mistake 3: Leaky Abstractions **Bad**: Component knows about database schema **Good**: Each layer only knows its neighbors ### Mistake 4: Every Consumer Parses The Same Payload **Bad**: A command reads JSONL events and casts fields inline: ```typescript const thread = (ev as { thread?: string }).thread; const labels = (ev as { labels?: string[] }).labels; ``` This looks local, but it means every consumer owns a private version of the event contract. The next field change will update one command and miss another. **Good**: Decode once at the event boundary, then export typed projections: ```typescript if (!isThreadEvent(ev)) return false; return ev.thread === filter.thread; ``` **Rule**: For append-only logs, JSON streams, RPC payloads, or config files, create one owner for: - event / payload type definitions - type guards and normalization from `unknown` - metadata projections used by UI commands - reducers that replay state from the source of truth Rendering code may format fields, but it must not redefine the payload contract. --- ## Checklist for Cross-Layer Features Before implementation: - [ ] Mapped the complete data flow - [ ] Identified all layer boundaries - [ ] Defined format at each boundary - [ ] Decided where validation happens After implementation: - [ ] Tested with edge cases (null, empty, invalid) - [ ] Verified error handling at each boundary - [ ] Checked data survives round-trip - [ ] Checked that consumers import shared decoders / projections instead of casting payload fields locally - [ ] Checked that derived state points back to the source event identifier (`seq`, `id`, `version`) instead of inventing a second cursor --- ## Cross-Platform Template Consistency In Trellis, command templates (e.g., `record-session.md`) exist in **multiple platforms** with identical or near-identical content. This is a cross-layer boundary. ### Checklist: After Modifying Any Command Template - [ ] Find all platforms with the same command: `find src/templates/*/commands/trellis/ -name ".*"` - [ ] Update all platform copies (Markdown `.md` and TOML `.toml`) - [ ] For Gemini TOML: adapt line continuations (`\\` vs `\`) and triple-quoted strings - [ ] Run `/trellis:check-cross-layer` to verify nothing was missed **Real-world example**: Updated `record-session.md` in Claude to use `--mode record`, but forgot iFlow, Kilo, OpenCode, and Gemini — caught by cross-layer check. --- ## Generated Runtime Template Upgrade Consistency Some generated files are both documentation and runtime input. In Trellis, `.trellis/workflow.md` is parsed by `get_context.py`, `workflow_phase.py`, SessionStart filters, and per-turn hooks. Template changes must be validated against both fresh init and upgrade paths. ### Checklist: After Modifying A Runtime-Parsed Template - [ ] Identify every runtime parser that reads the template, not just the file writer that installs it - [ ] Check whether relevant syntax lives outside obvious managed regions such as tag blocks - [ ] Verify fresh `init` output and a versioned `update` scenario that writes the older `.trellis/.version` - [ ] Add an upgrade regression using an older pristine template fixture, then assert the installed file reaches the current packaged shape - [ ] Update the backend spec that owns the runtime contract --- ## Versioned Documentation Boundary Versioned documentation is a cross-layer boundary: source paths, `docs.json` version routing, and the rendered version selector must all describe the same release line. ### Checklist: Before Editing Versioned Docs - [ ] Identify the target release line: stable, beta, or RC - [ ] Verify the edited MDX path matches that line: - stable: `docs-site/{start,advanced,...}` and `docs-site/zh/{start,advanced,...}` - beta: `docs-site/beta/**` and `docs-site/zh/beta/**` - RC: `docs-site/rc/**` and `docs-site/zh/rc/**` - [ ] Verify `docs.json` navigation points the version label to the same paths - [ ] Grep the opposite tree for release-line-specific terms before committing - [ ] Treat beta content appearing under root release paths as a source-path bug, not a rendering bug **Real-world example**: A beta-only task workflow change documented `prd.md` + `design.md` + `implement.md`, task-creation consent, and Codex mode banners under root `start/` and `advanced/` paths. The docs site then served 0.6 beta behavior under the Release selector. The fix was to restore root release docs, move the 0.6 content to `beta/` and `zh/beta/`, and add a grep audit for beta markers against the root release tree. **Real-world example**: Codex inline mode changed workflow platform markers from `[Codex]` / `[Kilo, Antigravity, Windsurf]` to `[codex-sub-agent]` / `[codex-inline, Kilo, Antigravity, Windsurf]`. Fresh init was correct, but `trellis update` only merged `[workflow-state:*]` blocks and preserved stale markers outside those blocks. Result: upgraded projects got new hook scripts but old workflow routing, so `get_context.py --mode phase --platform codex` could return empty Phase 2.1 detail. --- ## Mode-Detection Probe Checklist When a CLI auto-detects a mode by probing a remote resource (e.g., checking if `index.json` exists to decide marketplace vs direct download): ### Before implementing: - [ ] Probe runs in **ALL** code paths that use the result (interactive, `-y`, `--flag` combos) - [ ] 404 vs transient error are distinguished — don't treat both as "not found" - [ ] Transient errors **abort or retry**, never silently switch modes - [ ] Shared state (caches, prefetched data) is **reset** when context changes (e.g., user switches source) - [ ] **Shortcut paths** (e.g., `--template` skipping picker) must have the same error-handling quality as the probed path — check that downstream functions don't call catch-all wrappers ### After implementing: - [ ] Trace every path from probe result to the mode-decision branch — no fallthrough - [ ] External format contracts (giget URI, raw URLs) are tested or at least documented as comments - [ ] Metadata reads consume a complete response or use a streaming parser — never parse a fixed-size prefix as full JSON - [ ] When reconstructing a composite identifier from parsed parts, verify **all** fields are included and in the **correct position** (e.g., `provider:repo/path#ref` not `provider:repo#ref/path`) - [ ] Verify that **action functions** called after a shortcut don't internally use the old catch-all fetch — they must use the probe-quality variant when error distinction matters **Real-world example**: Custom registry flow had 8 bugs across 3 review rounds: (1) probe only ran in interactive mode, (2) transient errors fell through to wrong mode, (3) giget URI had `#ref` in wrong position, (4) prefetched templates leaked across source switches, (5) `--template` shortcut bypassed probe but `downloadTemplateById` internally used catch-all `fetchTemplateIndex`, turning timeouts into "Template not found". **Real-world example**: Agent-session update hints fetched npm `latest` metadata with `response.read(4096)` and then parsed it as complete JSON. The `@mindfoldhq/trellis` package metadata exceeded 4 KB, so the JSON was truncated, parse failed silently, and the first session injection showed no update hint. Fix: read the complete response before parsing, and add a regression where `version` is followed by an 8 KB metadata tail. --- ## Cross-Platform Template Consistency In Trellis, command templates (e.g., `record-session.md`) exist in **multiple platforms** with identical or near-identical content. This is a cross-layer boundary. ### Checklist: After Modifying Any Command Template - [ ] Find all platforms with the same command: `find src/templates/*/commands/trellis/ -name ".*"` - [ ] Update all platform copies (Markdown `.md` and TOML `.toml`) - [ ] For Gemini TOML: adapt line continuations (`\\` vs `\`) and triple-quoted strings - [ ] Run `/trellis:check-cross-layer` to verify nothing was missed **Real-world example**: Updated `record-session.md` in Claude to use `--mode record`, but forgot iFlow, Kilo, OpenCode, and Gemini — caught by cross-layer check. --- ## Generated Runtime Template Upgrade Consistency Some generated files are both documentation and runtime input. In Trellis, `.trellis/workflow.md` is parsed by `get_context.py`, `workflow_phase.py`, SessionStart filters, and per-turn hooks. Template changes must be validated against both fresh init and upgrade paths. ### Checklist: After Modifying A Runtime-Parsed Template - [ ] Identify every runtime parser that reads the template, not just the file writer that installs it - [ ] Check whether relevant syntax lives outside obvious managed regions such as tag blocks - [ ] Verify fresh `init` output and a versioned `update` scenario that writes the older `.trellis/.version` - [ ] Add an upgrade regression using an older pristine template fixture, then assert the installed file reaches the current packaged shape - [ ] Update the backend spec that owns the runtime contract **Real-world example**: Codex inline mode changed workflow platform markers from `[Codex]` / `[Kilo, Antigravity, Windsurf]` to `[codex-sub-agent]` / `[codex-inline, Kilo, Antigravity, Windsurf]`. Fresh init was correct, but `trellis update` only merged `[workflow-state:*]` blocks and preserved stale markers outside those blocks. Result: upgraded projects got new hook scripts but old workflow routing, so `get_context.py --mode phase --platform codex` could return empty Phase 2.1 detail. --- ## Mode-Detection Probe Checklist When a CLI auto-detects a mode by probing a remote resource (e.g., checking if `index.json` exists to decide marketplace vs direct download): ### Before implementing: - [ ] Probe runs in **ALL** code paths that use the result (interactive, `-y`, `--flag` combos) - [ ] 404 vs transient error are distinguished — don't treat both as "not found" - [ ] Transient errors **abort or retry**, never silently switch modes - [ ] Shared state (caches, prefetched data) is **reset** when context changes (e.g., user switches source) - [ ] **Shortcut paths** (e.g., `--template` skipping picker) must have the same error-handling quality as the probed path — check that downstream functions don't call catch-all wrappers ### After implementing: - [ ] Trace every path from probe result to the mode-decision branch — no fallthrough - [ ] External format contracts (giget URI, raw URLs) are tested or at least documented as comments - [ ] Metadata reads consume a complete response or use a streaming parser — never parse a fixed-size prefix as full JSON - [ ] When reconstructing a composite identifier from parsed parts, verify **all** fields are included and in the **correct position** (e.g., `provider:repo/path#ref` not `provider:repo#ref/path`) - [ ] Verify that **action functions** called after a shortcut don't internally use the old catch-all fetch — they must use the probe-quality variant when error distinction matters **Real-world example**: Custom registry flow had 8 bugs across 3 review rounds: (1) probe only ran in interactive mode, (2) transient errors fell through to wrong mode, (3) giget URI had `#ref` in wrong position, (4) prefetched templates leaked across source switches, (5) `--template` shortcut bypassed probe but `downloadTemplateById` internally used catch-all `fetchTemplateIndex`, turning timeouts into "Template not found". **Real-world example**: Agent-session update hints fetched npm `latest` metadata with `response.read(4096)` and then parsed it as complete JSON. The `@mindfoldhq/trellis` package metadata exceeded 4 KB, so the JSON was truncated, parse failed silently, and the first session injection showed no update hint. Fix: read the complete response before parsing, and add a regression where `version` is followed by an 8 KB metadata tail. --- ## When to Create Flow Documentation Create detailed flow docs when: - Feature spans 3+ layers - Multiple teams are involved - Data format is complex - Feature has caused bugs before --- ## Event Log / Projection Boundary Append-only logs are cross-layer contracts. A single event travels through: ``` CLI input → event writer → events.jsonl → reader → filter → reducer → display ``` ### Checklist: After Adding A New Event Kind Or Field - [ ] Add the event kind to the central event taxonomy - [ ] Add a typed event variant or type guard at the event layer - [ ] Add normalization helpers for array/object fields that come from user input or JSON - [ ] Keep `seq` / `id` assignment in the event writer only - [ ] Make filters and reducers consume the typed event guard, not local casts - [ ] Make display code consume reducer output or typed events, not raw JSON - [ ] Add at least one regression that proves history replay and live filtering use the same filter model **Real-world example**: Thread channels added `kind: "thread"`, `description`, `context`, labels, and `lastSeq`. The first implementation replayed thread state correctly, but several commands still re-parsed event payload fields with local casts. The fix was to make the core event layer own `ThreadChannelEvent` and `isThreadEvent`, make `reduceChannelMetadata` the only channel metadata projection, and make `reduceThreads` the only thread replay reducer.