Files
mengyastore/.trellis/spec/guides/cross-layer-thinking-guide.md

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 .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 "<command>.*"
  • 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.