Files
mengyastore/.trellis/spec/guides/code-reuse-thinking-guide.md

7.2 KiB

Code Reuse Thinking Guide

Purpose: Stop and think before creating new code - does it already exist?


The Problem

Duplicated code is the #1 source of inconsistency bugs.

When you copy-paste or rewrite existing logic:

  • Bug fixes don't propagate
  • Behavior diverges over time
  • Codebase becomes harder to understand

Before Writing New Code

Step 1: Search First

# Search for similar function names
grep -r "functionName" .

# Search for similar logic
grep -r "keyword" .

Step 2: Ask These Questions

Question If Yes...
Does a similar function exist? Use or extend it
Is this pattern used elsewhere? Follow the existing pattern
Could this be a shared utility? Create it in the right place
Am I copying code from another file? STOP - extract to shared

Common Duplication Patterns

Pattern 1: Copy-Paste Functions

Bad: Copying a validation function to another file

Good: Extract to shared utilities, import where needed

Pattern 2: Similar Components

Bad: Creating a new component that's 80% similar to existing

Good: Extend existing component with props/variants

Pattern 3: Repeated Constants

Bad: Defining the same constant in multiple files

Good: Single source of truth, import everywhere

Pattern 4: Repeated Payload Field Extraction

Bad: Multiple consumers cast the same JSON/event fields locally:

const description = (ev as { description?: string }).description;
const context = (ev as { context?: ContextEntry[] }).context;

This is duplicated contract logic even when the code is only two lines. Each consumer now has its own definition of what a valid payload means.

Good: Put the decoder, type guard, or projection next to the data owner:

if (isThreadEvent(ev)) {
  renderThreadEvent(ev);
}

Rule: If the same untyped payload field is read in 2+ places, create a shared type guard / normalizer / projection before adding a third reader.


When to Abstract

Abstract when:

  • Same code appears 3+ times
  • Logic is complex enough to have bugs
  • Multiple people might need this

Don't abstract when:

  • Only used once
  • Trivial one-liner
  • Abstraction would be more complex than duplication

After Batch Modifications

When you've made similar changes to multiple files:

  1. Review: Did you catch all instances?
  2. Search: Run grep to find any missed
  3. Consider: Should this be abstracted?

Reducers Should Use Exhaustive Structure

When state is derived from action-like values (action, kind, status, phase), prefer a reducer with one switch over scattered if/else updates.

// BAD - action-specific state transitions are hard to audit
if (action === "opened") { ... }
else if (action === "comment") { ... }
else if (action === "status") { ... }

// GOOD - one reducer owns the transition table
switch (event.action) {
  case "opened":
    ...
    return;
  case "comment":
    ...
    return;
}

This matters when the event log is the source of truth. A reducer is the documented replay model; display code and commands should not duplicate pieces of that replay model.


Checklist Before Commit

  • Searched for existing similar code
  • No copy-pasted logic that should be shared
  • No repeated untyped payload field extraction outside a shared decoder
  • Constants defined in one place
  • Similar patterns follow same structure
  • Reducer/action transitions live in one reducer or command dispatcher

Gotcha: Python if/elif/else Exhaustive Check

Problem: Python's if/elif/else chains have no compile-time exhaustive check. When you add a new value to a Literal type (e.g., Platform), existing if/elif/else chains silently fall through to else with wrong defaults.

Symptom: New platform works partially — some methods return Claude defaults instead of platform-specific values. No error is raised.

Example (cli_adapter.py):

# BAD: "gemini" falls through to else, returns "claude"
@property
def cli_name(self) -> str:
    if self.platform == "opencode":
        return "opencode"
    else:
        return "claude"  # gemini silently gets "claude"!

# GOOD: explicit branch for every platform
@property
def cli_name(self) -> str:
    if self.platform == "opencode":
        return "opencode"
    elif self.platform == "gemini":
        return "gemini"
    else:
        return "claude"

Prevention: When adding a new value to a Python Literal type, search for ALL if/elif/else chains that switch on that type and add explicit branches. Don't rely on else being correct for new values.


Gotcha: Asymmetric Mechanisms Producing Same Output

Problem: When two different mechanisms must produce the same file set (e.g., recursive directory copy for init vs. manual files.set() for update), structural changes (renaming, moving, adding subdirectories) only propagate through the automatic mechanism. The manual one silently drifts.

Symptom: Init works perfectly, but update creates files at wrong paths or misses files entirely.

Prevention:

  • Best: Eliminate the asymmetry — have the manual path call the automatic one (e.g., collectTemplateFiles() calls getAllScripts() instead of maintaining its own list)
  • If asymmetry is unavoidable: Add a regression test that compares outputs from both mechanisms
  • When migrating directory structures, search for ALL code paths that reference the old structure

Real example: trellis update had a manual files.set() list for 11 scripts that getAllScripts() already tracked. Fix: replaced the manual list with a for..of getAllScripts() loop. See update.ts refactor in v0.4.0-beta.3.


Template File Registration (Trellis-specific)

When adding new files to src/templates/trellis/scripts/:

Single registration point: src/templates/trellis/index.ts

  1. Add export const xxxScript = readTemplate("scripts/path/file.py");
  2. Add to getAllScripts() Map

That's it. commands/update.ts uses getAllScripts() directly — no manual sync needed.

Why this matters: Without registration in getAllScripts(), trellis update won't sync the file to user projects. Bug fixes and features won't propagate.

History: Before v0.4.0-beta.3, update.ts had its own hand-maintained file list that frequently fell out of sync with getAllScripts(). This caused 11 Python files to be silently skipped during trellis update. The fix was to eliminate the duplicate list and use getAllScripts() as the single source of truth.

Quick Checklist for New Scripts

# After adding a new .py file, verify it's in getAllScripts():
grep -l "newFileName" src/templates/trellis/index.ts  # Should match

Template Sync Convention

.trellis/scripts/ (dogfooded) and packages/cli/src/templates/trellis/scripts/ (template) must stay identical. After editing .trellis/scripts/, always sync:

rsync -av --delete --exclude='__pycache__' .trellis/scripts/ packages/cli/src/templates/trellis/scripts/

Gotcha: Running rsync with wrong source/destination paths can create nested garbage directories (e.g., .trellis/scripts/packages/cli/...). Always double-check paths before running.