15 KiB
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:
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:
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 "<command>.*" - Update all platform copies (Markdown
.mdand TOML.toml) - For Gemini TOML: adapt line continuations (
\\vs\) and triple-quoted strings - Run
/trellis:check-cross-layerto 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
initoutput and a versionedupdatescenario 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,...}anddocs-site/zh/{start,advanced,...} - beta:
docs-site/beta/**anddocs-site/zh/beta/** - RC:
docs-site/rc/**anddocs-site/zh/rc/**
- stable:
- Verify
docs.jsonnavigation 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,--flagcombos) - 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.,
--templateskipping 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#refnotprovider: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 "<command>.*" - Update all platform copies (Markdown
.mdand TOML.toml) - For Gemini TOML: adapt line continuations (
\\vs\) and triple-quoted strings - Run
/trellis:check-cross-layerto 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
initoutput and a versionedupdatescenario 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,--flagcombos) - 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.,
--templateskipping 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#refnotprovider: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/idassignment 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.